From 21bc0b3063e4400907033dd49aa25bb44343434a Mon Sep 17 00:00:00 2001 From: Steven Chim <655241+chimurai@users.noreply.github.com> Date: Tue, 19 May 2026 20:01:56 +0000 Subject: [PATCH] fix(url): normalizeHost with ipv6 hostname --- src/util/url.ts | 8 ++++++-- test/request-utils.spec.ts | 36 ++++++++++++++++++++++++++++++++-- test/url-normalization.spec.ts | 36 +++++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/util/url.ts b/src/util/url.ts index 2c03cc782..66597f0b0 100644 --- a/src/util/url.ts +++ b/src/util/url.ts @@ -2,6 +2,7 @@ import * as url from 'url'; import * as _ from 'lodash'; import { nthIndexOf } from './util'; +import { isIPv6Address } from './ip-utils'; import { Destination } from '../types'; // Is this URL fully qualified? @@ -92,11 +93,14 @@ export const getDestination = (protocol: string, host: string): Destination => { export const normalizeHost = (protocol: string, host: string) => { const { hostname, port } = getDestination(protocol, host); + const normalizedHostname = isIPv6Address(hostname) + ? `[${hostname}]` + : hostname; if (port === getDefaultPort(protocol)) { - return hostname; + return normalizedHostname; } else { - return `${hostname}:${port}`; + return `${normalizedHostname}:${port}`; } } diff --git a/test/request-utils.spec.ts b/test/request-utils.spec.ts index f0d7a0561..25090306e 100644 --- a/test/request-utils.spec.ts +++ b/test/request-utils.spec.ts @@ -1,8 +1,10 @@ import { Buffer } from 'buffer'; import * as zlib from 'zlib'; +import * as stream from 'stream'; import { expect, nodeOnly } from './test-utils'; -import { buildBodyReader } from '../src/util/request-utils'; +import { buildBodyReader, preprocessRequest } from '../src/util/request-utils'; +import { LastHopEncrypted } from '../src/util/socket-extensions'; nodeOnly(() => { describe("buildBodyReader", () => { @@ -88,4 +90,34 @@ nodeOnly(() => { }); }); -}); \ No newline at end of file + + describe("preprocessRequest", () => { + it('reconstructs valid absolute URLs from bracketed IPv6 host headers', () => { + const req = Object.assign(new stream.PassThrough(), { + method: 'GET', + url: '/api', + headers: { + host: '[::1]:8000' + }, + rawHeaders: ['Host', '[::1]:8000'], + httpVersion: '1.1', + socket: { + [LastHopEncrypted]: false + } + }) as any; + + const result = preprocessRequest(req, { + type: 'request', + serverPort: 45454, + maxBodySize: 1024 + }); + + expect(result).to.not.equal(null); + expect(req.url).to.equal('http://[::1]:8000/api'); + expect(req.destination).to.deep.equal({ + hostname: '::1', + port: 8000 + }); + }); + }); +}); diff --git a/test/url-normalization.spec.ts b/test/url-normalization.spec.ts index ba4589269..88fef2628 100644 --- a/test/url-normalization.spec.ts +++ b/test/url-normalization.spec.ts @@ -1,8 +1,42 @@ -import { normalizeUrl } from '../src/util/url'; +import { normalizeHost, normalizeUrl } from '../src/util/url'; import { expect } from "./test-utils"; describe("URL normalization for matching", () => { + describe("host normalization", () => { + it("should bracket IPv6 hosts with non-default HTTP ports", () => { + expect( + normalizeHost('http', '[::1]:8000') + ).to.equal('[::1]:8000'); + }); + + it("should bracket IPv6 hosts with non-default HTTPS ports", () => { + expect( + normalizeHost('https', '[::1]:8443') + ).to.equal('[::1]:8443'); + }); + + it("should bracket IPv6 hosts when normalizing away the default port", () => { + expect( + normalizeHost('http', '[::1]:80') + ).to.equal('[::1]'); + + expect( + normalizeHost('https', '[::1]:443') + ).to.equal('[::1]'); + }); + + it("should preserve existing formatting for IPv4 and domain hosts", () => { + expect( + normalizeHost('http', '127.0.0.1:8000') + ).to.equal('127.0.0.1:8000'); + + expect( + normalizeHost('https', 'example.com:443') + ).to.equal('example.com'); + }); + }); + it("should do nothing to fully specified URLs", () => { expect( normalizeUrl('https://example.com/abc')