diff --git a/lib/http-proxy/common.ts b/lib/http-proxy/common.ts index 7c8c318..3b5abc9 100644 --- a/lib/http-proxy/common.ts +++ b/lib/http-proxy/common.ts @@ -27,6 +27,13 @@ export interface Outgoing extends Outgoing0 { // See https://github.com/http-party/node-http-proxy/issues/1647 const HEADER_BLACKLIST = "trailer"; +const HTTP2_HEADER_BLACKLIST = [ + ':method', + ':path', + ':scheme', + ':authority', +] + // setupOutgoing -- Copies the right headers from `options` and `req` to // `outgoing` which is then used to fire the proxied request by calling // http.request or https.request with outgoing as input. @@ -81,6 +88,12 @@ export function setupOutgoing( } } + if (req.httpVersionMajor > 1) { + for (const header of HTTP2_HEADER_BLACKLIST) { + delete outgoing.headers[header]; + } + } + if (options.auth) { delete outgoing.headers.authorization; outgoing.auth = options.auth; diff --git a/lib/http-proxy/index.ts b/lib/http-proxy/index.ts index a04dce7..82faea8 100644 --- a/lib/http-proxy/index.ts +++ b/lib/http-proxy/index.ts @@ -1,5 +1,5 @@ import * as http from "node:http"; -import * as https from "node:https"; +import * as http2 from "node:http2"; import * as net from "node:net"; import { WEB_PASSES } from "./passes/web-incoming"; import { WS_PASSES } from "./passes/ws-incoming"; @@ -220,7 +220,7 @@ export class ProxyServer['web']>; private wsPasses: Array['ws']>; - private _server?: http.Server | https.Server | null; + private _server?: http.Server | http2.Http2SecureServer | null; /** * Creates the proxy server with specified options. @@ -367,13 +367,14 @@ export class ProxyServer { log("listen", { port, hostname }); - const requestListener = (req: InstanceType, res: InstanceType) => { - this.web(req, res); + const requestListener = (req: InstanceType | http2.Http2ServerRequest, res: InstanceType |http2.Http2ServerResponse) => { + this.web(req as InstanceType, res as InstanceType); }; - this._server = this.options.ssl - ? https.createServer(this.options.ssl, requestListener) - : http.createServer(requestListener); + this._server = this.options.ssl ? http2.createSecureServer( + { ...this.options.ssl, allowHTTP1: true }, + requestListener + ) : http.createServer(requestListener); if (this.options.ws) { this._server.on("upgrade", (req: InstanceType, socket, head) => { diff --git a/lib/http-proxy/passes/web-outgoing.ts b/lib/http-proxy/passes/web-outgoing.ts index 8f81cfd..fa39569 100644 --- a/lib/http-proxy/passes/web-outgoing.ts +++ b/lib/http-proxy/passes/web-outgoing.ts @@ -143,6 +143,10 @@ export function writeHeaders( for (const key0 in proxyRes.headers) { let key = key0; + if (_req.httpVersionMajor > 1 && key === "connection") { + // don't send connection header to http2 client + continue; + } const header = proxyRes.headers[key]; if (preserveHeaderKeyCase && rawHeaderKeyMap) { key = rawHeaderKeyMap[key] ?? key; @@ -158,11 +162,10 @@ export function writeStatusCode( proxyRes: ProxyResponse, ) { // From Node.js docs: response.writeHead(statusCode[, statusMessage][, headers]) - if (proxyRes.statusMessage) { - res.statusCode = proxyRes.statusCode!; + res.statusCode = proxyRes.statusCode!; + + if (proxyRes.statusMessage && _req.httpVersionMajor === 1) { res.statusMessage = proxyRes.statusMessage; - } else { - res.statusCode = proxyRes.statusCode!; } } diff --git a/lib/test/http/proxy-http2-to-http.test.ts b/lib/test/http/proxy-http2-to-http.test.ts new file mode 100644 index 0000000..1a59d56 --- /dev/null +++ b/lib/test/http/proxy-http2-to-http.test.ts @@ -0,0 +1,68 @@ +/* +pnpm test proxy-https-to-http.test.ts +*/ + +import * as http from "node:http"; +import * as httpProxy from "../.."; +import getPort from "../get-port"; +import { join } from "node:path"; +import { readFile } from "node:fs/promises"; +import fetch from "node-fetch"; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { Agent, setGlobalDispatcher } from "undici"; + +setGlobalDispatcher(new Agent({ + allowH2: true +})); + + +const fixturesDir = join(__dirname, "..", "fixtures"); + +describe("Basic example of proxying over HTTPS to a target HTTP server", () => { + let ports: Record<'http' | 'proxy', number>; + beforeAll(async () => { + ports = { http: await getPort(), proxy: await getPort() }; + }); + + const servers: any = {}; + + it("Create the target HTTP server", async () => { + servers.http = http + .createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("hello http over https\n"); + res.end(); + }) + .listen(ports.http); + }); + + it("Create the HTTPS proxy server", async () => { + servers.proxy = httpProxy + .createServer({ + target: { + host: "localhost", + port: ports.http, + }, + ssl: { + key: await readFile(join(fixturesDir, "agent2-key.pem"), "utf8"), + cert: await readFile(join(fixturesDir, "agent2-cert.pem"), "utf8"), + }, + }) + .listen(ports.proxy); + }); + + it("Use fetch to test non-https server", async () => { + const r = await (await fetch(`http://localhost:${ports.http}`)).text(); + expect(r).toContain("hello http over https"); + }); + + it("Use fetch to test the ACTUAL https server", async () => { + const r = await (await fetch(`https://localhost:${ports.proxy}`)).text(); + expect(r).toContain("hello http over https"); + }); + + afterAll(async () => { + // cleans up + Object.values(servers).map((x: any) => x?.close()); + }); +}); diff --git a/lib/test/http/proxy-http2-to-https.test.ts b/lib/test/http/proxy-http2-to-https.test.ts new file mode 100644 index 0000000..290fc10 --- /dev/null +++ b/lib/test/http/proxy-http2-to-https.test.ts @@ -0,0 +1,69 @@ +/* +pnpm test proxy-https-to-https.test.ts + +*/ + +import * as https from "node:https"; +import * as httpProxy from "../.."; +import getPort from "../get-port"; +import { join } from "node:path"; +import { readFile } from "node:fs/promises"; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { Agent, setGlobalDispatcher } from "undici"; + +setGlobalDispatcher(new Agent({ + allowH2: true +})); + +const fixturesDir = join(__dirname, "..", "fixtures"); + +describe("Basic example of proxying over HTTPS to a target HTTPS server", () => { + let ports: Record<'https' | 'proxy', number>; + beforeAll(async () => { + // Gets ports + ports = { https: await getPort(), proxy: await getPort() }; + }); + + const servers: any = {}; + let ssl: { key: string; cert: string }; + + it("Create the target HTTPS server", async () => { + ssl = { + key: await readFile(join(fixturesDir, "agent2-key.pem"), "utf8"), + cert: await readFile(join(fixturesDir, "agent2-cert.pem"), "utf8"), + }; + servers.https = https + .createServer(ssl, (_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("hello over https\n"); + res.end(); + }) + .listen(ports.https); + }); + + it("Create the HTTPS proxy server", async () => { + servers.proxy = httpProxy + .createServer({ + target: `https://localhost:${ports.https}`, + ssl, + // without secure false, clients will fail and this is broken: + secure: false, + }) + .listen(ports.proxy); + }); + + it("Use fetch to test direct non-proxied https server", async () => { + const r = await (await fetch(`https://localhost:${ports.https}`)).text(); + expect(r).toContain("hello over https"); + }); + + it("Use fetch to test the proxy server", async () => { + const r = await (await fetch(`https://localhost:${ports.proxy}`)).text(); + expect(r).toContain("hello over https"); + }); + + afterAll(async () => { + // cleanup + Object.values(servers).map((x: any) => x?.close()); + }); +}); diff --git a/package.json b/package.json index 2cd6860..e633ed5 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "typescript": "^5.8.3", + "undici": "^7.16.0", "vitest": "^3.2.4", "ws": "^8.18.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f6ef6d..2d3557b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 + undici: + specifier: ^7.16.0 + version: 7.16.0 vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.12)(@vitest/browser@3.2.4) @@ -1200,6 +1203,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.16.0: + resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} + engines: {node: '>=20.18.1'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -2421,6 +2428,8 @@ snapshots: undici-types@6.21.0: {} + undici@7.16.0: {} + unpipe@1.0.0: {} util-deprecate@1.0.2: {}