refactor: dns优选

pull/180/head
xiaojunnuo 2020-11-09 00:54:55 +08:00
parent 344c732ad6
commit dfd2db5285
18 changed files with 205 additions and 55 deletions

View File

@ -24,7 +24,7 @@ module.exports = {
'.*': { proxy: 'raw.fastgit.org' } '.*': { proxy: 'raw.fastgit.org' }
}, },
'github.githubassets.com': { 'github.githubassets.com': {
'.*': { proxy: 'assets.fastgit.org' } '.*': { proxy: 'assets.fastgit.org', test: 'https://github.githubassets.com/favicons/favicon.svg' }
}, },
'customer-stories-feed.github.com': { 'customer-stories-feed.github.com': {
'.*': { proxy: 'customer-stories-feed.fastgit.org' } '.*': { proxy: 'customer-stories-feed.fastgit.org' }

View File

@ -8,7 +8,7 @@ process.on('uncaughtException', function (err) {
// console.error(err.errno) // console.error(err.errno)
return return
} }
console.error('uncaughtException',err) console.error('uncaughtException', err)
}) })
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {

View File

@ -137,6 +137,7 @@ const NodePlugin = function (context) {
] ]
const ret = await shell.exec(cmds, { type: 'cmd' }) const ret = await shell.exec(cmds, { type: 'cmd' })
event.fire('status', { key: 'plugin.node.enabled', value: false }) event.fire('status', { key: 'plugin.node.enabled', value: false })
console.info('关闭【NPM】代理成功')
return ret return ret
} }
} }

View File

@ -97,9 +97,9 @@ const serverApi = {
} }
}) })
}, },
async restart () { async restart ({ mitmproxyPath }) {
await serverApi.kill() await serverApi.kill()
await serverApi.start() await serverApi.start({ mitmproxyPath })
}, },
getServer () { getServer () {
return server return server

View File

@ -35,7 +35,7 @@ function setTray (app) {
} }
] ]
// 设置系统托盘图标 // 设置系统托盘图标
const iconPath = path.join(__dirname, '../extra/favicon.ico') const iconPath = path.join(__dirname, '../extra/icons/128x128.png')
const appTray = new Tray(iconPath) const appTray = new Tray(iconPath)
// 图标的上下文菜单 // 图标的上下文菜单
@ -44,14 +44,25 @@ function setTray (app) {
// 设置托盘悬浮提示 // 设置托盘悬浮提示
appTray.setToolTip('DevSidecar-开发者边车辅助工具') appTray.setToolTip('DevSidecar-开发者边车辅助工具')
// 设置托盘菜单
appTray.setContextMenu(contextMenu)
// 单击托盘小图标显示应用 // 单击托盘小图标显示应用
appTray.on('click', () => { appTray.on('click', () => {
// 显示主程序 // 显示主程序
win.show() win.show()
}) })
// 设置托盘菜单
// appTray.setContextMenu(contextMenu)
// appTray.on('double-click', function () {
// console.log('double click')
// win.show()
// })
appTray.on('right-click', function (event, bounds) {
setTimeout(function () {
appTray.popUpContextMenu(contextMenu)
}, 200)
})
return appTray return appTray
} }

View File

@ -21,6 +21,9 @@ const localApi = {
server: { server: {
start () { start () {
return DevSidecar.api.server.start({ mitmproxyPath }) return DevSidecar.api.server.start({ mitmproxyPath })
},
restart () {
return DevSidecar.api.server.restart({ mitmproxyPath })
} }
}, },
config: { config: {

View File

@ -22,7 +22,7 @@ function register (app) {
} }
function handleServerStartError (err, app) { function handleServerStartError (err, app) {
if (err.message.indexOf('listen EADDRINUSE') >= 0) { if (err.message && err.message.indexOf('listen EADDRINUSE') >= 0) {
app.$confirm({ app.$confirm({
title: '端口被占用,代理服务启动失败', title: '端口被占用,代理服务启动失败',
content: '是否要杀掉占用进程?', content: '是否要杀掉占用进程?',

View File

@ -10,7 +10,8 @@ export default {
config: undefined, config: undefined,
status: status, status: status,
labelCol: { span: 4 }, labelCol: { span: 4 },
wrapperCol: { span: 20 } wrapperCol: { span: 20 },
applyLoading: false
} }
}, },
created () { created () {
@ -27,12 +28,17 @@ export default {
} }
}) })
}, },
apply () { async apply () {
return this.saveConfig().then(() => { this.applyLoading = true
if (this.applyAfter) { await this.applyBefore()
return this.applyAfter() await this.saveConfig()
} if (this.applyAfter) {
}) await this.applyAfter()
}
this.applyLoading = false
},
async applyBefore () {
}, },
reloadDefault (key) { reloadDefault (key) {
this.$api.config.resetDefault(key).then(ret => { this.$api.config.resetDefault(key).then(ret => {

View File

@ -59,8 +59,8 @@
</div> </div>
<template slot="footer"> <template slot="footer">
<div class="footer-bar"> <div class="footer-bar">
<a-button class="md-mr-10" @click="reloadDefault('plugin.node')"></a-button> <a-button class="md-mr-10" icon="sync" @click="reloadDefault('server')"></a-button>
<a-button type="primary" @click="apply()"></a-button> <a-button :loading="applyLoading" icon="check" type="primary" @click="apply()"></a-button>
</div> </div>
</template> </template>
</ds-container> </ds-container>
@ -91,13 +91,13 @@ export default {
this.npmVariables = ret this.npmVariables = ret
}) })
}, },
onSwitchRegistry (event) { async onSwitchRegistry (event) {
return this.setRegistry(event.target.value).then(() => { await this.setRegistry(event.target.value)
this.$message.success('切换成功') this.$message.success('切换成功')
})
}, },
setRegistry (registry) { async setRegistry (registry) {
return this.$api.plugin.node.setRegistry(registry) this.apply()
await this.$api.plugin.node.setRegistry(registry)
}, },
setNpmVariableAll () { setNpmVariableAll () {
this.saveConfig().then(() => { this.saveConfig().then(() => {

View File

@ -21,8 +21,8 @@
</div> </div>
<template slot="footer"> <template slot="footer">
<div class="footer-bar"> <div class="footer-bar">
<a-button class="md-mr-10" @click="reloadDefault('proxy')"></a-button> <a-button class="md-mr-10" icon="sync" @click="reloadDefault('server')"></a-button>
<a-button type="primary" @click="apply()"></a-button> <a-button :loading="applyLoading" icon="check" type="primary" @click="apply()"></a-button>
</div> </div>
</template> </template>
</ds-container> </ds-container>

View File

@ -69,8 +69,8 @@
</div> </div>
<template slot="footer"> <template slot="footer">
<div class="footer-bar"> <div class="footer-bar">
<a-button class="md-mr-10" @click="reloadDefault('server')"></a-button> <a-button class="md-mr-10" icon="sync" @click="reloadDefault('server')"></a-button>
<a-button type="primary" @click="apply()"></a-button> <a-button :loading="applyLoading" icon="check" type="primary" @click="apply()"></a-button>
</div> </div>
</template> </template>
</ds-container> </ds-container>
@ -109,8 +109,28 @@ export default {
}) })
} }
}, },
applyAfter () { async applyBefore () {
const dnsMapping = {}
for (const item of this.dnsMappings) {
if (item.key) {
dnsMapping[item.key] = item.value
}
}
this.config.server.dns.mapping = dnsMapping
},
async applyAfter () {
if (this.status.server.enabled) {
return this.$api.server.restart()
}
},
deleteDnsMapping (item, index) {
this.dnsMappings.splice(index, 1)
},
restoreDefDnsMapping (item, index) {
},
addDnsMapping () {
this.dnsMappings.unshift({ key: '', value: 'usa' })
} }
} }
} }

View File

@ -1,11 +1,72 @@
const LRU = require('lru-cache') const LRU = require('lru-cache')
const { isIP } = require('validator') // const { isIP } = require('validator')
const getLogger = require('../utils/logger') const getLogger = require('../utils/logger')
const logger = getLogger('dns') const logger = getLogger('dns')
const cacheSize = 1024 const cacheSize = 1024
function _isIP (v) { // eslint-disable-next-line no-unused-vars
return v && isIP(v) // function _isIP (v) {
// return v && isIP(v)
// }
class IpCache {
constructor (hostname) {
this.hostname = hostname
this.count = {}
this.lookupCount = 0
this.createTime = new Date()
}
/**
* 获取到新的ipList
* @param ipList
*/
setIpList (ipList) {
this.ip = ipList.shift()
this.ipList = ipList
this.lookupCount++
this.doCount(this.ip, false)
}
/**
* 换下一个ip
* @param count
*/
changeNext (count) {
count.keepErrorCount = 0 // 清空连续失败
if (this.ipList > 0) {
this.ip = this.ipList.shift()
} else {
this.ip = null
}
}
/**
* 记录使用次数或错误次数
* @param ip
* @param isError
*/
doCount (ip, isError) {
let count = this.count[ip]
if (count == null) {
count = this.count[ip] = { total: 0, error: 0, keepErrorCount: 0, successRate: 0 }
}
if (isError) {
count.error++
count.keepErrorCount++
} else {
count.total++
}
count.successRate = 1 - (count.error / count.total)
if (isError && this.ip === ip) {
if (count.keepErrorCount >= 5) {
this.changeNext(count)
}
if (count.successRate < 0.51) {
this.changeNext(count)
}
}
}
} }
module.exports = class BaseDNS { module.exports = class BaseDNS {
@ -13,30 +74,39 @@ module.exports = class BaseDNS {
this.cache = new LRU(cacheSize) this.cache = new LRU(cacheSize)
} }
count (hostname, ip, isError = true) {
const ipCache = this.cache.get(hostname)
if (ipCache) {
ipCache.doCount(ip, isError)
}
}
async lookup (hostname) { async lookup (hostname) {
try { try {
let ip = this.cache.get(hostname) let ipCache = this.cache.get(hostname)
if (ip) { if (ipCache) {
return ip if (ipCache.ip != null) {
ipCache.doCount(ipCache.ip, false)
return ipCache.ip
}
} else {
ipCache = new IpCache(hostname)
this.cache.set(hostname, ipCache)
} }
const t = new Date() const t = new Date()
let ipList = await this._lookup(hostname)
ip = hostname if (ipList == null) {
for (let depth = 0; !_isIP(ip) && depth < 5; depth++) { // 没有获取到ipv4地址
ip = await this._lookup(ip).catch(error => { ipList = []
logger.debug(ip, error)
return ip
})
} }
ipList.push(hostname) // 把原域名加入到统计里去
if (!_isIP(ip)) { ipCache.setIpList(ipList)
throw new Error(`BAD IP FORMAT (${ip})`)
}
logger.debug(`[DNS] ${hostname} -> ${ip} (${new Date() - t} ms)`) logger.debug(`[DNS] ${hostname} -> ${ipCache.ip} (${new Date() - t} ms)`)
this.cache.set(hostname, ip)
return ip return ipCache.ip
} catch (error) { } catch (error) {
logger.debug(`[DNS] cannot resolve hostname ${hostname} (${error})`) logger.debug(`[DNS] cannot resolve hostname ${hostname} (${error})`)
return hostname return hostname

View File

@ -12,6 +12,17 @@ module.exports = class DNSOverHTTPS extends BaseDNS {
async _lookup (hostname) { async _lookup (hostname) {
const result = await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }]) const result = await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }])
return result.answers[0].data if (result.answers.length === 0) {
// 说明没有获取到ip
console.log('该域名没有ip地址解析', hostname)
return []
}
const ret = result.answers.filter(item => { return item.type === 'A' }).map(item => { return item.data })
if (ret.length === 0) {
console.log('该域名没有ipv4地址解析', hostname)
} else {
console.log('获取到域名地址:', hostname, JSON.stringify(ret))
}
return ret
} }
} }

View File

@ -23,7 +23,6 @@ module.exports = {
} }
} }
if (providerName) { if (providerName) {
console.log('匹配到dns:', providerName, hostname)
return dnsConfig.providers[providerName] return dnsConfig.providers[providerName]
} }
} }

View File

@ -6,13 +6,13 @@ const tunnelAgent = require('tunnel-agent')
const util = exports const util = exports
const httpsAgent = new HttpsAgent({ const httpsAgent = new HttpsAgent({
keepAlive: true, keepAlive: true,
timeout: 5000, timeout: 15000,
keepAliveTimeout: 60000, // free socket keepalive for 30 seconds keepAliveTimeout: 60000, // free socket keepalive for 30 seconds
rejectUnauthorized: false rejectUnauthorized: false
}) })
const httpAgent = new Agent({ const httpAgent = new Agent({
keepAlive: true, keepAlive: true,
timeout: 5000, timeout: 15000,
keepAliveTimeout: 60000 // free socket keepalive for 30 seconds keepAliveTimeout: 60000 // free socket keepalive for 30 seconds
}) })
let socketId = 0 let socketId = 0
@ -34,7 +34,7 @@ util.getOptionsFormRequest = (req, ssl, externalProxy = null) => {
try { try {
externalProxyUrl = externalProxy(req, ssl) externalProxyUrl = externalProxy(req, ssl)
} catch (e) { } catch (e) {
console.error('externalProxy',e) console.error('externalProxy', e)
} }
} }
} }

View File

@ -21,7 +21,7 @@ module.exports = function createConnectHandler (sslConnectInterceptor, fakeServe
const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname) const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname)
if (dns) { if (dns) {
dns.lookup(hostname).then(ip => { dns.lookup(hostname).then(ip => {
connect(req, cltSocket, head, ip, srvUrl.port) connect(req, cltSocket, head, ip, srvUrl.port, { dns, hostname, ip })
}) })
} }
} }
@ -30,7 +30,7 @@ module.exports = function createConnectHandler (sslConnectInterceptor, fakeServe
} }
} }
function connect (req, cltSocket, head, hostname, port) { function connect (req, cltSocket, head, hostname, port, isDnsIntercept) {
// tunneling https // tunneling https
// console.log('connect:', hostname, port) // console.log('connect:', hostname, port)
const start = new Date().getTime() const start = new Date().getTime()
@ -57,6 +57,12 @@ function connect (req, cltSocket, head, hostname, port) {
const end = new Date().getTime() const end = new Date().getTime()
console.error('代理连接失败:', e.message, hostname, port, (end - start) + 'ms') console.error('代理连接失败:', e.message, hostname, port, (end - start) + 'ms')
cltSocket.destroy() cltSocket.destroy()
if (isDnsIntercept) {
const { dns, ip, hostname } = isDnsIntercept
dns.count(hostname, ip, true)
console.error('记录ip失败次数,用于优选ip', hostname, ip)
}
}) })
return proxySocket return proxySocket
} catch (error) { } catch (error) {

View File

@ -0,0 +1,23 @@
import dns from '../src/lib/dns/index.js'
const dnsProviders = dns.initDNS({
aliyun: {
type: 'https',
server: 'https://dns.alidns.com/dns-query',
cacheSize: 1000
},
usa: {
type: 'https',
server: 'https://cloudflare-dns.com/dns-query',
cacheSize: 1000
}
})
// let hostname = 'www.yonsz.com'
// dnsProviders.usa.lookup(hostname)
// const hostname = 'api.github.com'
// dnsProviders.usa.lookup(hostname)
const hostname1 = 'api.github.com'
dnsProviders.aliyun.lookup(hostname1)