407 lines
19 KiB
JavaScript
407 lines
19 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 cookie_1 = require("../../utils/cookie");
|
||
|
const incoming_message_like_1 = __importDefault(require("../incoming-message-like"));
|
||
|
const charset_1 = __importDefault(require("../../processing/encoding/charset"));
|
||
|
const urlUtils = __importStar(require("../../utils/url"));
|
||
|
const contentTypeUtils = __importStar(require("../../utils/content-type"));
|
||
|
const generate_unique_id_1 = __importDefault(require("../../utils/generate-unique-id"));
|
||
|
const same_origin_policy_1 = require("../same-origin-policy");
|
||
|
const headerTransforms = __importStar(require("../header-transforms"));
|
||
|
const service_routes_1 = __importDefault(require("../../proxy/service-routes"));
|
||
|
const builtin_header_names_1 = __importDefault(require("../builtin-header-names"));
|
||
|
const logger_1 = __importDefault(require("../../utils/logger"));
|
||
|
const create_special_page_response_1 = __importDefault(require("../create-special-page-response"));
|
||
|
const http_1 = require("../../utils/http");
|
||
|
const requestCache = __importStar(require("../cache"));
|
||
|
const base_1 = __importDefault(require("./base"));
|
||
|
const factory_1 = __importDefault(require("../request-hooks/events/factory"));
|
||
|
const stream_1 = require("stream");
|
||
|
const promisify_stream_1 = __importDefault(require("../../utils/promisify-stream"));
|
||
|
const buffer_1 = require("../../utils/buffer");
|
||
|
const is_redirect_status_code_1 = __importDefault(require("../../utils/is-redirect-status-code"));
|
||
|
const CANNOT_BE_USED_WITH_WEB_SOCKET_ERR_MSG = 'The function cannot be used with a WebSocket request.';
|
||
|
class RequestPipelineContext extends base_1.default {
|
||
|
constructor(req, res, serverInfo, proxyless) {
|
||
|
super((0, generate_unique_id_1.default)());
|
||
|
this.req = req;
|
||
|
this.res = res;
|
||
|
this.serverInfo = serverInfo;
|
||
|
this.proxyless = proxyless;
|
||
|
this.isDestResReadableEnded = false;
|
||
|
this.isAjax = false;
|
||
|
this.isPage = false;
|
||
|
this.isHTMLPage = false;
|
||
|
this.isHtmlImport = false;
|
||
|
this.isWebSocket = false;
|
||
|
this.isIframe = false;
|
||
|
this.isSpecialPage = false;
|
||
|
this.isWebSocketConnectionReset = false;
|
||
|
this.goToNextStage = true;
|
||
|
this.isSameOriginPolicyFailed = false;
|
||
|
this._initParsedClientSyncCookie();
|
||
|
this.eventFactory = new factory_1.default(this);
|
||
|
}
|
||
|
_initParsedClientSyncCookie() {
|
||
|
if (!this.req.headers.cookie)
|
||
|
return;
|
||
|
const parsedClientSyncCookieStr = (0, cookie_1.parseClientSyncCookieStr)(this.req.headers.cookie);
|
||
|
if (parsedClientSyncCookieStr)
|
||
|
this.parsedClientSyncCookie = parsedClientSyncCookieStr;
|
||
|
}
|
||
|
// TODO: Rewrite parseProxyUrl instead.
|
||
|
static _flattenParsedProxyUrl(parsed) {
|
||
|
if (!parsed)
|
||
|
return null;
|
||
|
const parsedResourceType = urlUtils.parseResourceType(parsed.resourceType);
|
||
|
const dest = {
|
||
|
url: parsed.destUrl,
|
||
|
protocol: parsed.destResourceInfo.protocol || '',
|
||
|
host: parsed.destResourceInfo.host || '',
|
||
|
hostname: parsed.destResourceInfo.hostname || '',
|
||
|
port: parsed.destResourceInfo.port || '',
|
||
|
partAfterHost: parsed.destResourceInfo.partAfterHost || '',
|
||
|
auth: parsed.destResourceInfo.auth,
|
||
|
isIframe: !!parsedResourceType.isIframe,
|
||
|
isForm: !!parsedResourceType.isForm,
|
||
|
isScript: !!(parsedResourceType.isScript || parsedResourceType.isServiceWorker),
|
||
|
isEventSource: !!parsedResourceType.isEventSource,
|
||
|
isHtmlImport: !!parsedResourceType.isHtmlImport,
|
||
|
isWebSocket: !!parsedResourceType.isWebSocket,
|
||
|
isServiceWorker: !!parsedResourceType.isServiceWorker,
|
||
|
isAjax: !!parsedResourceType.isAjax,
|
||
|
isObject: !!parsedResourceType.isObject,
|
||
|
charset: parsed.charset || '',
|
||
|
reqOrigin: parsed.reqOrigin || '',
|
||
|
credentials: parsed.credentials,
|
||
|
};
|
||
|
return { dest, sessionId: parsed.sessionId, windowId: parsed.windowId };
|
||
|
}
|
||
|
_isFileDownload() {
|
||
|
const contentDisposition = this.destRes.headers[builtin_header_names_1.default.contentDisposition];
|
||
|
return !!contentDisposition &&
|
||
|
contentDisposition.includes('attachment') &&
|
||
|
contentDisposition.includes('filename');
|
||
|
}
|
||
|
_resolveInjectableUrls(injectableUrls) {
|
||
|
return injectableUrls.map(url => this.resolveInjectableUrl(url));
|
||
|
}
|
||
|
_initRequestNatureInfo() {
|
||
|
const acceptHeader = this.req.headers[builtin_header_names_1.default.accept];
|
||
|
this.isWebSocket = this.dest.isWebSocket;
|
||
|
this.isHtmlImport = this.dest.isHtmlImport;
|
||
|
this.isAjax = this.dest.isAjax;
|
||
|
this.isPage = !this.isAjax && !this.isWebSocket && acceptHeader &&
|
||
|
contentTypeUtils.isPage(acceptHeader) || this.isHtmlImport;
|
||
|
this.isIframe = this.dest.isIframe;
|
||
|
this.isSpecialPage = urlUtils.isSpecialPage(this.dest.url);
|
||
|
this.isFileProtocol = this.dest.protocol === 'file:';
|
||
|
this.isHTMLPage = this.isPage && !this.isIframe && !this.isHtmlImport;
|
||
|
}
|
||
|
_getDestFromReferer(parsedReferer) {
|
||
|
const dest = parsedReferer.dest;
|
||
|
dest.partAfterHost = this.req.url || '';
|
||
|
dest.url = urlUtils.formatUrl(dest);
|
||
|
return { dest, sessionId: parsedReferer.sessionId, windowId: parsedReferer.windowId };
|
||
|
}
|
||
|
_addTemporaryEntryToCache() {
|
||
|
if (!this.temporaryCacheEntry)
|
||
|
return;
|
||
|
this.temporaryCacheEntry.value.res.setBody(this.destResBody);
|
||
|
requestCache.add(this.temporaryCacheEntry);
|
||
|
this.temporaryCacheEntry = void 0;
|
||
|
}
|
||
|
// API
|
||
|
dispatch(openSessions) {
|
||
|
const parsedReqUrl = urlUtils.parseProxyUrl(this.req.url || '');
|
||
|
const referer = this.req.headers[builtin_header_names_1.default.referer];
|
||
|
let parsedReferer = referer && urlUtils.parseProxyUrl(referer) || null;
|
||
|
// TODO: Remove it after parseProxyURL is rewritten.
|
||
|
let flattenParsedReqUrl = RequestPipelineContext._flattenParsedProxyUrl(parsedReqUrl);
|
||
|
let flattenParsedReferer = RequestPipelineContext._flattenParsedProxyUrl(parsedReferer);
|
||
|
// NOTE: Remove that after implementing the https://github.com/DevExpress/testcafe-hammerhead/issues/2155
|
||
|
if (!flattenParsedReqUrl && flattenParsedReferer)
|
||
|
flattenParsedReqUrl = this._getDestFromReferer(flattenParsedReferer);
|
||
|
if (!flattenParsedReqUrl)
|
||
|
return false;
|
||
|
const session = openSessions.get(flattenParsedReqUrl.sessionId);
|
||
|
if (session)
|
||
|
this.session = session;
|
||
|
if (!this.session)
|
||
|
return false;
|
||
|
if (!flattenParsedReferer && this.session.options.referer) {
|
||
|
parsedReferer = urlUtils.parseProxyUrl(this.session.options.referer) || null;
|
||
|
flattenParsedReferer = RequestPipelineContext._flattenParsedProxyUrl(parsedReferer);
|
||
|
}
|
||
|
this.dest = flattenParsedReqUrl.dest;
|
||
|
this.windowId = flattenParsedReqUrl.windowId;
|
||
|
this.dest.partAfterHost = RequestPipelineContext._preparePartAfterHost(this.dest.partAfterHost);
|
||
|
this.dest.domain = urlUtils.getDomain(this.dest);
|
||
|
if (flattenParsedReferer) {
|
||
|
this.dest.referer = flattenParsedReferer.dest.url;
|
||
|
this.dest.reqOrigin = this.dest.reqOrigin || urlUtils.getDomain(flattenParsedReferer.dest);
|
||
|
}
|
||
|
else
|
||
|
this.dest.reqOrigin = this.dest.reqOrigin || this.dest.domain;
|
||
|
this._initRequestNatureInfo();
|
||
|
this._applyClientSyncCookie();
|
||
|
return true;
|
||
|
}
|
||
|
_applyClientSyncCookie() {
|
||
|
if (!this.parsedClientSyncCookie)
|
||
|
return;
|
||
|
const clientCookie = this.parsedClientSyncCookie.actual.filter(syncCookie => syncCookie.isClientSync && syncCookie.sid === this.session.id);
|
||
|
this.session.cookies.setByClient(clientCookie);
|
||
|
}
|
||
|
static _preparePartAfterHost(str) {
|
||
|
// Browsers add a leading slash to the pathname part of url (GH-608)
|
||
|
// For example: url http://www.example.com?gd=GID12082014 will be converted
|
||
|
// to http://www.example.com/?gd=GID12082014
|
||
|
return (str[0] === '/' ? '' : '/') + str;
|
||
|
}
|
||
|
buildContentInfo() {
|
||
|
const contentType = this.destRes.headers[builtin_header_names_1.default.contentType] || '';
|
||
|
const accept = this.req.headers[builtin_header_names_1.default.accept] || '';
|
||
|
const encoding = (this.destRes.headers[builtin_header_names_1.default.contentEncoding] || '').toLowerCase();
|
||
|
const isTextPage = this.isPage && contentTypeUtils.isTextPage(contentType);
|
||
|
if (this.isPage && contentType && !isTextPage)
|
||
|
this.isPage = !this.isAjax && contentTypeUtils.isPage(contentType);
|
||
|
const isCSS = contentTypeUtils.isCSSResource(contentType, accept);
|
||
|
const isManifest = contentTypeUtils.isManifest(contentType);
|
||
|
const isScript = this.dest.isScript || contentTypeUtils.isScriptResource(contentType, accept);
|
||
|
const isForm = this.dest.isForm;
|
||
|
const isObject = this.dest.isObject;
|
||
|
const isFormWithEmptyResponse = isForm && this.destRes.statusCode === 204;
|
||
|
const isRedirect = this.destRes.headers[builtin_header_names_1.default.location] &&
|
||
|
this.destRes.statusCode &&
|
||
|
(0, is_redirect_status_code_1.default)(this.destRes.statusCode) ||
|
||
|
false;
|
||
|
const requireAssetsProcessing = (isCSS || isScript || isManifest) && this.destRes.statusCode !== 204;
|
||
|
const isNotModified = this.req.method === 'GET' && this.destRes.statusCode === 304 &&
|
||
|
!!(this.req.headers[builtin_header_names_1.default.ifModifiedSince] ||
|
||
|
this.req.headers[builtin_header_names_1.default.ifNoneMatch]);
|
||
|
const requireProcessing = !this.isAjax && !isFormWithEmptyResponse && !isRedirect &&
|
||
|
!isNotModified && (this.isPage || this.isIframe || requireAssetsProcessing);
|
||
|
const isFileDownload = this._isFileDownload() && !this.dest.isScript;
|
||
|
const isIframeWithImageSrc = this.isIframe && !this.isPage && /^\s*image\//.test(contentType);
|
||
|
const isAttachment = !this.isPage && !this.isAjax && !this.isWebSocket && !this.isIframe &&
|
||
|
!isTextPage && !isManifest && !isScript && !isForm && !isObject;
|
||
|
const charset = new charset_1.default();
|
||
|
const contentTypeUrlToken = urlUtils.getResourceTypeString({
|
||
|
isIframe: this.isIframe,
|
||
|
isAjax: this.isAjax,
|
||
|
isForm, isScript,
|
||
|
}) || '';
|
||
|
// NOTE: We need charset information if we are going to process the resource.
|
||
|
if (requireProcessing && !charset.fromContentType(contentType))
|
||
|
charset.fromUrl(this.dest.charset);
|
||
|
if (isFileDownload)
|
||
|
this.session.handleFileDownload();
|
||
|
if (isAttachment)
|
||
|
this._handleAttachment();
|
||
|
this.contentInfo = {
|
||
|
charset,
|
||
|
requireProcessing,
|
||
|
isIframeWithImageSrc,
|
||
|
isCSS,
|
||
|
isScript,
|
||
|
isManifest,
|
||
|
isObject,
|
||
|
encoding,
|
||
|
contentTypeUrlToken,
|
||
|
isFileDownload,
|
||
|
isNotModified,
|
||
|
isRedirect,
|
||
|
isAttachment,
|
||
|
isTextPage,
|
||
|
};
|
||
|
logger_1.default.proxy.onContentInfoBuilt(this);
|
||
|
}
|
||
|
_handleAttachment() {
|
||
|
let isOpenedInNewWindow = false;
|
||
|
if (this.req.url) {
|
||
|
const url1 = urlUtils.parseProxyUrl(this.req.url);
|
||
|
const url2 = urlUtils.parseProxyUrl(this.req.headers[builtin_header_names_1.default.referer]);
|
||
|
isOpenedInNewWindow = (url1 === null || url1 === void 0 ? void 0 : url1.windowId) !== (url2 === null || url2 === void 0 ? void 0 : url2.windowId);
|
||
|
}
|
||
|
this.session.handleAttachment({ isOpenedInNewWindow });
|
||
|
}
|
||
|
async _getDestResBody(res) {
|
||
|
if (incoming_message_like_1.default.isIncomingMessageLike(res)) {
|
||
|
const body = res.getBody();
|
||
|
if (body)
|
||
|
return body;
|
||
|
}
|
||
|
return (0, http_1.fetchBody)(this.destRes, this.destRes.headers[builtin_header_names_1.default.contentLength]);
|
||
|
}
|
||
|
calculateIsDestResReadableEnded() {
|
||
|
if (!this.contentInfo.isNotModified &&
|
||
|
!this.contentInfo.isRedirect &&
|
||
|
!incoming_message_like_1.default.isIncomingMessageLike(this.destRes)) {
|
||
|
this.destRes.once('end', () => {
|
||
|
this.isDestResReadableEnded = true;
|
||
|
});
|
||
|
}
|
||
|
else
|
||
|
this.isDestResReadableEnded = true;
|
||
|
}
|
||
|
getInjectableScripts() {
|
||
|
const taskScript = this.isIframe ? service_routes_1.default.iframeTask : service_routes_1.default.task;
|
||
|
const scripts = this.session.injectable.scripts.concat(taskScript, this.injectableUserScripts);
|
||
|
return this._resolveInjectableUrls(scripts);
|
||
|
}
|
||
|
getInjectableStyles() {
|
||
|
return this._resolveInjectableUrls(this.session.injectable.styles);
|
||
|
}
|
||
|
redirect(url) {
|
||
|
if (this.isWebSocket)
|
||
|
throw new Error(CANNOT_BE_USED_WITH_WEB_SOCKET_ERR_MSG);
|
||
|
const res = this.res;
|
||
|
res.statusCode = 302;
|
||
|
res.setHeader(builtin_header_names_1.default.location, url);
|
||
|
res.end();
|
||
|
}
|
||
|
saveNonProcessedDestResBody(value) {
|
||
|
this.nonProcessedDestResBody = value;
|
||
|
}
|
||
|
closeWithError(statusCode, resBody = '') {
|
||
|
if ('setHeader' in this.res && !this.res.headersSent) {
|
||
|
this.res.statusCode = statusCode;
|
||
|
this.res.setHeader(builtin_header_names_1.default.contentType, 'text/html');
|
||
|
this.res.write(resBody);
|
||
|
}
|
||
|
this.res.end();
|
||
|
this.goToNextStage = false;
|
||
|
}
|
||
|
toProxyUrl(url, isCrossDomain, resourceType, charset, reqOrigin, credentials) {
|
||
|
const proxyHostname = this.serverInfo.hostname;
|
||
|
const proxyProtocol = this.serverInfo.protocol;
|
||
|
const proxyPort = isCrossDomain ? this.serverInfo.crossDomainPort.toString() : this.serverInfo.port.toString();
|
||
|
const sessionId = this.session.id;
|
||
|
const windowId = this.windowId;
|
||
|
if (isCrossDomain)
|
||
|
reqOrigin = this.dest.domain;
|
||
|
return urlUtils.getProxyUrl(url, {
|
||
|
proxyHostname,
|
||
|
proxyProtocol,
|
||
|
proxyPort,
|
||
|
sessionId,
|
||
|
resourceType,
|
||
|
charset,
|
||
|
windowId,
|
||
|
reqOrigin,
|
||
|
credentials,
|
||
|
});
|
||
|
}
|
||
|
getProxyOrigin(isCrossDomain = false) {
|
||
|
return urlUtils.getDomain({
|
||
|
protocol: this.serverInfo.protocol,
|
||
|
hostname: this.serverInfo.hostname,
|
||
|
port: isCrossDomain ? this.serverInfo.crossDomainPort : this.serverInfo.port,
|
||
|
});
|
||
|
}
|
||
|
isPassSameOriginPolicy() {
|
||
|
const shouldPerformCORSCheck = this.isAjax && !this.contentInfo.isNotModified;
|
||
|
return !shouldPerformCORSCheck || (0, same_origin_policy_1.check)(this);
|
||
|
}
|
||
|
sendResponseHeaders() {
|
||
|
if (this.isWebSocket)
|
||
|
throw new Error(CANNOT_BE_USED_WITH_WEB_SOCKET_ERR_MSG);
|
||
|
const headers = headerTransforms.forResponse(this);
|
||
|
const res = this.res;
|
||
|
if (this.isHTMLPage && this.session.options.disablePageCaching)
|
||
|
headerTransforms.setupPreventCachingHeaders(headers);
|
||
|
logger_1.default.proxy.onResponse(this, headers);
|
||
|
res.writeHead(this.destRes.statusCode, headers);
|
||
|
res.addTrailers(this.destRes.trailers);
|
||
|
}
|
||
|
async mockResponse(eventProvider) {
|
||
|
logger_1.default.destination.onMockedRequest(this);
|
||
|
this.destRes = await this.getMockResponse();
|
||
|
this.buildContentInfo();
|
||
|
if (!this.mock.hasError)
|
||
|
return;
|
||
|
await this.handleMockError(eventProvider);
|
||
|
logger_1.default.proxy.onMockResponseError(this.requestFilterRules[0], this.mock.error);
|
||
|
}
|
||
|
resolveInjectableUrl(url) {
|
||
|
return this.serverInfo.domain + url;
|
||
|
}
|
||
|
respondForSpecialPage() {
|
||
|
this.destRes = (0, create_special_page_response_1.default)();
|
||
|
this.buildContentInfo();
|
||
|
}
|
||
|
async fetchDestResBody() {
|
||
|
this.destResBody = await this._getDestResBody(this.destRes);
|
||
|
if (!this.temporaryCacheEntry)
|
||
|
return;
|
||
|
this._addTemporaryEntryToCache();
|
||
|
}
|
||
|
async pipeNonProcessedResponse() {
|
||
|
if (!this.serverInfo.cacheRequests) {
|
||
|
this.destRes.pipe(this.res);
|
||
|
return;
|
||
|
}
|
||
|
this.destResBody = await this._getDestResBody(this.destRes);
|
||
|
if (this.temporaryCacheEntry && this.destResBody.length < requestCache.MAX_SIZE_FOR_NON_PROCESSED_RESOURCE)
|
||
|
this._addTemporaryEntryToCache();
|
||
|
this.res.write(this.destResBody);
|
||
|
this.res.end();
|
||
|
}
|
||
|
createCacheEntry(res) {
|
||
|
if (requestCache.shouldCache(this) && !incoming_message_like_1.default.isIncomingMessageLike(res))
|
||
|
this.temporaryCacheEntry = requestCache.create(this.reqOpts, res);
|
||
|
}
|
||
|
async callOnResponseEventCallbackWithoutBodyForNonProcessedResource(ctx, onResponseEventDataWithoutBody) {
|
||
|
await Promise.all(onResponseEventDataWithoutBody.map(async (eventData) => {
|
||
|
await ctx.onRequestHookResponse(ctx.session.requestHookEventProvider, ctx.eventFactory, eventData.rule, eventData.opts);
|
||
|
}));
|
||
|
ctx.destRes.pipe(ctx.res);
|
||
|
}
|
||
|
async callOnResponseEventCallbackForMotModifiedResource(ctx) {
|
||
|
await Promise.all(ctx.onResponseEventData.map(async (eventData) => {
|
||
|
await ctx.onRequestHookResponse(ctx.session.requestHookEventProvider, ctx.eventFactory, eventData.rule, eventData.opts);
|
||
|
}));
|
||
|
ctx.res.end();
|
||
|
}
|
||
|
async callOnResponseEventCallbackWithBodyForNonProcessedRequest(ctx, onResponseEventDataWithBody) {
|
||
|
const destResBodyCollectorStream = new stream_1.PassThrough();
|
||
|
ctx.destRes.pipe(destResBodyCollectorStream);
|
||
|
(0, promisify_stream_1.default)(destResBodyCollectorStream).then(async (data) => {
|
||
|
ctx.saveNonProcessedDestResBody(data);
|
||
|
await Promise.all(onResponseEventDataWithBody.map(async (eventData) => {
|
||
|
await ctx.onRequestHookResponse(ctx.session.requestHookEventProvider, ctx.eventFactory, eventData.rule, eventData.opts);
|
||
|
}));
|
||
|
(0, buffer_1.toReadableStream)(data).pipe(ctx.res);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
exports.default = RequestPipelineContext;module.exports = exports.default;
|
||
|
|