"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;