diff --git a/lib/http-proxy/common.ts b/lib/http-proxy/common.ts index 91458cf..0d073ac 100644 --- a/lib/http-proxy/common.ts +++ b/lib/http-proxy/common.ts @@ -81,6 +81,9 @@ export function setupOutgoing( outgoing.method = options.method || req.method; outgoing.headers = { ...req.headers }; + if (req.headers?.[":authority"]) { + outgoing.headers.host = req.headers[":authority"]; + } if (options.headers) { outgoing.headers = { ...outgoing.headers, ...options.headers }; @@ -189,7 +192,8 @@ export function getPort( req: Request, // Return the port number, as a string. ): string { - const res = req.headers.host ? req.headers.host.match(/:(\d+)/) : ""; + const hostHeader = (req.headers[":authority"] as string | undefined) || req.headers.host; + const res = hostHeader ? hostHeader.match(/:(\d+)/) : ""; return res ? res[1] : hasEncryptedConnection(req) ? "443" : "80"; } diff --git a/lib/http-proxy/passes/web-incoming.ts b/lib/http-proxy/passes/web-incoming.ts index 0f1da56..b819c9b 100644 --- a/lib/http-proxy/passes/web-incoming.ts +++ b/lib/http-proxy/passes/web-incoming.ts @@ -69,7 +69,7 @@ export function XHeaders(req: Request, _res: Response, options: ServerOptions) { (req.headers["x-forwarded-" + header] || "") + (req.headers["x-forwarded-" + header] ? "," : "") + values[header]; } - req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers["host"] || ""; + req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers["host"] || ""; } // Does the actual proxying. If `forward` is enabled fires up diff --git a/lib/http-proxy/passes/web-outgoing.ts b/lib/http-proxy/passes/web-outgoing.ts index b65e81e..bda2301 100644 --- a/lib/http-proxy/passes/web-outgoing.ts +++ b/lib/http-proxy/passes/web-outgoing.ts @@ -77,7 +77,7 @@ export function setRedirectHostRewrite( if (options.hostRewrite) { u.host = options.hostRewrite; } else if (options.autoRewrite) { - u.host = req.headers["host"] ?? ""; + u.host = (req.headers[":authority"] as string | undefined) ?? req.headers["host"] ?? ""; } if (options.protocolRewrite) { u.protocol = options.protocolRewrite; @@ -143,7 +143,7 @@ export function writeHeaders( for (const key0 in proxyRes.headers) { let key = key0; - if (_req.httpVersionMajor > 1 && (key === "connection" || key === "keep-alive")) { + if (_req.httpVersionMajor > 1 && (key === "connection" || key === "keep-alive")) { // don't send connection header to http2 client continue; } diff --git a/lib/test/lib/http-proxy-passes-web-incoming.test.ts b/lib/test/lib/http-proxy-passes-web-incoming.test.ts index ba85b37..3286f66 100644 --- a/lib/test/lib/http-proxy-passes-web-incoming.test.ts +++ b/lib/test/lib/http-proxy-passes-web-incoming.test.ts @@ -72,6 +72,15 @@ describe("#XHeaders", () => { host: "192.168.1.2:8080", } as Record, }; + const stubHttp2Request = { + connection: { + remoteAddress: "192.168.1.2", + remotePort: "8080", + }, + headers: { + ':authority': "192.168.1.2:8080", + } as Record, + }; it("set the correct x-forwarded-* headers", () => { // @ts-ignore @@ -79,6 +88,16 @@ describe("#XHeaders", () => { expect(stubRequest.headers["x-forwarded-for"]).toEqual("192.168.1.2"); expect(stubRequest.headers["x-forwarded-port"]).toEqual("8080"); expect(stubRequest.headers["x-forwarded-proto"]).toEqual("http"); + expect(stubRequest.headers["x-forwarded-host"]).toEqual("192.168.1.2:8080"); + }); + + it("set the correct x-forwarded-* headers for http2", () => { + // @ts-ignore + XHeaders(stubHttp2Request, {}, { xfwd: true }); + expect(stubHttp2Request.headers["x-forwarded-for"]).toEqual("192.168.1.2"); + expect(stubHttp2Request.headers["x-forwarded-port"]).toEqual("8080"); + expect(stubHttp2Request.headers["x-forwarded-proto"]).toEqual("http"); + expect(stubHttp2Request.headers["x-forwarded-host"]).toEqual("192.168.1.2:8080"); }); }); diff --git a/lib/test/lib/http-proxy-passes-web-outgoing.test.ts b/lib/test/lib/http-proxy-passes-web-outgoing.test.ts index cb9555b..2c71147 100644 --- a/lib/test/lib/http-proxy-passes-web-outgoing.test.ts +++ b/lib/test/lib/http-proxy-passes-web-outgoing.test.ts @@ -105,6 +105,16 @@ describe.each( ["http://backend.com", url.parse("http://backend.com")])("#setRed "http://ext-auto.com/", ); }); + + it("on " + code + " (http2)", () => { + state.proxyRes.statusCode = code; + state.req.headers[":authority"] = state.req.headers.host; + delete state.req.headers.host; + setRedirectHostRewrite(state.req, {}, state.proxyRes, state.options); + expect(state.proxyRes.headers.location).toEqual( + "http://ext-auto.com/", + ); + }); }); it("not on 200", () => { diff --git a/lib/test/lib/http2-proxy.test.ts b/lib/test/lib/http2-proxy.test.ts new file mode 100644 index 0000000..3e193ca --- /dev/null +++ b/lib/test/lib/http2-proxy.test.ts @@ -0,0 +1,122 @@ +import * as httpProxy from "../.."; +import * as http from "node:http"; +import * as http2 from "node:http2"; +import getPort from "../get-port"; +import { join } from "node:path"; +import { readFileSync } from "node:fs"; +import { describe, it, expect } from 'vitest'; + +const ports: { [port: string]: number } = {}; +let portIndex = -1; +const gen = {} as { port: number }; +Object.defineProperty(gen, "port", { + get: function get() { + portIndex++; + return ports[portIndex]; + }, +}); + +describe("HTTP2 to HTTP", () => { + it("creates some ports", async () => { + for (let n = 0; n < 50; n++) { + ports[n] = await getPort(); + } + }); + + it("should proxy the request, then send back the response", () => new Promise(done => { + const ports = { source: gen.port, proxy: gen.port }; + const source = http + .createServer((req, res) => { + expect(req.method).toEqual("GET"); + expect(req.headers.host?.split(":")[1]).toEqual(`${ports.proxy}`); + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + ports.source); + }) + .listen(ports.source); + + const proxy = httpProxy + .createProxyServer({ + target: "http://127.0.0.1:" + ports.source, + ssl: { + key: readFileSync( + join(__dirname, "..", "fixtures", "agent2-key.pem"), + ), + cert: readFileSync( + join(__dirname, "..", "fixtures", "agent2-cert.pem"), + ), + ciphers: "AES128-GCM-SHA256", + }, + }) + .listen(ports.proxy); + + const client = http2.connect(`https://localhost:${ports.proxy}`) + const req = client.request({ ':path': '/' }); + req.on('response', (headers, _flags) => { + expect(headers[':status']).toEqual(200); + req.setEncoding('utf8'); + req.on('data', (chunk) => { + expect(chunk.toString()).toEqual("Hello from " + ports.source); + }); + req.on('end', () => { + source.close(); + proxy.close(); + done(); + }); + }); + req.end(); + })); +}); + +describe("HTTP2 to HTTP using own server", () => { + it("should proxy the request, then send back the response", () => new Promise(done => { + const ports = { source: gen.port, proxy: gen.port }; + const source = http + .createServer((req, res) => { + expect(req.method).toEqual("GET"); + expect(req.headers.host?.split(":")[1]).toEqual(`${ports.proxy}`); + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + ports.source); + }) + .listen(ports.source); + + const proxy = httpProxy.createServer({ + agent: new http.Agent({ maxSockets: 2 }), + }); + + const ownServer = http2 + .createSecureServer( + { + key: readFileSync( + join(__dirname, "..", "fixtures", "agent2-key.pem"), + ), + cert: readFileSync( + join(__dirname, "..", "fixtures", "agent2-cert.pem"), + ), + ciphers: "AES128-GCM-SHA256", + }, + (req, res) => { + // @ts-expect-error -- ignore type incompatibility + proxy.web(req, res, { + target: "http://127.0.0.1:" + ports.source, + }); + }, + ) + .listen(ports.proxy); + + const client = http2.connect(`https://localhost:${ports.proxy}`) + const req = client.request({ ':path': '/' }); + req.on('response', (headers, _flags) => { + expect(headers[':status']).toEqual(200); + req.setEncoding('utf8'); + req.on('data', (chunk) => { + expect(chunk.toString()).toEqual("Hello from " + ports.source); + }); + req.on('end', () => { + source.close(); + ownServer.close(); + done(); + }); + }); + req.end(); + })); +});