Skip to content

Commit bb521f0

Browse files
author
vhess
committed
feat: add incoming http2 support
1 parent 2082faa commit bb521f0

File tree

7 files changed

+175
-11
lines changed

7 files changed

+175
-11
lines changed

lib/http-proxy/common.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ export interface Outgoing extends Outgoing0 {
2727
// See https://github.com/http-party/node-http-proxy/issues/1647
2828
const HEADER_BLACKLIST = "trailer";
2929

30+
const HTTP2_HEADER_BLACKLIST = [
31+
':method',
32+
':path',
33+
':scheme',
34+
':authority',
35+
]
36+
3037
// setupOutgoing -- Copies the right headers from `options` and `req` to
3138
// `outgoing` which is then used to fire the proxied request by calling
3239
// http.request or https.request with outgoing as input.
@@ -81,6 +88,12 @@ export function setupOutgoing(
8188
}
8289
}
8390

91+
if (req.httpVersionMajor > 1) {
92+
for (const header of HTTP2_HEADER_BLACKLIST) {
93+
delete outgoing.headers[header];
94+
}
95+
}
96+
8497
if (options.auth) {
8598
delete outgoing.headers.authorization;
8699
outgoing.auth = options.auth;

lib/http-proxy/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as http from "node:http";
2-
import * as https from "node:https";
2+
import * as http2 from "node:http2";
33
import * as net from "node:net";
44
import { WEB_PASSES } from "./passes/web-incoming";
55
import { WS_PASSES } from "./passes/ws-incoming";
@@ -220,7 +220,7 @@ export class ProxyServer<TIncomingMessage extends typeof http.IncomingMessage =
220220
private options: ServerOptions;
221221
private webPasses: Array<PassFunctions<TIncomingMessage, TServerResponse, TError>['web']>;
222222
private wsPasses: Array<PassFunctions<TIncomingMessage, TServerResponse, TError>['ws']>;
223-
private _server?: http.Server<TIncomingMessage, TServerResponse> | https.Server<TIncomingMessage, TServerResponse> | null;
223+
private _server?: http.Server<TIncomingMessage, TServerResponse> | http2.Http2SecureServer<TIncomingMessage, TServerResponse> | null;
224224

225225
/**
226226
* Creates the proxy server with specified options.
@@ -367,13 +367,14 @@ export class ProxyServer<TIncomingMessage extends typeof http.IncomingMessage =
367367
listen = (port: number, hostname?: string) => {
368368
log("listen", { port, hostname });
369369

370-
const requestListener = (req: InstanceType<TIncomingMessage>, res: InstanceType<TServerResponse>) => {
371-
this.web(req, res);
370+
const requestListener = (req: InstanceType<TIncomingMessage> | http2.Http2ServerRequest, res: InstanceType<TServerResponse> |http2.Http2ServerResponse) => {
371+
this.web(req as InstanceType<TIncomingMessage>, res as InstanceType<TServerResponse>);
372372
};
373373

374-
this._server = this.options.ssl
375-
? https.createServer<TIncomingMessage, TServerResponse>(this.options.ssl, requestListener)
376-
: http.createServer<TIncomingMessage, TServerResponse>(requestListener);
374+
this._server = this.options.ssl ? http2.createSecureServer(
375+
{ ...this.options.ssl, allowHTTP1: true },
376+
requestListener
377+
) : http.createServer<TIncomingMessage, TServerResponse>(requestListener);
377378

378379
if (this.options.ws) {
379380
this._server.on("upgrade", (req: InstanceType<TIncomingMessage>, socket, head) => {

lib/http-proxy/passes/web-outgoing.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ export function writeHeaders(
143143

144144
for (const key0 in proxyRes.headers) {
145145
let key = key0;
146+
if (_req.httpVersionMajor > 1 && key === "connection") {
147+
// don't send connection header to http2 client
148+
continue;
149+
}
146150
const header = proxyRes.headers[key];
147151
if (preserveHeaderKeyCase && rawHeaderKeyMap) {
148152
key = rawHeaderKeyMap[key] ?? key;
@@ -158,11 +162,10 @@ export function writeStatusCode(
158162
proxyRes: ProxyResponse,
159163
) {
160164
// From Node.js docs: response.writeHead(statusCode[, statusMessage][, headers])
161-
if (proxyRes.statusMessage) {
162-
res.statusCode = proxyRes.statusCode!;
165+
res.statusCode = proxyRes.statusCode!;
166+
167+
if (proxyRes.statusMessage && _req.httpVersionMajor === 1) {
163168
res.statusMessage = proxyRes.statusMessage;
164-
} else {
165-
res.statusCode = proxyRes.statusCode!;
166169
}
167170
}
168171

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
pnpm test proxy-https-to-http.test.ts
3+
*/
4+
5+
import * as http from "node:http";
6+
import * as httpProxy from "../..";
7+
import getPort from "../get-port";
8+
import { join } from "node:path";
9+
import { readFile } from "node:fs/promises";
10+
import fetch from "node-fetch";
11+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
12+
import { Agent, setGlobalDispatcher } from "undici";
13+
14+
setGlobalDispatcher(new Agent({
15+
allowH2: true
16+
}));
17+
18+
19+
const fixturesDir = join(__dirname, "..", "fixtures");
20+
21+
describe("Basic example of proxying over HTTPS to a target HTTP server", () => {
22+
let ports: Record<'http' | 'proxy', number>;
23+
beforeAll(async () => {
24+
ports = { http: await getPort(), proxy: await getPort() };
25+
});
26+
27+
const servers: any = {};
28+
29+
it("Create the target HTTP server", async () => {
30+
servers.http = http
31+
.createServer((_req, res) => {
32+
res.writeHead(200, { "Content-Type": "text/plain" });
33+
res.write("hello http over https\n");
34+
res.end();
35+
})
36+
.listen(ports.http);
37+
});
38+
39+
it("Create the HTTPS proxy server", async () => {
40+
servers.proxy = httpProxy
41+
.createServer({
42+
target: {
43+
host: "localhost",
44+
port: ports.http,
45+
},
46+
ssl: {
47+
key: await readFile(join(fixturesDir, "agent2-key.pem"), "utf8"),
48+
cert: await readFile(join(fixturesDir, "agent2-cert.pem"), "utf8"),
49+
},
50+
})
51+
.listen(ports.proxy);
52+
});
53+
54+
it("Use fetch to test non-https server", async () => {
55+
const r = await (await fetch(`http://localhost:${ports.http}`)).text();
56+
expect(r).toContain("hello http over https");
57+
});
58+
59+
it("Use fetch to test the ACTUAL https server", async () => {
60+
const r = await (await fetch(`https://localhost:${ports.proxy}`)).text();
61+
expect(r).toContain("hello http over https");
62+
});
63+
64+
afterAll(async () => {
65+
// cleans up
66+
Object.values(servers).map((x: any) => x?.close());
67+
});
68+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
pnpm test proxy-https-to-https.test.ts
3+
4+
*/
5+
6+
import * as https from "node:https";
7+
import * as httpProxy from "../..";
8+
import getPort from "../get-port";
9+
import { join } from "node:path";
10+
import { readFile } from "node:fs/promises";
11+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
12+
import { Agent, setGlobalDispatcher } from "undici";
13+
14+
setGlobalDispatcher(new Agent({
15+
allowH2: true
16+
}));
17+
18+
const fixturesDir = join(__dirname, "..", "fixtures");
19+
20+
describe("Basic example of proxying over HTTPS to a target HTTPS server", () => {
21+
let ports: Record<'https' | 'proxy', number>;
22+
beforeAll(async () => {
23+
// Gets ports
24+
ports = { https: await getPort(), proxy: await getPort() };
25+
});
26+
27+
const servers: any = {};
28+
let ssl: { key: string; cert: string };
29+
30+
it("Create the target HTTPS server", async () => {
31+
ssl = {
32+
key: await readFile(join(fixturesDir, "agent2-key.pem"), "utf8"),
33+
cert: await readFile(join(fixturesDir, "agent2-cert.pem"), "utf8"),
34+
};
35+
servers.https = https
36+
.createServer(ssl, (_req, res) => {
37+
res.writeHead(200, { "Content-Type": "text/plain" });
38+
res.write("hello over https\n");
39+
res.end();
40+
})
41+
.listen(ports.https);
42+
});
43+
44+
it("Create the HTTPS proxy server", async () => {
45+
servers.proxy = httpProxy
46+
.createServer({
47+
target: `https://localhost:${ports.https}`,
48+
ssl,
49+
// without secure false, clients will fail and this is broken:
50+
secure: false,
51+
})
52+
.listen(ports.proxy);
53+
});
54+
55+
it("Use fetch to test direct non-proxied https server", async () => {
56+
const r = await (await fetch(`https://localhost:${ports.https}`)).text();
57+
expect(r).toContain("hello over https");
58+
});
59+
60+
it("Use fetch to test the proxy server", async () => {
61+
const r = await (await fetch(`https://localhost:${ports.proxy}`)).text();
62+
expect(r).toContain("hello over https");
63+
});
64+
65+
afterAll(async () => {
66+
// cleanup
67+
Object.values(servers).map((x: any) => x?.close());
68+
});
69+
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"socket.io": "^4.8.1",
5151
"socket.io-client": "^4.8.1",
5252
"typescript": "^5.8.3",
53+
"undici": "^7.16.0",
5354
"vitest": "^3.2.4",
5455
"ws": "^8.18.2"
5556
},

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)