UNPKG

http-proxy-3

Version:
295 lines (294 loc) 10.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isSSL = void 0; exports.setupOutgoing = setupOutgoing; exports.setupSocket = setupSocket; exports.getPort = getPort; exports.hasEncryptedConnection = hasEncryptedConnection; exports.urlJoin = urlJoin; exports.rewriteCookieProperty = rewriteCookieProperty; exports.toURL = toURL; const node_tls_1 = require("node:tls"); const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; // Simple Regex for testing if protocol is https exports.isSSL = /^https|wss/; // If we allow this header and a user sends it with a request, // then serving this request goes into a weird broken state, which // wastes resources. This could be a DOS security vulnerability. // We strip this header if it appears in any request, and then things // work fine. // See https://github.com/http-party/node-http-proxy/issues/1647 const HEADER_BLACKLIST = "trailer"; const HTTP2_HEADER_BLACKLIST = [ ":method", ":path", ":scheme", ":authority", "connection", "keep-alive", ]; // setupOutgoing -- Copies the right headers from `options` and `req` to // `outgoing` which is then used to fire the proxied request by calling // http.request or https.request with outgoing as input. // Returns Object with all required properties outgoing options. function setupOutgoing( // Base object to be filled with required properties outgoing, // Config object passed to the proxy options, // Request Object req, // String to select forward or target forward) { // the final path is target path + relative path requested by user: const target = options[forward || "target"]; outgoing.port = +(target.port ?? (target.protocol !== undefined && exports.isSSL.test(target.protocol) ? 443 : 80)); for (const e of [ "host", "hostname", "socketPath", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "secureProtocol", ]) { // @ts-expect-error -- this mapping is valid outgoing[e] = target[e]; } outgoing.method = options.method || req.method; outgoing.headers = { ...req.headers }; if (req.headers?.[":authority"]) { outgoing.headers.host = req.headers[":authority"]; } if (options.headers) { outgoing.headers = { ...outgoing.headers, ...options.headers }; } // note -- we do the scan in this order since // the header could be any case, i.e., doing // outgoing.headers['Trailer'] won't work, because // it might be {'TrAiLeR':...} for (const header in outgoing.headers) { if (HEADER_BLACKLIST == header.toLowerCase()) { delete outgoing.headers[header]; break; } } if (req.httpVersionMajor > 1) { for (const header of HTTP2_HEADER_BLACKLIST) { delete outgoing.headers[header]; } } if (options.auth) { delete outgoing.headers.authorization; outgoing.auth = options.auth; } if (options.ca) { outgoing.ca = options.ca; } if (target.protocol !== undefined && exports.isSSL.test(target.protocol)) { outgoing.rejectUnauthorized = typeof options.secure === "undefined" ? true : options.secure; } outgoing.agent = options.agent || false; outgoing.localAddress = options.localAddress; // Remark: If we are false and not upgrading, set the connection: close. This is the right thing to do // as node core doesn't handle this COMPLETELY properly yet. if (!outgoing.agent) { outgoing.headers = outgoing.headers || {}; if (typeof outgoing.headers.connection !== "string" || !upgradeHeader.test(outgoing.headers.connection)) { outgoing.headers.connection = "close"; } } // target if defined is a URL object so has attribute "pathname", not "path". const targetPath = target && options.prependPath !== false && "pathname" in target ? getPath(`${target.pathname}${target.search ?? ""}`) : "/"; let outgoingPath = options.toProxy ? req.url : getPath(req.url); // Remark: ignorePath will just straight up ignore whatever the request's // path is. This can be labeled as FOOT-GUN material if you do not know what // you are doing and are using conflicting options. outgoingPath = !options.ignorePath ? outgoingPath : ""; outgoing.path = urlJoin(targetPath, outgoingPath ?? ""); if (options.changeOrigin) { outgoing.headers.host = target.protocol !== undefined && required(outgoing.port, target.protocol) && !hasPort(outgoing.host) ? outgoing.host + ":" + outgoing.port : outgoing.host; } outgoing.url = ("href" in target && target.href) || (target.protocol === "https" ? "https" : "http") + "://" + outgoing.host + (outgoing.port ? ":" + outgoing.port : ""); if (req.httpVersionMajor > 1) { for (const header of HTTP2_HEADER_BLACKLIST) { delete outgoing.headers[header]; } } return outgoing; } // Set the proper configuration for sockets, // set no delay and set keep alive, also set // the timeout to 0. // Return the configured socket. function setupSocket(socket) { socket.setTimeout(0); socket.setNoDelay(true); socket.setKeepAlive(true, 0); return socket; } // Get the port number from the host. Or guess it based on the connection type. function getPort( // Incoming HTTP request. req) { const hostHeader = req.headers[":authority"] || req.headers.host; const res = hostHeader ? hostHeader.match(/:(\d+)/) : ""; return res ? res[1] : hasEncryptedConnection(req) ? "443" : "80"; } // Check if the request has an encrypted connection. function hasEncryptedConnection( // Incoming HTTP request. req) { const conn = req.connection; return ((conn instanceof node_tls_1.TLSSocket && conn.encrypted) || Boolean(conn.pair)); } // OS-agnostic join (doesn't break on URLs like path.join does on Windows)> function urlJoin(...args) { // join url and merge all query string. const queryParams = []; let queryParamRaw = ""; args.forEach((url, index) => { const qpStart = url.indexOf("?"); if (qpStart !== -1) { queryParams.push(url.substring(qpStart + 1)); args[index] = url.substring(0, qpStart); } }); queryParamRaw = queryParams.filter(Boolean).join("&"); // Join all strings, but remove empty strings so we don't get extra slashes from // joining e.g. ['', 'am']. // Also we respect strings that start and end in multiple slashes, e.g., so // ['/', '//test', '///foo'] --> '//test' // since e.g., http://localhost//test///foo is a valid URL. See // lib/test/http/double-slashes.test.ts // The algorithm for joining is just straightforward and simple, instead // of the complicated "too clever" code from http-proxy. This just concats // the strings together, not adding any slashes, and also combining adjacent // slashes in two segments, e.g., ['/foo/','/bar'] --> '/foo/bar' let retSegs = ""; for (const seg of args) { if (!seg) { continue; } if (retSegs.endsWith("/")) { if (seg.startsWith("/")) { retSegs += seg.slice(1); } else { retSegs += seg; } } else { if (seg.startsWith("/")) { retSegs += seg; } else { retSegs += "/" + seg; } } } // Only join the query string if it exists so we don't have trailing a '?' // on every request return queryParamRaw ? retSegs + "?" + queryParamRaw : retSegs; } function rewriteCookieProperty(header, // config = mapping of domain to rewritten domain. // '*' key to match any domain, null value to remove the domain. config, property) { if (Array.isArray(header)) { return header.map((headerElement) => { return rewriteCookieProperty(headerElement, config, property); }); } return header.replace(new RegExp("(;\\s*" + property + "=)([^;]+)", "i"), (match, prefix, previousValue) => { let newValue; if (previousValue in config) { newValue = config[previousValue]; } else if ("*" in config) { newValue = config["*"]; } else { //no match, return previous value return match; } if (newValue) { //replace value return prefix + newValue; } else { //remove value return ""; } }); } // Check the host and see if it potentially has a port in it (keep it simple) function hasPort(host) { return !!~host.indexOf(":"); } function getPath(url) { if (url === "" || url?.startsWith("?")) { return url; } const u = toURL(url); return `${u.pathname ?? ""}${u.search ?? ""}`; } function toURL(url) { if (url instanceof URL) { return url; } else if (typeof url === "object" && "href" in url && typeof url.href === "string") { url = url.href; } if (!url) { url = ""; } if (typeof url != "string") { // it has to be a string at this point, but to keep typescript happy: url = `${url}`; } if (url.startsWith("//")) { // special case -- this would be viewed as a this is a "network-path reference", // so we explicitly prefix with our http schema. See url = `http://base.invalid${url}`; } // urllib.Url is deprecated but we support it by converting to URL return new URL(url, "http://base.invalid"); } // vendor simplified version of https://www.npmjs.com/package/requires-port to // reduce dep and add typescript. function required(port, protocol) { protocol = protocol.split(":")[0]; port = +port; if (!port) return false; switch (protocol) { case "http": case "ws": return port !== 80; case "https": case "wss": return port !== 443; } return port !== 0; }