246 lines
11 KiB
JavaScript
246 lines
11 KiB
JavaScript
"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;
|
|
|