From 12de8d29b0d2897118d7ad7a490019c3d0019e61 Mon Sep 17 00:00:00 2001 From: zhangshuo12 Date: Mon, 17 Aug 2026 10:41:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=80=9A=E9=85=8D?= =?UTF-8?q?=E7=AC=A6=E4=BB=A3=E7=90=86=E7=BB=95=E8=BF=87=E5=9F=9F=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/popup/NetworkPage.tsx | 4 +- src/utils/network.ts | 11 ++--- tests/network-bypass.test.mjs | 76 ++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 tests/network-bypass.test.mjs diff --git a/src/features/popup/NetworkPage.tsx b/src/features/popup/NetworkPage.tsx index 490c026..495aa45 100644 --- a/src/features/popup/NetworkPage.tsx +++ b/src/features/popup/NetworkPage.tsx @@ -1208,8 +1208,8 @@ export default function NetworkPage() { className='w-full resize-none rounded-lg border border-slate-300 bg-white px-2.5 py-2 font-mono text-xs leading-5 text-slate-700 placeholder:text-slate-400 outline-none transition-colors focus:border-blue-500' />

- 每行一个域名或 Chrome - 代理绕过规则;代理模式、自动模式与每个自定义代理模式都会独立保存。 + 每行一个域名或 Chrome 代理绕过规则;支持 `*.example.com` + 匹配子域名。代理模式、自动模式与每个自定义代理模式都会独立保存。

diff --git a/src/utils/network.ts b/src/utils/network.ts index b7e1954..8c1e50d 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -44,6 +44,8 @@ function normalizeHostPattern(value: string): string { } function extractHostPatternFromUrlLike(value: string): string | null { + const rawHost = value.replace(WILDCARD_SCHEME_RE, '').replace(URL_SCHEME_RE, '').split(/[/?#]/)[0] + const hasSubdomainWildcard = /^(?:\*|%2a)\./i.test(stripPortFromHostPattern(rawHost)) let urlInput = `http://${value}` if (WILDCARD_SCHEME_RE.test(value)) { urlInput = value.replace(WILDCARD_SCHEME_RE, 'http://') @@ -53,13 +55,10 @@ function extractHostPatternFromUrlLike(value: string): string | null { try { const url = new URL(urlInput) - return normalizeHostPattern(url.hostname) + const hostname = normalizeHostPattern(url.hostname) + return hasSubdomainWildcard ? `*.${hostname.replace(/^(?:\*|%2a)\./i, '')}` : hostname } catch { - const withoutScheme = value.replace(WILDCARD_SCHEME_RE, '').replace(URL_SCHEME_RE, '') - const host = withoutScheme.startsWith('[') - ? withoutScheme.slice(0, withoutScheme.indexOf(']') + 1 || undefined) - : withoutScheme.split(/[/?#]/)[0] - return host ? normalizeHostPattern(host) : null + return null } } diff --git a/tests/network-bypass.test.mjs b/tests/network-bypass.test.mjs new file mode 100644 index 0000000..fc7ab87 --- /dev/null +++ b/tests/network-bypass.test.mjs @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +const networkModule = globalThis.process.env.DEVGO_NETWORK_MODULE + +if (!networkModule) { + throw new Error('DEVGO_NETWORK_MODULE is required') +} + +const { buildNetworkPacScript, normalizeBypassList } = await import(networkModule) + +function evaluatePac(profile, ruleList, host) { + const { data } = buildNetworkPacScript(profile, ruleList) + const FindProxyForURL = new Function( + 'dnsDomainIs', + 'isPlainHostName', + `${data}; return FindProxyForURL`, + )( + (value, domain) => value === domain || value.endsWith(domain), + (value) => !value.includes('.'), + ) + + return FindProxyForURL(`https://${host}/`, host) +} + +const profile = { + scheme: 'http', + host: '127.0.0.1', + port: 7890, + bypassList: ['*.guazi.com'], +} + +test('normalizes wildcard bypass domains and discards invalid entries', () => { + assert.deepEqual(normalizeBypassList([' *.GUAZI.com ', 'invalid host', '*.guazi.com']), [ + '*.guazi.com', + ]) +}) + +test('preserves wildcard syntax when the browser URL API escapes the asterisk', () => { + const NativeURL = globalThis.URL + globalThis.URL = class { + constructor(value) { + return { hostname: new NativeURL(value).hostname.replace('*', '%2a') } + } + } + + try { + assert.deepEqual(normalizeBypassList('*.guazi.com'), ['*.guazi.com']) + } finally { + globalThis.URL = NativeURL + } +}) + +test('restores wildcard syntax from previously encoded bypass rules', () => { + const NativeURL = globalThis.URL + globalThis.URL = class { + constructor(value) { + return { hostname: new NativeURL(value).hostname.replace('*', '%2a') } + } + } + + try { + assert.deepEqual(normalizeBypassList('%2A.GUAZI.COM'), ['*.guazi.com']) + } finally { + globalThis.URL = NativeURL + } +}) + +test('bypasses subdomains but not the wildcard root domain', () => { + assert.equal(evaluatePac(profile, '||guazi.com^', 'www.guazi.com'), 'DIRECT') + assert.equal(evaluatePac(profile, '||guazi.com^', 'guazi.com'), 'PROXY 127.0.0.1:7890') +}) + +test('uses bypass rules before automatic-mode proxy rules', () => { + assert.equal(evaluatePac(profile, '||api.guazi.com^', 'api.guazi.com'), 'DIRECT') +})