'use strict'; const http = require('node:http'); const https = require('node:https'); const node_events = require('node:events'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const http__default = /*#__PURE__*/_interopDefaultCompat(http); const https__default = /*#__PURE__*/_interopDefaultCompat(https); const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; const isSSL = /^https|wss/; function setupOutgoing(outgoing, options, req, forward) { outgoing.port = options[forward || "target"].port || (isSSL.test(options[forward || "target"].protocol) ? 443 : 80); for (const e of [ "host", "hostname", "socketPath", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "secureProtocol" ]) { outgoing[e] = options[forward || "target"][e]; } outgoing.method = options.method || req.method; outgoing.headers = { ...req.headers }; if (options.headers) { outgoing.headers = { ...outgoing.headers, ...options.headers }; } if (options.auth) { outgoing.auth = options.auth; } if (options.ca) { outgoing.ca = options.ca; } if (isSSL.test(options[forward || "target"].protocol)) { outgoing.rejectUnauthorized = options.secure === undefined ? true : options.secure; } outgoing.agent = options.agent || false; outgoing.localAddress = options.localAddress; if (!outgoing.agent) { outgoing.headers = outgoing.headers || {}; if (typeof outgoing.headers.connection !== "string" || !upgradeHeader.test(outgoing.headers.connection)) { outgoing.headers.connection = "close"; } } const target = options[forward || "target"]; const targetPath = target && options.prependPath !== false ? target.pathname || target.path || "" : ""; const parsed = new URL(req.url, "http://localhost"); let outgoingPath = options.toProxy ? req.url : parsed.pathname + parsed.search || ""; outgoingPath = options.ignorePath ? "" : outgoingPath; outgoing.path = urlJoin(targetPath, outgoingPath); if (options.changeOrigin) { outgoing.headers.host = requiresPort(outgoing.port, options[forward || "target"].protocol) && !hasPort(outgoing.host) ? outgoing.host + ":" + outgoing.port : outgoing.host; } return outgoing; } function setupSocket(socket) { socket.setTimeout(0); socket.setNoDelay(true); socket.setKeepAlive(true, 0); return socket; } function getPort(req) { const res = req.headers.host ? req.headers.host.match(/:(\d+)/) : ""; if (res) { return res[1]; } return hasEncryptedConnection(req) ? "443" : "80"; } function hasEncryptedConnection(req) { return Boolean(req.connection.encrypted || req.connection.pair); } function urlJoin(...args) { const lastIndex = args.length - 1; const last = args[lastIndex]; const lastSegs = last.split("?"); args[lastIndex] = lastSegs.shift(); const retSegs = [ args.filter(Boolean).join("/").replace(/\/+/g, "/").replace("http:/", "http://").replace("https:/", "https://") ]; retSegs.push(...lastSegs); return retSegs.join("?"); } function rewriteCookieProperty(header, config, property) { if (Array.isArray(header)) { return header.map(function(headerElement) { return rewriteCookieProperty(headerElement, config, property); }); } return header.replace( new RegExp(String.raw`(;\s*` + property + "=)([^;]+)", "i"), function(match, prefix, previousValue) { let newValue; if (previousValue in config) { newValue = config[previousValue]; } else if ("*" in config) { newValue = config["*"]; } else { return match; } return newValue ? prefix + newValue : ""; } ); } function hasPort(host) { return !!~host.indexOf(":"); } function requiresPort(_port, _protocol) { const protocol = _protocol.split(":")[0]; const port = +_port; if (!port) return false; switch (protocol) { case "http": case "ws": { return port !== 80; } case "https": case "wss": { return port !== 443; } case "ftp": { return port !== 21; } case "gopher": { return port !== 70; } case "file": { return false; } } return port !== 0; } function defineProxyMiddleware(m) { return m; } function defineProxyOutgoingMiddleware(m) { return m; } const redirectRegex = /^201|30([1278])$/; const removeChunked = defineProxyOutgoingMiddleware((req, res, proxyRes) => { if (req.httpVersion === "1.0") { delete proxyRes.headers["transfer-encoding"]; } }); 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) { proxyRes.headers.connection = req.headers.connection || "keep-alive"; } }); const setRedirectHostRewrite = defineProxyOutgoingMiddleware( (req, res, proxyRes, options) => { if ((options.hostRewrite || options.autoRewrite || options.protocolRewrite) && proxyRes.headers.location && redirectRegex.test(String(proxyRes.statusCode))) { const target = new URL(options.target); const u = new URL(proxyRes.headers.location); if (target.host !== u.host) { return; } if (options.hostRewrite) { u.host = options.hostRewrite; } else if (options.autoRewrite) { u.host = req.headers.host; } if (options.protocolRewrite) { u.protocol = options.protocolRewrite; } proxyRes.headers.location = u.toString(); } } ); const writeHeaders = defineProxyOutgoingMiddleware( (req, res, proxyRes, options) => { let rewriteCookieDomainConfig = options.cookieDomainRewrite; let rewriteCookiePathConfig = options.cookiePathRewrite; const preserveHeaderKeyCase = options.preserveHeaderKeyCase; let rawHeaderKeyMap; const setHeader = function(key, header) { if (header === undefined) { return; } if (rewriteCookieDomainConfig && key.toLowerCase() === "set-cookie") { header = rewriteCookieProperty( header, rewriteCookieDomainConfig, "domain" ); } if (rewriteCookiePathConfig && key.toLowerCase() === "set-cookie") { header = rewriteCookieProperty(header, rewriteCookiePathConfig, "path"); } res.setHeader(String(key).trim(), header); }; if (typeof rewriteCookieDomainConfig === "string") { rewriteCookieDomainConfig = { "*": rewriteCookieDomainConfig }; } if (typeof rewriteCookiePathConfig === "string") { rewriteCookiePathConfig = { "*": rewriteCookiePathConfig }; } if (preserveHeaderKeyCase && proxyRes.rawHeaders !== undefined) { rawHeaderKeyMap = {}; for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) { const key = proxyRes.rawHeaders[i]; rawHeaderKeyMap[key.toLowerCase()] = key; } } for (let key of Object.keys(proxyRes.headers)) { const header = proxyRes.headers[key]; if (preserveHeaderKeyCase && rawHeaderKeyMap) { key = rawHeaderKeyMap[key] || key; } setHeader(key, header); } } ); const writeStatusCode = defineProxyOutgoingMiddleware((req, res, proxyRes) => { if (proxyRes.statusMessage) { res.statusCode = proxyRes.statusCode; res.statusMessage = proxyRes.statusMessage; } else { res.statusCode = proxyRes.statusCode; } }); const webOutgoingMiddleware = [ removeChunked, setConnection, setRedirectHostRewrite, writeHeaders, writeStatusCode ]; const nativeAgents = { http: http__default, https: https__default }; const deleteLength = defineProxyMiddleware((req) => { if ((req.method === "DELETE" || req.method === "OPTIONS") && !req.headers["content-length"]) { req.headers["content-length"] = "0"; delete req.headers["transfer-encoding"]; } }); const timeout = defineProxyMiddleware((req, res, options) => { if (options.timeout) { req.socket.setTimeout(options.timeout); } }); const XHeaders$1 = defineProxyMiddleware((req, res, options) => { if (!options.xfwd) { return; } const encrypted = req.isSpdy || hasEncryptedConnection(req); const values = { for: req.connection.remoteAddress || req.socket.remoteAddress, port: getPort(req), proto: encrypted ? "https" : "http" }; for (const header of ["for", "port", "proto"]) { req.headers["x-forwarded-" + header] = (req.headers["x-forwarded-" + header] || "") + (req.headers["x-forwarded-" + header] ? "," : "") + values[header]; } req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers.host || ""; }); const stream$1 = defineProxyMiddleware( (req, res, options, server, head, callback) => { server.emit("start", req, res, options.target || options.forward); const agents = nativeAgents; const http = agents.http; const https = agents.https; if (options.forward) { const forwardReq = (options.forward.protocol === "https:" ? https : http).request(setupOutgoing(options.ssl || {}, options, req, "forward")); const forwardError = createErrorHandler(forwardReq, options.forward); req.on("error", forwardError); forwardReq.on("error", forwardError); (options.buffer || req).pipe(forwardReq); if (!options.target) { res.end(); return; } } const proxyReq = (options.target.protocol === "https:" ? https : http).request(setupOutgoing(options.ssl || {}, options, req)); proxyReq.on("socket", (socket) => { if (server && !proxyReq.getHeader("expect")) { server.emit("proxyReq", proxyReq, req, res, options); } }); if (options.proxyTimeout) { proxyReq.setTimeout(options.proxyTimeout, function() { proxyReq.abort(); }); } req.on("aborted", function() { proxyReq.abort(); }); const proxyError = createErrorHandler(proxyReq, options.target); req.on("error", proxyError); proxyReq.on("error", proxyError); function createErrorHandler(proxyReq2, url) { return function proxyError2(err) { if (req.socket.destroyed && err.code === "ECONNRESET") { server.emit("econnreset", err, req, res, url); return proxyReq2.abort(); } if (callback) { callback(err, req, res, url); } else { server.emit("error", err, req, res, url); } }; } (options.buffer || req).pipe(proxyReq); proxyReq.on("response", function(proxyRes) { if (server) { server.emit("proxyRes", proxyRes, req, res); } if (!res.headersSent && !options.selfHandleResponse) { for (const pass of webOutgoingMiddleware) { if (pass(req, res, proxyRes, options)) { break; } } } if (res.finished) { if (server) { server.emit("end", req, res, proxyRes); } } else { res.on("close", function() { proxyRes.destroy(); }); proxyRes.on("end", function() { if (server) { server.emit("end", req, res, proxyRes); } }); if (!options.selfHandleResponse) { proxyRes.pipe(res); } } }); } ); const webIncomingMiddleware = [ deleteLength, timeout, XHeaders$1, stream$1 ]; const checkMethodAndHeader = defineProxyMiddleware((req, socket) => { if (req.method !== "GET" || !req.headers.upgrade) { socket.destroy(); return true; } if (req.headers.upgrade.toLowerCase() !== "websocket") { socket.destroy(); return true; } }); const XHeaders = defineProxyMiddleware((req, socket, options) => { if (!options.xfwd) { return; } const values = { for: req.connection.remoteAddress || req.socket.remoteAddress, port: getPort(req), proto: hasEncryptedConnection(req) ? "wss" : "ws" }; for (const header of ["for", "port", "proto"]) { req.headers["x-forwarded-" + header] = (req.headers["x-forwarded-" + header] || "") + (req.headers["x-forwarded-" + header] ? "," : "") + values[header]; } }); const stream = defineProxyMiddleware( (req, socket, options, server, head, callback) => { const createHttpHeader = function(line, headers) { return Object.keys(headers).reduce( function(head2, key) { const value = headers[key]; if (!Array.isArray(value)) { head2.push(key + ": " + value); return head2; } for (const element of value) { head2.push(key + ": " + element); } return head2; }, [line] ).join("\r\n") + "\r\n\r\n"; }; setupSocket(socket); if (head && head.length > 0) { socket.unshift(head); } const proxyReq = (isSSL.test(options.target.protocol) ? https__default : http__default).request(setupOutgoing(options.ssl || {}, options, req)); if (server) { server.emit("proxyReqWs", proxyReq, req, socket, options, head); } proxyReq.on("error", onOutgoingError); proxyReq.on("response", function(res) { if (!res.upgrade) { socket.write( createHttpHeader( "HTTP/" + res.httpVersion + " " + res.statusCode + " " + res.statusMessage, res.headers ) ); res.pipe(socket); } }); proxyReq.on("upgrade", function(proxyRes, proxySocket, proxyHead) { proxySocket.on("error", onOutgoingError); proxySocket.on("end", function() { server.emit("close", proxyRes, proxySocket, proxyHead); }); socket.on("error", function() { proxySocket.end(); }); setupSocket(proxySocket); if (proxyHead && proxyHead.length > 0) { proxySocket.unshift(proxyHead); } socket.write( createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers) ); proxySocket.pipe(socket).pipe(proxySocket); server.emit("open", proxySocket); server.emit("proxySocket", proxySocket); }); proxyReq.end(); function onOutgoingError(err) { if (callback) { callback(err, req, socket); } else { server.emit("error", err, req, socket); } socket.end(); } } ); const websocketIncomingMiddleware = [ checkMethodAndHeader, XHeaders, stream ]; class ProxyServer extends node_events.EventEmitter { _server; _webPasses = [...webIncomingMiddleware]; _wsPasses = [...websocketIncomingMiddleware]; options; web; ws; /** * Creates the proxy server with specified options. * @param options - Config object passed to the proxy */ constructor(options = {}) { super(); this.options = options || {}; this.options.prependPath = options.prependPath !== false; this.web = _createProxyFn("web", this); this.ws = _createProxyFn("ws", this); } /** * A function that wraps the object in a webserver, for your convenience * @param port - Port to listen on * @param hostname - The hostname to listen on */ listen(port, hostname) { const closure = (req, res) => { this.web(req, res); }; this._server = this.options.ssl ? https__default.createServer(this.options.ssl, closure) : http__default.createServer(closure); if (this.options.ws) { this._server.on("upgrade", (req, socket, head) => { this._ws(req, socket, head); }); } this._server.listen(port, hostname); return this; } /** * A function that closes the inner webserver and stops listening on given port */ close(callback) { if (this._server) { this._server.close((...args) => { this._server = undefined; if (callback) { Reflect.apply(callback, undefined, args); } }); } } before(type, passName, pass) { if (type !== "ws" && type !== "web") { throw new Error("type must be `web` or `ws`"); } const passes = type === "ws" ? this._wsPasses : this._webPasses; let i = false; for (const [idx, v] of passes.entries()) { if (v.name === passName) { i = idx; } } if (i === false) { throw new Error("No such pass"); } passes.splice(i, 0, pass); } after(type, passName, pass) { if (type !== "ws" && type !== "web") { throw new Error("type must be `web` or `ws`"); } const passes = type === "ws" ? this._wsPasses : this._webPasses; let i = false; for (const [idx, v] of passes.entries()) { if (v.name === passName) { i = idx; } } if (i === false) { throw new Error("No such pass"); } passes.splice(i++, 0, pass); } } function createProxyServer(options = {}) { return new ProxyServer(options); } function _createProxyFn(type, server) { return function(req, res, opts, head) { const requestOptions = { ...opts, ...server.options }; for (const key of ["target", "forward"]) { if (typeof requestOptions[key] === "string") { requestOptions[key] = new URL(requestOptions[key]); } } if (!requestOptions.target && !requestOptions.forward) { return this.emit( "error", new Error("Must provide a proper URL as target") ); } let _resolve; let _reject; const callbackPromise = new Promise((resolve, reject) => { _resolve = resolve; _reject = reject; }); res.on("close", () => { _resolve(); }); res.on("error", (error) => { _reject(error); }); for (const pass of type === "ws" ? server._wsPasses : server._webPasses) { const stop = pass( req, res, requestOptions, server, head, (error) => { _reject(error); } ); if (stop) { _resolve(); break; } } return callbackPromise; }; } exports.ProxyServer = ProxyServer; exports.createProxyServer = createProxyServer;