From 04decbed48817fb5a50a852b9c1456ce7c89e97a Mon Sep 17 00:00:00 2001 From: 8odream Date: Fri, 16 May 2025 12:21:36 +0800 Subject: [PATCH 01/23] =?UTF-8?q?doc:=20=E6=B7=BB=E5=8A=A0=E6=8B=A6?= =?UTF-8?q?=E6=88=AA=E5=99=A8=E9=85=8D=E7=BD=AE=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...77\347\224\250\350\257\264\346\230\216.md" | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git "a/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" "b/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" index c6cf4ba127..2cd1d1e1d7 100644 --- "a/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" +++ "b/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" @@ -35,8 +35,71 @@ | 响应篡改拦截器 | responseReplace | 203 | | 脚本拦截器 | script | 211 | + 3. 配置示例: + ```json + "*.example.com": { + ".*":{ //后续url匹配规则 + // 请求篡改配置 + "requestReplace": { + "headers": { + "User-Agent": "Mozilla/5.0", // 替换User-Agent + "Referer": "[remove]" // 删除Referer头 + }, + "doDownload": true // 启用下载请求处理.要转换为下载请求,需要 responseReplace 拦截器的配合使用 + }, + + // 代理配置 + "proxy": "proxy.example.com", // 代理目标地址 + "backup": [ // 备用代理服务器列表 + "backup1.example.com", + "backup2.example.com" + ], + + //"proxy": "https://$1.proxy.com", + //"replace": "https://(.*?)\\.example\\.com" + //使用${path}数组捕获和替换 + + "sni": "example.com", // SNI服务器名称指示 + "unVerifySsl": true, // 跳过SSL证书验证 + + // 基本拦截配置 + "abort": false, // 是否拦截请求(返回403) + "success": true, // 是否快速返回成功(返回200) + + "cacheDays": 365, // 缓存天数 + //cache系列包含如下关键词:cacheSecods,cacheMinutes,cacheHours,cacheDays,cacheWeeks,cacheMonths,cacheYears + + // OPTIONS请求配置 + "options": { + "headers": { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET", //不设置默认返回GET,POST,PUT,DELETE,HEAD,OPTIONS,PATCH + "Access-Control-Allow-Headers": "Content-Type, X-Requested-With, X-Custom-Header", //不设置默认返回* + "Access-Control-Max-Age": "86400" //不设置默认一个月 + } + } + + // 响应篡改配置 + "responseReplace": { + "headers": { + "Cache-Control": "no-cache", // 禁用缓存 + "X-Powered-By": "[remove]", // 删除服务器信息头 + "Access-Control-Allow-Origin": "*" // 添加CORS头 + }, + "doDownload": true // 强制响应为文件下载 + }, + "cacheExcludeStatusCodeList":[200], // 缓存排除状态码列表 + "cacheMinStatusCode": 200, // 缓存最小状态码,默认200 + "cacheMaxStatusCode": 303, // 缓存最大状态码,默认303 + "cacheControlType": "public", // 缓存控制类型,默认public,可选private + "cacheImmutable": true, + + "tampermonkeyScript": "UrlOrPathToTampermonkeyScript.js", // 注入Tampermonkey + "script" : "UrlOrPathToYourScript.js" //支持数组,填入多个地址 + } + } + ``` # 5. 域名白名单: - # 6. DNS服务管理: ## 6.1. 配置 `DNS-over-HTTPS` 的DNS服务: From 8a722282fb8d01b123804371fdc3f6b66e5f4d2c Mon Sep 17 00:00:00 2001 From: 8odream Date: Thu, 22 May 2025 16:10:36 +0800 Subject: [PATCH 02/23] =?UTF-8?q?=E5=B0=9D=E8=AF=95ipv6=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...77\347\224\250\350\257\264\346\230\216.md" | 5 +- packages/gui/src/view/pages/server.vue | 54 +++++++++++++- .../src/lib/proxy/mitmproxy/dnsLookup.js | 73 +++++++++++++------ .../mitmproxy/src/lib/speed/SpeedTester.js | 31 +++++++- packages/mitmproxy/test/ipv6nettest.js | 55 ++++++++++++++ 5 files changed, 188 insertions(+), 30 deletions(-) create mode 100644 packages/mitmproxy/test/ipv6nettest.js diff --git "a/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" "b/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" index 2cd1d1e1d7..4cbfb5c245 100644 --- "a/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" +++ "b/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" @@ -43,7 +43,7 @@ "requestReplace": { "headers": { "User-Agent": "Mozilla/5.0", // 替换User-Agent - "Referer": "[remove]" // 删除Referer头 + "Referer": "[remove]" // 删除Refzerer头 }, "doDownload": true // 启用下载请求处理.要转换为下载请求,需要 responseReplace 拦截器的配合使用 }, @@ -55,9 +55,6 @@ "backup2.example.com" ], - //"proxy": "https://$1.proxy.com", - //"replace": "https://(.*?)\\.example\\.com" - //使用${path}数组捕获和替换 "sni": "example.com", // SNI服务器名称指示 "unVerifySsl": true, // 跳过SSL证书验证 diff --git a/packages/gui/src/view/pages/server.vue b/packages/gui/src/view/pages/server.vue index 7ceb64fc04..a3b02a9066 100644 --- a/packages/gui/src/view/pages/server.vue +++ b/packages/gui/src/view/pages/server.vue @@ -163,7 +163,43 @@ export default { const listener = async (event, message) => { console.log('get speed event', event, message) if (message.key === 'getList') { - this.speedTestList = message.value + // 详细记录接收到的原始数据 + console.log('speedTestList raw data:', JSON.stringify(message.value, null, 2)) + + // 数据验证和标准化 + const validatedData = {} + for (const hostname in message.value) { + const item = message.value[hostname] + if (!item.backupList) { + console.warn(`Missing backupList for ${hostname}`) + continue + } + + validatedData[hostname] = { + alive: item.alive || [], + backupList: item.backupList.map(ipObj => { + // 标准化IP地址格式 + const standardized = { + host: ipObj.host, + port: ipObj.port || 443, + dns: ipObj.dns || 'unknown', + time: ipObj.time || null + } + + // 特殊处理IPv6地址 + if (ipObj.host.includes(':')) { + console.log('Found IPv6 address:', { + original: ipObj.host, + standardized: standardized.host + }) + } + return standardized + }) + } + } + + this.speedTestList = validatedData + console.log('Validated speed test data:', JSON.stringify(validatedData, null, 2)) } } this.$api.ipc.on('speed', listener) @@ -453,8 +489,10 @@ export default { {{ element.host }} {{ element.time }}{{ element.time ? 'ms' : '' }} {{ element.dns }} + IPv6 @@ -509,4 +547,18 @@ export default { width: 45px; } } +.ipv6-tag { + position: relative; + padding-right: 40px; +} +.ipv6-badge { + position: absolute; + right: 5px; + top: 2px; + font-size: 10px; + background: #1890ff; + color: white; + padding: 0 4px; + border-radius: 3px; +} diff --git a/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js b/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js index 6668ac0a28..adc2dcdaf4 100644 --- a/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js +++ b/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js @@ -11,52 +11,77 @@ module.exports = { if (tester) { const aliveIpObj = tester.pickFastAliveIpObj() if (aliveIpObj) { + const family = aliveIpObj.host.includes(':') ? 6 : 4 log.info(`----- ${action}: ${hostname}, use alive ip from dns '${aliveIpObj.dns}': ${aliveIpObj.host}${target} -----`) if (res) { res.setHeader('DS-DNS-Lookup', `IpTester: ${aliveIpObj.host} ${aliveIpObj.dns === '预设IP' ? 'PreSet' : aliveIpObj.dns}`) } - callback(null, aliveIpObj.host, 4) + callback(null, aliveIpObj.host, family) return } else { log.info(`----- ${action}: ${hostname}, no alive ip${target}, tester: { "ready": ${tester.ready}, "backupList": ${JSON.stringify(tester.backupList)} }`) } } - dns.lookup(hostname).then((ip) => { + + // 优先尝试IPv6查询 + dns.lookup(hostname, { family: 6 }).then((ip) => { + if (ip && ip !== hostname) { + if (isDnsIntercept) { + isDnsIntercept.dns = dns + isDnsIntercept.hostname = hostname + isDnsIntercept.ip = ip + } + log.info(`----- ${action}: ${hostname}, use ipv6 from dns '${dns.dnsName}': ${ip}${target} -----`) + if (res) { + res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`) + } + callback(null, ip, 6) + return + } + + // 回退到IPv4查询 + return dns.lookup(hostname) + }).then((ip) => { + if (!ip || ip === hostname) { + // 使用默认dns + log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}${target}, options:`, options, ', dns:', dns) + return defaultDns.lookup(hostname, options, callback) + } + if (isDnsIntercept) { isDnsIntercept.dns = dns isDnsIntercept.hostname = hostname isDnsIntercept.ip = ip } - if (ip !== hostname) { - // 判断是否为测速失败的IP,如果是,则不使用当前IP - let isTestFailedIp = false - if (tester && tester.ready && tester.backupList && tester.backupList.length > 0) { - for (let i = 0; i < tester.backupList.length; i++) { - const item = tester.backupList[i] - if (item.host === ip) { - if (item.time == null) { - isTestFailedIp = true - } - break + // 判断是否为测速失败的IP + let isTestFailedIp = false + if (tester && tester.ready && tester.backupList && tester.backupList.length > 0) { + for (let i = 0; i < tester.backupList.length; i++) { + const item = tester.backupList[i] + if (item.host === ip) { + if (item.time == null) { + isTestFailedIp = true } + break } } - if (isTestFailedIp === false) { - log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`) - if (res) { - res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`) - } - callback(null, ip, 4) - return - } else { - // 使用默认dns - log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}, skip test failed ip from dns '${dns.dnsName}: ${ip}'${target}, options:`, options) + } + + if (!isTestFailedIp) { + const family = ip.includes(':') ? 6 : 4 + log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`) + if (res) { + res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`) } + callback(null, ip, family) } else { // 使用默认dns - log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}${target}, options:`, options, ', dns:', dns) + log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}, skip test failed ip from dns '${dns.dnsName}: ${ip}'${target}, options:`, options) + defaultDns.lookup(hostname, options, callback) } + }).catch((e) => { + log.error(`DNS lookup error for ${hostname}:`, e) defaultDns.lookup(hostname, options, callback) }) } diff --git a/packages/mitmproxy/src/lib/speed/SpeedTester.js b/packages/mitmproxy/src/lib/speed/SpeedTester.js index 59f4995ced..26858e88e7 100644 --- a/packages/mitmproxy/src/lib/speed/SpeedTester.js +++ b/packages/mitmproxy/src/lib/speed/SpeedTester.js @@ -79,7 +79,31 @@ class SpeedTester { } async getFromOneDns (dns) { - return await dns._lookupInternal(this.hostname) + // 优先尝试IPv6查询 + try { + const ipv6Result = await dns._lookupInternal(this.hostname, { family: 6 }) + if (ipv6Result && ipv6Result.length > 0) { + // 标准化IPv6地址格式 + const standardized = ipv6Result.map(ip => { + // 确保IPv6地址格式统一 + if (ip.includes(':')) { + return ip.toLowerCase().replace(/\[|\]/g, '') + } + return ip + }) + log.debug(`[dns] Got IPv6 addresses for ${this.hostname}:`, standardized) + return standardized + } + } catch (e) { + log.debug(`[dns] IPv6 lookup failed for ${this.hostname}: ${e.message}`) + } + + // 回退到IPv4查询 + const ipv4Result = await dns._lookupInternal(this.hostname) + if (ipv4Result) { + log.debug(`[dns] Got IPv4 addresses for ${this.hostname}:`, ipv4Result) + } + return ipv4Result } async test () { @@ -88,6 +112,11 @@ class SpeedTester { this.backupList = _.unionBy(newBackupList, 'host') this.testCount++ + // 详细记录IPv6地址 + const ipv6List = this.backupList.filter(item => item.host.includes(':')) + if (ipv6List.length > 0) { + log.info('[speed] IPv6 addresses found for', this.hostname, ':', ipv6List) + } log.info('[speed]', this.hostname, '➜ ip-list:', this.backupList) await this.testBackups() if (config.notify) { diff --git a/packages/mitmproxy/test/ipv6nettest.js b/packages/mitmproxy/test/ipv6nettest.js new file mode 100644 index 0000000000..b31eb109d6 --- /dev/null +++ b/packages/mitmproxy/test/ipv6nettest.js @@ -0,0 +1,55 @@ +const net = require('net'); +const { setTimeout } = require('timers/promises'); + +// 测试的IPv6地址和端口 +const TEST_HOST = '6.ipw.cn'; +const TEST_PORT = 80; +const TIMEOUT = 5000; // 5秒超时 + +async function testIPv6Connection() { + const socket = new net.Socket(); + + // 设置超时 + socket.setTimeout(TIMEOUT); + + try { + // 尝试连接 + await new Promise((resolve, reject) => { + socket.on('connect', () => { + const { address, port } = socket.address(); + console.log(`成功连接到 ${TEST_HOST} 的IPv6地址 [${address}]:${port}`); + socket.end(); + resolve(); + }); + + socket.on('timeout', () => { + socket.destroy(); + reject(new Error('连接超时')); + }); + + socket.on('error', (err) => { + reject(err); + }); + + socket.connect(TEST_PORT, TEST_HOST); + }); + + return true; + } catch (err) { + console.error('IPv6连接测试失败:', err.message); + return false; + } finally { + socket.destroy(); + } +} + +// 执行测试 +testIPv6Connection() + .then(success => { + console.log(`IPv6连接测试结果: ${success ? '成功' : '失败'}`); + process.exit(success ? 0 : 1); + }) + .catch(err => { + console.error('测试过程中发生错误:', err); + process.exit(1); + }); From a19a73813cbf86d4e3b28235aff8886986314df6 Mon Sep 17 00:00:00 2001 From: 8odream Date: Fri, 13 Jun 2025 17:48:49 +0800 Subject: [PATCH 03/23] =?UTF-8?q?=E6=94=AF=E6=8C=81ipv6=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/gui/src/view/pages/server.vue | 102 +++++++++++++++--- packages/mitmproxy/src/lib/dns/base.js | 24 +++-- packages/mitmproxy/src/lib/dns/https.js | 14 ++- packages/mitmproxy/src/lib/dns/tcp.js | 8 +- packages/mitmproxy/src/lib/dns/tls.js | 10 +- packages/mitmproxy/src/lib/dns/udp.js | 4 +- .../src/lib/proxy/mitmproxy/dnsLookup.js | 7 -- .../mitmproxy/src/lib/speed/SpeedTester.js | 29 +++-- 8 files changed, 140 insertions(+), 58 deletions(-) diff --git a/packages/gui/src/view/pages/server.vue b/packages/gui/src/view/pages/server.vue index a3b02a9066..d44855af75 100644 --- a/packages/gui/src/view/pages/server.vue +++ b/packages/gui/src/view/pages/server.vue @@ -161,11 +161,7 @@ export default { }, registerSpeedTestEvent () { const listener = async (event, message) => { - console.log('get speed event', event, message) if (message.key === 'getList') { - // 详细记录接收到的原始数据 - console.log('speedTestList raw data:', JSON.stringify(message.value, null, 2)) - // 数据验证和标准化 const validatedData = {} for (const hostname in message.value) { @@ -185,21 +181,12 @@ export default { dns: ipObj.dns || 'unknown', time: ipObj.time || null } - - // 特殊处理IPv6地址 - if (ipObj.host.includes(':')) { - console.log('Found IPv6 address:', { - original: ipObj.host, - standardized: standardized.host - }) - } return standardized }) } } this.speedTestList = validatedData - console.log('Validated speed test data:', JSON.stringify(validatedData, null, 2)) } } this.$api.ipc.on('speed', listener) @@ -549,16 +536,101 @@ export default { } .ipv6-tag { position: relative; - padding-right: 40px; + padding-right: 45px !important; + margin-right: 5px !important; + display: inline-flex !important; + align-items: center !important; + min-width: 200px !important; } .ipv6-badge { position: absolute; right: 5px; - top: 2px; + top: 50%; + transform: translateY(-50%); font-size: 10px; background: #1890ff; color: white; padding: 0 4px; border-radius: 3px; + line-height: 16px; + height: 16px; +} +.ip-box { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 8px; + background-color: #fafafa; + border-radius: 4px; + margin-top: 8px; + max-width: 100%; + overflow: hidden; +} +.ip-item { + display: flex; + align-items: center; + padding: 4px 8px; + background-color: #fff; + border: 1px solid #e8e8e8; + border-radius: 4px; + font-size: 12px; + color: #666; + word-break: break-all; + max-width: calc(100% - 16px); + flex: 1 1 auto; + min-width: 0; +} +.ip-item .ip-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ip-item .ip-speed { + margin-left: 8px; + white-space: nowrap; +} +.ip-item .ip-speed.success { + color: #52c41a; +} +.ip-item .ip-speed.warning { + color: #faad14; +} +.ip-item .ip-speed.error { + color: #ff4d4f; +} +.domain-box { + margin-bottom: 16px; + padding: 12px; + background-color: #fff; + border: 1px solid #e8e8e8; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + overflow: hidden; +} +.domain-box .domain-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} +.domain-box .domain-title { + font-size: 14px; + font-weight: 500; + color: #333; + margin: 0; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.domain-box .domain-actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: 8px; + flex-shrink: 0; } diff --git a/packages/mitmproxy/src/lib/dns/base.js b/packages/mitmproxy/src/lib/dns/base.js index fb448445ea..6a745c610e 100644 --- a/packages/mitmproxy/src/lib/dns/base.js +++ b/packages/mitmproxy/src/lib/dns/base.js @@ -52,7 +52,7 @@ module.exports = class BaseDNS { } } - async lookup (hostname) { + async lookup (hostname, options = {}) { try { let ipCache = this.cache.get(hostname) if (ipCache) { @@ -66,9 +66,9 @@ module.exports = class BaseDNS { } const t = new Date() - let ipList = await this._lookupInternal(hostname) + let ipList = await this._lookupInternal(hostname, options) if (ipList == null) { - // 没有获取到ipv4地址 + // 没有获取到ip ipList = [] } ipList.push(hostname) // 把原域名加入到统计里去 @@ -83,7 +83,7 @@ module.exports = class BaseDNS { } } - async _lookupInternal (hostname) { + async _lookupInternal (hostname, options = {}) { // 获取当前域名的预设IP列表 let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, `matched preSetIpList(${this.dnsName})`) if (hostnamePreSetIpList && (hostnamePreSetIpList.length > 0 || hostnamePreSetIpList.length === undefined)) { @@ -100,24 +100,28 @@ module.exports = class BaseDNS { } } - return await this._lookup(hostname) + return await this._lookup(hostname, options) } - async _lookup (hostname) { + async _lookup (hostname, options = {}) { const start = Date.now() try { - const response = await this._doDnsQuery(hostname) + const response = await this._doDnsQuery(hostname, options) const cost = Date.now() - start if (response == null || response.answers == null || response.answers.length == null || response.answers.length === 0) { // 说明没有获取到ip log.warn(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms, response:`, response) return [] } - const ret = response.answers.filter(item => item.type === 'A').map(item => item.data) + + // 根据查询类型过滤结果 + const type = options.family === 6 ? 'AAAA' : 'A' + const ret = response.answers.filter(item => item.type === type).map(item => item.data) + if (ret.length === 0) { - log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IPv4地址: ${hostname}, cost: ${cost} ms`) + log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IPv${options.family === 6 ? '6' : '4'}地址: ${hostname}, cost: ${cost} ms`) } else { - log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的IPv4地址: ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`) + log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的IPv${options.family === 6 ? '6' : '4'}地址: ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`) } return ret } catch (e) { diff --git a/packages/mitmproxy/src/lib/dns/https.js b/packages/mitmproxy/src/lib/dns/https.js index 3ab889c57e..42f773da7a 100644 --- a/packages/mitmproxy/src/lib/dns/https.js +++ b/packages/mitmproxy/src/lib/dns/https.js @@ -8,9 +8,19 @@ module.exports = class DNSOverHTTPS extends BaseDNS { constructor (dnsName, cacheSize, preSetIpList, dnsServer) { super(dnsName, 'HTTPS', cacheSize, preSetIpList) this.dnsServer = dnsServer + this.isIPv6 = dnsServer.includes(':') && dnsServer.includes('[') && dnsServer.includes(']') } - async _doDnsQuery (hostname) { - return await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }]) + async _doDnsQuery (hostname, options = {}) { + return await dohQueryAsync( + { + url: this.dnsServer, + family: this.isIPv6 ? 6 : 4 + }, + [{ + type: options.family === 6 ? 'AAAA' : 'A', + name: hostname + }] + ) } } diff --git a/packages/mitmproxy/src/lib/dns/tcp.js b/packages/mitmproxy/src/lib/dns/tcp.js index 6141609358..8ab8209a8c 100644 --- a/packages/mitmproxy/src/lib/dns/tcp.js +++ b/packages/mitmproxy/src/lib/dns/tcp.js @@ -4,16 +4,17 @@ const dnsPacket = require('dns-packet') const randi = require('random-int') const BaseDNS = require('./base') -const defaultPort = 53 // UDP类型的DNS服务默认端口号 +const defaultPort = 53 // TCP类型的DNS服务默认端口号 module.exports = class DNSOverTCP extends BaseDNS { constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) { super(dnsName, 'TCP', cacheSize, preSetIpList) this.dnsServer = dnsServer this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort + this.isIPv6 = dnsServer.includes(':') && dnsServer.includes('[') && dnsServer.includes(']') } - _doDnsQuery (hostname) { + _doDnsQuery (hostname, options = {}) { return new Promise((resolve, reject) => { // 构造 DNS 查询报文 const packet = dnsPacket.encode({ @@ -21,7 +22,7 @@ module.exports = class DNSOverTCP extends BaseDNS { type: 'query', id: randi(0x0, 0xFFFF), questions: [{ - type: 'A', + type: options.family === 6 ? 'AAAA' : 'A', name: hostname, }], }) @@ -30,6 +31,7 @@ module.exports = class DNSOverTCP extends BaseDNS { const tcpClient = net.createConnection({ host: this.dnsServer, port: this.dnsServerPort, + family: this.isIPv6 ? 6 : 4 }, () => { // TCP DNS 报文前需添加 2 字节长度头 const lengthBuffer = Buffer.alloc(2) diff --git a/packages/mitmproxy/src/lib/dns/tls.js b/packages/mitmproxy/src/lib/dns/tls.js index b4b5d99f6c..80b01eef5b 100644 --- a/packages/mitmproxy/src/lib/dns/tls.js +++ b/packages/mitmproxy/src/lib/dns/tls.js @@ -9,19 +9,21 @@ module.exports = class DNSOverTLS extends BaseDNS { this.dnsServer = dnsServer this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort this.dnsServerName = dnsServerName + this.isIPv6 = dnsServer.includes(':') && dnsServer.includes('[') && dnsServer.includes(']') } - async _doDnsQuery (hostname) { - const options = { + async _doDnsQuery (hostname, options = {}) { + const queryOptions = { host: this.dnsServer, port: this.dnsServerPort, servername: this.dnsServerName || this.dnsServer, + family: this.isIPv6 ? 6 : 4, name: hostname, klass: 'IN', - type: 'A', + type: options.family === 6 ? 'AAAA' : 'A', } - return await dnstls.query(options) + return await dnstls.query(queryOptions) } } diff --git a/packages/mitmproxy/src/lib/dns/udp.js b/packages/mitmproxy/src/lib/dns/udp.js index 86d9b30b48..57552bcf8b 100644 --- a/packages/mitmproxy/src/lib/dns/udp.js +++ b/packages/mitmproxy/src/lib/dns/udp.js @@ -15,7 +15,7 @@ module.exports = class DNSOverUDP extends BaseDNS { this.socketType = this.isIPv6 ? 'udp6' : 'udp4' } - _doDnsQuery (hostname) { + _doDnsQuery (hostname, options = {}) { return new Promise((resolve, reject) => { // 构造 DNS 查询报文 const packet = dnsPacket.encode({ @@ -23,7 +23,7 @@ module.exports = class DNSOverUDP extends BaseDNS { type: 'query', id: randi(0x0, 0xFFFF), questions: [{ - type: 'A', + type: options.family === 6 ? 'AAAA' : 'A', name: hostname, }], }) diff --git a/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js b/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js index adc2dcdaf4..0dea8c4b46 100644 --- a/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js +++ b/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js @@ -12,14 +12,11 @@ module.exports = { const aliveIpObj = tester.pickFastAliveIpObj() if (aliveIpObj) { const family = aliveIpObj.host.includes(':') ? 6 : 4 - log.info(`----- ${action}: ${hostname}, use alive ip from dns '${aliveIpObj.dns}': ${aliveIpObj.host}${target} -----`) if (res) { res.setHeader('DS-DNS-Lookup', `IpTester: ${aliveIpObj.host} ${aliveIpObj.dns === '预设IP' ? 'PreSet' : aliveIpObj.dns}`) } callback(null, aliveIpObj.host, family) return - } else { - log.info(`----- ${action}: ${hostname}, no alive ip${target}, tester: { "ready": ${tester.ready}, "backupList": ${JSON.stringify(tester.backupList)} }`) } } @@ -31,7 +28,6 @@ module.exports = { isDnsIntercept.hostname = hostname isDnsIntercept.ip = ip } - log.info(`----- ${action}: ${hostname}, use ipv6 from dns '${dns.dnsName}': ${ip}${target} -----`) if (res) { res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`) } @@ -44,7 +40,6 @@ module.exports = { }).then((ip) => { if (!ip || ip === hostname) { // 使用默认dns - log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}${target}, options:`, options, ', dns:', dns) return defaultDns.lookup(hostname, options, callback) } @@ -70,14 +65,12 @@ module.exports = { if (!isTestFailedIp) { const family = ip.includes(':') ? 6 : 4 - log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`) if (res) { res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`) } callback(null, ip, family) } else { // 使用默认dns - log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}, skip test failed ip from dns '${dns.dnsName}: ${ip}'${target}, options:`, options) defaultDns.lookup(hostname, options, callback) } }).catch((e) => { diff --git a/packages/mitmproxy/src/lib/speed/SpeedTester.js b/packages/mitmproxy/src/lib/speed/SpeedTester.js index 26858e88e7..c781e99264 100644 --- a/packages/mitmproxy/src/lib/speed/SpeedTester.js +++ b/packages/mitmproxy/src/lib/speed/SpeedTester.js @@ -79,6 +79,8 @@ class SpeedTester { } async getFromOneDns (dns) { + const results = [] + // 优先尝试IPv6查询 try { const ipv6Result = await dns._lookupInternal(this.hostname, { family: 6 }) @@ -91,19 +93,23 @@ class SpeedTester { } return ip }) - log.debug(`[dns] Got IPv6 addresses for ${this.hostname}:`, standardized) - return standardized + results.push(...standardized) } } catch (e) { - log.debug(`[dns] IPv6 lookup failed for ${this.hostname}: ${e.message}`) + // IPv6查询失败,继续尝试IPv4 } - // 回退到IPv4查询 - const ipv4Result = await dns._lookupInternal(this.hostname) - if (ipv4Result) { - log.debug(`[dns] Got IPv4 addresses for ${this.hostname}:`, ipv4Result) + // 尝试IPv4查询 + try { + const ipv4Result = await dns._lookupInternal(this.hostname) + if (ipv4Result) { + results.push(...ipv4Result) + } + } catch (e) { + // IPv4查询失败 } - return ipv4Result + + return results } async test () { @@ -111,13 +117,6 @@ class SpeedTester { const newBackupList = [...newList, ...this.backupList] this.backupList = _.unionBy(newBackupList, 'host') this.testCount++ - - // 详细记录IPv6地址 - const ipv6List = this.backupList.filter(item => item.host.includes(':')) - if (ipv6List.length > 0) { - log.info('[speed] IPv6 addresses found for', this.hostname, ':', ipv6List) - } - log.info('[speed]', this.hostname, '➜ ip-list:', this.backupList) await this.testBackups() if (config.notify) { config.notify({ key: 'test' }) From 6d18a3ba09a6c3b9f20dde72876b4ffe61c06d52 Mon Sep 17 00:00:00 2001 From: 8odream Date: Sat, 14 Jun 2025 01:08:01 +0800 Subject: [PATCH 04/23] =?UTF-8?q?=E6=94=AF=E6=8C=81IP=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E4=BD=BF=E7=94=A8CNAME=EF=BC=8C=E5=90=8C=E6=97=B6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E4=BA=86=E6=9D=A5=E6=BA=90=E6=98=BE=E7=A4=BA=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/mitmproxy/src/lib/dns/base.js | 27 ++++++++++++++++--- .../mitmproxy/src/lib/speed/SpeedTester.js | 6 +++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/mitmproxy/src/lib/dns/base.js b/packages/mitmproxy/src/lib/dns/base.js index 6a745c610e..3ee308c046 100644 --- a/packages/mitmproxy/src/lib/dns/base.js +++ b/packages/mitmproxy/src/lib/dns/base.js @@ -1,4 +1,5 @@ const LRUCache = require('lru-cache') +const net = require('node:net') const log = require('../../utils/util.log.server') const matchUtil = require('../../utils/util.match') const { DynamicChoice } = require('../choice/index') @@ -94,9 +95,29 @@ module.exports = class BaseDNS { } if (hostnamePreSetIpList.length > 0) { - hostnamePreSetIpList.isPreSet = true - log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的预设IP列表: ${hostname} - ${JSON.stringify(hostnamePreSetIpList)}`) - return hostnamePreSetIpList + const result = [] + for (const item of hostnamePreSetIpList) { + if (net.isIP(item)) { + // 如果是IP地址,直接使用 + result.push(item) + } else { + // 如果是域名,进行DNS解析 + try { + const resolved = await this._lookup(item, options) + if (resolved && resolved.length > 0) { + result.push(...resolved) + } + } catch (e) { + log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] 解析预设域名失败: ${item}`, e) + } + } + } + + if (result.length > 0) { + result.isPreSet = true + log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的预设IP列表: ${hostname} - ${JSON.stringify(result)}`) + return result + } } } diff --git a/packages/mitmproxy/src/lib/speed/SpeedTester.js b/packages/mitmproxy/src/lib/speed/SpeedTester.js index c781e99264..0ac40adfcb 100644 --- a/packages/mitmproxy/src/lib/speed/SpeedTester.js +++ b/packages/mitmproxy/src/lib/speed/SpeedTester.js @@ -80,11 +80,13 @@ class SpeedTester { async getFromOneDns (dns) { const results = [] + let isPreSet = false // 优先尝试IPv6查询 try { const ipv6Result = await dns._lookupInternal(this.hostname, { family: 6 }) if (ipv6Result && ipv6Result.length > 0) { + isPreSet = ipv6Result.isPreSet === true // 标准化IPv6地址格式 const standardized = ipv6Result.map(ip => { // 确保IPv6地址格式统一 @@ -103,12 +105,16 @@ class SpeedTester { try { const ipv4Result = await dns._lookupInternal(this.hostname) if (ipv4Result) { + isPreSet = isPreSet || ipv4Result.isPreSet === true results.push(...ipv4Result) } } catch (e) { // IPv4查询失败 } + if (isPreSet) { + results.isPreSet = true + } return results } From 076795dc6e370a5bdeac2f889cd303508b8d0ac5 Mon Sep 17 00:00:00 2001 From: 8odream Date: Sat, 14 Jun 2025 11:23:31 +0800 Subject: [PATCH 05/23] Merge branch 'master' into master --- ...77\347\224\250\350\257\264\346\230\216.md" | 246 +++++++++--------- 1 file changed, 125 insertions(+), 121 deletions(-) diff --git "a/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" "b/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" index 4cbfb5c245..2ea608a0b2 100644 --- "a/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" +++ "b/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" @@ -1,39 +1,50 @@ # 1. 加速服务: - 1. 什么是 `加速服务`? - `加速服务` 即 `代理服务`,它通过中间人攻击的方式,将网络请求拦截下来,并经过DNS加速、篡改、重定向、代理等一系列的功能,达到加速访问、或访问原本无法访问的站点等目的。 - 2. 启动加速服务:点击首页的 `代理服务` 右侧的开关按钮,即可启动加速服务。 +1. 什么是 `加速服务`? + + - `加速服务` 即 `代理服务`,它通过中间人攻击的方式,将网络请求拦截下来,并经过DNS加速、修改、重定向、代理等一系列的功能,达到加速访问、或访问原本无法访问的站点等目的。
+2. 如何启动加速服务:
+ - 点击首页的【代理服务】右侧的开关按钮,即可启动/关闭加速服务。
+ - 点击首页的【系统代理】右侧的开关按钮,即可将dev-sidecar设置/不设置为系统默认代理。(系统只能有一个默认代理,在将dev-sidecar与其他网络辅助软件共用时请谨慎开启本开关)
+ - 点击首页的【NPM加速】和【Git.exe代理】右侧的开关按钮,即可启动/关闭dev-sidecar为对应软件提供的加速服务。如果你的电脑上并未安装NPM或Git,则这两个按钮将不可用,这是正常情况。 + # 2. 根证书使用说明: - 1. 什么是根证书? - 2. [为什么需要安装根证书这么高风险性的步骤](https://github.com/docmirror/dev-sidecar/blob/master/doc/caroot.md) - 3. 如何安装根证书: + +1. 什么是根证书:TODO +2. [为什么需要安装根证书这么高风险性的步骤](https://github.com/docmirror/dev-sidecar/blob/master/doc/caroot.md) +3. 如何安装根证书:参见dev-sidecar【首页】的【安装根证书】按钮(注意Firefox浏览器还需要一次手动导入根证书) # 3. 模式: - 1. 安全模式: - 2. 默认模式: - 3. 增强模式(彩蛋): + +1. 安全模式:TODO +2. 默认模式:TODO +3. 增强模式(彩蛋):TODO # 4. 拦截功能使用和配置说明: - 1. 请求拦截器: - | 请求拦截器名称 | 拦截器配置名 | 请求拦截优先级 | 快速响应码 | - | ----------------- | -------------- | ------------- | --------- | - | OPTIONS请求拦截器 | options | 101 | 200 | - | 快速成功拦截器 | success | 102 | 200 | - | 快速失败拦截器 | abort | 103 | 403 | - | 缓存请求拦截器 | cacheXxx | 104 | 304 | - | 重定向拦截器 | redirect | 105 | 302 | - | 请求篡改拦截器 | requestReplace | 111 | | - | 代理拦截器 | proxy | 121 | | - | SNI拦截器 | sni | 122 | | - - 2. 响应拦截器: - | 响应拦截器名称 | 拦截器配置名 | 响应拦截优先级 | - | ---------------- | --------------- | ------------- | - | OPTIONS响应拦截器 | options | 201 | - | 缓存响应拦截器 | cacheXxx | 202 | - | 响应篡改拦截器 | responseReplace | 203 | - | 脚本拦截器 | script | 211 | + +## 4.1. 拦截器类型: + +### 1)请求拦截器: +| 请求拦截器名称 | 拦截器配置名 | 请求拦截优先级 | 作用 | +| ----------------- | -------------- | ------------- | --------- | +| OPTIONS请求拦截器 | options | 101 | 直接响应200,不发送该OPTIONS请求 | +| 快速成功拦截器 | success | 102 | 直接响应200,不发送该请求 | +| 快速失败拦截器 | abort | 103 | 直接响应403,不发送该请求 | +| 缓存请求拦截器 | cacheXxx | 104 | 如果缓存还生效,直接响应304,不发送该请求
如果缓存已过期或无缓存,则发送请求
注:只对GET请求生效! | +| 重定向拦截器 | redirect | 105 | 重定向到指定地址,直接响应302,不发送该请求 | +| 请求篡改拦截器 | requestReplace | 111 | 篡改请求头,达到想要的目的 | +| 代理拦截器 | proxy | 121 | 将请求转发到指定地址 | +| SNI拦截器 | sni | 122 | 设置 `servername`,用于避开GFW | + +### 2)响应拦截器: + +| 响应拦截器名称 | 拦截器配置名 | 响应拦截优先级 | 作用 | +| ---------------- | --------------- | ------------- | --------- | +| OPTIONS响应拦截器 | options | 201 | 设置跨域所需的响应头,避免被浏览器的跨域策略阻拦 | +| 缓存响应拦截器 | cacheXxx | 202 | 设置缓存所需的响应头,使浏览器缓存当前请求
注:只对GET请求生效! | +| 响应篡改拦截器 | responseReplace | 203 | 篡改响应头,避免被浏览器的安全策略阻拦 | +| 脚本拦截器 | script | 211 | 注入JavaScript脚本到页面中,如:Github油猴脚本 | 3. 配置示例: ```json @@ -97,137 +108,130 @@ } ``` # 5. 域名白名单: -# 6. DNS服务管理: -## 6.1. 配置 `DNS-over-HTTPS` 的DNS服务: -```json -{ - "cloudflare": { - "type": "https", - "server": "https://1.1.1.1/dns-query", - "cacheSize": 1000 - } -} -``` -或 -```json -{ - "cloudflare": { - "server": "https://1.1.1.1/dns-query", // 地址上带有 `https://`,type可以不配置 - "cacheSize": 1000 - } -} -``` +选择哪些域名不会被dev-sidecar处理。 -## 6.2. 配置 `DNS-over-TLS` 的DNS服务: +**注意:** 该设置与【系统代理-自定义排除域名】的区别在于: -```json -{ - "cloudflareTLS": { - "type": "tls", - "server": "1.1.1.1", - "port": 853, // 不配置时,默认端口为:853 - "servername": "cloudflare-dns.com", // SNI - "cacheSize": 1000 - } -} -``` -或 +1. 前者只是被dev-sidecar自身忽略,后者则是写入系统设置、不会被(任何的)系统代理处理,在手动修改系统代理设置时务必小心后者可能残留的作用! +2. 在条目较多时,前者的性能不如后者,可能产生明显延迟。
-```json -{ - "cloudflareTLS": { - "server": "tls://1.1.1.1", - "port": 853, // 不配置时,默认端口为:853 - "servername": "cloudflare-dns.com", // SNI - //"sni": "cloudflare-dns.com", // SNI缩写配置 - "cacheSize": 1000 - } -} -``` -或 +在config.json的 `proxy.excludeIpList:object` 中设置,**该字段**格式如下:
+> 注意:这里点号用来作为JSON object嵌套关系的缩写,冒号指明该条目的类型(主要用来区分object和list),并没有哪一个Object的key为 `proxy.excludeIpList`。为避免歧义,配置中object和list的key总不应包含点号。下同) ```json { - "cloudflareTLS": { - "server": "tls://1.1.1.1:853", - "servername": "cloudflare-dns.com", // SNI - //"sni": "cloudflare-dns.com", // SNI缩写配置 - "cacheSize": 1000 + "proxy": { + "excludeIpList": { + "example1.com": true, + "example2.com": false, + "example3.com": null, + "example4.com": { + "desc1": "域名对应字段设置为false时会被处理,null会移除现有设置(多用于远程配置)", + "desc2": "其他情况下就和设置true一样,不会被处理。因而你可以像这样插入注释", + "desc3": "同样的技巧可以用在其他本应设置一个bool值的地方", + "desc4": "原则上来说config.json不支持//形式的注释,但下文为了方便阅读,还是这么写了" + } + } } } ``` -## 6.3. 配置 `TCP` 的DNS服务: +# 6. DNS服务管理: + +用来配置在dev-sidecar中需要的指定DNS,出于保密和可靠起见建议使用DoH和DoT。
+在 `server.dns.provider:object` 中设置,**其中的每个条目** 格式如下: + +## 6.1. 配置 `DNS-over-HTTPS`(简称DoH): +> 注:并非被所有DNS支持,但是保证只要能使用就一定匿名且可靠的DNS服务。 ```json -{ - "googleTCP": { - "type": "tcp", - "server": "8.8.8.8", - "port": 53, // 不配置时,默认端口为:53 - "cacheSize": 1000, - } +"cloudflare": { + "type": "https", // 如果server上以"https://"开头指明了协议,就不需要写type了 + "server": "https://1.1.1.1/dns-query", + "cacheSize": 1000 } ``` -或 + +## 6.2. 配置 `DNS-over-TLS`(简称DoT): +> 并非被所有DNS支持,但是保证只要能使用就一定匿名且可靠的DNS服务。 ```json -{ - "googleTCP": { - "server": "tcp://8.8.8.8", - "port": 53, // 不配置时,默认端口为:53 - "cacheSize": 1000, - } +"cloudflareTLS": { + "type": "tls", // 如果server上以"tls://"开头指明了协议,就不需要写type了 + "server": "1.1.1.1", + "port": 853, // 不配置时,默认端口为:853 + "servername": "cloudflare-dns.com", // 需要伪造成的SNI + //"sni": "cloudflare-dns.com", // SNI缩写配置 + "cacheSize": 1000 } ``` -或 + +## 6.3. 配置 `TCP` 的DNS服务: +> 并非被所有DNS支持,该方法既不保密也不可靠 ```json -{ - "googleTCP": { - "server": "tcp://8.8.8.8:53", - "cacheSize": 1000, - } +"googleTCP": { + "type": "tcp", // 如果server上以"tcp://"开头指明了协议,就不需要写type了 + "server": "8.8.8.8", + "port": 53, // 不配置时,默认端口为:53 + "cacheSize": 1000 } ``` ## 6.4. 配置 `UDP` 的DNS服务: +> 所有DNS服务器均支持UDP方式,但该方法既不保密也不可靠 ```json -{ - "google": { - "type": "udp", - "server": "8.8.8.8", - "port": 53, // 不配置时,默认端口为:53 - "cacheSize": 1000, - } +"google": { + "type": "udp", // 如果server上以"udp://"开头指明了协议,就不需要写type了 + "server": "8.8.8.8", + "port": 53, // 不配置时,默认端口为:53 + "cacheSize": 1000 } ``` -或 + +# 7. DNS设置: + +选择哪些域名需要使用指定的DNS(需要先在【DNS服务管理】中设置)获取IP。
+在config.json中的 `server.dns.mapping:key-value` 中设置,**其中的每个条目**格式如下: ```json -{ - "google": { - "server": "udp://8.8.8.8", - "port": 53, // 不配置时,默认端口为:53 - "cacheSize": 1000, - } -} +"*.example.com": "your-dns-name" ``` -或 + +# 8. IP预设置: + +为一些DNS无法获取的域名手动设置ip,起到类似于hosts的作用(仅在dev-sidecar开启时生效)。
+在config.json中的 `server.preSetIpList:object` 中设置,**其中的每个条目**格式如下: ```json { - "google": { - "server": "udp://8.8.8.8:53", - "cacheSize": 1000, + "example.com": { + "1.1.1.1": true, // 如果有多个IP,可以继续添加 + "1.0.0.1": false, // 指定为false时,不使用该IP + "2.2.2.2": { + "desc": "这样可以合法的在配置中插入注释。上面使用的//注释方式在文件中是不允许的" + } } } ``` -# 7. DNS设置: +# 9. IP测速: -# 8. IP预设置: +用来对从指定的DNS与IP预设置中获取到的IP测试TCP延迟,也可以用来测试DoH和DoT服务器的可用性,后者操作如下:先在【DNS服务管理】中配置好需要测试的DNS设置,然后在【IP测速】里添加一个没有设置【IP预设置】的辅助域名,并选择使用需检测的DNS进行解析。
+对于DoH/DoT而言,由于答案不能被篡改和窃听,所以辅助域名要么获得真实IP(说明可用)要么没有收到答案(说明不可用)。该方法不适用于常规TCP/UDP的DNS,因为它们没有加密,即使收到答案也可能被篡改而不可用)。
+在config.json中的 `server.dns.speedTest:object`中设置,**该条目** 格式如下: -# 9. IP测速: +```json +"speedTest": { + "hostnameList": [ + "example1.com", + "example2.com" + ], + "dnsProviders": [ + "your-DNS-name-used-in-test1", + "your-DNS-name-used-in-test2" + ] +} +``` From c613c33ae63bffe828bbfa0dbd4ca31b92e29262 Mon Sep 17 00:00:00 2001 From: 8odream Date: Sat, 15 Aug 2026 22:00:16 +0800 Subject: [PATCH 06/23] =?UTF-8?q?-=20feat:=20=E6=96=B0=E5=A2=9E=E6=B5=81?= =?UTF-8?q?=E9=87=8F=E7=9B=91=E6=8E=A7=E4=B8=8E=E6=97=A5=E5=BF=97=E5=BC=80?= =?UTF-8?q?=E5=85=B3=EF=BC=8C=E5=AE=8C=E5=96=84=20overwall=20=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E9=80=89=E6=8B=A9=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=89=93=E5=8C=85=E9=97=AE=E9=A2=98"=20-=20feat(gui):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B5=81=E9=87=8F=E7=9B=91=E6=8E=A7=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=EF=BC=8Cadblock=20=E9=9B=B6=20IP=20=E4=B8=8E=E5=B7=B2?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E5=9F=9F=E5=90=8D=E4=B8=8D=E8=AE=A1=E5=85=A5?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=BB=9F=E8=AE=A1=20-=20feat(core):=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20DEV=5FSIDECAR=5FLOG=5FDISABLED=20=E5=AE=8C?= =?UTF-8?q?=E5=85=A8=E5=85=B3=E9=97=AD=E6=97=A5=E5=BF=97=EF=BC=8C=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E5=A2=9E=E5=8A=A0=E5=BC=80=E5=85=B3=20-=20fe?= =?UTF-8?q?at(overwall):=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20ID=20=E6=A0=87=E8=AF=86=EF=BC=8C=E5=8F=AF=E6=8C=89=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E9=80=89=E6=8B=A9=E7=9B=AE=E6=A0=87=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=20-=20feat(speed):=20=E6=B5=8B=E9=80=9F=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E9=9B=B6=20IP=EF=BC=8C=E7=AD=89=E5=BE=85=20Cloudflare?= =?UTF-8?q?=20=E8=B7=AF=E7=94=B1=E5=B0=B1=E7=BB=AA=E5=90=8E=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=20-=20fix(gui):=20=E4=BF=AE=E5=A4=8D=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=90=8E=E4=BB=BB=E5=8A=A1=E6=A0=8F=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E4=BB=8D=E4=B8=BA=20Electron=20=E9=BB=98=E8=AE=A4=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=EF=BC=88signAndEditExecutable=20=E6=94=B9=E4=B8=BA=20?= =?UTF-8?q?true=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=86=99=E5=85=A5=20exe=20?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E4=B8=8E=E7=89=88=E6=9C=AC=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=EF=BC=89=20-=20fix(build):=20=E5=B9=B3=E5=8F=B0=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E5=8F=AF=E9=80=89=E4=BE=9D=E8=B5=96=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E4=B8=BA=E7=9B=B4=E6=8E=A5=E4=BE=9D=E8=B5=96=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AE=89=E8=A3=85=E5=90=8E=20sysproxy/shutdown-handle?= =?UTF-8?q?r=20=E6=A8=A1=E5=9D=97=E7=BC=BA=E5=A4=B1=20-=20fix(proxy):=20DS?= =?UTF-8?q?-Proxy-Request=20=E5=93=8D=E5=BA=94=E5=A4=B4=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E5=AE=9E=E9=99=85=E4=B8=8A=E6=B8=B8=E8=B7=AF=E5=BE=84=20-=20fi?= =?UTF-8?q?x(system-proxy):=20=E4=BC=98=E5=8C=96=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=BC=80=E5=85=B3=E4=B8=8E=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20-=20chore(script):=20=E7=BB=9F=E4=B8=80=20?= =?UTF-8?q?dev.ps1=20=E5=90=AF=E5=81=9C/=E6=A3=80=E6=9F=A5=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 内容使用DeepSeekV4Pro0813 high+DSH完成 --- _script/dev.ps1 | 144 ++++++ _script/electron-dev.mjs | 14 +- packages/core/src/config-api.js | 6 +- packages/core/src/config/index.js | 6 + packages/core/src/expose.js | 82 ++-- .../core/src/modules/plugin/node/config.js | 13 +- .../core/src/modules/plugin/node/index.js | 130 ++++-- .../src/modules/plugin/overwall/config.js | 2 + packages/core/src/modules/server/index.js | 37 +- .../core/src/shell/scripts/set-system-env.js | 74 ++-- .../shell/scripts/set-system-proxy/index.js | 146 +++--- .../core/src/utils/util.log-or-console.js | 11 +- packages/core/src/utils/util.logger.js | 20 +- packages/gui/electron-builder.config.cjs | 12 +- packages/gui/package.json | 2 + packages/gui/src/background.js | 43 +- packages/gui/src/bridge/api/backend.js | 11 +- packages/gui/src/utils/util.log-env.js | 20 + packages/gui/src/view/pages/index.vue | 42 +- packages/gui/src/view/pages/plugin/node.vue | 36 +- .../gui/src/view/pages/plugin/overwall.vue | 129 +++++- packages/gui/src/view/pages/server.vue | 32 +- packages/gui/src/view/pages/setting.vue | 9 + packages/gui/src/view/pages/traffic.vue | 417 ++++++++++++++++++ packages/gui/src/view/router/index.js | 2 + packages/gui/src/view/router/menu.js | 1 + packages/mitmproxy/src/index.js | 5 + packages/mitmproxy/src/lib/cloudflareRoute.js | 301 +++++++++++++ packages/mitmproxy/src/lib/dns/util.ip.js | 28 ++ .../src/lib/proxy/middleware/overwall.js | 119 +++-- .../proxy/mitmproxy/createConnectHandler.js | 4 + .../proxy/mitmproxy/createRequestHandler.js | 40 +- .../src/lib/proxy/mitmproxy/dnsLookup.js | 104 ++++- .../src/lib/proxy/mitmproxy/index.js | 26 +- .../mitmproxy/src/lib/speed/SpeedTester.js | 110 ++++- packages/mitmproxy/src/lib/speed/config.js | 2 + packages/mitmproxy/src/lib/speed/index.js | 10 +- .../src/lib/traffic/TrafficMonitor.js | 332 ++++++++++++++ .../src/lib/traffic/processResolver.js | 120 +++++ pnpm-lock.yaml | 12 +- 40 files changed, 2345 insertions(+), 309 deletions(-) create mode 100644 _script/dev.ps1 create mode 100644 packages/gui/src/utils/util.log-env.js create mode 100644 packages/gui/src/view/pages/traffic.vue create mode 100644 packages/mitmproxy/src/lib/cloudflareRoute.js create mode 100644 packages/mitmproxy/src/lib/traffic/TrafficMonitor.js create mode 100644 packages/mitmproxy/src/lib/traffic/processResolver.js diff --git a/_script/dev.ps1 b/_script/dev.ps1 new file mode 100644 index 0000000000..d5419d68e0 --- /dev/null +++ b/_script/dev.ps1 @@ -0,0 +1,144 @@ +#Requires -Version 5.1 +<# + dev-sidecar 开发环境统一启动/检查脚本 + + 用法(在仓库根目录或任意目录执行): + pwsh -File _script\dev.ps1 -Action start # 启动开发环境并等待端口就绪 + pwsh -File _script\dev.ps1 -Action check # 检查端口监听状态 + pwsh -File _script\dev.ps1 -Action stop # 停止开发环境(按端口结束进程) + pwsh -File _script\dev.ps1 -Action restart # 停止后重新启动 + + 可选参数: + -Port 8081 # 前端 dev server 端口,默认 8081 + -Foreground # start 时在前台运行,日志直接输出到当前终端(Ctrl+C 停止) + -SkipKill # start/restart 时不先结束已占用端口的进程 +#> +param( + [ValidateSet('start', 'check', 'stop', 'restart')] + [string]$Action = 'start', + + [int]$Port = 8081, + + [switch]$Foreground, + + [switch]$SkipKill +) + +$ErrorActionPreference = 'SilentlyContinue' + +$repoRoot = Split-Path -Parent $PSScriptRoot +$guiDir = Join-Path $repoRoot 'packages\gui' +$electronDev = Join-Path $PSScriptRoot 'electron-dev.mjs' +$proxyHttpPort = 31180 +$proxyHttpsPort = 31181 +$ports = @($Port, $proxyHttpPort, $proxyHttpsPort) + +function Get-Listeners([int[]]$TargetPorts) { + Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | + Where-Object { $TargetPorts -contains $_.LocalPort } +} + +function Show-Status([string]$Title) { + Write-Host "== $Title ==" + $listeners = @(Get-Listeners $ports) + if ($listeners.Count -gt 0) { + $listeners | Sort-Object LocalPort | ForEach-Object { + $procName = (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName + Write-Host (" {0}:{1,-6} PID {2,-7} {3}" -f $_.LocalAddress, $_.LocalPort, $_.OwningProcess, $procName) + } + } else { + Write-Host ' no listeners' + } + Write-Host '' +} + +function Stop-DevSidecar([int[]]$TargetPorts) { + Write-Host '== stopping dev-sidecar ==' + $listeners = @(Get-Listeners $TargetPorts) + $procIds = @($listeners | Select-Object -ExpandProperty OwningProcess -Unique) + if ($procIds.Count -eq 0) { + Write-Host ' no listeners' + } else { + foreach ($procId in $procIds) { + Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue + Write-Host " killed PID $procId" + } + } + Start-Sleep -Milliseconds 600 + Write-Host '' +} + +function Wait-Ports([int[]]$TargetPorts, [int]$TimeoutSec = 120) { + $deadline = (Get-Date).AddSeconds($TimeoutSec) + $ready = @{} + + while ((Get-Date) -lt $deadline) { + $listeners = @(Get-Listeners $TargetPorts) + foreach ($p in $TargetPorts) { + if ($listeners.LocalPort -contains $p) { + $ready[$p] = $true + } + } + + if ($ready.Count -ge $TargetPorts.Count) { + break + } + + Start-Sleep -Seconds 1 + } + + if ($ready.Count -lt $TargetPorts.Count) { + $missing = @($TargetPorts | Where-Object { -not $ready.ContainsKey($_) }) + Write-Host "等待端口超时,未就绪端口: $($missing -join ', ')" + } else { + Write-Host "端口已全部就绪: $($TargetPorts -join ', ')" + } +} + +function Start-DevSidecar([int]$DevPort) { + $node = (Get-Command node -ErrorAction Stop).Source + + if ($Foreground) { + Write-Host "== starting dev-sidecar in foreground (Ctrl+C to stop) ==" + Push-Location $guiDir + try { + & $node $electronDev '--port' "$DevPort" + } finally { + Pop-Location + } + return + } + + Write-Host "== starting dev-sidecar ==" + $proc = Start-Process -FilePath $node -ArgumentList @($electronDev, '--port', "$DevPort") -WorkingDirectory $guiDir -WindowStyle Hidden -PassThru + Write-Host " launcher PID: $($proc.Id)" + Wait-Ports $ports + Show-Status 'port status after start' +} + +switch ($Action) { + 'check' { + Show-Status "port status (dev: $Port, http proxy: $proxyHttpPort, https proxy: $proxyHttpsPort)" + break + } + + 'stop' { + Stop-DevSidecar $ports + Show-Status 'port status after stop' + break + } + + 'start' { + if (-not $SkipKill) { + Stop-DevSidecar $ports + } + Start-DevSidecar $Port + break + } + + 'restart' { + Stop-DevSidecar $ports + Start-DevSidecar $Port + break + } +} diff --git a/_script/electron-dev.mjs b/_script/electron-dev.mjs index 1c2d67ff15..149891f7a8 100644 --- a/_script/electron-dev.mjs +++ b/_script/electron-dev.mjs @@ -6,7 +6,17 @@ import { setTimeout as delay } from 'node:timers/promises' const guiDir = process.cwd() const require = createRequire(import.meta.url) -const devServerUrl = 'http://localhost:8080' +function resolveDevServer () { + const argv = process.argv.slice(2) + const portIndex = argv.indexOf('--port') + const port = portIndex >= 0 ? Number.parseInt(argv[portIndex + 1], 10) : 8080 + if (!Number.isInteger(port) || port <= 0 || port > 65535) { + throw new Error(`无效的端口号: ${argv[portIndex + 1]}`) + } + return { port, url: `http://localhost:${port}` } +} + +const { port: devServerPort, url: devServerUrl } = resolveDevServer() const state = { closing: false, devServer: null, @@ -86,7 +96,7 @@ async function main () { const vueCliServiceBin = resolveVueCliServiceBin() const electronBin = resolveElectronBin() - state.devServer = spawnCommand(process.execPath, [vueCliServiceBin, 'serve', '--port', '8080']) + state.devServer = spawnCommand(process.execPath, [vueCliServiceBin, 'serve', '--port', String(devServerPort)]) state.devServer.on('exit', (code, signal) => { if (!state.closing) { void shutdown(code ?? (signal ? 1 : 0)) diff --git a/packages/core/src/config-api.js b/packages/core/src/config-api.js index c0aed46604..07610048b4 100644 --- a/packages/core/src/config-api.js +++ b/packages/core/src/config-api.js @@ -60,7 +60,11 @@ const configApi = { if (remoteConfigUrl.startsWith('https://raw.githubusercontent.com/')) { headers['Server-Name'] = 'baidu.com' } - request(remoteConfigUrl, { headers }, (error, response, body) => { + // 禁用环境变量代理(HTTPS_PROXY/HTTP_PROXY): + // 当用户开启 proxy.setEnv 后,环境变量会指向 dev-sidecar 自己的代理端口, + // 启动时本地代理尚未监听,走代理会导致下载失败(ECONNREFUSED 127.0.0.1:31181), + // 新装用户会因此无法下载远程配置,只能使用内置规则。 + request(remoteConfigUrl, { headers, proxy: null }, (error, response, body) => { if (error) { log.error(`下载远程配置失败: ${remoteConfigUrl}, error:`, error, ', response:', response, ', body:', body) reject(error) diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index 4a430822c1..fa330b5477 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -41,6 +41,7 @@ const defaultConfig = { showShutdownTip: true, // 日志相关配置 + logDisabled: false, // 完全禁用日志:控制台不输出,日志文件也不写入 logFileSavePath: path.join(configLoader.getUserBasePath(), '/logs'), // 日志文件保存路径 keepLogFileCount: 15, // 保留日志文件数 maxLogFileSize: 1, // 最大日志文件大小 @@ -99,6 +100,11 @@ const defaultConfig = { // } }, }, + // Cloudflare 路由重定向:命中 Cloudflare IP 段时改写为优选地址 + cloudflareRoute: { + enabled: false, + preferredEndpoint: '', // 优选地址,可填写 IP 或 CNAME 域名 + }, intercept: { enabled: true, }, diff --git a/packages/core/src/expose.js b/packages/core/src/expose.js index a617cd257d..d64eb11395 100644 --- a/packages/core/src/expose.js +++ b/packages/core/src/expose.js @@ -55,80 +55,96 @@ config.resetDefault() const server = modules.server const serverStart = server.start -function newServerStart ({ mitmproxyPath }) { - return serverStart({ mitmproxyPath, plugins: plugin }) +function newServerStart ({ mitmproxyPath, setting }) { + return serverStart({ mitmproxyPath, plugins: plugin, setting }) } server.start = newServerStart -async function startup ({ mitmproxyPath }) { +async function startup ({ mitmproxyPath, setting }) { const conf = config.get() + const tasks = [] + if (conf.server.enabled && !status.server.enabled) { - try { - await server.start({ mitmproxyPath }) - } catch (err) { - log.error('代理服务启动失败:', err) - } + tasks.push((async () => { + try { + await server.start({ mitmproxyPath }) + } catch (err) { + log.error('代理服务启动失败:', err) + } + })()) } if (conf.proxy.enabled && !status.proxy.enabled) { - try { - await proxy.start() - } catch (err) { - log.error('开启系统代理失败:', err) - } + tasks.push((async () => { + try { + await proxy.start() + } catch (err) { + log.error('开启系统代理失败:', err) + } + })()) } + try { - const plugins = [] for (const key in plugin) { if (conf.plugin[key].enabled && !status.plugin[key]?.enabled) { - const start = async () => { + if (key === 'overwall' && setting && setting.overwall !== true) { + log.info(`插件【${key}】未启动:setting.json 未开启 overwall`) + continue + } + tasks.push((async () => { try { await plugin[key].start() log.info(`插件【${key}】已启动`) } catch (err) { log.error(`插件【${key}】启动失败:`, err) } - } - plugins.push(start()) + })()) } } - if (plugins && plugins.length > 0) { - await Promise.all(plugins) - } } catch (err) { log.error('开启插件失败:', err) } + + if (tasks.length > 0) { + // server、系统代理、各插件之间没有相互依赖,并行启动以缩短整体等待时间 + await Promise.all(tasks) + } } async function shutdown () { + const tasks = [] + try { - const plugins = [] for (const key in plugin) { if (status.plugin[key] && status.plugin[key].enabled && plugin[key].close) { - const close = async () => { + tasks.push((async () => { try { await plugin[key].close() log.info(`插件【${key}】已关闭`) } catch (err) { log.error(`插件【${key}】关闭失败:`, err) } - } - plugins.push(close()) + })()) } } - if (plugins.length > 0) { - await Promise.all(plugins) - } } catch (error) { log.error('插件关闭失败:', error) } if (status.proxy.enabled) { - try { - await proxy.close() - log.info('系统代理已关闭') - } catch (err) { - log.error('系统代理关闭失败:', err) - } + tasks.push((async () => { + try { + await proxy.close() + log.info('系统代理已关闭') + } catch (err) { + log.error('系统代理关闭失败:', err) + } + })()) } + + if (tasks.length > 0) { + // 插件关闭(清理 git/npm 配置)与关闭系统代理互不依赖,并行执行 + await Promise.all(tasks) + } + if (status.server.enabled) { try { await server.close() diff --git a/packages/core/src/modules/plugin/node/config.js b/packages/core/src/modules/plugin/node/config.js index 9a02451a51..ab6544a68d 100644 --- a/packages/core/src/modules/plugin/node/config.js +++ b/packages/core/src/modules/plugin/node/config.js @@ -30,15 +30,16 @@ module.exports = { }, }, }, - variables: { - phantomjs_cdnurl: 'https://npmmirror.com/mirrors/phantomjs', - chromedriver_cdnurl: 'https://npmmirror.com/mirrors/chromedriver', - sass_binary_site: 'https://npmmirror.com/mirrors/node-sass', + // npm 11 开始不再接受未知的 npm config,以下镜像变量全部改为直接设置系统环境变量 + variables: {}, + envVariables: { ELECTRON_MIRROR: 'https://npmmirror.com/mirrors/electron/', - NVM_NODEJS_ORG_MIRROR: 'https://npmmirror.com/mirrors/node', + ELECTRON_BUILDER_BINARIES_MIRROR: 'https://npmmirror.com/mirrors/electron-builder-binaries/', + PHANTOMJS_CDNURL: 'https://npmmirror.com/mirrors/phantomjs', CHROMEDRIVER_CDNURL: 'https://npmmirror.com/mirrors/chromedriver', + SASS_BINARY_SITE: 'https://npmmirror.com/mirrors/node-sass', + NVM_NODEJS_ORG_MIRROR: 'https://npmmirror.com/mirrors/node', OPERADRIVER: 'https://npmmirror.com/mirrors/operadriver', - ELECTRON_BUILDER_BINARIES_MIRROR: 'https://npmmirror.com/mirrors/electron-builder-binaries/', PYTHON_MIRROR: 'https://npmmirror.com/mirrors/python', }, } diff --git a/packages/core/src/modules/plugin/node/index.js b/packages/core/src/modules/plugin/node/index.js index 1f060cdaab..1c4cc4f274 100644 --- a/packages/core/src/modules/plugin/node/index.js +++ b/packages/core/src/modules/plugin/node/index.js @@ -1,6 +1,31 @@ +const fs = require('node:fs') +const path = require('node:path') const jsonApi = require('@docmirror/mitmproxy/src/json') const nodeConfig = require('./config') +function getUserNpmrcPath () { + return path.join(process.env.USERPROFILE || process.env.HOME || '', '.npmrc') +} + +function isKeyInUserNpmrc (key) { + try { + if (!fs.existsSync(getUserNpmrcPath())) { + return false + } + const content = fs.readFileSync(getUserNpmrcPath(), 'utf8') + const target = key.toLowerCase() + return content.split(/\r?\n/).some((line) => { + const index = line.indexOf('=') + if (index <= 0) { + return false + } + return line.slice(0, index).trim().toLowerCase() === target + }) + } catch { + return false + } +} + const NodePlugin = function (context) { const { config, shell, event, log } = context const nodeApi = { @@ -43,25 +68,35 @@ const NodePlugin = function (context) { async setNpmEnv (list) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [] + // npm config set 支持一次设置多个 key=value,合并成一条命令,避免启动多个 npm 进程拖慢速度 + const setArgs = [] + const deleteKeys = [] for (const item of list) { if (item.value != null && item.value.length > 0 && item.value !== 'default' && item.value !== 'null') { - cmds.push(`${command} config set ${item.key} ${item.value}`) + setArgs.push(`${item.key}=${item.value}`) } else { - cmds.push(`${command} config delete ${item.key}`) + deleteKeys.push(item.key) } } + + const cmds = [] + if (setArgs.length > 0) { + cmds.push(`${command} config set ${setArgs.join(' ')}`) + } + if (deleteKeys.length > 0) { + cmds.push(`${command} config delete ${deleteKeys.join(' ')}`) + } return await shell.exec(cmds, { type: 'cmd' }) }, async unsetNpmEnv (list) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [] - for (const item of list) { - cmds.push(`${command} config delete ${item} `) - } - return await shell.exec(cmds, { type: 'cmd' }) + // npm config delete 支持一次删除多个 key,合并成一条命令 + const keys = list.map((item) => { + return typeof item === 'string' ? item : item.key + }).filter(Boolean) + return await shell.exec([`${command} config delete ${keys.join(' ')}`], { type: 'cmd' }) }, async setYarnEnv (list) { @@ -99,16 +134,57 @@ const NodePlugin = function (context) { hadSet: currentMap[key] === map[key], }) } + + // 环境变量形式的镜像配置,直接检查 process.env + const envMap = config.get().plugin.node.envVariables || {} + const defaultEnvMap = nodeConfig.envVariables || {} + for (const key in envMap) { + const oldValue = process.env[key] + list.push({ + key, + value: envMap[key], + oldValue, + exists: oldValue != null, + hadSet: oldValue === envMap[key], + defaultValue: defaultEnvMap[key], + }) + } return list }, async setVariables () { - const list = await nodeApi.getVariables() - const noSetList = list.filter((item) => { - return !item.exists + const nodeConfig = config.get().plugin.node + const envMap = nodeConfig.envVariables || {} + const variables = nodeConfig.variables || {} + + // 清理旧版通过 npm config 写入的镜像变量,避免 npm 未知配置告警。 + // 只有用户级 .npmrc 中确实存在这些 key 时才启动 npm 执行删除,避免每次启动/应用都白跑一遍 npm。 + const staleKeys = Object.keys(envMap).filter((key) => isKeyInUserNpmrc(key)) + if (staleKeys.length > 0) { + try { + await nodeApi.unsetNpmEnv(staleKeys) + } catch (e) { + log.warn('删除旧的 npm config 镜像变量失败:', e) + } + } + + // 每次启动/应用时都强制重写所有镜像环境变量,覆盖手动修改的值 + const envList = Object.keys(envMap).map((key) => { + return { key, value: envMap[key] } }) - if (noSetList.length > 0) { - return nodeApi.setNpmEnv(noSetList) + if (envList.length > 0) { + await shell.setSystemEnv({ list: envList }) + } + + // 其余非环境变量形式的 npm config 变量(当前默认为空),仅在确实配置了 variables 时才查询 npm + if (Object.keys(variables).length > 0) { + const list = await nodeApi.getVariables() + const noSetList = list.filter((item) => { + return !item.hadSet && envMap[item.key] == null + }) + if (noSetList.length > 0) { + await nodeApi.setNpmEnv(noSetList) + } } }, @@ -124,9 +200,10 @@ const NodePlugin = function (context) { async setProxy (ip, port) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [ - `${command} config set proxy=http://${ip}:${port - 1}`, - `${command} config set https-proxy=http://${ip}:${port}`, + // npm config set 支持一次设置多个 key=value,合并成一条命令 + const setArgs = [ + `proxy=http://${ip}:${port - 1}`, + `https-proxy=http://${ip}:${port}`, ] const env = [] @@ -140,23 +217,23 @@ const NodePlugin = function (context) { const nodeConfig = config.get().plugin.node const rootCaCertFile = config.get().server.setting.rootCaFile.certPath if (nodeConfig.setting['strict-ssl']) { - cmds.push(`${command} config set strict-ssl false`) + setArgs.push('strict-ssl=false') } if (nodeConfig.setting.cafile) { - cmds.push(`${command} config set cafile "${rootCaCertFile}"`) + setArgs.push(`cafile=${rootCaCertFile}`) } if (nodeConfig.setting.NODE_EXTRA_CA_CERTS) { - cmds.push(`${command} config set NODE_EXTRA_CA_CERTS "${rootCaCertFile}"`) + setArgs.push(`NODE_EXTRA_CA_CERTS=${rootCaCertFile}`) env.push({ key: 'NODE_EXTRA_CA_CERTS', value: rootCaCertFile }) } if (nodeConfig.setting.NODE_TLS_REJECT_UNAUTHORIZED) { - cmds.push(`${command} config set NODE_TLS_REJECT_UNAUTHORIZED 0`) + setArgs.push('NODE_TLS_REJECT_UNAUTHORIZED=0') env.push({ key: 'NODE_TLS_REJECT_UNAUTHORIZED', value: '0' }) } - const ret = await shell.exec(cmds, { type: 'cmd' }) + const ret = await shell.exec([`${command} config set ${setArgs.join(' ')}`], { type: 'cmd' }) if (env.length > 0) { await shell.setSystemEnv({ list: env }) } @@ -169,15 +246,8 @@ const NodePlugin = function (context) { async unsetProxy () { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [ - `${command} config delete proxy`, - `${command} config delete https-proxy`, - `${command} config delete strict-ssl`, - `${command} config delete cafile`, - `${command} config delete NODE_EXTRA_CA_CERTS`, - `${command} config delete NODE_TLS_REJECT_UNAUTHORIZED`, - ] - const ret = await shell.exec(cmds, { type: 'cmd' }) + // npm config delete 支持一次删除多个 key,合并成一条命令 + const ret = await shell.exec([`${command} config delete proxy https-proxy strict-ssl cafile NODE_EXTRA_CA_CERTS NODE_TLS_REJECT_UNAUTHORIZED`], { type: 'cmd' }) event.fire('status', { key: 'plugin.node.enabled', value: false }) log.info('关闭【NPM】代理成功') return ret diff --git a/packages/core/src/modules/plugin/overwall/config.js b/packages/core/src/modules/plugin/overwall/config.js index 870cae95d5..762c2b947a 100644 --- a/packages/core/src/modules/plugin/overwall/config.js +++ b/packages/core/src/modules/plugin/overwall/config.js @@ -1,9 +1,11 @@ module.exports = { name: '梯子', enabled: false, // 默认关闭梯子 + restartServer: true, // 首页开关切换后需要重启代理服务才能生效 server: {}, serverDefault: { 'ow-prod.docmirror.top': { + id: 0, // 自带默认服务器 ID 固定为 0,用户新增服务器依次为 1、2、3... port: 443, path: 'X2dvX292ZXJfd2FsbF8', password: 'dev_sidecar_is_666', diff --git a/packages/core/src/modules/server/index.js b/packages/core/src/modules/server/index.js index c27a56ad47..5894d28d5b 100644 --- a/packages/core/src/modules/server/index.js +++ b/packages/core/src/modules/server/index.js @@ -19,6 +19,16 @@ function sleep (time) { }, time) }) } + +function onceExit (child) { + return new Promise((resolve) => { + if (child.exitCode != null || child.signalCode != null) { + resolve(true) + return + } + child.once('exit', () => resolve(true)) + }) +} const serverApi = { async startup () { if (config.get().server.startup) { @@ -30,7 +40,7 @@ const serverApi = { return this.close() } }, - async start ({ mitmproxyPath, plugins }) { + async start ({ mitmproxyPath, plugins, setting }) { // 防止重复启动:如果已有子进程存活,直接返回 if (server && server.process && !server.process.killed && server.process.exitCode == null) { log.warn('server is already running, skip start (pid:', server.id, ')') @@ -73,7 +83,11 @@ const serverApi = { plugin.overrideRunningConfig(serverConfig) } } - serverConfig.plugin = allConfig.plugin + serverConfig.plugin = lodash.cloneDeep(allConfig.plugin || {}) + if (setting && setting.overwall !== true && serverConfig.plugin.overwall) { + // setting.json 未开启 overwall 时,梯子插件不生效 + serverConfig.plugin.overwall.enabled = false + } if (allConfig.proxy && allConfig.proxy.enabled) { serverConfig.proxy = allConfig.proxy @@ -137,23 +151,34 @@ const serverApi = { event.fire('error', { key: 'server', value: code, error: msg.event, message: msg.message }) } else if (msg.type === 'speed') { event.fire('speed', msg.event) + } else if (msg.type === 'traffic') { + event.fire('traffic', msg.event) } }) return { port: serverConfig.port } }, async kill () { if (server) { - server.process.kill('SIGINT') - await sleep(1000) + const child = server.process + if (child.exitCode == null && child.signalCode == null) { + const exited = onceExit(child) + child.kill('SIGINT') + const exitedBySigint = await Promise.race([exited, sleep(1000).then(() => false)]) + if (!exitedBySigint) { + log.warn('server process 未在 1 秒内响应 SIGINT,尝试强制结束') + child.kill('SIGKILL') + await Promise.race([exited, sleep(1000).then(() => false)]) + } + } } fireStatus(false) }, async close () { return await serverApi.kill() }, - async restart ({ mitmproxyPath }) { + async restart ({ mitmproxyPath, setting }) { await serverApi.kill() - await serverApi.start({ mitmproxyPath }) + await serverApi.start({ mitmproxyPath, setting }) }, getServer () { return server diff --git a/packages/core/src/shell/scripts/set-system-env.js b/packages/core/src/shell/scripts/set-system-env.js index e9721c72a7..c2e648ad34 100644 --- a/packages/core/src/shell/scripts/set-system-env.js +++ b/packages/core/src/shell/scripts/set-system-env.js @@ -1,59 +1,41 @@ /** * 设置环境变量 */ +const Registry = require('winreg') const Shell = require('../shell') const execute = Shell.execute const executor = { async windows (exec, { list }) { - const psCmdsMachine = [] - const psCmdsUser = [] - for (const item of list) { - const v = item.value == null ? '' : String(item.value) - // escape single quotes for PowerShell single-quoted string - const escaped = v.replace(/'/g, "''") - psCmdsMachine.push(`[Environment]::SetEnvironmentVariable('${item.key}', '${escaped}', 'Machine')`) - psCmdsUser.push(`[Environment]::SetEnvironmentVariable('${item.key}', '${escaped}', 'User')`) + const regKey = new Registry({ + hive: Registry.HKCU, + key: '\\Environment', + }) + + const setItem = (item) => { + const value = item.value == null ? '' : String(item.value) + return new Promise((resolve, reject) => { + regKey.set(item.key, Registry.REG_SZ, value, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) } - // Wrap whole flow and return structured result so caller can show detailed errors try { - let ret - let appliedScope = null + for (const item of list) { + await setItem(item) + } + + // 广播环境变量变更(setx 一个临时值触发 WM_SETTINGCHANGE) try { - ret = await exec(psCmdsMachine, { type: 'ps' }) - appliedScope = 'Machine:PowerShell' - } catch (eMachine) { - // try User scope via PowerShell - try { - ret = await exec(psCmdsUser, { type: 'ps' }) - appliedScope = 'User:PowerShell' - } catch (eUser) { - // PowerShell attempts failed; fallback to setx via cmd - const cmdsSetxMachine = [] - const cmdsSetxUser = [] - for (const item of list) { - const v = item.value == null ? '' : String(item.value) - // escape backslashes first, then double quotes (cmd.exe quoting order) - const escaped = v.replace(/\\/g, '\\\\').replace(/"/g, '\\"') - cmdsSetxMachine.push(`setx ${item.key} "${escaped}" /M`) - cmdsSetxUser.push(`setx ${item.key} "${escaped}"`) - } - try { - ret = await exec(cmdsSetxMachine, { type: 'cmd' }) - appliedScope = 'Machine:setx' - } catch (eSetxMachine) { - try { - ret = await exec(cmdsSetxUser, { type: 'cmd' }) - appliedScope = 'User:setx' - } catch (eSetxUser) { - // all attempts failed — include all error messages - const combined = [eMachine, eUser, eSetxMachine, eSetxUser].map(e => (e && e.message) || String(e)).join(' | ') - return { success: false, error: 'Failed to set environment variables', details: combined } - } - } - } + await exec('setx DS_REFRESH "1"', { type: 'cmd' }) + } catch { + // 广播失败不影响主流程 } // inject into current process so subsequent exec/child processes can inherit immediately @@ -70,9 +52,9 @@ const executor = { envUpdateError = e.message || String(e) } - return { success: true, scope: appliedScope, output: ret, envUpdateError } - } catch (finalErr) { - return { success: false, error: finalErr.message || String(finalErr), details: finalErr.stack } + return { success: true, scope: 'User:winreg', envUpdateError } + } catch (e) { + return { success: false, error: 'Failed to set environment variables', details: e.message || String(e) } } }, async linux (exec, { port }) { diff --git a/packages/core/src/shell/scripts/set-system-proxy/index.js b/packages/core/src/shell/scripts/set-system-proxy/index.js index 65ba6e536a..dadda3b35c 100644 --- a/packages/core/src/shell/scripts/set-system-proxy/index.js +++ b/packages/core/src/shell/scripts/set-system-proxy/index.js @@ -25,12 +25,81 @@ function getDomesticDomainAllowListTmpFilePath () { return path.join(config.get().server.setting.userBasePath, '/domestic-domain-allowlist.txt') } +// 通过 HKCU\Environment 注册表直接写入/删除环境变量,避免每次 setx 都启动 PowerShell 造成数秒卡顿 +function createEnvRegKey () { + return new Registry({ + hive: Registry.HKCU, + key: '\\Environment', + }) +} + +function setWindowsEnvVariable (regKey, key, value) { + return new Promise((resolve, reject) => { + regKey.set(key, Registry.REG_SZ, value == null ? '' : String(value), (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +function removeWindowsEnvVariable (regKey, key) { + return new Promise((resolve) => { + regKey.remove(key, (removeErr) => { + resolve(!removeErr) + }) + }) +} + +async function broadcastWindowsEnvChange (exec) { + try { + await exec('setx DS_REFRESH "1"', { type: 'cmd' }) + } catch { + // 广播失败不影响主流程 + } +} + +async function setWindowsEnvVariables (exec, envList) { + if (!envList || envList.length === 0) { + return + } + + const regKey = createEnvRegKey() + for (const item of envList) { + await setWindowsEnvVariable(regKey, item.key, item.value) + process.env[item.key] = String(item.value) + } + await broadcastWindowsEnvChange(exec) +} + +async function removeWindowsEnvVariables (exec, keys) { + if (!keys || keys.length === 0) { + return + } + + const regKey = createEnvRegKey() + let removed = false + for (const key of keys) { + const existed = await removeWindowsEnvVariable(regKey, key) + if (existed) { + delete process.env[key] + removed = true + } + } + if (removed) { + await broadcastWindowsEnvChange(exec) + } +} + async function downloadDomesticDomainAllowListAsync () { loadConfig() const remoteFileUrl = config.get().proxy.remoteDomesticDomainAllowListFileUrl log.info('开始下载远程 domestic-domain-allowlist.txt 文件:', remoteFileUrl) - request(remoteFileUrl, (error, response, body) => { + // 禁用环境变量代理:防止走 dev-sidecar 自己的代理(127.0.0.1:31181)导致启动时下载失败 + request(remoteFileUrl, { proxy: null }, (error, response, body) => { if (error) { log.error(`下载远程 domestic-domain-allowlist.txt 文件失败: ${remoteFileUrl}, error:`, error, ', response:', response, ', body:', body) return @@ -458,18 +527,27 @@ const executor = { log.info('开始设置windows系统代理:', ip, port, setEnv) - // https - let proxyAddr = `https=http://${ip}:${port}` - // http - if (config.get().proxy.proxyHttp) { - proxyAddr = `http=http://${ip}:${port - 1};${proxyAddr}` + const sysproxy = require('@starknt/sysproxy') + const proxyHttp = config.get().proxy.proxyHttp + + // 同时代理 HTTP+HTTPS 时需要两个端口,使用 WinINET 协议格式(协议=地址:端口); + // 只代理 HTTPS 时,地址和端口分开传参,Windows 会按规范分别填入“地址”和“端口”两个框。 + let proxyAddr + if (proxyHttp) { + proxyAddr = `http=${ip}:${port - 1};https=${ip}:${port}` + } else { + proxyAddr = `${ip}:${port}` } // 读取排除域名 const excludeIpStr = getProxyExcludeIpStr(';') // 设置代理,同时设置排除域名 try { - require('@starknt/sysproxy').triggerManualProxyByUrl(true, proxyAddr, excludeIpStr, true) + if (proxyHttp) { + sysproxy.triggerManualProxyByUrl(true, proxyAddr, excludeIpStr, true) + } else { + sysproxy.triggerManualProxy(true, ip, port, excludeIpStr) + } log.info(`设置windows系统代理成功: ${proxyAddr} ......(省略排除IP列表)`) } catch (e1) { log.warn('设置windows系统代理失败:执行 `@starknt/sysproxy` 失败,现尝试通过执行 `sysproxy.exe global ...` 来设置系统代理!\r\n捕获的异常:', e1) @@ -488,23 +566,24 @@ const executor = { if (setEnv) { // 设置全局代理所需的环境变量 try { - await exec(`echo '设置环境变量 HTTPS_PROXY${config.get().proxy.proxyHttp ? '、HTTP_PROXY' : ''}${setCaBundle ? '、REQUEST_CA_BUNDLE' : ''}'`) - - log.info(`开启系统代理的同时设置环境变量:HTTPS_PROXY = "http://${ip}:${port}/"`) - await exec(`setx HTTPS_PROXY "http://${ip}:${port}/"`) + const envList = [] + const httpsProxy = `http://${ip}:${port}/` + log.info(`开启系统代理的同时设置环境变量:HTTPS_PROXY = "${httpsProxy}"`) + envList.push({ key: 'HTTPS_PROXY', value: httpsProxy }) if (config.get().proxy.proxyHttp) { - log.info(`开启系统代理的同时设置环境变量:HTTP_PROXY = "http://${ip}:${port - 1}/"`) - await exec(`setx HTTP_PROXY "http://${ip}:${port - 1}/"`) + const httpProxy = `http://${ip}:${port - 1}/` + log.info(`开启系统代理的同时设置环境变量:HTTP_PROXY = "${httpProxy}"`) + envList.push({ key: 'HTTP_PROXY', value: httpProxy }) } if (setCaBundle) { const caCertPath = config.get().server.setting.rootCaFile.certPath log.info(`开启系统代理的同时设置环境变量:REQUEST_CA_BUNDLE = "${caCertPath}"`) - await exec(`setx REQUEST_CA_BUNDLE "${caCertPath}"`) + envList.push({ key: 'REQUEST_CA_BUNDLE', value: caCertPath }) } - // await addClearScriptIni() + await setWindowsEnvVariables(exec, envList) } catch (e) { log.error('设置环境变量 HTTPS_PROXY、HTTP_PROXY、REQUEST_CA_BUNDLE 失败:', e) } @@ -530,42 +609,9 @@ const executor = { } try { - await exec('echo \'删除环境变量 HTTPS_PROXY、HTTP_PROXY、REQUEST_CA_BUNDLE\'') - const regKey = new Registry({ // new operator is optional - hive: Registry.HKCU, // open registry hive HKEY_CURRENT_USER - key: '\\Environment', // key containing autostart programs - }) - regKey.get('HTTPS_PROXY', (err) => { - if (!err) { - regKey.remove('HTTPS_PROXY', async (err) => { - if (err) { - log.warn('删除环境变量 HTTPS_PROXY 失败:', err) - } else { - await exec('setx DS_REFRESH "1"') - } - }) - } - }) - regKey.get('HTTP_PROXY', (err) => { - if (!err) { - regKey.remove('HTTP_PROXY', async (err) => { - if (err) { - log.warn('删除环境变量 HTTP_PROXY 失败:', err) - } - }) - } - }) - regKey.get('REQUEST_CA_BUNDLE', (err) => { - if (!err) { - regKey.remove('REQUEST_CA_BUNDLE', async (err) => { - if (err) { - log.warn('删除环境变量 REQUEST_CA_BUNDLE 失败:', err) - } - }) - } - }) + await removeWindowsEnvVariables(exec, ['HTTPS_PROXY', 'HTTP_PROXY', 'REQUEST_CA_BUNDLE']) } catch (e) { - log.error('删除环境变量 HTTPS_PROXY、HTTP_PROXY 失败:', e) + log.error('删除环境变量 HTTPS_PROXY、HTTP_PROXY、REQUEST_CA_BUNDLE 失败:', e) } return true diff --git a/packages/core/src/utils/util.log-or-console.js b/packages/core/src/utils/util.log-or-console.js index f547f41991..3fb7afcc51 100644 --- a/packages/core/src/utils/util.log-or-console.js +++ b/packages/core/src/utils/util.log-or-console.js @@ -1,15 +1,18 @@ const dateUtil = require('./util.date') +// DEV_SIDECAR_LOG_DISABLED=true 时完全静默:不输出控制台,也不备份历史日志 +const disabled = process.env.DEV_SIDECAR_LOG_DISABLED === 'true' + // CLI 命令(如 status)会 require core 但不会启动,若默认输出到 console 会污染命令结果。 // 当 DEV_SIDECAR_LOG_TO_CONSOLE=false 时保持静默,但仍备份,待 setLogger 后回放进日志文件 -const silent = process.env.DEV_SIDECAR_LOG_TO_CONSOLE === 'false' +const silent = disabled || process.env.DEV_SIDECAR_LOG_TO_CONSOLE === 'false' let log = silent ? { debug () {}, info () {}, warn () {}, error () {} } : console // 将console中的日志缓存起来,当setLogger时,将控制台的日志写入日志文件 -let backupLogs = [] +let backupLogs = disabled ? null : [] function backup (fun, args) { if (backupLogs === null) { @@ -61,6 +64,10 @@ function _doLog (fun, args) { module.exports = { setLogger (logger) { + if (disabled) { + return + } + if (logger == null) { log.error('logger 不能为空') return diff --git a/packages/core/src/utils/util.logger.js b/packages/core/src/utils/util.logger.js index 6175f18676..9f33fa3526 100644 --- a/packages/core/src/utils/util.logger.js +++ b/packages/core/src/utils/util.logger.js @@ -7,8 +7,18 @@ const configFromFiles = defaultConfig.configFromFiles // 日志级别 const level = process.env.NODE_ENV === 'development' ? 'debug' : 'info' +// 是否完全禁用日志:不输出 stdout,也不写日志文件 +const logDisabled = process.env.DEV_SIDECAR_LOG_DISABLED === 'true' + // 是否同时输出到 stdout。默认开启(GUI/开发调试需要),CLI 守护进程通过环境变量关闭 -const logToConsole = process.env.DEV_SIDECAR_LOG_TO_CONSOLE !== 'false' +const logToConsole = !logDisabled && process.env.DEV_SIDECAR_LOG_TO_CONSOLE !== 'false' + +function createNoopLogger () { + const noop = () => {} + return { debug: noop, info: noop, warn: noop, error: noop, level: 'off', category: 'noop' } +} + +const noopLogger = logDisabled ? createNoopLogger() : null function getDefaultConfigBasePath () { if (configFromFiles.app.logFileSavePath) { @@ -40,6 +50,10 @@ let log = null // 设置一组日志配置 function log4jsConfigure (categories) { + if (logDisabled) { + return + } + if (log != null) { log.error('当前进程已经设置过日志配置,无法再设置更多日志配置:', categories) return @@ -69,6 +83,10 @@ function log4jsConfigure (categories) { module.exports = { getLogger (category) { + if (logDisabled) { + return noopLogger + } + if (!category) { if (log) { log.error('未指定日志类型,无法配置并获取日志对象!!!') diff --git a/packages/gui/electron-builder.config.cjs b/packages/gui/electron-builder.config.cjs index e45f4af3dd..31cb15baef 100644 --- a/packages/gui/electron-builder.config.cjs +++ b/packages/gui/electron-builder.config.cjs @@ -63,8 +63,10 @@ module.exports = { allowToChangeInstallationDirectory: true, }, win: { - icon: 'build/icons/', - signAndEditExecutable: isCI, // 本地开发跳过签名 + icon: 'build/icons/icon.ico', + // 必须为 true 才会用 rcedit 写入 exe 图标和版本信息; + // 未配置证书时 electron-builder 会自动跳过签名,不会失败。 + signAndEditExecutable: true, target: isCI ? [ { target: 'nsis', arch: ['x64'] }, @@ -85,11 +87,7 @@ module.exports = { { target: 'rpm', arch: ['x64', 'arm64', 'armv7l'] }, { target: 'flatpak', arch: ['x64'] }, // Arch Linux (fpm/pacman), 产物为 .pkg.tar.xz, 可直接 pacman -U 安装 - { - target: 'pacman', - arch: ['x64', 'arm64'], - artifactName: 'DevSidecar-${version}-${arch}.pkg.tar.xz', - }, + { target: 'pacman', arch: ['x64', 'arm64'] }, ] : [ { target: 'deb', arch: [localArch] }, diff --git a/packages/gui/package.json b/packages/gui/package.json index 637846724f..e414060e7a 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -24,7 +24,9 @@ "@docmirror/dev-sidecar": "workspace:*", "@docmirror/mitmproxy": "workspace:*", "@starknt/shutdown-handler-napi": "^0.0.3", + "@starknt/shutdown-handler-napi-win32-x64-msvc": "^0.0.3", "@starknt/sysproxy": "^0.0.3", + "@starknt/sysproxy-win32-x64-msvc": "^0.0.3", "@vscode/sudo-prompt": "^9.3.2", "adm-zip": "^0.5.17", "ant-design-vue": "^4.2.6", diff --git a/packages/gui/src/background.js b/packages/gui/src/background.js index 0c5b3ede38..0f51e4c0b1 100644 --- a/packages/gui/src/background.js +++ b/packages/gui/src/background.js @@ -1,4 +1,5 @@ 'use strict' +import './utils/util.log-env.js' import path from 'node:path' import { fileURLToPath } from 'node:url' import DevSidecar from '@docmirror/dev-sidecar' @@ -15,6 +16,12 @@ const isWindows = process.platform === 'win32' const isLinux = process.platform === 'linux' const isMac = process.platform === 'darwin' +if (isWindows) { + // 让 Windows 托盘气泡/通知/任务栏显示正常软件名,而不是开发模式默认的 electron.app + app.setName('dev-sidecar') + app.setAppUserModelId('dev-sidecar') +} + // 禁用不需要的 Chromium 组件以减少内存和 CPU 占用 // 这些开关必须在 app.whenReady() 之前设置 @@ -240,18 +247,30 @@ function changeAppConfig (config) { } function loadAppIcon () { - // 优先:从 asar 内读取(fs.readFileSync 被 Electron 补丁支持 asar) - try { - const p = path.join(app.getAppPath(), 'dist', 'icon.png') - if (fs.existsSync(p)) return nativeImage.createFromBuffer(fs.readFileSync(p)) - } catch { /* ignore */ } + const candidates = [] + + // Windows 下优先使用 exe 同款 ico,任务栏图标更清晰 + if (isWindows) { + candidates.push(path.join(process.resourcesPath, 'extra', 'icons', 'icon.ico')) + } + // 优先:从 asar 内读取 + candidates.push(path.join(app.getAppPath(), 'dist', 'icon.png')) // 回退:asar.unpacked 真实路径 - try { - const p = path.join(app.getAppPath(), '..', 'app.asar.unpacked', 'dist', 'icon.png') - if (fs.existsSync(p)) return nativeImage.createFromPath(p) - } catch { /* ignore */ } + candidates.push(path.join(app.getAppPath(), '..', 'app.asar.unpacked', 'dist', 'icon.png')) + // 再回退:extra 资源目录 + candidates.push(path.join(process.resourcesPath, 'extra', 'icons', '512x512.png')) // 开发模式回退 - return nativeImage.createFromPath(path.resolve('public/icon.png')) + candidates.push(path.resolve('public/icon.png')) + + for (const p of candidates) { + try { + if (p && fs.existsSync(p)) { + return nativeImage.createFromPath(p) + } + } catch { /* ignore */ } + } + + return nativeImage.createEmpty() } function createWindow (startHideWindow, autoQuitIfError = true) { @@ -295,9 +314,7 @@ function createWindow (startHideWindow, autoQuitIfError = true) { if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) - if (!process.env.IS_TEST) { - setTimeout(openDevTools, 2000) - } + // 默认不自动打开 DevTools,需要调试时可手动按 F12 或通过托盘菜单打开 } else { // Load the index.html when not in development win.loadFile(path.join(app.getAppPath(), 'dist', 'index.html')) diff --git a/packages/gui/src/bridge/api/backend.js b/packages/gui/src/bridge/api/backend.js index f89be23a05..f41b2e6247 100644 --- a/packages/gui/src/bridge/api/backend.js +++ b/packages/gui/src/bridge/api/backend.js @@ -175,7 +175,7 @@ const localApi = { * @returns {Promise} */ startup () { - return DevSidecar.api.startup({ mitmproxyPath }) + return DevSidecar.api.startup({ mitmproxyPath, setting: localApi.setting.load() }) }, server: { /** @@ -183,14 +183,14 @@ const localApi = { * @returns {Promise<{port: *}>} */ start () { - return DevSidecar.api.server.start({ mitmproxyPath }) + return DevSidecar.api.server.start({ mitmproxyPath, setting: localApi.setting.load() }) }, /** * 重启代理服务 * @returns {Promise} */ restart () { - return DevSidecar.api.server.restart({ mitmproxyPath }) + return DevSidecar.api.server.restart({ mitmproxyPath, setting: localApi.setting.load() }) }, }, shell: { @@ -281,6 +281,11 @@ export default { win.webContents.send('speed', event) } }) + DevSidecar.api.event.register('traffic', (event) => { + if (win) { + win.webContents.send('traffic', event) + } + }) // 合并用户配置 DevSidecar.api.config.reload() diff --git a/packages/gui/src/utils/util.log-env.js b/packages/gui/src/utils/util.log-env.js new file mode 100644 index 0000000000..078b1232a4 --- /dev/null +++ b/packages/gui/src/utils/util.log-env.js @@ -0,0 +1,20 @@ +// 必须在加载 core/mitmproxy 日志模块之前 import,确保 DEV_SIDECAR_LOG_DISABLED 尽早生效 +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' + +if (process.env.DEV_SIDECAR_LOG_DISABLED !== 'true') { + const userBase = path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') + const configPath = path.join(userBase, 'config.json') + + try { + if (fs.existsSync(configPath)) { + const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + if (userConfig && userConfig.app && userConfig.app.logDisabled === true) { + process.env.DEV_SIDECAR_LOG_DISABLED = 'true' + } + } + } catch { + // 配置文件不存在或格式异常时,按默认(允许日志)处理 + } +} diff --git a/packages/gui/src/view/pages/index.vue b/packages/gui/src/view/pages/index.vue index e426f69151..9b19e0eb20 100644 --- a/packages/gui/src/view/pages/index.vue +++ b/packages/gui/src/view/pages/index.vue @@ -62,6 +62,15 @@ export default { await this.doCheckRootCa() await this.reloadConfig() this.status = this.$status + // setting.json 未开启 overwall 时,梯子开关不显示,同时确保梯子配置不生效 + if (!this.setting.overwall && this.config?.plugin?.overwall?.enabled) { + this.config.plugin.overwall.enabled = false + const saveRet = await this.$api.config.save(lodash.cloneDeep(this.config)) + this.config = saveRet.allConfig + if (this.status.server?.enabled) { + await this.$api.server.restart() + } + } this.switchBtns = this.createSwitchBtns() // 合并全局更新参数到 data() 中的默认值,避免 $global.update 尚未 // 初始化时(update/front.js 在 mount 之后才执行)覆盖为 undefined @@ -203,19 +212,25 @@ export default { btns.server = this.createSwitchBtn('server', '代理服务', this.$api.server, status) btns.proxy = this.createSwitchBtn('proxy', '系统代理', this.$api.proxy, status) lodash.forEach(status.plugin, (item, key) => { + // setting.json 未开启 overwall 时,首页不显示“梯子”开关 + if (key === 'overwall' && !this.setting.overwall) { + return + } if (this.config.plugin[key].statusOff) { return } - btns[key] = this.createSwitchBtn(key, this.config.plugin[key].name, this.$api.plugin[key], status.plugin, this.config.plugin[key].tip) + btns[key] = this.createSwitchBtn(key, this.config.plugin[key].name, this.$api.plugin[key], status.plugin, this.config.plugin[key].tip, this.config.plugin[key]) }) return btns }, - createSwitchBtn (key, label, apiTarget, statusParent, tip) { + createSwitchBtn (key, label, apiTarget, statusParent, tip, pluginConfig) { return { loading: false, key, label, tip, + isPlugin: pluginConfig != null, + restartServer: pluginConfig != null && pluginConfig.restartServer === true, status: () => { return statusParent[key].enabled }, @@ -238,12 +253,25 @@ export default { } }, - onSwitchClick (btn, openApi, closeApi, checked) { - if (checked) { - return this.apiCall(btn, openApi) - } else { - return this.apiCall(btn, closeApi) + async onSwitchClick (btn, openApi, closeApi, checked) { + const ret = checked + ? await this.apiCall(btn, openApi) + : await this.apiCall(btn, closeApi) + + // 插件快捷开关需要持久化 enabled 状态,并与对应设置页保持同步 + if (btn.isPlugin) { + try { + this.config.plugin[btn.key].enabled = checked + const saveRet = await this.$api.config.save(lodash.cloneDeep(this.config)) + this.config = saveRet.allConfig + if (btn.restartServer && this.status.server && this.status.server.enabled) { + await this.$api.server.restart() + } + } catch (e) { + console.error('保存插件开关状态失败:', e) + } } + return ret }, onServerClick (checked) { return this.onSwitchClick(this.server, this.$api.server.start, this.$api.server.close, checked) diff --git a/packages/gui/src/view/pages/plugin/node.vue b/packages/gui/src/view/pages/plugin/node.vue index d77fdff9a4..4fdbeeb3d6 100644 --- a/packages/gui/src/view/pages/plugin/node.vue +++ b/packages/gui/src/view/pages/plugin/node.vue @@ -54,6 +54,33 @@ export default defineComponent({ console.log('type', type) await this.$api.plugin.node.setRegistry({ registry, type }) }, + restoreVariableDefault (item) { + if (item && item.defaultValue != null) { + item.value = item.defaultValue + } + }, + async applyBefore () { + // 将页面上编辑后的镜像变量值保存到 config,再交给 saveConfig 持久化 + if (this.npmVariables && this.npmVariables.length > 0) { + const envVariables = {} + for (const item of this.npmVariables) { + if (item.key) { + envVariables[item.key] = item.value + } + } + this.config.plugin.node.envVariables = envVariables + } + }, + async applyAfter () { + // 应用按钮点击后,应用当前配置里的镜像环境变量 + try { + await this.$api.plugin.node.setVariables() + await this.ready() + } catch (e) { + console.error('set variables error:', e) + this.$message.warning('镜像变量设置失败,请查看日志') + } + }, setNpmVariableAll () { this.saveConfig().then(() => { this.$api.plugin.node.setVariables() @@ -137,13 +164,18 @@ export default defineComponent({ - - + + + + + 默认 + + diff --git a/packages/gui/src/view/pages/plugin/overwall.vue b/packages/gui/src/view/pages/plugin/overwall.vue index 39218de649..5de309d1f8 100644 --- a/packages/gui/src/view/pages/plugin/overwall.vue +++ b/packages/gui/src/view/pages/plugin/overwall.vue @@ -29,6 +29,30 @@ export default defineComponent({ } }, + computed: { + serverOptions () { + const options = [] + const defaultServer = this.config?.plugin?.overwall?.serverDefault || {} + for (const [domain, cfg] of Object.entries(defaultServer)) { + options.push({ + id: cfg && cfg.id != null ? Number.parseInt(cfg.id, 10) : 0, + domain, + label: `ID 0(默认) - ${domain}`, + }) + } + for (const item of this.servers || []) { + if (item.key) { + options.push({ + id: item.id, + domain: item.key, + label: `ID ${item.id} - ${item.key}`, + }) + } + } + return options + }, + }, + created () { console.log('status:', this.status) }, @@ -46,26 +70,55 @@ export default defineComponent({ } }, ready () { - this.initTarget() this.initServer() + this.initTarget() }, async applyBefore () { this.submitTarget() this.submitServer() }, + preferredServerId () { + return this.serverOptions.some((item) => item.id === 1) + ? 1 + : (this.serverOptions.some((item) => item.id === 0) ? 0 : null) + }, + nextServerId () { + const ids = (this.servers || []).map((item) => item.id).filter((id) => id != null) + return ids.length > 0 ? Math.max(...ids) + 1 : 1 + }, + onTargetValueChange (item) { + if (item.value === 'true' && (item.serverId == null || item.serverId === '')) { + item.serverId = 'default' + } + }, initTarget () { this.targets = [] - const targetsMap = this.config.plugin.overwall.targets + const targetsMap = this.config.plugin.overwall.targets || {} for (const key in targetsMap) { const value = targetsMap[key] - this.targets.push({ - key: key || '', - value: value === true ? 'true' : 'false', - }) + if (value && typeof value === 'object') { + this.targets.push({ + key: key || '', + value: value.enabled === false ? 'false' : 'true', + serverId: value.serverId != null ? Number.parseInt(value.serverId, 10) : 'default', + }) + } else if (value === false || value === 'false') { + this.targets.push({ + key: key || '', + value: 'false', + serverId: 'default', + }) + } else { + this.targets.push({ + key: key || '', + value: 'true', + serverId: 'default', + }) + } } }, addTarget () { - this.targets.unshift({ key: '', value: 'true' }) + this.targets.unshift({ key: '', value: 'true', serverId: 'default' }) this.focusFirst(this.$refs.targets) }, deleteTarget (item, index) { @@ -77,7 +130,16 @@ export default defineComponent({ if (item.key) { const hostname = this.handleHostname(item.key) if (hostname) { - map[hostname] = (item.value === 'true') + if (item.value === 'true') { + // 选择“默认”时不写死服务器,交给中间件动态选择:优先 ID1,否则 ID0 + if (item.serverId === 'default' || item.serverId == null || item.serverId === '') { + map[hostname] = true + } else { + map[hostname] = { enabled: true, serverId: Number.parseInt(item.serverId, 10) } + } + } else { + map[hostname] = false + } } } } @@ -86,12 +148,19 @@ export default defineComponent({ initServer () { this.servers = [] - const targetsMap = this.config.plugin.overwall.server - for (const key in targetsMap) { - const value = targetsMap[key] + const serverMap = this.config.plugin.overwall.server || {} + for (const key in serverMap) { + const value = serverMap[key] || {} + const id = value.id != null ? Number.parseInt(value.id, 10) : this.nextServerId() this.servers.push({ + id, key, - value, + value: { + type: value.type || 'path', + port: value.port, + path: value.path, + password: value.password, + }, }) } if (this.servers.length === 0) { @@ -102,7 +171,11 @@ export default defineComponent({ this.servers.splice(index, 1) }, addServer (needFocus = true) { - this.servers.unshift({ key: '', value: { type: 'path' } }) + this.servers.unshift({ + id: this.nextServerId(), + key: '', + value: { type: 'path', port: 443, path: '', password: '' }, + }) if (needFocus) { this.focusFirst(this.$refs.servers) } @@ -113,7 +186,13 @@ export default defineComponent({ if (item.key) { const hostname = this.handleHostname(item.key) if (hostname) { - map[hostname] = item.value + map[hostname] = { + id: item.id != null ? Number.parseInt(item.id, 10) : this.nextServerId(), + type: item.value.type || 'path', + port: item.value.port, + path: item.value.path, + password: item.value.password, + } } } } @@ -173,23 +252,31 @@ export default defineComponent({
- PAC没有拦截到的域名,可以在此处定义;配置为禁用时,将不使用梯子 + PAC没有拦截到的域名,可以在此处定义;配置为启用时,可为该域名选择代理服务器,默认优先使用 ID 1,若 ID 1 不存在则自动使用 ID 0;配置为禁用时,将不使用梯子 - + - + {{ item2.label }} + + + 默认(优先 ID1,否则 ID0) + + {{ server.label }} + + + @@ -207,13 +294,16 @@ export default defineComponent({ - + + ID {{ item.id }} + + - + @@ -224,6 +314,7 @@ export default defineComponent({
+ ID 0 为内置默认服务器;在此处新增的服务器 ID 会依次为 1、2、3...
您可以在此处配置自己的代理服务器地址。
警告:请勿使用来源不明的服务器地址,有安全风险!
diff --git a/packages/gui/src/view/pages/server.vue b/packages/gui/src/view/pages/server.vue index ce4412a575..c74c68c250 100644 --- a/packages/gui/src/view/pages/server.vue +++ b/packages/gui/src/view/pages/server.vue @@ -3,7 +3,7 @@ import { defineComponent } from 'vue'; import _ from 'lodash' import JsonEditor from '@/view/components/JsonEditor.vue' -import { CheckOutlined, InfoCircleOutlined, PlusOutlined, MinusOutlined, SyncOutlined, ReloadOutlined } from '@ant-design/icons-vue' +import { CheckOutlined, CloudOutlined, InfoCircleOutlined, PlusOutlined, MinusOutlined, SyncOutlined, ReloadOutlined } from '@ant-design/icons-vue' import Plugin from '../mixins/plugin' export default defineComponent({ @@ -12,6 +12,7 @@ export default defineComponent({ components: { JsonEditor, CheckOutlined, + CloudOutlined, InfoCircleOutlined, PlusOutlined, MinusOutlined, @@ -211,6 +212,13 @@ export default defineComponent({ reSpeedTest () { this.$api.server.reSpeedTest() }, + hasCf (item) { + if (!item) { + return false + } + const list = (item.alive || []).concat(item.backupList || []) + return list.some((element) => element.cf === true) + }, registerSpeedTestEvent () { const listener = async (event, message) => { console.log('get speed event', event, message) @@ -510,6 +518,7 @@ export default defineComponent({