Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: 新增兼容程序,在遇到部分特定异常时,通过自动调整参数达到规避异常的目的 #375

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/core/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ module.exports = {
}
}
},
compatible: {
// **** 自定义兼容配置 **** //
// connect阶段所需的兼容性配置
connect: {
// 参考配置(无path)
// 'xxx.xxx.xxx.xxx:443': {
// ssl: false
// }
},
// request阶段所需的兼容性配置
request: {
// 参考配置(配置方式同 `拦截配置`)
// 'xxx.xxx.xxx.xxx:443': {
// '.*': {
// rejectUnauthorized: false
// }
// }
}
},
intercept: {
enabled: true
},
Expand Down
19 changes: 14 additions & 5 deletions packages/gui/src/view/pages/server.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,16 @@
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane tab="IP预设置" key="5">
<a-tab-pane tab="兼容程序" key="5">
<div style="height:100%;display:flex;flex-direction:column">
<div>
说明:<code>兼容程序</code>会自动根据错误信息进行兼容性调整,并将兼容设置保存在 <code>~/.dev-sidecar/automaticCompatibleConfig.json</code> 文件中。但并不是所有的兼容设置都是正确的,所以需要通过以下配置来覆盖错误的兼容设置。
</div>
<vue-json-editor style="flex-grow:1;min-height:300px;margin-top:10px;" ref="editor" v-model="config.server.compatible" mode="code"
:show-btns="false" :expandedOnStart="true"></vue-json-editor>
</div>
</a-tab-pane>
<a-tab-pane tab="IP预设置" key="6">
<div style="height:100%;display:flex;flex-direction:column">
<div>
提示:<code>IP预设置</code>功能,优先级高于 <code>DNS设置</code>
Expand All @@ -116,11 +125,11 @@
:show-btns="false" :expandedOnStart="true"></vue-json-editor>
</div>
</a-tab-pane>
<a-tab-pane tab="DNS服务管理" key="6">
<a-tab-pane tab="DNS服务管理" key="7">
<vue-json-editor style="height:100%" ref="editor" v-model="config.server.dns.providers" mode="code"
:show-btns="false" :expandedOnStart="true"></vue-json-editor>
</a-tab-pane>
<a-tab-pane tab="DNS设置" key="7">
<a-tab-pane tab="DNS设置" key="8">
<div>
<a-row style="margin-top:10px">
<a-col span="19">
Expand Down Expand Up @@ -148,7 +157,7 @@
</a-row>
</div>
</a-tab-pane>
<a-tab-pane tab="IP测速" key="8">
<a-tab-pane tab="IP测速" key="9">
<div class="ip-tester" style="padding-right: 10px">
<a-alert type="info" message="对从DNS获取到的IP进行测速,使用速度最快的IP进行访问(注意:对使用了增强功能的域名没啥用)"></a-alert>
<a-form-item label="开启DNS测速" :label-col="labelCol" :wrapper-col="wrapperCol">
Expand Down Expand Up @@ -384,7 +393,7 @@ export default {
}, 5000)
},
async handleTabChange (key) {
if (key !== '2' && key !== '3' && key !== '5' && key !== '6') {
if (key !== '2' && key !== '3' && key !== '5' && key !== '6' && key !== '7') {
return
}

Expand Down
5 changes: 3 additions & 2 deletions packages/mitmproxy/src/lib/proxy/common/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ util.parseHostnameAndPort = (host, defaultPort) => {
return arr
}

util.getOptionsFromRequest = (req, ssl, externalProxy = null, serverSetting) => {
util.getOptionsFromRequest = (req, ssl, externalProxy = null, serverSetting, compatibleConfig = null) => {
// eslint-disable-next-line node/no-deprecated-api
const urlObject = url.parse(req.url)
const defaultPort = ssl ? 443 : 80
Expand Down Expand Up @@ -148,7 +148,8 @@ util.getOptionsFromRequest = (req, ssl, externalProxy = null, serverSetting) =>
port,
path: urlObject.path,
headers: req.headers,
agent
agent,
compatibleConfig
}

// eslint-disable-next-line node/no-deprecated-api
Expand Down
143 changes: 143 additions & 0 deletions packages/mitmproxy/src/lib/proxy/compatible/compatible.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* 兼容程序自适应生成配置
* 此脚本会针对各种兼容性问题,为对应域名生成相应的兼容性配置,并将自适应配置写入到 `~/.dev-sidecar/automaticCompatibleConfig.json` 文件中。
* 当然,也有可能会生成错误的配置,导致无法兼容,这时候可以通过 `config.server.compatible` 配置项,来覆盖这里生成的配置,达到主动适配的效果。
*
* @author WangLiang
*/
const fs = require('fs')
const path = require('path')
const jsonApi = require('../../../json')
const log = require('../../../utils/util.log')
const matchUtil = require('../../../utils/util.match')

const defaultConfig = {
// connect阶段所需的兼容性配置
connect: {
// 参考配置
// 'xxx.xxx.xxx.xxx:443': {
// ssl: false
// }
},
// request阶段所需的兼容性配置
request: {
// 参考配置
// 'xxx.xxx.xxx.xxx:443': {
// rejectUnauthorized: false
// }
}
}

const config = _loadFromFile(defaultConfig)

function _getConnectConfig (hostname, port) {
const connectConfig = config.connect[`${hostname}:${port}`]
log.info(`getConnectConfig: ${hostname}:${port}, ${jsonApi.stringify2(connectConfig)}`)
return connectConfig
}
function _getRequestConfig (hostname, port) {
const requestConfig = config.request[`${hostname}:${port}`]
log.info(`getRequestConfig: ${hostname}:${port}, ${jsonApi.stringify2(requestConfig)}`)
return requestConfig
}

// region 本地配置文件所需函数

function _getConfigPath () {
const userHome = process.env.USERPROFILE || process.env.HOME || '/'
const dir = path.resolve(userHome, './.dev-sidecar')
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
return path.join(dir, '/automaticCompatibleConfig.json')
}

function _loadFromFile (defaultConfig) {
const configPath = _getConfigPath()
let config
if (!fs.existsSync(configPath)) {
config = defaultConfig
log.info('automaticCompatibleConfig.json 文件不存在,使用默认配置:', configPath)
} else {
const file = fs.readFileSync(configPath)
log.info('读取 automaticCompatibleConfig.json 成功:', configPath)
const fileStr = file.toString()
config = fileStr && fileStr.length > 2 ? jsonApi.parse(fileStr) : {}
}

return config
}

function _saveConfigToFile () {
const filePath = _getConfigPath()
try {
fs.writeFileSync(filePath, jsonApi.stringify(config))
log.info('保存 automaticCompatibleConfig.json 成功:', filePath)
} catch (e) {
log.error('保存 automaticCompatibleConfig.json 失败:', filePath, e)
}
}

// endregion

module.exports = {
/**
* 获取 connect 阶段所需的兼容性配置
*
* @param hostname 域名
* @param port 端口
* @param manualCompatibleConfig 手动兼容性配置
* @returns connect阶段所需的兼容性配置
*/
getConnectCompatibleConfig (hostname, port, manualCompatibleConfig = null) {
let connectCompatibleConfig = manualCompatibleConfig == null ? null : matchUtil.matchHostname(manualCompatibleConfig.connect, `${hostname}:${port}`, 'getConnectCompatibleConfig')
if (connectCompatibleConfig == null) {
connectCompatibleConfig = _getConnectConfig(hostname, port)
}
return connectCompatibleConfig
},

setConnectSsl (hostname, port, ssl, autoSave = true) {
const connectCompatibleConfig = this.getConnectCompatibleConfig(hostname, port)
if (connectCompatibleConfig) {
connectCompatibleConfig.ssl = ssl
} else {
config.connect[`${hostname}:${port}`] = { ssl }
}

// 配置保存到文件
if (autoSave) _saveConfigToFile()

log.info(`【兼容程序】${hostname}:${port}: 设置 connect.ssl = ${ssl}`)
},

// --------------------------------------------------------------------------------------------------------------------------

/**
* 获取 request 阶段所需的兼容性配置
*
* @param rOptions
* @param manualCompatibleConfig
*/
getRequestCompatibleConfig (rOptions, manualCompatibleConfig = null) {
let requestCompatibleConfig = manualCompatibleConfig == null ? null : matchUtil.matchHostname(manualCompatibleConfig.request, `${rOptions.hostname}:${rOptions.port}`, 'getRequestCompatibleConfig')
if (requestCompatibleConfig == null) {
requestCompatibleConfig = _getRequestConfig(rOptions.hostname, rOptions.port)
}
return requestCompatibleConfig
},

setRequestRejectUnauthorized (rOptions, rejectUnauthorized, autoSave = true) {
const requestCompatibleConfig = this.getRequestCompatibleConfig(rOptions.hostname, rOptions.port)
if (requestCompatibleConfig) {
requestCompatibleConfig.rejectUnauthorized = rejectUnauthorized
} else {
config.request[`${rOptions.hostname}:${rOptions.port}`] = { rejectUnauthorized }
}

// 配置保存到文件
if (autoSave) _saveConfigToFile()

log.info(`【兼容程序】${rOptions.hostname}:${rOptions.port}: 设置 request.rejectUnauthorized = ${rejectUnauthorized}`)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function isSslConnect (sslConnectInterceptors, req, cltSocket, head) {
}

// create connectHandler function
module.exports = function createConnectHandler (sslConnectInterceptor, middlewares, fakeServerCenter, dnsConfig) {
module.exports = function createConnectHandler (sslConnectInterceptor, middlewares, fakeServerCenter, dnsConfig, compatibleConfig) {
// return
const sslConnectInterceptors = []
sslConnectInterceptors.push(sslConnectInterceptor)
Expand All @@ -27,14 +27,14 @@ module.exports = function createConnectHandler (sslConnectInterceptor, middlewar
}
}

return function connectHandler (req, cltSocket, head) {
return function connectHandler (req, cltSocket, head, ssl) {
// eslint-disable-next-line node/no-deprecated-api
let { hostname, port } = url.parse(`https://${req.url}`)
let { hostname, port } = url.parse(`${ssl ? 'https' : 'http'}://${req.url}`)
port = parseInt(port)

if (isSslConnect(sslConnectInterceptors, req, cltSocket, head)) {
// 需要拦截,代替目标服务器,让客户端连接DS在本地启动的代理服务
fakeServerCenter.getServerPromise(hostname, port).then((serverObj) => {
fakeServerCenter.getServerPromise(hostname, port, ssl, compatibleConfig).then((serverObj) => {
log.info(`----- fakeServer connect: ${localIP}:${serverObj.port} ➜ ${req.url} -----`)
connect(req, cltSocket, head, localIP, serverObj.port)
}, (e) => {
Expand Down
23 changes: 21 additions & 2 deletions packages/mitmproxy/src/lib/proxy/mitmproxy/createRequestHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ const log = require('../../../utils/util.log')
const RequestCounter = require('../../choice/RequestCounter')
const InsertScriptMiddleware = require('../middleware/InsertScriptMiddleware')
const dnsLookup = require('./dnsLookup')
const compatible = require('../compatible/compatible')
const MAX_SLOW_TIME = 8000 // 超过此时间 则认为太慢了

// create requestHandler function
module.exports = function createRequestHandler (createIntercepts, middlewares, externalProxy, dnsConfig, setting) {
module.exports = function createRequestHandler (createIntercepts, middlewares, externalProxy, dnsConfig, setting, compatibleConfig) {
// return
return function requestHandler (req, res, ssl) {
let proxyReq

const rOptions = commonUtil.getOptionsFromRequest(req, ssl, externalProxy, setting)
const rOptions = commonUtil.getOptionsFromRequest(req, ssl, externalProxy, setting, compatibleConfig)
let url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}`

if (rOptions.headers.connection === 'close') {
Expand Down Expand Up @@ -130,6 +131,19 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e
// log.debug('agent:', rOptions.agent)
// log.debug('agent.options:', rOptions.agent.options)
res.setHeader('DS-Proxy-Request', rOptions.hostname)

// 兼容程序:2
if (rOptions.agent) {
const compatibleConfig = compatible.getRequestCompatibleConfig(rOptions, rOptions.compatibleConfig)
if (compatibleConfig && compatibleConfig.rejectUnauthorized != null && rOptions.agent.options.rejectUnauthorized !== compatibleConfig.rejectUnauthorized) {
if (compatibleConfig.rejectUnauthorized === false && rOptions.agent.unVerifySslAgent) {
log.info(`【兼容程序】${rOptions.hostname}:${rOptions.port}: 设置 'rOptions.agent.options.rejectUnauthorized = ${compatibleConfig.rejectUnauthorized}'`)
rOptions.agent = rOptions.agent.unVerifySslAgent
res.setHeader('DS-Compatible', 'unVerifySsl')
}
}
}

proxyReq = (rOptions.protocol === 'https:' ? https : http).request(rOptions, (proxyRes) => {
const cost = new Date() - start
if (rOptions.protocol === 'https:') {
Expand Down Expand Up @@ -163,6 +177,11 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e
log.error(`代理请求错误: ${url}, cost: ${cost} ms, error:`, e, ', rOptions:', jsonApi.stringify2(rOptions))
countSlow(isDnsIntercept, '代理请求错误: ' + e.message)
reject(e)

// 兼容程序:2
if (e.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
compatible.setRequestRejectUnauthorized(rOptions, false)
}
})
proxyReq.on('aborted', () => {
const cost = new Date() - start
Expand Down
Loading