From 4345ffff79854a3b03f59a40867b43a0616106a9 Mon Sep 17 00:00:00 2001 From: openclaw-bot Date: Sun, 1 Feb 2026 17:57:15 +0000 Subject: [PATCH] Fix forwarded headers + preserveHost behavior --- package-lock.json | 1 + src/index.spec.ts | 5 +++-- src/index.ts | 45 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2d984e..9ec33f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -641,6 +641,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz", "integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~5.26.4" } diff --git a/src/index.spec.ts b/src/index.spec.ts index f40d4da..666f771 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -193,8 +193,9 @@ describe('ProxyServer', () => { expect(res.writeHead).toHaveBeenCalledWith(200, 'OK'); expect(res.end).toHaveBeenCalledWith(); - expect(res.body).toContain('x-forwarded-for: example.com'); - expect(res.body).toContain('x-forwarded-proto: http'); + // When preserveHost is not enabled, the proxy should not invent forwarded headers. + // The upstream should receive the target host header. + expect(res.body).toContain(`host: localhost:${port}`); expect(res.body).toContain('GET /test'); server.reset(); diff --git a/src/index.ts b/src/index.ts index 96a884b..c02ae06 100644 --- a/src/index.ts +++ b/src/index.ts @@ -218,7 +218,10 @@ export class ProxyServer extends EventEmitter { } protected matchProxy(req: IncomingMessage) { - const originHost = [req.headers['x-forwarded-for'], req.headers.host].filter(Boolean)[0]; + // IMPORTANT: + // - x-forwarded-for is a client IP chain, NOT a host. + // - routing should use x-forwarded-host (if present) or host. + const originHost = [req.headers['x-forwarded-host'], req.headers.host].filter(Boolean)[0]; const originlUrl = originHost ? new URL('http://' + originHost) : null; const proxyEntry = originlUrl ? this.findProxyEntry(originlUrl.hostname, req.url) : null; @@ -315,10 +318,42 @@ export class ProxyServer extends EventEmitter { } if (proxyEntry.preserveHost) { - proxyRequest.setHeader('host', req.headers.host); - proxyRequest.setHeader('x-forwarded-for', req.headers.host); - proxyRequest.setHeader('x-forwarded-proto', isSsl ? 'https' : 'http'); - proxyRequest.setHeader('forwarded', 'host=' + req.headers.host + ';proto=' + (isSsl ? 'https' : 'http')); + // Preserve the original Host for virtual-hosted upstreams, but keep forwarded headers + // standards-compliant. (Never put a domain into X-Forwarded-For.) + const incomingHost = req.headers.host; + if (incomingHost) { + proxyRequest.setHeader('host', incomingHost); + proxyRequest.setHeader('x-forwarded-host', incomingHost); + } + + const proto = isSsl ? 'https' : 'http'; + proxyRequest.setHeader('x-forwarded-proto', proto); + + const remoteAddress = req.socket?.remoteAddress; + const prior = req.headers['x-forwarded-for']; + const priorValue = Array.isArray(prior) ? prior.join(', ') : prior; + const xff = remoteAddress + ? priorValue + ? `${priorValue}, ${remoteAddress}` + : remoteAddress + : priorValue; + + if (xff) { + proxyRequest.setHeader('x-forwarded-for', xff); + } + + // RFC 7239 Forwarded + // - for= + // - proto=http|https + // - host= + const forwardedParts: string[] = []; + if (remoteAddress) { + const needsQuotes = remoteAddress.includes(':'); + forwardedParts.push(`for=${needsQuotes ? `"${remoteAddress}"` : remoteAddress}`); + } + if (incomingHost) forwardedParts.push(`host=${incomingHost}`); + forwardedParts.push(`proto=${proto}`); + proxyRequest.setHeader('forwarded', forwardedParts.join(';')); } else { const host = targetUrl.hostname + (targetUrl.port ? ':' + targetUrl.port : ''); proxyRequest.setHeader('host', host);