"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const dns_1 = __importDefault(require("dns")); const router_1 = __importDefault(require("./router")); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); const urlUtils = __importStar(require("../utils/url")); const script_1 = __importDefault(require("../processing/resources/script")); const http_2 = require("../utils/http"); const request_pipeline_1 = require("../request-pipeline"); const create_shadow_stylesheet_1 = __importDefault(require("../shadow-ui/create-shadow-stylesheet")); const agent_1 = require("../request-pipeline/destination-request/agent"); const service_routes_1 = __importDefault(require("./service-routes")); const builtin_header_names_1 = __importDefault(require("../request-pipeline/builtin-header-names")); const logger_1 = __importDefault(require("../utils/logger")); const err_to_string_1 = __importDefault(require("../utils/err-to-string")); const json_1 = require("../utils/json"); const load_client_script_1 = __importDefault(require("../utils/load-client-script")); const SESSION_IS_NOT_OPENED_ERR = 'Session is not opened in proxy'; function parseAsJson(msg) { try { return (0, json_1.parse)(msg.toString()); } catch (err) { return null; } } function createServerInfo(hostname, port, crossDomainPort, protocol, cacheRequests) { return { hostname, port, crossDomainPort, protocol, cacheRequests, domain: `${protocol}//${hostname}:${port}`, }; } const DEFAULT_PROXY_OPTIONS = { developmentMode: false, cache: false, proxyless: false, }; class Proxy extends router_1.default { constructor(hostname, port1, port2, options) { const prepareOptions = Object.assign({}, DEFAULT_PROXY_OPTIONS, options); super(prepareOptions); this.openSessions = new Map(); // NOTE: to avoid https://github.com/DevExpress/testcafe/issues/7447 if (typeof dns_1.default.setDefaultResultOrder === 'function') // NOTE: to avoid https://github.com/nodejs/node/issues/40537 dns_1.default.setDefaultResultOrder('ipv4first'); const { ssl, developmentMode, cache, } = prepareOptions; const protocol = ssl ? 'https:' : 'http:'; const opts = this._getOpts(ssl); const createServer = this._getCreateServerMethod(ssl); this.server1Info = createServerInfo(hostname, port1, port2, protocol, cache); this.server2Info = createServerInfo(hostname, port2, port1, protocol, cache); this.server1 = createServer(opts, (req, res) => this._onRequest(req, res, this.server1Info)); this.server2 = createServer(opts, (req, res) => this._onRequest(req, res, this.server2Info)); this.server1.on('upgrade', (req, socket, head) => this._onUpgradeRequest(req, socket, head, this.server1Info)); this.server2.on('upgrade', (req, socket, head) => this._onUpgradeRequest(req, socket, head, this.server2Info)); this.server1.listen(port1); this.server2.listen(port2); this.sockets = new Set(); // BUG: GH-89 this._startSocketsCollecting(); this._registerServiceRoutes(developmentMode); } _getOpts(ssl) { let opts = {}; if (ssl) opts = ssl; opts.maxHeaderSize = Proxy.MAX_REQUEST_HEADER_SIZE; return opts; } _getCreateServerMethod(ssl) { return ssl ? https_1.default.createServer : http_1.default.createServer; } _closeSockets() { this.sockets.forEach(socket => socket.destroy()); } _startSocketsCollecting() { const handler = (socket) => { this.sockets.add(socket); socket.on('close', () => this.sockets.delete(socket)); }; this.server1.on('connection', handler); this.server2.on('connection', handler); } _registerServiceRoutes(developmentMode) { const hammerheadScriptContent = (0, load_client_script_1.default)(service_routes_1.default.hammerhead, developmentMode); const transportWorkerContent = (0, load_client_script_1.default)(service_routes_1.default.transportWorker, developmentMode); const workerHammerheadContent = (0, load_client_script_1.default)(service_routes_1.default.workerHammerhead, developmentMode); this.GET(service_routes_1.default.hammerhead, { contentType: 'application/x-javascript', content: hammerheadScriptContent, }); this.GET(service_routes_1.default.transportWorker, { contentType: 'application/x-javascript', content: transportWorkerContent, }); this.GET(service_routes_1.default.workerHammerhead, { contentType: 'application/x-javascript', content: workerHammerheadContent, }); this.POST(service_routes_1.default.messaging, (req, res, serverInfo) => this._onServiceMessage(req, res, serverInfo)); if (this.options.proxyless) this.OPTIONS(service_routes_1.default.messaging, (req, res) => this._onServiceMessagePreflight(req, res)); this.GET(service_routes_1.default.task, (req, res, serverInfo) => this._onTaskScriptRequest(req, res, serverInfo, false)); this.GET(service_routes_1.default.iframeTask, (req, res, serverInfo) => this._onTaskScriptRequest(req, res, serverInfo, true)); } async _onServiceMessage(req, res, serverInfo) { const body = await (0, http_2.fetchBody)(req); const msg = parseAsJson(body); const session = msg && this.openSessions.get(msg.sessionId); if (msg && session) { try { const result = await session.handleServiceMessage(msg, serverInfo); logger_1.default.serviceMsg.onMessage(msg, result); res.setHeader(builtin_header_names_1.default.setCookie, session.takePendingSyncCookies()); (0, http_2.respondWithJSON)(res, result, false, this.options.proxyless); } catch (err) { logger_1.default.serviceMsg.onError(msg, err); (0, http_2.respond500)(res, (0, err_to_string_1.default)(err)); } } else (0, http_2.respond500)(res, SESSION_IS_NOT_OPENED_ERR); } _onServiceMessagePreflight(_req, res) { // NOTE: 'Cache-control' header set in the 'Transport' sandbox on the client side. // Request becomes non-simple (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) // and initiates the CORS preflight request. res.setHeader('access-control-allow-headers', builtin_header_names_1.default.cacheControl); (0, http_2.acceptCrossOrigin)(res); (0, http_2.respond204)(res); } async _onTaskScriptRequest(req, res, serverInfo, isIframe) { const referer = req.headers[builtin_header_names_1.default.referer]; const refererDest = referer && urlUtils.parseProxyUrl(referer); const session = refererDest && this.openSessions.get(refererDest.sessionId); const windowId = refererDest && refererDest.windowId || void 0; if (session) { if (referer && !isIframe) session.options.referer = referer; res.setHeader(builtin_header_names_1.default.contentType, 'application/x-javascript'); (0, http_2.addPreventCachingHeaders)(res); const taskScript = await session.getTaskScript({ referer, cookieUrl: refererDest ? refererDest.destUrl : '', serverInfo, isIframe, withPayload: true, windowId, }); res.end(taskScript); } else (0, http_2.respond500)(res, SESSION_IS_NOT_OPENED_ERR); } _onRequest(req, res, serverInfo) { // NOTE: Not a service request, execute the proxy pipeline. if (!this._route(req, res, serverInfo)) (0, request_pipeline_1.run)(req, res, serverInfo, this.openSessions, this.options.proxyless); } _onUpgradeRequest(req, socket, head, serverInfo) { if (head && head.length) socket.unshift(head); this._onRequest(req, socket, serverInfo); } _processStaticContent(handler) { if (handler.isShadowUIStylesheet) handler.content = (0, create_shadow_stylesheet_1.default)(handler.content); } // API close() { script_1.default.jsCache.reset(); this.server1.close(); this.server2.close(); this._closeSockets(); (0, agent_1.resetKeepAliveConnections)(); } openSession(url, session, externalProxySettings) { session.proxy = this; this.openSessions.set(session.id, session); if (externalProxySettings) session.setExternalProxySettings(externalProxySettings); if (this.options.disableHttp2) session.disableHttp2(); if (this.options.disableCrossDomain) session.disableCrossDomain(); url = urlUtils.prepareUrl(url); if (this.options.proxyless) return url; return urlUtils.getProxyUrl(url, { proxyHostname: this.server1Info.hostname, proxyPort: this.server1Info.port.toString(), proxyProtocol: this.server1Info.protocol, sessionId: session.id, windowId: session.options.windowId, }); } closeSession(session) { session.proxy = null; this.openSessions.delete(session.id); } resolveRelativeServiceUrl(relativeServiceUrl, domain = this.server1Info.domain) { return new URL(relativeServiceUrl, domain).toString(); } } exports.default = Proxy; // Max header size for incoming HTTP requests // Set to 80 KB as it was the original limit: // https://github.com/nodejs/node/blob/186035243fad247e3955fa0c202987cae99e82db/deps/http_parser/http_parser.h#L63 // Before the change to 8 KB: // https://github.com/nodejs/node/commit/186035243fad247e3955fa0c202987cae99e82db#diff-1d0d420098503156cddb601e523b82e7R59 Proxy.MAX_REQUEST_HEADER_SIZE = 80 * 1024;module.exports = exports.default;