diff --git a/CHANGELOG.md b/CHANGELOG.md index ba6ce8e2..c3c8a420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## next - feat(definePlugin): helper function to create plugins +- chore(package.json): update to httpxy@0.5.3 +- fix(ipv6): preserve credentials when normalizing bracketed IPv6 target string +- fix(ipv6): unspecified IPv6 target hostname (::)" ## [v4.0.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v4.0.0) diff --git a/package.json b/package.json index 793d0484..bd455e0f 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ }, "dependencies": { "debug": "^4.4.3", - "httpxy": "^0.5.1", + "httpxy": "^0.5.3", "is-glob": "^4.0.3", "is-plain-obj": "^4.1.0", "micromatch": "^4.0.8" diff --git a/src/utils/ipv6.ts b/src/utils/ipv6.ts index 4d492834..63f21c30 100644 --- a/src/utils/ipv6.ts +++ b/src/utils/ipv6.ts @@ -34,10 +34,18 @@ function normalizeIPv6ProxyTarget(target: Options['target'], optionName: 'target const targetUrl = toTargetUrl(target); if (targetUrl && isBracketedIPv6Hostname(targetUrl.hostname)) { + const normalizedHostname = normalizeIPv6DestinationHostname(stripBrackets(targetUrl.hostname)); + debug('normalized IPv6 "%s" %s', optionName, target); + const auth = + targetUrl.username || targetUrl.password + ? `${targetUrl.username}:${targetUrl.password}` + : undefined; + return { - hostname: stripBrackets(targetUrl.hostname), + hostname: normalizedHostname, + auth, pathname: targetUrl.pathname, port: targetUrl.port, protocol: targetUrl.protocol, @@ -67,3 +75,13 @@ function isBracketedIPv6Hostname(hostname: string): boolean { function stripBrackets(hostname: string): string { return hostname.replace(/^\[|\]$/g, ''); } + +function normalizeIPv6DestinationHostname(hostname: string): string { + // The unspecified address (::) is not a routable destination for outbound client requests. + // Treat it as loopback so a target like http://[::]:port reaches local IPv6 listeners. + if (hostname === '::') { + debug('normalizing hostname unspecified IPv6 address (::) to loopback (::1)'); + return '::1'; + } + return hostname; +} diff --git a/test/unit/utils/ipv6.spec.ts b/test/unit/utils/ipv6.spec.ts index f256eca3..e85f1374 100644 --- a/test/unit/utils/ipv6.spec.ts +++ b/test/unit/utils/ipv6.spec.ts @@ -47,6 +47,39 @@ describe('normalizeIPv6Targets()', () => { }); }); + it('should normalize unspecified bracketed IPv6 destination to loopback', () => { + const options: Options = { + target: 'http://[::]:8888/api', + }; + + normalizeIPv6LiteralTargets(options); + + expect(options.target).toEqual({ + hostname: '::1', + pathname: '/api', + port: '8888', + protocol: 'http:', + search: '', + }); + }); + + it('should preserve credentials when normalizing bracketed IPv6 target string', () => { + const options: Options = { + target: 'http://user:pass@[::1]:8888/api', + }; + + normalizeIPv6LiteralTargets(options); + + expect(options.target).toEqual({ + auth: 'user:pass', + hostname: '::1', + pathname: '/api', + port: '8888', + protocol: 'http:', + search: '', + }); + }); + it('should normalize bracketed IPv6 target URL into a target object', () => { const options: Options = { target: new URL('http://[::1]:8888/api'), diff --git a/yarn.lock b/yarn.lock index 4582c8ed..9fdf5f9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2532,10 +2532,10 @@ https-proxy-agent@^7.0.6: agent-base "^7.1.2" debug "4" -httpxy@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.5.1.tgz#c0cccd78672995968eee57666c3e3cfa822c2806" - integrity sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A== +httpxy@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.5.3.tgz#e9d4d9b584ec3a2c5d46d7d87635419e780353aa" + integrity sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw== husky@9.1.7: version "9.1.7"