From ee62dfbaf1d9623682df2593e4231f1d377efa5e Mon Sep 17 00:00:00 2001 From: lbyxiaolizi Date: Sat, 22 Aug 2026 14:10:01 +0800 Subject: [PATCH] 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. --- frontend/src/views/admin/ProxiesView.vue | 15 ++++-- .../admin/__tests__/ProxiesView.ipv6.spec.ts | 51 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 frontend/src/views/admin/__tests__/ProxiesView.ipv6.spec.ts diff --git a/frontend/src/views/admin/ProxiesView.vue b/frontend/src/views/admin/ProxiesView.vue index 0572cc254..a5cd50f07 100644 --- a/frontend/src/views/admin/ProxiesView.vue +++ b/frontend/src/views/admin/ProxiesView.vue @@ -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() || '' diff --git a/frontend/src/views/admin/__tests__/ProxiesView.ipv6.spec.ts b/frontend/src/views/admin/__tests__/ProxiesView.ipv6.spec.ts new file mode 100644 index 000000000..6cea0ac86 --- /dev/null +++ b/frontend/src/views/admin/__tests__/ProxiesView.ipv6.spec.ts @@ -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') + }) +})