diff --git a/package.json b/package.json index 8139186..696e1a8 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "sse": "^0.0.8", "typescript": "^5.9.3", "unbuild": "^3.6.1", + "undici": "^7.21.0", "vitest": "^4.0.18", "ws": "^8.19.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e120945..6050230 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: unbuild: specifier: ^3.6.1 version: 3.6.1(typescript@5.9.3) + undici: + specifier: ^7.21.0 + version: 7.21.0 vitest: specifier: ^4.0.18 version: 4.0.18(@types/node@25.2.3)(jiti@2.6.1) @@ -2439,6 +2442,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.21.0: + resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} + engines: {node: '>=20.18.1'} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -4967,6 +4974,8 @@ snapshots: undici-types@7.16.0: {} + undici@7.21.0: {} + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 diff --git a/src/_utils.ts b/src/_utils.ts index f378b67..05e3672 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -1,8 +1,8 @@ import httpNative from "node:http"; import httpsNative from "node:https"; import net from "node:net"; -import type tls from "node:tls"; import type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from "./types.ts"; +import type { Http2ServerRequest } from "node:http2"; const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; @@ -11,6 +11,15 @@ const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; */ export const isSSL = /^https|wss/; +/** + * Node.js HTTP/2 accepts pseudo headers and it may conflict + * with request options. + * + * Let's just blacklist those potential conflicting pseudo + * headers. + */ +const HTTP2_HEADER_BLACKLIST = [":method", ":path", ":scheme", ":authority"]; + /** * Copies the right headers from `options` and `req` to * `outgoing` which is then used to fire the proxied @@ -38,7 +47,7 @@ export function setupOutgoing( ca?: string; method?: string; }, - req: httpNative.IncomingMessage, + req: httpNative.IncomingMessage | Http2ServerRequest, forward?: "forward" | "target", ): httpNative.RequestOptions | httpsNative.RequestOptions { outgoing.port = @@ -64,10 +73,23 @@ export function setupOutgoing( outgoing.method = options.method || req.method; outgoing.headers = { ...req.headers }; + // before clean up HTTP/2 blacklist header, we might wanna override host first + if (req.headers?.[":authority"]) { + outgoing.headers.host = req.headers[":authority"] as string; + } + // host override must happen before composing/merging the final outgoing headers + if (options.headers) { outgoing.headers = { ...outgoing.headers, ...options.headers }; } + if (req.httpVersionMajor > 1) { + // ignore potential conflicting HTTP/2 pseudo headers + for (const header of HTTP2_HEADER_BLACKLIST) { + delete outgoing.headers[header]; + } + } + if (options.auth) { outgoing.auth = options.auth; } @@ -181,8 +203,9 @@ export function setupSocket(socket: net.Socket): net.Socket { * * @api private */ -export function getPort(req: httpNative.IncomingMessage): string { - const res = req.headers.host ? req.headers.host.match(/:(\d+)/) : ""; +export function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): string { + const hostHeader = (req.headers[":authority"] as string | undefined) || req.headers.host; + const res = hostHeader ? hostHeader.match(/:(\d+)/) : ""; if (res) { return res[1]!; } @@ -198,11 +221,43 @@ export function getPort(req: httpNative.IncomingMessage): string { * * @api private */ -export function hasEncryptedConnection(req: httpNative.IncomingMessage): boolean { - return Boolean( - // req.connection.pair probably does not exist anymore - (req.connection as tls.TLSSocket).encrypted || (req.connection as any).pair, - ); +export function hasEncryptedConnection( + req: httpNative.IncomingMessage | Http2ServerRequest, +): boolean { + // Since Node.js v16 we now have req.socket + if ("socket" in req) { + /* v8 ignore start */ + + // encrypted is only present in TLS sockets, not plain net sockets + if ("encrypted" in req.socket) { + return req.socket.encrypted; + } + + // "pair" is deprecated and is not typed by @types/node, but it actually hasn't been removed yet + // we can still fall back to "pair" for backward compatibility, but normally not reachable + if ("pair" in req.socket) { + return !!req.socket.pair; + } + /* v8 ignore stop */ + } + + if ("connection" in req) { + /* v8 ignore start */ + + // encrypted is only present in TLS sockets, not plain net sockets + if ("encrypted" in req.connection) { + return req.connection.encrypted; + } + + // "pair" is deprecated and is not typed by @types/node, but it actually hasn't been removed yet + // we can still fall back to "pair" for backward compatibility, but normally not reachable + if ("pair" in req.connection) { + return !!req.connection.pair; + } + /* v8 ignore stop */ + } + + return false; } /** diff --git a/src/middleware/_utils.ts b/src/middleware/_utils.ts index 3704c58..66b5ac6 100644 --- a/src/middleware/_utils.ts +++ b/src/middleware/_utils.ts @@ -2,25 +2,26 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { Socket } from "node:net"; import type { ProxyServer } from "../server.ts"; import type { ProxyServerOptions, ProxyTargetDetailed } from "../types.ts"; +import type { Http2ServerRequest, Http2ServerResponse } from "node:http2"; export type ResOfType = T extends "ws" ? T extends "web" - ? ServerResponse | Socket + ? ServerResponse | Http2ServerResponse | Socket : Socket : T extends "web" - ? ServerResponse + ? ServerResponse | Http2ServerResponse : never; -export type ProxyMiddleware = ( - req: IncomingMessage, +export type ProxyMiddleware = ( + req: IncomingMessage | Http2ServerRequest, res: T, opts: ProxyServerOptions & { target: URL | ProxyTargetDetailed; forward: URL; }, - server: ProxyServer, + server: ProxyServer, head?: Buffer, - callback?: (err: any, req: IncomingMessage, socket: T, url?: any) => void, + callback?: (err: any, req: IncomingMessage | Http2ServerRequest, socket: T, url?: any) => void, ) => void | true; export function defineProxyMiddleware( @@ -30,8 +31,8 @@ export function defineProxyMiddleware { 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 || ""; }); /** diff --git a/src/middleware/web-outgoing.ts b/src/middleware/web-outgoing.ts index 168de65..b7aae38 100644 --- a/src/middleware/web-outgoing.ts +++ b/src/middleware/web-outgoing.ts @@ -7,7 +7,8 @@ const redirectRegex = /^201|30([1278])$/; * If is a HTTP 1.0 request, remove chunk headers */ export const removeChunked = defineProxyOutgoingMiddleware((req, res, proxyRes) => { - if (req.httpVersion === "1.0") { + // HTTP/1.0 and HTTP/2 do not have transfer-encoding: chunked + if (req.httpVersion !== "1.1") { delete proxyRes.headers["transfer-encoding"]; } }); @@ -15,12 +16,17 @@ export const removeChunked = defineProxyOutgoingMiddleware((req, res, proxyRes) /** * If is a HTTP 1.0 request, set the correct connection header * or if connection header not present, then use `keep-alive` + * + * If is a HTTP/2 request, remove connection header no matter what, + * this avoids sending connection header to the underlying http2 client */ export const setConnection = defineProxyOutgoingMiddleware((req, res, proxyRes) => { if (req.httpVersion === "1.0") { proxyRes.headers.connection = req.headers.connection || "close"; - } else if (req.httpVersion !== "2.0" && !proxyRes.headers.connection) { + } else if (req.httpVersionMajor < 2 && !proxyRes.headers.connection) { proxyRes.headers.connection = req.headers.connection || "keep-alive"; + } else if (req.httpVersionMajor >= 2) { + delete proxyRes.headers.connection; } }); @@ -42,8 +48,12 @@ export const setRedirectHostRewrite = defineProxyOutgoingMiddleware( if (options.hostRewrite) { u.host = options.hostRewrite; - } else if (options.autoRewrite && req.headers.host) { - u.host = req.headers.host; + } else if (options.autoRewrite) { + if (req.headers[":authority"]) { + u.host = req.headers[":authority"] as string; + } else if (req.headers.host) { + u.host = req.headers.host; + } } if (options.protocolRewrite) { u.protocol = options.protocolRewrite; @@ -116,13 +126,16 @@ export const writeHeaders = defineProxyOutgoingMiddleware((req, res, proxyRes, o */ export const writeStatusCode = defineProxyOutgoingMiddleware((req, res, proxyRes) => { // From Node.js docs: response.writeHead(statusCode[, statusMessage][, headers]) - if (proxyRes.statusMessage) { - // @ts-expect-error - res.statusCode = proxyRes.statusCode; + + // @ts-expect-error + res.statusCode = proxyRes.statusCode; + + if ( + proxyRes.statusMessage && + // Only HTTP/1.0 and HTTP/1.1 support statusMessage + req.httpVersionMajor < 2 + ) { res.statusMessage = proxyRes.statusMessage; - } else { - // @ts-expect-error - res.statusCode = proxyRes.statusCode; } }); diff --git a/src/server.ts b/src/server.ts index d45e653..e8d8283 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,6 @@ import http from "node:http"; import https from "node:https"; +import http2 from "node:http2"; import { EventEmitter } from "node:events"; import { webIncomingMiddleware } from "./middleware/web-incoming.ts"; import { websocketIncomingMiddleware } from "./middleware/ws-incoming.ts"; @@ -8,8 +9,8 @@ import type { ProxyMiddleware, ResOfType } from "./middleware/_utils.ts"; import type net from "node:net"; export interface ProxyServerEventMap< - Req extends http.IncomingMessage = http.IncomingMessage, - Res extends http.ServerResponse = http.ServerResponse, + Req extends http.IncomingMessage | http2.Http2ServerRequest = http.IncomingMessage, + Res extends http.ServerResponse | http2.Http2ServerResponse = http.ServerResponse, > { error: [err: Error, req?: Req, res?: Res | net.Socket, target?: URL | ProxyTarget]; start: [req: Req, res: Res, target: URL | ProxyTarget]; @@ -32,29 +33,20 @@ export interface ProxyServerEventMap< // eslint-disable-next-line unicorn/prefer-event-target export class ProxyServer< - Req extends http.IncomingMessage = http.IncomingMessage, - Res extends http.ServerResponse = http.ServerResponse, + Req extends http.IncomingMessage | http2.Http2ServerRequest = http.IncomingMessage, + Res extends http.ServerResponse | http2.Http2ServerResponse = http.ServerResponse, > extends EventEmitter> { - private _server?: http.Server | https.Server; + // we use http2.Http2Server to handle HTTP/1.1 HTTPS as well (with allowHTTP1 enabled) + private _server?: http.Server | http2.Http2SecureServer; _webPasses: ProxyMiddleware[] = [...webIncomingMiddleware]; _wsPasses: ProxyMiddleware[] = [...websocketIncomingMiddleware]; options: ProxyServerOptions; - web: ( - req: http.IncomingMessage, - res: http.ServerResponse, - opts?: ProxyServerOptions, - head?: any, - ) => Promise; + web: (req: Req, res: Res, opts?: ProxyServerOptions, head?: any) => Promise; - ws: ( - req: http.IncomingMessage, - socket: net.Socket, - opts: ProxyServerOptions, - head?: any, - ) => Promise; + ws: (req: Req, socket: net.Socket, opts: ProxyServerOptions, head?: any) => Promise; /** * Creates the proxy server with specified options. @@ -76,18 +68,28 @@ export class ProxyServer< * @param hostname - The hostname to listen on */ listen(port: number, hostname?: string) { - const closure = (req: http.IncomingMessage, res: http.ServerResponse) => { - this.web(req, res); + interface ListenerCallback { + ( + req: http.IncomingMessage | http2.Http2ServerRequest, + res: http.ServerResponse | http2.Http2ServerResponse, + ): Promise; + } + + const closure: ListenerCallback = (req, res) => { + return this.web(req as any, res as any); }; - this._server = this.options.ssl - ? https.createServer(this.options.ssl, closure) - : http.createServer(closure); + if (this.options.http2) { + this._server = http2.createSecureServer({ ...this.options.ssl, allowHTTP1: true }, closure); + } else if (this.options.ssl) { + this._server = https.createServer(this.options.ssl, closure); + } else { + this._server = http.createServer(closure); + } if (this.options.ws) { this._server.on("upgrade", (req, socket, head) => { - // @ts-expect-error - this.ws(req, socket, head).catch(() => {}); + this.ws(req, socket, this.options, head).catch(() => {}); }); } @@ -181,12 +183,15 @@ export function createProxyServer(options: ProxyServerOptions = {}) { // --- Internal --- -function _createProxyFn(type: Type, server: ProxyServer) { - type Res = ResOfType; +function _createProxyFn< + Type extends "web" | "ws", + ProxyServerReq extends http.IncomingMessage | http2.Http2ServerRequest, + ProxyServerRes extends http.ServerResponse | http2.Http2ServerResponse, +>(type: Type, server: ProxyServer) { return function ( - this: ProxyServer, - req: http.IncomingMessage, - res: Res, + this: ProxyServer, + req: ProxyServerReq, + res: ResOfType, opts?: ProxyServerOptions, head?: any, ): Promise { @@ -222,11 +227,14 @@ function _createProxyFn(type: Type, server: ProxyServ req, res, requestOptions as ProxyServerOptions & { target: URL; forward: URL }, - server, + server as ProxyServer< + http.IncomingMessage | http2.Http2ServerRequest, + http.ServerResponse | http2.Http2ServerResponse + >, head, (error) => { if (server.listenerCount("error") > 0) { - server.emit("error", error, req, res); + server.emit("error", error, req, res as ProxyServerRes | net.Socket); _resolve(); } else { _reject(error); diff --git a/src/types.ts b/src/types.ts index 5146b00..9bf4f16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,7 +29,11 @@ export interface ProxyServerOptions { forward?: ProxyTarget; /** Object to be passed to http(s).request. */ agent?: any; - /** Object to be passed to https.createServer(). */ + /** Enable HTTP/2 listener, default is `false` */ + http2?: boolean; + /** Object to be passed to https.createServer() + * or http2.createSecureServer() if the `http2` option is enabled + */ ssl?: any; /** If you want to proxy websockets. */ ws?: boolean; diff --git a/test/_stubs.ts b/test/_stubs.ts index 2623a4f..5f1ed01 100644 --- a/test/_stubs.ts +++ b/test/_stubs.ts @@ -22,7 +22,14 @@ export function createOutgoing(): OutgoingOptions { // --- IncomingMessage stubs --- export function stubIncomingMessage(overrides: Record = {}): IncomingMessage { - return { method: "GET", url: "/", headers: {}, ...overrides } as unknown as IncomingMessage; + return { + method: "GET", + url: "/", + headers: {}, + httpVersion: "1.1", + httpVersionMajor: 1, + ...overrides, + } as unknown as IncomingMessage; } // --- ServerResponse stub --- @@ -50,6 +57,6 @@ export function stubMiddlewareOptions(overrides: Record = {}): // --- ProxyServer stub --- -export function stubProxyServer(overrides: Record = {}): ProxyServer { +export function stubProxyServer(overrides: Record = {}): ProxyServer { return overrides as unknown as ProxyServer; } diff --git a/test/_utils.ts b/test/_utils.ts new file mode 100644 index 0000000..74aeb73 --- /dev/null +++ b/test/_utils.ts @@ -0,0 +1,27 @@ +import http from "node:http"; +import https from "node:https"; +import net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as httpProxy from "../src/index.ts"; + +export function listenOn(server: http.Server | https.Server | net.Server): Promise { + return new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + resolve((server.address() as AddressInfo).port); + }); + }); +} + +export function proxyListen( + proxy: ReturnType, +): Promise { + return new Promise((resolve, reject) => { + proxy.listen(0, "127.0.0.1"); + const server = (proxy as any)._server as net.Server; + server.once("error", reject); + server.once("listening", () => { + resolve((server.address() as AddressInfo).port); + }); + }); +} diff --git a/test/http-proxy.test.ts b/test/http-proxy.test.ts index d5b8c77..7c6a79d 100644 --- a/test/http-proxy.test.ts +++ b/test/http-proxy.test.ts @@ -7,29 +7,10 @@ import * as io from "socket.io"; import SSE from "sse"; import ioClient from "socket.io-client"; import type { AddressInfo } from "node:net"; +import { listenOn, proxyListen } from "./_utils.ts"; // Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-test.js -function listenOn(server: http.Server | net.Server): Promise { - return new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - -function proxyListen(proxy: ReturnType): Promise { - return new Promise((resolve, reject) => { - proxy.listen(0, "127.0.0.1"); - const server = (proxy as any)._server as net.Server; - server.once("error", reject); - server.once("listening", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - describe("http-proxy", () => { describe("#createProxyServer", () => { it.skip("should throw without options", () => { diff --git a/test/http2-proxy.test.ts b/test/http2-proxy.test.ts new file mode 100644 index 0000000..024fc63 --- /dev/null +++ b/test/http2-proxy.test.ts @@ -0,0 +1,167 @@ +import * as http from "node:http"; +import * as https from "node:https"; +import * as httpProxy from "../src/index.ts"; +import * as path from "node:path"; +import * as fs from "node:fs"; +import { describe, it, expect, afterAll, beforeAll } from "vitest"; + +import { Agent, fetch } from "undici"; +import { listenOn, proxyListen } from "./_utils.ts"; + +import { inspect } from "node:util"; + +const http1Agent = new Agent({ + allowH2: false, + connect: { + // Allow to use SSL self signed + rejectUnauthorized: false, + }, +}); +const http2Agent = new Agent({ + allowH2: true, + connect: { + // Allow to use SSL self signed + rejectUnauthorized: false, + }, +}); + +describe("http/2 listener", () => { + describe("http2 -> http", async () => { + let source: http.Server; + let sourcePort: number; + let proxy: httpProxy.ProxyServer; + let proxyPort: number; + + beforeAll(async () => { + source = http.createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + sourcePort); + }); + sourcePort = await listenOn(source); + + proxy = httpProxy.createProxyServer({ + target: "http://127.0.0.1:" + sourcePort, + ssl: { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + http2: true, + // Allow to use SSL self signed + secure: false, + }); + proxyPort = await proxyListen(proxy); + }); + + it("target http server should be working", async () => { + try { + const r = await ( + await fetch(`http://127.0.0.1:${sourcePort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } + }); + + it("fetch proxy server over http1", async () => { + try { + const r = await ( + await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } + }); + + it("fetch proxy server over http2", async () => { + try { + const resp = await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http2Agent }); + const r = await resp.text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } + }); + + afterAll(async () => { + // cleans up + await new Promise((resolve) => proxy.close(resolve)); + source.close(); + }); + }); + + describe("http2 -> https", async () => { + let source: https.Server; + let sourcePort: number; + let proxy: httpProxy.ProxyServer; + let proxyPort: number; + + beforeAll(async () => { + source = https.createServer( + { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + ciphers: "AES128-GCM-SHA256", + }, + function (req, res) { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + sourcePort); + }, + ); + + sourcePort = await listenOn(source); + + proxy = httpProxy.createProxyServer({ + target: "https://127.0.0.1:" + sourcePort, + ssl: { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + http2: true, + // Allow to use SSL self signed + secure: false, + }); + + proxyPort = await proxyListen(proxy); + }); + + it("target https server should be working", async () => { + try { + const r = await ( + await fetch(`https://127.0.0.1:${sourcePort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } + }); + + it("fetch proxy server over http1", async () => { + try { + const r = await ( + await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } + }); + + it("fetch proxy server over http2", async () => { + try { + const resp = await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http2Agent }); + const r = await resp.text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } + }); + + afterAll(async () => { + // cleans up + await new Promise((resolve) => proxy.close(resolve)); + source.close(); + }); + }); +}); diff --git a/test/https-proxy.test.ts b/test/https-proxy.test.ts index 71aba33..d9e6c4d 100644 --- a/test/https-proxy.test.ts +++ b/test/https-proxy.test.ts @@ -2,33 +2,12 @@ import { describe, it, expect } from "vitest"; import * as httpProxy from "../src/index.ts"; import http from "node:http"; import https from "node:https"; -import net from "node:net"; import path from "node:path"; import fs from "node:fs"; -import type { AddressInfo } from "node:net"; +import { listenOn, proxyListen } from "./_utils.ts"; // Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-https-proxy-test.js -function listenOn(server: http.Server | https.Server | net.Server): Promise { - return new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - -function proxyListen(proxy: ReturnType): Promise { - return new Promise((resolve, reject) => { - proxy.listen(0, "127.0.0.1"); - const server = (proxy as any)._server as net.Server; - server.once("error", reject); - server.once("listening", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - describe("lib/http-proxy.js", () => { describe("HTTPS #createProxyServer", () => { describe("HTTPS to HTTP", () => { diff --git a/test/middleware/web-incoming.test.ts b/test/middleware/web-incoming.test.ts index 5407992..2260ce3 100644 --- a/test/middleware/web-incoming.test.ts +++ b/test/middleware/web-incoming.test.ts @@ -4,25 +4,16 @@ import * as webPasses from "../../src/middleware/web-incoming.ts"; import * as httpProxy from "../../src/index.ts"; import concat from "concat-stream"; import http from "node:http"; -import type { AddressInfo } from "node:net"; import { stubIncomingMessage, stubServerResponse, stubMiddlewareOptions, stubProxyServer, } from "../_stubs.ts"; +import { listenOn } from "../_utils.ts"; // Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-passes-web-incoming-test.js -function listenOn(server: http.Server): Promise { - return new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - describe("middleware:web-incoming", () => { describe("#deleteLength", () => { it("should change `content-length` for DELETE requests", () => { diff --git a/test/middleware/web-outgoing.test.ts b/test/middleware/web-outgoing.test.ts index 593de73..77152b8 100644 --- a/test/middleware/web-outgoing.test.ts +++ b/test/middleware/web-outgoing.test.ts @@ -104,6 +104,7 @@ describe("middleware:web-outgoing", () => { describe("rewrites location host with autoRewrite", () => { beforeEach(() => { ctx.options.autoRewrite = true; + delete ctx.req.headers[":authority"]; }); for (const code of [201, 301, 302, 307, 308]) { it("on " + code, () => { @@ -118,6 +119,17 @@ describe("middleware:web-outgoing", () => { }); } + it("with HTTP/2 :authority", () => { + ctx.req.headers[":authority"] = "ext-auto.com"; + webOutgoing.setRedirectHostRewrite( + ctx.req, + stubServerResponse(), + ctx.proxyRes, + ctx.options, + ); + expect(ctx.proxyRes.headers.location).to.eql("http://ext-auto.com/"); + }); + it("not on 200", () => { ctx.proxyRes.statusCode = 200; webOutgoing.setRedirectHostRewrite( @@ -274,6 +286,42 @@ describe("middleware:web-outgoing", () => { expect(proxyRes.headers.connection).to.eql("hola"); }); + it("set the right connection (HTTP/1.1) - req.connection", () => { + const proxyRes = { headers: {} as any }; + webOutgoing.setConnection( + { + httpVersion: "1.0", + httpVersionMajor: 1, + headers: { + connection: "hola", + }, + } as any, + {} as any, + proxyRes as any, + {} as any, + ); + + expect(proxyRes.headers.connection).to.eql("hola"); + }); + + it("set the right connection (HTTP/2) - req.connection", () => { + const proxyRes = { headers: {} as any }; + webOutgoing.setConnection( + { + httpVersion: "2.0", + httpVersionMajor: 2, + headers: { + connection: "hola", + }, + } as any, + {} as any, + proxyRes as any, + {} as any, + ); + + expect(proxyRes.headers.connection).to.eql(undefined); + }); + it("set the right connection - `keep-alive`", () => { const proxyRes = stubIncomingMessage({ headers: {} }); webOutgoing.setConnection( @@ -294,6 +342,7 @@ describe("middleware:web-outgoing", () => { webOutgoing.setConnection( stubIncomingMessage({ httpVersion: "2.0", + httpVersionMajor: 2, headers: { connection: "namstey" }, }), stubServerResponse(), @@ -309,6 +358,7 @@ describe("middleware:web-outgoing", () => { webOutgoing.setConnection( stubIncomingMessage({ httpVersion: "2.0", + httpVersionMajor: 2, headers: {}, }), stubServerResponse(),