http-mitm-proxy
Version:
HTTP Man In The Middle (MITM) Proxy
1,444 lines (1,369 loc) • 44 kB
text/typescript
import async from "async";
import type { AddressInfo } from "net";
import net from "net";
import type {
Server as HTTPServer,
IncomingHttpHeaders,
IncomingMessage,
ServerResponse,
} from "http";
import http from "http";
import type { Server, ServerOptions } from "https";
import https from "https";
import fs from "fs";
import path from "path";
import type { WebSocket as WebSocketType } from "ws";
import WebSocket, { WebSocketServer } from "ws";
import url from "url";
import semaphore from "semaphore";
import ca from "./ca";
import { ProxyFinalResponseFilter } from "./ProxyFinalResponseFilter";
import { ProxyFinalRequestFilter } from "./ProxyFinalRequestFilter";
import { v4 as uuid } from "uuid";
import gunzip from "./middleware/gunzip";
import wildcard from "./middleware/wildcard";
import type {
ICertDetails,
IContext,
IProxy,
IProxyOptions,
ErrorCallback,
ICertficateContext,
ICreateServerCallback,
IProxySSLServer,
IWebSocketContext,
OnCertificateRequiredCallback,
OnConnectParams,
OnErrorParams,
OnRequestDataParams,
OnRequestParams,
OnWebSocketCloseParams,
OnWebSocketErrorParams,
OnWebSocketFrameParams,
OnWebSocketMessageParams,
OnWebsocketRequestParams,
OnWebSocketSendParams,
IWebSocketCallback,
OnRequestDataCallback,
} from "./types";
import type stream from "node:stream";
export { wildcard, gunzip };
type HandlerType<T extends (...args: any[]) => any> = Array<Parameters<T>[0]>;
interface WebSocketFlags {
mask?: boolean | undefined;
binary?: boolean | undefined;
compress?: boolean | undefined;
fin?: boolean | undefined;
}
export class Proxy implements IProxy {
ca!: ca;
connectRequests: Record<string, http.IncomingMessage> = {};
forceSNI!: boolean;
httpAgent!: http.Agent;
httpHost?: string;
httpPort!: number;
httpServer: HTTPServer | undefined;
httpsAgent!: https.Agent;
httpsPort?: number;
httpsServer: Server | undefined;
keepAlive!: boolean;
onConnectHandlers: HandlerType<IProxy["onConnect"]>;
onErrorHandlers: HandlerType<IProxy["onError"]>;
onRequestDataHandlers: HandlerType<IProxy["onRequestData"]>;
onRequestEndHandlers: HandlerType<IProxy["onRequestEnd"]>;
onRequestHandlers: HandlerType<IProxy["onRequest"]>;
onRequestHeadersHandlers: HandlerType<IProxy["onRequestHeaders"]>;
onResponseDataHandlers: HandlerType<IProxy["onResponseData"]>;
onResponseEndHandlers: HandlerType<IProxy["onResponseEnd"]>;
onResponseHandlers: HandlerType<IProxy["onResponse"]>;
onResponseHeadersHandlers: HandlerType<IProxy["onResponseHeaders"]>;
onWebSocketCloseHandlers: HandlerType<IProxy["onWebSocketClose"]>;
onWebSocketConnectionHandlers: HandlerType<IProxy["onWebSocketConnection"]>;
onWebSocketErrorHandlers: HandlerType<IProxy["onWebSocketError"]>;
onWebSocketFrameHandlers: HandlerType<IProxy["onWebSocketFrame"]>;
options!: IProxyOptions;
responseContentPotentiallyModified: boolean;
sslCaDir!: string;
sslSemaphores: Record<string, semaphore.Semaphore> = {};
sslServers: Record<string, IProxySSLServer> = {};
timeout!: number;
wsServer: WebSocketServer | undefined;
wssServer: WebSocketServer | undefined;
static wildcard = wildcard;
static gunzip = gunzip;
constructor() {
this.onConnectHandlers = [];
this.onRequestHandlers = [];
this.onRequestHeadersHandlers = [];
this.onWebSocketConnectionHandlers = [];
this.onWebSocketFrameHandlers = [];
this.onWebSocketCloseHandlers = [];
this.onWebSocketErrorHandlers = [];
this.onErrorHandlers = [];
this.onRequestDataHandlers = [];
this.onRequestEndHandlers = [];
this.onResponseHandlers = [];
this.onResponseHeadersHandlers = [];
this.onResponseDataHandlers = [];
this.onResponseEndHandlers = [];
this.responseContentPotentiallyModified = false;
}
listen(options: IProxyOptions, callback: ErrorCallback = () => undefined) {
const self = this;
this.options = options || {};
this.httpPort = options.port || options.port === 0 ? options.port : 8080;
this.httpHost = options.host || "localhost";
this.timeout = options.timeout || 0;
this.keepAlive = !!options.keepAlive;
this.httpAgent =
typeof options.httpAgent !== "undefined"
? options.httpAgent
: new http.Agent({ keepAlive: this.keepAlive });
this.httpsAgent =
typeof options.httpsAgent !== "undefined"
? options.httpsAgent
: new https.Agent({ keepAlive: this.keepAlive });
this.forceSNI = !!options.forceSNI;
if (this.forceSNI) {
console.info("SNI enabled. Clients not supporting SNI may fail");
}
this.httpsPort = this.forceSNI ? options.httpsPort : undefined;
this.sslCaDir =
options.sslCaDir || path.resolve(process.cwd(), ".http-mitm-proxy");
ca.create(this.sslCaDir, (err, ca) => {
if (err) {
return callback(err);
}
self.ca = ca;
self.sslServers = {};
self.sslSemaphores = {};
self.connectRequests = {};
self.httpServer = http.createServer();
self.httpServer!.timeout = self.timeout;
self.httpServer!.on("connect", self._onHttpServerConnect.bind(self));
self.httpServer!.on(
"request",
self._onHttpServerRequest.bind(self, false)
);
self.wsServer = new WebSocketServer({ server: self.httpServer });
self.wsServer.on(
"error",
self._onError.bind(self, "HTTP_SERVER_ERROR", null)
);
self.wsServer.on("connection", (ws, req) => {
ws.upgradeReq = req;
self._onWebSocketServerConnect.call(self, false, ws, req);
});
const listenOptions = {
host: self.httpHost,
port: self.httpPort,
};
if (self.forceSNI) {
// start the single HTTPS server now
self._createHttpsServer({}, (port, httpsServer, wssServer) => {
console.debug(`https server started on ${port}`);
self.httpsServer = httpsServer;
self.wssServer = wssServer;
self.httpsPort = port;
self.httpServer!.listen(listenOptions, () => {
self.httpPort = (self.httpServer!.address() as AddressInfo).port;
callback();
});
});
} else {
self.httpServer.listen(listenOptions, () => {
self.httpPort = (self.httpServer!.address() as AddressInfo).port;
callback();
});
}
});
return this;
}
_createHttpsServer(
options: ServerOptions & { hosts?: string[] },
callback: ICreateServerCallback
) {
const httpsServer = https.createServer({
...options,
} as ServerOptions);
httpsServer.timeout = this.timeout;
httpsServer.on(
"error",
this._onError.bind(this, "HTTPS_SERVER_ERROR", null)
);
httpsServer.on(
"clientError",
this._onError.bind(this, "HTTPS_CLIENT_ERROR", null)
);
httpsServer.on("connect", this._onHttpServerConnect.bind(this));
httpsServer.on("request", this._onHttpServerRequest.bind(this, true));
const self = this;
const wssServer = new WebSocketServer({ server: httpsServer });
wssServer.on("connection", (ws, req) => {
ws.upgradeReq = req;
self._onWebSocketServerConnect.call(self, true, ws, req);
});
// Using listenOptions to bind the server to a particular IP if requested via options.host
// port 0 to get the first available port
const listenOptions = {
port: 0,
host: "0.0.0.0",
};
if (this.httpsPort && !options.hosts) {
listenOptions.port = this.httpsPort;
}
if (this.httpHost) {
listenOptions.host = this.httpHost;
}
httpsServer.listen(listenOptions, () => {
if (callback) {
callback(
(httpsServer.address() as AddressInfo).port,
httpsServer,
wssServer
);
}
});
}
close() {
this.httpServer!.close();
delete this.httpServer;
if (this.httpsServer) {
this.httpsServer.close();
delete this.httpsServer;
delete this.wssServer;
this.sslServers = {};
}
if (this.sslServers) {
for (const srvName of Object.keys(this.sslServers)) {
const server = this.sslServers[srvName].server;
if (server) {
server.close();
}
delete this.sslServers[srvName];
}
}
return this;
}
onError(fn: OnErrorParams) {
this.onErrorHandlers.push(fn);
return this;
}
onConnect(fn: OnConnectParams) {
this.onConnectHandlers.push(fn);
return this;
}
onRequestHeaders(fn: OnRequestParams) {
this.onRequestHeadersHandlers.push(fn);
return this;
}
onRequest(fn: OnRequestParams) {
this.onRequestHandlers.push(fn);
return this;
}
onWebSocketConnection(fn: OnWebsocketRequestParams) {
this.onWebSocketConnectionHandlers.push(fn);
return this;
}
onWebSocketSend(fn: OnWebSocketSendParams) {
this.onWebSocketFrameHandlers.push(
function (ctx, type, fromServer, data, flags, callback) {
if (!fromServer && type === "message") {
return this(ctx, data, flags, callback);
} else {
callback(null, data, flags);
}
}.bind(fn)
);
return this;
}
onWebSocketMessage(fn: OnWebSocketMessageParams) {
this.onWebSocketFrameHandlers.push(
function (ctx, type, fromServer, data, flags, callback) {
if (fromServer && type === "message") {
return this(ctx, data, flags, callback);
} else {
callback(null, data, flags);
}
}.bind(fn)
);
return this;
}
onWebSocketFrame(fn: OnWebSocketFrameParams) {
this.onWebSocketFrameHandlers.push(fn);
return this;
}
onWebSocketClose(fn: OnWebSocketCloseParams) {
this.onWebSocketCloseHandlers.push(fn);
return this;
}
onWebSocketError(fn: OnWebSocketErrorParams) {
this.onWebSocketErrorHandlers.push(fn);
return this;
}
onRequestData(fn: OnRequestDataParams) {
this.onRequestDataHandlers.push(fn);
return this;
}
onRequestEnd(fn: OnRequestParams) {
this.onRequestEndHandlers.push(fn);
return this;
}
onResponse(fn: OnRequestParams) {
this.onResponseHandlers.push(fn);
return this;
}
onResponseHeaders(fn: OnRequestParams) {
this.onResponseHeadersHandlers.push(fn);
return this;
}
onResponseData(fn: OnRequestDataParams) {
this.onResponseDataHandlers.push(fn);
this.responseContentPotentiallyModified = true;
return this;
}
onResponseEnd(fn: OnRequestParams) {
this.onResponseEndHandlers.push(fn);
return this;
}
use(mod) {
if (mod.onError) {
this.onError(mod.onError);
}
if (mod.onCertificateRequired) {
this.onCertificateRequired = mod.onCertificateRequired;
}
if (mod.onCertificateMissing) {
this.onCertificateMissing = mod.onCertificateMissing;
}
if (mod.onConnect) {
this.onConnect(mod.onConnect);
}
if (mod.onRequest) {
this.onRequest(mod.onRequest);
}
if (mod.onRequestHeaders) {
this.onRequestHeaders(mod.onRequestHeaders);
}
if (mod.onRequestData) {
this.onRequestData(mod.onRequestData);
}
if (mod.onResponse) {
this.onResponse(mod.onResponse);
}
if (mod.onResponseHeaders) {
this.onResponseHeaders(mod.onResponseHeaders);
}
if (mod.onResponseData) {
this.onResponseData(mod.onResponseData);
}
if (mod.onWebSocketConnection) {
this.onWebSocketConnection(mod.onWebSocketConnection);
}
if (mod.onWebSocketSend) {
this.onWebSocketFrame(
function (ctx, type, fromServer, data, flags, callback) {
if (!fromServer && type === "message") {
return this(ctx, data, flags, callback);
} else {
callback(null, data, flags);
}
}.bind(mod.onWebSocketSend)
);
}
if (mod.onWebSocketMessage) {
this.onWebSocketFrame(
function (ctx, type, fromServer, data, flags, callback) {
if (fromServer && type === "message") {
return this(ctx, data, flags, callback);
} else {
callback(null, data, flags);
}
}.bind(mod.onWebSocketMessage)
);
}
if (mod.onWebSocketFrame) {
this.onWebSocketFrame(mod.onWebSocketFrame);
}
if (mod.onWebSocketClose) {
this.onWebSocketClose(mod.onWebSocketClose);
}
if (mod.onWebSocketError) {
this.onWebSocketError(mod.onWebSocketError);
}
return this;
}
// Since node 0.9.9, ECONNRESET on sockets are no longer hidden
_onSocketError(socketDescription: string, err: NodeJS.ErrnoException) {
if (err.errno === -54 || err.code === "ECONNRESET") {
console.debug(`Got ECONNRESET on ${socketDescription}, ignoring.`);
} else {
this._onError(`${socketDescription}_ERROR`, null, err);
}
}
_onHttpServerConnect(
req: http.IncomingMessage,
socket: stream.Duplex,
head: Buffer
) {
const self = this;
socket.on(
"error",
self._onSocketError.bind(self, "CLIENT_TO_PROXY_SOCKET")
);
// you can forward HTTPS request directly by adding custom CONNECT method handler
return async.forEach(
self.onConnectHandlers,
(fn, callback) => fn.call(self, req, socket, head, callback),
(err) => {
if (err) {
return self._onError("ON_CONNECT_ERROR", null, err);
}
// we need first byte of data to detect if request is SSL encrypted
if (!head || head.length === 0) {
socket.once(
"data",
self._onHttpServerConnectData.bind(self, req, socket)
);
socket.write("HTTP/1.1 200 OK\r\n");
if (
self.keepAlive &&
req.headers["proxy-connection"] === "keep-alive"
) {
socket.write("Proxy-Connection: keep-alive\r\n");
socket.write("Connection: keep-alive\r\n");
}
return socket.write("\r\n");
} else {
self._onHttpServerConnectData(req, socket, head);
}
}
);
}
_onHttpServerConnectData(
req: http.IncomingMessage,
socket: stream.Duplex,
head: Buffer
) {
const self = this;
socket.pause();
function makeConnection(port: number) {
// open a TCP connection to the remote host
const conn = net.connect(
{
port,
host: "0.0.0.0",
allowHalfOpen: true,
},
() => {
// create a tunnel between the two hosts
const connectKey = `${conn.localPort}:${conn.remotePort}`;
self.connectRequests[connectKey] = req;
const cleanupFunction = () => {
delete self.connectRequests[connectKey];
};
conn.on("close", () => {
cleanupFunction();
socket.destroy();
});
socket.on("close", () => {
conn.destroy();
});
conn.on("error", (err) => {
console.error("Connection error:");
console.error(err);
conn.destroy();
});
socket.on("error", (err) => {
console.error("Socket error:");
console.error(err);
});
socket.pipe(conn);
conn.pipe(socket);
socket.emit("data", head);
return socket.resume();
}
);
conn.on("error", self._onSocketError.bind(self, "PROXY_TO_PROXY_SOCKET"));
}
function getHttpsServer(hostname: string, callback: ErrorCallback) {
self.onCertificateRequired(hostname, (err, files) => {
if (err) {
return callback(err);
}
const httpsOptions = [
"keyFileExists",
"certFileExists",
(data: ICertficateContext["data"], callback) => {
if (data.keyFileExists && data.certFileExists) {
return fs.readFile(files.keyFile, (err, keyFileData) => {
if (err) {
return callback(err);
}
return fs.readFile(files.certFile, (err, certFileData) => {
if (err) {
return callback(err);
}
return callback(null, {
key: keyFileData,
cert: certFileData,
hosts: files.hosts,
});
});
});
} else {
const ctx: ICertficateContext = {
hostname,
files,
data,
};
return self.onCertificateMissing(ctx, files, (err, files) => {
if (err) {
return callback(err);
}
return callback(null, {
key: files.keyFileData,
cert: files.certFileData,
hosts: files.hosts,
});
});
}
},
];
async.auto(
{
keyFileExists(callback) {
return fs.exists(files.keyFile, (exists) =>
callback(null, exists)
);
},
certFileExists(callback) {
return fs.exists(files.certFile, (exists) =>
callback(null, exists)
);
},
// @ts-ignore
httpsOptions,
},
(err, results) => {
if (err) {
return callback(err);
}
let hosts;
if (
results.httpsOptions &&
results.httpsOptions.hosts &&
results.httpsOptions.hosts.length
) {
hosts = results.httpsOptions.hosts;
if (!hosts.includes(hostname)) {
hosts.push(hostname);
}
} else {
hosts = [hostname];
}
delete results.httpsOptions.hosts;
if (self.forceSNI && !hostname.match(/^[\d.]+$/)) {
console.debug(`creating SNI context for ${hostname}`);
hosts.forEach((host) => {
self.httpsServer!.addContext(host, results.httpsOptions);
self.sslServers[host] = { port: Number(self.httpsPort) };
});
return callback(null, self.httpsPort);
} else {
console.debug(`starting server for ${hostname}`);
results.httpsOptions.hosts = hosts;
try {
self._createHttpsServer(
results.httpsOptions,
(port, httpsServer, wssServer) => {
console.debug(
`https server started for ${hostname} on ${port}`
);
const sslServer = {
server: httpsServer,
wsServer: wssServer,
port,
};
hosts.forEach((host) => {
self.sslServers[host] = sslServer;
});
return callback(null, port);
}
);
} catch (err: any) {
return callback(err);
}
}
}
);
});
}
/*
* Detect TLS from first bytes of data
* Inspired from https://gist.github.com/tg-x/835636
* used heuristic:
* - an incoming connection using SSLv3/TLSv1 records should start with 0x16
* - an incoming connection using SSLv2 records should start with the record size
* and as the first record should not be very big we can expect 0x80 or 0x00 (the MSB is a flag)
* - everything else is considered to be unencrypted
*/
if (head[0] == 0x16 || head[0] == 0x80 || head[0] == 0x00) {
// URL is in the form 'hostname:port'
const hostname = req.url!.split(":", 2)[0];
const sslServer = this.sslServers[hostname];
if (sslServer) {
return makeConnection(sslServer.port);
}
const wildcardHost = hostname.replace(/[^.]+\./, "*.");
let sem = self.sslSemaphores[wildcardHost];
if (!sem) {
sem = self.sslSemaphores[wildcardHost] = semaphore(1);
}
sem.take(() => {
if (self.sslServers[hostname]) {
process.nextTick(sem.leave.bind(sem));
return makeConnection(self.sslServers[hostname].port);
}
if (self.sslServers[wildcardHost]) {
process.nextTick(sem.leave.bind(sem));
self.sslServers[hostname] = {
// @ts-ignore
port: self.sslServers[wildcardHost].port,
};
return makeConnection(self.sslServers[hostname].port);
}
getHttpsServer(hostname, (err, port) => {
process.nextTick(sem.leave.bind(sem));
if (err) {
console.error("Error getting HTTPs server");
console.error(err);
return self._onError("OPEN_HTTPS_SERVER_ERROR", null, err);
}
return makeConnection(port);
});
delete self.sslSemaphores[wildcardHost];
});
} else {
return makeConnection(this.httpPort);
}
}
onCertificateRequired(
hostname: string,
callback: OnCertificateRequiredCallback
) {
const self = this;
return callback(null, {
keyFile: `${self.sslCaDir}/keys/${hostname}.key`,
certFile: `${self.sslCaDir}/certs/${hostname}.pem`,
hosts: [hostname],
});
}
onCertificateMissing(
ctx: ICertficateContext,
files: ICertDetails,
callback: ErrorCallback
) {
const hosts = files.hosts || [ctx.hostname];
this.ca.generateServerCertificateKeys(hosts, (certPEM, privateKeyPEM) => {
callback(null, {
certFileData: certPEM,
keyFileData: privateKeyPEM,
hosts,
});
});
}
_onError(kind: string, ctx: IContext | null, err: Error) {
console.error(kind);
console.error(err);
this.onErrorHandlers.forEach((handler) => handler(ctx, err, kind));
if (ctx) {
ctx.onErrorHandlers.forEach((handler) => handler(ctx, err, kind));
if (ctx.proxyToClientResponse && !ctx.proxyToClientResponse.headersSent) {
ctx.proxyToClientResponse.writeHead(504, "Proxy Error");
}
if (ctx.proxyToClientResponse && !ctx.proxyToClientResponse.finished) {
ctx.proxyToClientResponse.end(`${kind}: ${err}`, "utf8");
}
}
}
_onWebSocketServerConnect(
isSSL: boolean,
ws: WebSocketType,
upgradeReq: IncomingMessage
) {
const self = this;
// @ts-ignore
const socket = ws._socket;
const ctx: IWebSocketContext = {
uuid: uuid(),
proxyToServerWebSocketOptions: undefined,
proxyToServerWebSocket: undefined,
isSSL,
connectRequest:
self.connectRequests[`${socket.remotePort}:${socket.localPort}`],
clientToProxyWebSocket: ws,
onWebSocketConnectionHandlers: [],
onWebSocketFrameHandlers: [],
onWebSocketCloseHandlers: [],
onWebSocketErrorHandlers: [],
onWebSocketConnection(fn) {
ctx.onWebSocketConnectionHandlers.push(fn);
return ctx;
},
onWebSocketSend(fn) {
ctx.onWebSocketFrameHandlers.push(
function (ctx, type, fromServer, data, flags, callback) {
if (!fromServer && type === "message") {
return this(ctx, data, flags, callback);
} else {
callback(null, data, flags);
}
}.bind(fn)
);
return ctx;
},
onWebSocketMessage(fn) {
ctx.onWebSocketFrameHandlers.push(
function (ctx, type, fromServer, data, flags, callback) {
if (fromServer && type === "message") {
return this(ctx, data, flags, callback);
} else {
callback(null, data, flags);
}
}.bind(fn)
);
return ctx;
},
onWebSocketFrame(fn) {
ctx.onWebSocketFrameHandlers.push(fn);
return ctx;
},
onWebSocketClose(fn) {
ctx.onWebSocketCloseHandlers.push(fn);
return ctx;
},
onWebSocketError(fn) {
ctx.onWebSocketErrorHandlers.push(fn);
return ctx;
},
use(mod) {
if (mod.onWebSocketConnection) {
ctx.onWebSocketConnection(mod.onWebSocketConnection);
}
if (mod.onWebSocketSend) {
ctx.onWebSocketFrame(
function (ctx, type, fromServer, data, flags, callback) {
if (!fromServer && type === "message") {
return this(ctx, data, flags, callback);
} else {
callback(null, data, flags);
}
}.bind(mod.onWebSocketSend)
);
}
if (mod.onWebSocketMessage) {
ctx.onWebSocketFrame(
function (ctx, type, fromServer, data, flags, callback) {
if (fromServer && type === "message") {
return this(ctx, data, flags, callback);
} else {
callback(null, data, flags);
}
}.bind(mod.onWebSocketMessage)
);
}
if (mod.onWebSocketFrame) {
ctx.onWebSocketFrame(mod.onWebSocketFrame);
}
if (mod.onWebSocketClose) {
ctx.onWebSocketClose(mod.onWebSocketClose);
}
if (mod.onWebSocketError) {
ctx.onWebSocketError(mod.onWebSocketError);
}
return ctx;
},
};
const clientToProxyWebSocket = ctx.clientToProxyWebSocket!;
clientToProxyWebSocket.on(
"message",
self._onWebSocketFrame.bind(self, ctx, "message", false)
);
clientToProxyWebSocket.on(
"ping",
self._onWebSocketFrame.bind(self, ctx, "ping", false)
);
clientToProxyWebSocket.on(
"pong",
self._onWebSocketFrame.bind(self, ctx, "pong", false)
);
clientToProxyWebSocket.on("error", self._onWebSocketError.bind(self, ctx));
// @ts-ignore
clientToProxyWebSocket._socket.on(
"error",
self._onWebSocketError.bind(self, ctx)
);
clientToProxyWebSocket.on(
"close",
self._onWebSocketClose.bind(self, ctx, false)
);
// @ts-ignore
clientToProxyWebSocket._socket.pause();
let url;
if (upgradeReq.url == "" || /^\//.test(upgradeReq.url!)) {
const hostPort = Proxy.parseHostAndPort(upgradeReq);
const prefix = ctx.isSSL ? "wss" : "ws";
const port = hostPort!.port ? ":" + hostPort!.port : "";
url = `${prefix}://${hostPort!.host}${port}${upgradeReq.url}`;
} else {
url = upgradeReq.url;
}
const ptosHeaders = {};
const ctopHeaders = upgradeReq.headers;
for (const key in ctopHeaders) {
if (key.indexOf("sec-websocket") !== 0) {
ptosHeaders[key] = ctopHeaders[key];
}
}
ctx.proxyToServerWebSocketOptions = {
url,
agent: ctx.isSSL ? self.httpsAgent : self.httpAgent,
headers: ptosHeaders,
};
function makeProxyToServerWebSocket() {
ctx.proxyToServerWebSocket = new WebSocket(
ctx.proxyToServerWebSocketOptions!.url!,
ctx.proxyToServerWebSocketOptions
);
ctx.proxyToServerWebSocket.on(
"message",
self._onWebSocketFrame.bind(self, ctx, "message", true)
);
ctx.proxyToServerWebSocket.on(
"ping",
self._onWebSocketFrame.bind(self, ctx, "ping", true)
);
ctx.proxyToServerWebSocket.on(
"pong",
self._onWebSocketFrame.bind(self, ctx, "pong", true)
);
ctx.proxyToServerWebSocket.on(
"error",
self._onWebSocketError.bind(self, ctx)
);
ctx.proxyToServerWebSocket.on(
"close",
self._onWebSocketClose.bind(self, ctx, true)
);
ctx.proxyToServerWebSocket.on("open", () => {
// @ts-ignore
ctx.proxyToServerWebSocket._socket.on(
"error",
self._onWebSocketError.bind(self, ctx)
);
if (clientToProxyWebSocket!.readyState === WebSocket.OPEN) {
// @ts-ignore
clientToProxyWebSocket._socket.resume();
}
});
}
return self._onWebSocketConnection(ctx, (err) => {
if (err) {
return self._onWebSocketError(ctx, err);
}
return makeProxyToServerWebSocket();
});
}
_onHttpServerRequest(
isSSL: boolean,
clientToProxyRequest: IncomingMessage,
proxyToClientResponse: ServerResponse
) {
const self = this;
const ctx: IContext = {
uuid: uuid(),
isSSL,
serverToProxyResponse: undefined,
proxyToServerRequestOptions: undefined,
proxyToServerRequest: undefined,
connectRequest:
self.connectRequests[
`${clientToProxyRequest.socket.remotePort}:${clientToProxyRequest.socket.localPort}`
] || undefined,
clientToProxyRequest,
proxyToClientResponse,
onRequestHandlers: [],
onErrorHandlers: [],
onRequestDataHandlers: [],
onResponseHeadersHandlers: [],
onRequestHeadersHandlers: [],
onRequestEndHandlers: [],
onResponseHandlers: [],
onResponseDataHandlers: [],
onResponseEndHandlers: [],
requestFilters: [],
responseFilters: [],
responseContentPotentiallyModified: false,
onRequest(fn) {
ctx.onRequestHandlers.push(fn);
return ctx;
},
onError(fn) {
ctx.onErrorHandlers.push(fn);
return ctx;
},
onRequestData(fn) {
ctx.onRequestDataHandlers.push(fn);
return ctx;
},
onRequestHeaders(fn) {
ctx.onRequestHeadersHandlers.push(fn);
return ctx;
},
onResponseHeaders(fn) {
ctx.onResponseHeadersHandlers.push(fn);
return ctx;
},
onRequestEnd(fn) {
ctx.onRequestEndHandlers.push(fn);
return ctx;
},
addRequestFilter(filter) {
ctx.requestFilters.push(filter);
return ctx;
},
onResponse(fn) {
ctx.onResponseHandlers.push(fn);
return ctx;
},
onResponseData(fn) {
ctx.onResponseDataHandlers.push(fn);
ctx.responseContentPotentiallyModified = true;
return ctx;
},
onResponseEnd(fn) {
ctx.onResponseEndHandlers.push(fn);
return ctx;
},
addResponseFilter(filter) {
ctx.responseFilters.push(filter);
ctx.responseContentPotentiallyModified = true;
return ctx;
},
use(mod) {
if (mod.onError) {
ctx.onError(mod.onError);
}
if (mod.onRequest) {
ctx.onRequest(mod.onRequest);
}
if (mod.onRequestHeaders) {
ctx.onRequestHeaders(mod.onRequestHeaders);
}
if (mod.onRequestData) {
ctx.onRequestData(mod.onRequestData);
}
if (mod.onResponse) {
ctx.onResponse(mod.onResponse);
}
if (mod.onResponseData) {
ctx.onResponseData(mod.onResponseData);
}
return ctx;
},
};
ctx.clientToProxyRequest.on(
"error",
self._onError.bind(self, "CLIENT_TO_PROXY_REQUEST_ERROR", ctx)
);
ctx.proxyToClientResponse.on(
"error",
self._onError.bind(self, "PROXY_TO_CLIENT_RESPONSE_ERROR", ctx)
);
ctx.clientToProxyRequest.pause();
const hostPort = Proxy.parseHostAndPort(
ctx.clientToProxyRequest,
ctx.isSSL ? 443 : 80
);
function proxyToServerRequestComplete(
serverToProxyResponse: http.IncomingMessage
) {
serverToProxyResponse.on(
"error",
self._onError.bind(self, "SERVER_TO_PROXY_RESPONSE_ERROR", ctx)
);
serverToProxyResponse.pause();
ctx.serverToProxyResponse = serverToProxyResponse;
return self._onResponse(ctx, (err) => {
if (err) {
return self._onError("ON_RESPONSE_ERROR", ctx, err);
}
const servToProxyResp = ctx.serverToProxyResponse!;
if (
self.responseContentPotentiallyModified ||
ctx.responseContentPotentiallyModified
) {
servToProxyResp.headers["transfer-encoding"] = "chunked";
delete servToProxyResp.headers["content-length"];
}
if (self.keepAlive) {
if (ctx.clientToProxyRequest.headers["proxy-connection"]) {
servToProxyResp.headers["proxy-connection"] = "keep-alive";
servToProxyResp.headers["connection"] = "keep-alive";
}
} else {
servToProxyResp.headers["connection"] = "close";
}
return self._onResponseHeaders(ctx, (err) => {
if (err) {
return self._onError("ON_RESPONSEHEADERS_ERROR", ctx, err);
}
ctx.proxyToClientResponse.writeHead(
servToProxyResp!.statusCode!,
Proxy.filterAndCanonizeHeaders(servToProxyResp.headers)
);
// @ts-ignore
ctx.responseFilters.push(new ProxyFinalResponseFilter(self, ctx));
let prevResponsePipeElem = servToProxyResp;
ctx.responseFilters.forEach((filter) => {
filter.on(
"error",
self._onError.bind(self, "RESPONSE_FILTER_ERROR", ctx)
);
prevResponsePipeElem = prevResponsePipeElem.pipe(filter);
});
return servToProxyResp.resume();
});
});
}
function makeProxyToServerRequest() {
const proto = ctx.isSSL ? https : http;
ctx.proxyToServerRequest = proto.request(
ctx.proxyToServerRequestOptions!,
proxyToServerRequestComplete
);
ctx.proxyToServerRequest.on(
"error",
self._onError.bind(self, "PROXY_TO_SERVER_REQUEST_ERROR", ctx)
);
ctx.requestFilters.push(new ProxyFinalRequestFilter(self, ctx));
let prevRequestPipeElem = ctx.clientToProxyRequest;
ctx.requestFilters.forEach((filter) => {
filter.on(
"error",
self._onError.bind(self, "REQUEST_FILTER_ERROR", ctx)
);
prevRequestPipeElem = prevRequestPipeElem.pipe(filter);
});
ctx.clientToProxyRequest.resume();
}
if (hostPort === null) {
ctx.clientToProxyRequest.resume();
ctx.proxyToClientResponse.writeHead(400, {
"Content-Type": "text/html; charset=utf-8",
});
ctx.proxyToClientResponse.end("Bad request: Host missing...", "utf-8");
} else {
const headers = {};
for (const h in ctx.clientToProxyRequest.headers) {
// don't forward proxy-headers
if (!/^proxy-/i.test(h)) {
headers[h] = ctx.clientToProxyRequest.headers[h];
}
}
if (this.options.forceChunkedRequest) {
delete headers["content-length"];
}
ctx.proxyToServerRequestOptions = {
method: ctx.clientToProxyRequest.method!,
path: ctx.clientToProxyRequest.url!,
host: hostPort.host,
port: hostPort.port,
headers,
agent: ctx.isSSL ? self.httpsAgent : self.httpAgent,
};
return self._onRequest(ctx, (err) => {
if (err) {
return self._onError("ON_REQUEST_ERROR", ctx, err);
}
return self._onRequestHeaders(ctx, (err: Error | undefined | null) => {
if (err) {
return self._onError("ON_REQUESTHEADERS_ERROR", ctx, err);
}
return makeProxyToServerRequest();
});
});
}
}
_onRequestHeaders(ctx: IContext, callback: ErrorCallback) {
async.forEach(
this.onRequestHeadersHandlers,
(fn, callback) => fn(ctx, callback),
callback
);
}
_onRequest(ctx: IContext, callback: ErrorCallback) {
async.forEach(
this.onRequestHandlers.concat(ctx.onRequestHandlers),
(fn, callback) => fn(ctx, callback),
callback
);
}
_onWebSocketConnection(ctx: IWebSocketContext, callback: ErrorCallback) {
async.forEach(
this.onWebSocketConnectionHandlers.concat(
ctx.onWebSocketConnectionHandlers
),
(fn, callback) => fn(ctx, callback),
callback
);
}
_onWebSocketFrame(
ctx: IWebSocketContext,
type: string,
fromServer: boolean,
data: WebSocket.RawData,
flags?: WebSocketFlags | boolean
) {
const self = this;
async.forEach(
this.onWebSocketFrameHandlers.concat(ctx.onWebSocketFrameHandlers),
(fn, callback: IWebSocketCallback) =>
fn(ctx, type, fromServer, data, flags, (err, newData, newFlags) => {
if (err) {
return callback(err);
}
data = newData;
flags = newFlags;
return callback(null, data, flags);
}),
(err) => {
if (err) {
return self._onWebSocketError(ctx, err);
}
const destWebSocket = fromServer
? ctx.clientToProxyWebSocket!
: ctx.proxyToServerWebSocket!;
if (destWebSocket.readyState === WebSocket.OPEN) {
switch (type) {
case "message":
destWebSocket.send(data, {binary: flags as boolean});
break;
case "ping":
destWebSocket.ping(data, flags as boolean);
break;
case "pong":
destWebSocket.pong(data, flags as boolean);
break;
}
} else {
self._onWebSocketError(
ctx,
new Error(
`Cannot send ${type} because ${
fromServer ? "clientToProxy" : "proxyToServer"
} WebSocket connection state is not OPEN`
)
);
}
}
);
}
_onWebSocketClose(
ctx: IWebSocketContext,
closedByServer: boolean,
code: number,
message: Buffer
) {
const self = this;
if (!ctx.closedByServer && !ctx.closedByClient) {
ctx.closedByServer = closedByServer;
ctx.closedByClient = !closedByServer;
async.forEach(
this.onWebSocketCloseHandlers.concat(ctx.onWebSocketCloseHandlers),
(fn, callback) => fn(ctx, code, message, callback),
(err) => {
if (err) {
return self._onWebSocketError(ctx, err);
}
const clientToProxyWebSocket = ctx.clientToProxyWebSocket!;
const proxyToServerWebSocket = ctx.proxyToServerWebSocket!;
if (
clientToProxyWebSocket.readyState !==
proxyToServerWebSocket.readyState
) {
try {
if (
clientToProxyWebSocket.readyState === WebSocket.CLOSED &&
proxyToServerWebSocket.readyState === WebSocket.OPEN
) {
code === 1005
? proxyToServerWebSocket.close()
: proxyToServerWebSocket.close(code, message);
} else if (
proxyToServerWebSocket.readyState === WebSocket.CLOSED &&
clientToProxyWebSocket.readyState === WebSocket.OPEN
) {
code === 1005
? proxyToServerWebSocket.close()
: clientToProxyWebSocket.close(code, message);
}
} catch (err: any) {
return self._onWebSocketError(ctx, err);
}
}
}
);
}
}
_onWebSocketError(ctx: IWebSocketContext, err: Error) {
this.onWebSocketErrorHandlers.forEach((handler) => handler(ctx, err));
if (ctx) {
ctx.onWebSocketErrorHandlers.forEach((handler) => handler(ctx, err));
}
const clientToProxyWebSocket = ctx.clientToProxyWebSocket!;
const proxyToServerWebSocket = ctx.proxyToServerWebSocket!;
if (
proxyToServerWebSocket &&
clientToProxyWebSocket.readyState !== proxyToServerWebSocket.readyState
) {
try {
if (
clientToProxyWebSocket.readyState === WebSocket.CLOSED &&
proxyToServerWebSocket.readyState === WebSocket.OPEN
) {
proxyToServerWebSocket.close();
} else if (
proxyToServerWebSocket.readyState === WebSocket.CLOSED &&
clientToProxyWebSocket.readyState === WebSocket.OPEN
) {
clientToProxyWebSocket.close();
}
} catch (err) {
// ignore
}
}
}
_onRequestData(ctx: IContext, chunk, callback) {
const self = this;
async.forEach(
this.onRequestDataHandlers.concat(ctx.onRequestDataHandlers),
(fn, callback: OnRequestDataCallback) =>
fn(ctx, chunk, (err, newChunk) => {
if (err) {
return callback(err);
}
chunk = newChunk;
return callback(null, newChunk);
}),
(err) => {
if (err) {
return self._onError("ON_REQUEST_DATA_ERROR", ctx, err);
}
return callback(null, chunk);
}
);
}
_onRequestEnd(ctx: IContext, callback: ErrorCallback) {
const self = this;
async.forEach(
this.onRequestEndHandlers.concat(ctx.onRequestEndHandlers),
(fn, callback) => fn(ctx, callback),
(err) => {
if (err) {
return self._onError("ON_REQUEST_END_ERROR", ctx, err);
}
return callback(null);
}
);
}
_onResponse(ctx: IContext, callback: ErrorCallback) {
async.forEach(
this.onResponseHandlers.concat(ctx.onResponseHandlers),
(fn, callback) => fn(ctx, callback),
callback
);
}
_onResponseHeaders(ctx: IContext, callback: ErrorCallback) {
async.forEach(
this.onResponseHeadersHandlers,
(fn, callback) => fn(ctx, callback),
callback
);
}
_onResponseData(ctx: IContext, chunk, callback: ErrorCallback) {
async.forEach(
this.onResponseDataHandlers.concat(ctx.onResponseDataHandlers),
(fn, callback: OnRequestDataCallback) =>
fn(ctx, chunk, (err, newChunk) => {
if (err) {
return callback(err);
}
chunk = newChunk;
return callback(null, newChunk);
}),
(err) => {
if (err) {
return this._onError("ON_RESPONSE_DATA_ERROR", ctx, err);
}
return callback(null, chunk);
}
);
}
_onResponseEnd(ctx: IContext, callback: ErrorCallback) {
async.forEach(
this.onResponseEndHandlers.concat(ctx.onResponseEndHandlers),
(fn, callback) => fn(ctx, callback),
(err) => {
if (err) {
return this._onError("ON_RESPONSE_END_ERROR", ctx, err);
}
return callback(null);
}
);
}
static parseHostAndPort(req: http.IncomingMessage, defaultPort?: number) {
const m = req.url!.match(/^http:\/\/([^/]+)(.*)/);
if (m) {
req.url = m[2] || "/";
return Proxy.parseHost(m[1], defaultPort);
} else if (req.headers.host) {
return Proxy.parseHost(req.headers.host, defaultPort);
} else {
return null;
}
}
static parseHost(
hostString: string,
defaultPort?: number
): { host: string; port: number | undefined } {
const m = hostString.match(/^http:\/\/(.*)/);
if (m) {
const parsedUrl = url.parse(hostString);
return {
host: parsedUrl.hostname as string,
port: Number(parsedUrl.port),
};
}
const hostPort = hostString.split(":");
const host = hostPort[0];
const port = hostPort.length === 2 ? +hostPort[1] : defaultPort;
return {
host,
port,
};
}
static filterAndCanonizeHeaders(originalHeaders: IncomingHttpHeaders) {
const headers = {};
for (const key in originalHeaders) {
const canonizedKey = key.trim();
if (/^public-key-pins/i.test(canonizedKey)) {
// HPKP header => filter
continue;
}
headers[canonizedKey] = originalHeaders[key];
}
return headers;
}
}