fix(proxy): support bracketed IPv6 hosts in batch proxy URL parsing

The quick-add parser rejected every IPv6 proxy: the host group [^:]+
cannot match IPv6 literals (colons) and the pattern had no bracketed
form, so lines like socks5://[2001:db8::1]:1080 were reported invalid.

Add a bracketed-IPv6 host alternative and strip the brackets before
storing; the backend re-brackets via net.JoinHostPort when building the
proxy URL. Bare (unbracketed) IPv6 stays rejected because it is
ambiguous with host:port. Also add a regression test.
This commit is contained in:
lbyxiaolizi
2026-08-22 14:10:01 +08:00
parent d45135d87d
commit ee62dfbaf1
2 changed files with 62 additions and 4 deletions
+11 -4
View File
@@ -1276,6 +1276,7 @@ const handleDataImported = () => {
}
// Parse proxy URL: protocol://user:pass@host:port or protocol://host:port
// Host may be a domain, IPv4, or bracketed IPv6 ([2001:db8::1]).
const parseProxyUrl = (
line: string
): {
@@ -1288,20 +1289,26 @@ const parseProxyUrl = (
const trimmed = line.trim()
if (!trimmed) return null
// Regex to parse proxy URL (supports http, https, socks5, socks5h)
const regex = /^(https?|socks5h?):\/\/(?:([^:@]+):([^@]+)@)?([^:]+):(\d+)$/i
// Regex to parse proxy URL (supports http, https, socks5, socks5h).
// Host alternatives: [bracketed-IPv6] | hostname/IPv4 (colon-free, so the
// match stops before the final :port).
const regex =
/^(https?|socks5h?):\/\/(?:([^:@\[\]]+):([^@\[\]]+)@)?(\[[0-9a-f:.]+\]|[^:\[\]]+):(\d+)$/i
const match = trimmed.match(regex)
if (!match) return null
const [, protocol, username, password, host, port] = match
const [, protocol, username, password, rawHost, port] = match
const portNum = parseInt(port, 10)
if (portNum < 1 || portNum > 65535) return null
// Strip brackets from IPv6 literals; the backend re-brackets via net.JoinHostPort.
const host = rawHost.replace(/^\[|\]$/g, '').trim()
return {
protocol: protocol.toLowerCase() as ProxyProtocol,
host: host.trim(),
host,
port: portNum,
username: username?.trim() || '',
password: password?.trim() || ''
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
// parseProxyUrl is not exported; assert on the source to lock in the
// bracketed-IPv6 host alternative and exercise the regex directly.
const source = readFileSync(
resolve(process.cwd(), 'src/views/admin/ProxiesView.vue'),
'utf8'
)
function extractRegex(): RegExp {
const match = source.match(/const regex =\s*\n?\s*(\/\^\(https\?[^;\n]+\/i)\n/)
expect(match, 'parseProxyUrl regex not found in ProxiesView.vue').toBeTruthy()
return new RegExp((match as RegExpMatchArray)[1].slice(1, -2), 'i')
}
describe('proxy batch URL parsing (IPv6 support)', () => {
it('keeps bracketed-IPv6 host alternative in the regex', () => {
expect(source).toContain('\\[[0-9a-f:.]+\\]')
})
const regex = extractRegex()
it.each([
['socks5://[2001:db8::1]:1080', true],
['socks5h://[2001:db8::1]:1080', true],
['http://[::1]:8080', true],
['socks5://user:pass@[2001:db8::1]:1080', true],
['socks5://proxy.example.com:1080', true],
['http://192.168.1.1:8080', true],
['socks5://user:pass@proxy.example.com:1080', true],
// bare IPv6 without brackets is ambiguous with host:port — rejected
['socks5://2001:db8::1:1080', false],
// unsupported schemes / malformed ports stay invalid
['ftp://example.com:21', false],
['socks5://example.com:port', false]
])('%s => %s', (line, expected) => {
expect(regex.test(line)).toBe(expected)
})
it('extracts bare IPv6 host without brackets', () => {
const m = 'socks5://user:pass@[2001:db8::1]:1080'.match(regex)
expect(m).toBeTruthy()
const [, , username, password, rawHost, port] = m as RegExpMatchArray
expect(username).toBe('user')
expect(password).toBe('pass')
expect(rawHost.replace(/^\[|\]$/g, '')).toBe('2001:db8::1')
expect(port).toBe('1080')
})
})