Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/features/popup/NetworkPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
/>
<p className='mt-1.5 text-[10px] leading-4 text-slate-400'>
每行一个域名或 Chrome
代理绕过规则;代理模式、自动模式与每个自定义代理模式都会独立保存。
每行一个域名或 Chrome 代理绕过规则;支持 `*.example.com`
匹配子域名。代理模式、自动模式与每个自定义代理模式都会独立保存。
</p>
</div>
</div>
Expand Down
11 changes: 5 additions & 6 deletions src/utils/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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://')
Expand All @@ -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
}
}

Expand Down
76 changes: 76 additions & 0 deletions tests/network-bypass.test.mjs
Original file line number Diff line number Diff line change
@@ -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')
})