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:
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user