Skip to content
122 changes: 86 additions & 36 deletions src/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import httpNative from "node:http";
import httpsNative from "node:https";
import type tls from "node:tls";
import type { Url as LegacyURL } from "node:url";
import net from "node:net";
import type {
ProxyServerOptions,
ProxyTarget,
ProxyTargetDetailed,
} from "./types";

const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i;

/**
Expand All @@ -15,19 +26,29 @@ export const isSSL = /^https|wss/;
* common.setupOutgoing(outgoing, options, req)
* // => { host: ..., hostname: ...}
*
* @param {Object} Outgoing Base object to be filled with required properties
* @param {Object} Options Config object passed to the proxy
* @param {ClientRequest} Req Request Object
* @param {String} Forward String to select forward or target
* @param outgoing Base object to be filled with required properties
* @param options Config object passed to the proxy
* @param req Request Object
* @param forward String to select forward or target
*
* @return {Object} Outgoing Object with all required properties set
* @return Outgoing Object with all required properties set
*
* @api private
*/
export function setupOutgoing(outgoing, options, req, forward?) {
export function setupOutgoing(
outgoing: httpNative.RequestOptions & httpsNative.RequestOptions,
options: ProxyServerOptions & {
target: ProxyTarget;
forward?: ProxyTarget;
},
req: httpNative.IncomingMessage,
forward?: "forward" | "target",
): httpNative.RequestOptions | httpsNative.RequestOptions {
outgoing.port =
options[forward || "target"].port ||
(isSSL.test(options[forward || "target"].protocol) ? 443 : 80);
(options[forward || "target"] as URL).port ||
(isSSL.test((options[forward || "target"] as URL).protocol ?? "http")
? 443
: 80);

for (const e of [
"host",
Expand All @@ -40,10 +61,13 @@ export function setupOutgoing(outgoing, options, req, forward?) {
"ca",
"ciphers",
"secureProtocol",
]) {
outgoing[e] = options[forward || "target"][e];
] as const) {
const value = (options[forward || "target"] as ProxyTargetDetailed)[e];
// @ts-expect-error -- this mapping is valid
outgoing[e] = value;
}

// @ts-expect-error - options.method is undocumented
outgoing.method = options.method || req.method;
outgoing.headers = { ...req.headers };

Expand All @@ -55,11 +79,13 @@ export function setupOutgoing(outgoing, options, req, forward?) {
outgoing.auth = options.auth;
}

// @ts-expect-error - options.ca is undocumented
if (options.ca) {
// @ts-expect-error - options.ca is undocumented
outgoing.ca = options.ca;
}

if (isSSL.test(options[forward || "target"].protocol)) {
if (isSSL.test((options[forward || "target"] as URL).protocol ?? "http")) {
outgoing.rejectUnauthorized =
options.secure === undefined ? true : options.secure;
}
Expand All @@ -85,10 +111,10 @@ export function setupOutgoing(outgoing, options, req, forward?) {
const target = options[forward || "target"];
const targetPath =
target && options.prependPath !== false
? target.pathname || target.path || ""
? (target as URL).pathname || (target as LegacyURL).path || ""
: "";

const parsed = new URL(req.url, "http://localhost");
const parsed = new URL(req.url!, "http://localhost");
let outgoingPath = options.toProxy
? req.url
: parsed.pathname + parsed.search || "";
Expand All @@ -103,10 +129,12 @@ export function setupOutgoing(outgoing, options, req, forward?) {

if (options.changeOrigin) {
outgoing.headers.host =
requiresPort(outgoing.port, options[forward || "target"].protocol) &&
!hasPort(outgoing.host)
requiresPort(
outgoing.port,
(options[forward || "target"] as URL).protocol,
) && !hasPort(outgoing.host)
? outgoing.host + ":" + outgoing.port
: outgoing.host;
: (outgoing.host ?? undefined);
}
return outgoing;
}
Expand Down Expand Up @@ -144,14 +172,14 @@ export function joinURL(
* common.setupSocket(socket)
* // => Socket
*
* @param {Socket} Socket instance to setup
* @param socket instance to setup
*
* @return {Socket} Return the configured socket.
* @return Return the configured socket.
*
* @api private
*/

export function setupSocket(socket) {
export function setupSocket(socket: net.Socket): net.Socket {
socket.setTimeout(0);
socket.setNoDelay(true);

Expand All @@ -163,13 +191,13 @@ export function setupSocket(socket) {
/**
* Get the port number from the host. Or guess it based on the connection type.
*
* @param {Request} req Incoming HTTP request.
* @param req Incoming HTTP request.
*
* @return {String} The port number.
* @return The port number.
*
* @api private
*/
export function getPort(req) {
export function getPort(req: httpNative.IncomingMessage): string {
const res = req.headers.host ? req.headers.host.match(/:(\d+)/) : "";
if (res) {
return res[1];
Expand All @@ -180,26 +208,45 @@ export function getPort(req) {
/**
* Check if the request has an encrypted connection.
*
* @param {Request} req Incoming HTTP request.
* @param req Incoming HTTP request.
*
* @return {Boolean} Whether the connection is encrypted or not.
* @return Whether the connection is encrypted or not.
*
* @api private
*/
export function hasEncryptedConnection(req) {
return Boolean(req.connection.encrypted || req.connection.pair);
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,
);
}

/**
* Rewrites or removes the domain of a cookie header
*
* @param {String|Array} Header
* @param {Object} Config, mapping of domain to rewritten domain.
* '*' key to match any domain, null value to remove the domain.
* @param header
* @param config, mapping of domain to rewritten domain.
* '*' key to match any domain, null value to remove the domain.
*
* @api private
*/
export function rewriteCookieProperty(header, config, property) {
export function rewriteCookieProperty(
header: string,
config: Record<string, string>,
property: string,
): string;
export function rewriteCookieProperty(
header: string | string[],
config: Record<string, string>,
property: string,
): string | string[];
export function rewriteCookieProperty(
header: string | string[],
config: Record<string, string>,
property: string,
): string | string[] {
if (Array.isArray(header)) {
return header.map(function (headerElement) {
return rewriteCookieProperty(headerElement, config, property);
Expand All @@ -226,25 +273,28 @@ export function rewriteCookieProperty(header, config, property) {
/**
* Check the host and see if it potentially has a port in it (keep it simple)
*
* @returns {Boolean} Whether we have one or not
* @returns Whether we have one or not
*
* @api private
*/
export function hasPort(host: string) {
return !!~host.indexOf(":");
export function hasPort(host: string | null | undefined): boolean {
return host ? !!~host.indexOf(":") : false;
}

/**
* Check if the port is required for the protocol
*
* Ported from https://github.com/unshiftio/requires-port/blob/master/index.js
*
* @returns {Boolean} Whether the port is required for the protocol
* @returns Whether the port is required for the protocol
*
* @api private
*/
export function requiresPort(_port: string, _protocol: string) {
const protocol = _protocol.split(":")[0];
export function requiresPort(
_port: string | number,
_protocol: string | undefined,
): boolean {
const protocol = _protocol?.split(":")[0];
const port = +_port;

if (!port) return false;
Expand Down
38 changes: 25 additions & 13 deletions src/middleware/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
import type { IncomingMessage, OutgoingMessage } from "node:http";
import type { Socket } from "node:net";
import type { ProxyServer } from "../server";
import type { ProxyServerOptions } from "../types";
import type { ProxyServerOptions, ProxyTargetDetailed } from "../types";

export type ProxyMiddleware = (
export type ResOfType<T extends "web" | "ws"> = T extends "ws"
? T extends "web"
? OutgoingMessage | Socket
: Socket
: T extends "web"
? OutgoingMessage
: never;

export type ProxyMiddleware<T extends OutgoingMessage | Socket> = (
req: IncomingMessage,
res: OutgoingMessage,
opts?: ProxyServerOptions & { target: URL; forward: URL },
server?: ProxyServer,
res: T,
Comment thread
pi0 marked this conversation as resolved.
opts: ProxyServerOptions & {
target: URL | ProxyTargetDetailed;
forward: URL;
},
server: ProxyServer,
head?: Buffer,
callback?: (
err: any,
req: IncomingMessage,
socket: OutgoingMessage,
url?: any,
) => void,
callback?: (err: any, req: IncomingMessage, socket: T, url?: any) => void,
) => void | true;

export function defineProxyMiddleware(m: ProxyMiddleware) {
export function defineProxyMiddleware<
T extends OutgoingMessage | Socket = OutgoingMessage,
>(m: ProxyMiddleware<T>) {
return m;
}

export type ProxyOutgoingMiddleware = (
req: IncomingMessage,
res: OutgoingMessage,
proxyRes: IncomingMessage,
opts?: ProxyServerOptions & { target: URL; forward: URL },
opts: ProxyServerOptions & {
target: URL | ProxyTargetDetailed;
forward: URL;
},
) => void | true;

export function defineProxyOutgoingMiddleware(m: ProxyOutgoingMiddleware) {
Expand Down
32 changes: 18 additions & 14 deletions src/middleware/web-incoming.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import httpNative from "node:http";
import httpsNative from "node:https";
import type { ClientRequest, OutgoingMessage } from "node:http";
import type { ProxyTargetDetailed } from "../types";
import nodeHTTP from "node:http";
import nodeHTTPS from "node:https";
import { getPort, hasEncryptedConnection, setupOutgoing } from "../_utils";
import { webOutgoingMiddleware } from "./web-outgoing";
import { ProxyMiddleware, defineProxyMiddleware } from "./_utils";

const nativeAgents = { http: httpNative, https: httpsNative };
const nativeAgents = { http: nodeHTTP, https: nodeHTTPS };

/**
* Sets `content-length` to '0' if request is of DELETE type.
Expand Down Expand Up @@ -43,7 +45,7 @@ export const XHeaders = defineProxyMiddleware((req, res, options) => {
proto: encrypted ? "https" : "http",
};

for (const header of ["for", "port", "proto"]) {
for (const header of ["for", "port", "proto"] as const) {
req.headers["x-forwarded-" + header] =
(req.headers["x-forwarded-" + header] || "") +
(req.headers["x-forwarded-" + header] ? "," : "") +
Expand Down Expand Up @@ -95,7 +97,7 @@ export const stream = defineProxyMiddleware(
).request(setupOutgoing(options.ssl || {}, options, req));

// Enable developers to modify the proxyReq before headers are sent
proxyReq.on("socket", (socket) => {
proxyReq.on("socket", (_socket) => {
if (server && !proxyReq.getHeader("expect")) {
server.emit("proxyReq", proxyReq, req, res, options);
}
Expand All @@ -119,9 +121,15 @@ export const stream = defineProxyMiddleware(
req.on("error", proxyError);
proxyReq.on("error", proxyError);

function createErrorHandler(proxyReq, url) {
return function proxyError(err) {
if (req.socket.destroyed && err.code === "ECONNRESET") {
function createErrorHandler(
proxyReq: ClientRequest,
url: URL | ProxyTargetDetailed,
) {
return function proxyError(err: Error) {
if (
req.socket.destroyed &&
(err as NodeJS.ErrnoException).code === "ECONNRESET"
) {
server.emit("econnreset", err, req, res, url);
return proxyReq.abort();
}
Expand Down Expand Up @@ -173,9 +181,5 @@ export const stream = defineProxyMiddleware(
},
);

export const webIncomingMiddleware: readonly ProxyMiddleware[] = [
deleteLength,
timeout,
XHeaders,
stream,
] as const;
export const webIncomingMiddleware: readonly ProxyMiddleware<OutgoingMessage>[] =
[deleteLength, timeout, XHeaders, stream] as const;
Loading