UNPKG

http-mitm-proxy

Version:
1,444 lines (1,369 loc) 44 kB
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; } }