diff --git a/open-sse/utils/proxyDispatcher.ts b/open-sse/utils/proxyDispatcher.ts index 6166df9604..879ec80aca 100644 --- a/open-sse/utils/proxyDispatcher.ts +++ b/open-sse/utils/proxyDispatcher.ts @@ -20,6 +20,23 @@ export function clearDispatcherCache() { cache.clear(); } +/** + * Extract the port from a proxy URL string before URL parsing. + * `new URL("http://host:80")` strips port 80 since it's the HTTP default, + * but proxy servers commonly listen on port 80/443, so we need to preserve it. + */ +function extractExplicitPort(urlStr) { + try { + // Match port in the host portion: "scheme://[user:pass@]host:PORT[/...]" + const match = urlStr.match(/:\/\/(?:[^@]*@)?[^:/\s]+:(\d+)/); + if (match) { + const port = Number(match[1]); + if (Number.isInteger(port) && port >= 1 && port <= 65535) return String(port); + } + } catch {} + return null; +} + function defaultPortForProtocol(protocol) { if (protocol === "https:" || protocol === "wss:") return "443"; if (protocol === "socks5:") return "1080"; @@ -35,13 +52,28 @@ function normalizePort(port, protocol) { return String(parsed); } +/** + * Build a proxy URL string manually from parsed URL components. + * We cannot use URL.toString() because the URL serializer silently strips + * default ports (80 for http, 443 for https). Proxy servers commonly + * listen on these ports, so we must always include the port explicitly. + */ +function buildProxyUrlString(parsed, port) { + const auth = + parsed.username + ? `${parsed.username}${parsed.password ? `:${parsed.password}` : ""}@` + : ""; + return `${parsed.protocol}//${auth}${parsed.hostname}:${port}`; +} + export function isSocks5ProxyEnabled() { return process.env.ENABLE_SOCKS5_PROXY === "true"; } export function proxyUrlForLogs(proxyUrl) { + const explicitPort = extractExplicitPort(proxyUrl); const parsed = new URL(proxyUrl); - const port = parsed.port || defaultPortForProtocol(parsed.protocol); + const port = explicitPort || parsed.port || defaultPortForProtocol(parsed.protocol); return `${parsed.protocol}//${parsed.hostname}:${port}`; } @@ -50,6 +82,11 @@ export function normalizeProxyUrl( source = "proxy", { allowSocks5 = isSocks5ProxyEnabled() } = {} ) { + // Extract the explicit port from the raw URL string BEFORE parsing, + // because `new URL()` silently strips default ports (80 for http, + // 443 for https), which are valid and common for proxy servers. + const explicitPort = extractExplicitPort(proxyUrl); + let parsed; try { parsed = new URL(proxyUrl); @@ -71,8 +108,12 @@ export function normalizeProxyUrl( throw new Error(`[ProxyDispatcher] Invalid ${source} host`); } - parsed.port = normalizePort(parsed.port, parsed.protocol); - return parsed.toString(); + // Use the explicit port from the raw string if present, otherwise apply default. + const port = explicitPort || normalizePort(parsed.port, parsed.protocol); + + // Build the URL string manually instead of using parsed.toString(), + // which would strip default ports (80/443) and break the proxy connection. + return buildProxyUrlString(parsed, port); } export function proxyConfigToUrl(proxyConfig, { allowSocks5 = isSocks5ProxyEnabled() } = {}) { @@ -102,14 +143,15 @@ export function proxyConfigToUrl(proxyConfig, { allowSocks5 = isSocks5ProxyEnabl } const port = normalizePort(proxyConfig.port, protocol); - const proxyUrl = new URL(`${type}://${proxyConfig.host}:${port}`); - if (proxyConfig.username) { - proxyUrl.username = encodeURIComponent(proxyConfig.username); - proxyUrl.password = proxyConfig.password ? encodeURIComponent(proxyConfig.password) : ""; - } + // Build the URL string manually to preserve the port through normalization. + const auth = proxyConfig.username + ? `${encodeURIComponent(proxyConfig.username)}:${proxyConfig.password ? encodeURIComponent(proxyConfig.password) : ""}@` + : ""; - return normalizeProxyUrl(proxyUrl.toString(), "context proxy", { allowSocks5 }); + const proxyUrlStr = `${type}://${auth}${proxyConfig.host}:${port}`; + + return normalizeProxyUrl(proxyUrlStr, "context proxy", { allowSocks5 }); } export function createProxyDispatcher(proxyUrl) { @@ -120,11 +162,14 @@ export function createProxyDispatcher(proxyUrl) { if (dispatcher) return dispatcher; const parsed = new URL(normalizedUrl); + const explicitPort = extractExplicitPort(normalizedUrl); + const port = explicitPort || normalizePort(parsed.port, parsed.protocol); + if (parsed.protocol === "socks5:") { const socksOptions: Record = { type: 5, host: parsed.hostname, - port: Number(normalizePort(parsed.port, parsed.protocol)), + port: Number(port), }; if (parsed.username) socksOptions.userId = decodeURIComponent(parsed.username); if (parsed.password) socksOptions.password = decodeURIComponent(parsed.password); @@ -135,4 +180,4 @@ export function createProxyDispatcher(proxyUrl) { dispatcherCache.set(normalizedUrl, dispatcher); return dispatcher; -} +} \ No newline at end of file