From 5abd99ebe3e8cebbd2ed813ef0825b1ddddcc3b6 Mon Sep 17 00:00:00 2001 From: Mikhail Losev Date: Wed, 1 Feb 2023 14:00:56 +0400 Subject: [PATCH] proxyless: client scripts (#2848) * proxyless: client scripts * bump version * update typing --------- Co-authored-by: Popov Aleksey --- package.json | 2 +- src/processing/interfaces.ts | 1 + src/processing/resources/index.ts | 2 +- src/processing/resources/page.ts | 6 ++++ src/request-pipeline/context/base.ts | 28 +++++++++++++++++-- src/request-pipeline/context/index.ts | 22 +-------------- src/session/index.ts | 2 +- .../without-request-pipeline/expected.html | 2 +- .../proxy/without-request-pipeline-test.js | 1 + ts-defs/index.d.ts | 7 +++++ 10 files changed, 46 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 5a0ef76bc..0bfe601b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "testcafe-hammerhead", "description": "A powerful web-proxy used as a core for the TestCafe testing framework (https://github.com/DevExpress/testcafe).", - "version": "28.3.1", + "version": "28.4.0", "homepage": "https://github.com/DevExpress/testcafe-hammerhead", "bugs": { "url": "https://github.com/DevExpress/testcafe-hammerhead/issues" diff --git a/src/processing/interfaces.ts b/src/processing/interfaces.ts index 7dbe9ff58..8eea3fc85 100644 --- a/src/processing/interfaces.ts +++ b/src/processing/interfaces.ts @@ -9,6 +9,7 @@ export interface PageInjectableResources { stylesheets: string[]; scripts: string[]; embeddedScripts: string[]; + userScripts: string[]; } export interface PageRestoreStoragesOptions { diff --git a/src/processing/resources/index.ts b/src/processing/resources/index.ts index b66235a31..7c029e74f 100644 --- a/src/processing/resources/index.ts +++ b/src/processing/resources/index.ts @@ -58,7 +58,7 @@ export async function process (ctx: RequestPipelineContext): Promise { const urlReplacer = getResourceUrlReplacer(ctx); if (pageProcessor === processor) - await ctx.prepareInjectableUserScripts(); + await ctx.prepareInjectableUserScripts(ctx.eventFactory, ctx.session.injectable.userScripts); // @ts-ignore: Cannot invoke an expression whose type lacks a call signature const processed = processor.processResource(decoded, ctx, charset, urlReplacer); diff --git a/src/processing/resources/page.ts b/src/processing/resources/page.ts index ccf7d4637..70cc5bbe6 100644 --- a/src/processing/resources/page.ts +++ b/src/processing/resources/page.ts @@ -138,6 +138,12 @@ class PageProcessor extends ResourceProcessorBase { }); } + if ((processingOptions as PageInjectableResources).userScripts) { + (processingOptions as PageInjectableResources).userScripts.forEach(script => { + injectedResources.push(PageProcessor._createShadowUIScriptWithUrlNode(script)); + }); + } + for (let i = injectedResources.length - 1; i > -1; i--) parse5Utils.insertBeforeFirstScript(injectedResources[i], head); diff --git a/src/request-pipeline/context/base.ts b/src/request-pipeline/context/base.ts index 0e1fcb205..3ea38b84a 100644 --- a/src/request-pipeline/context/base.ts +++ b/src/request-pipeline/context/base.ts @@ -11,6 +11,8 @@ import { PreparedResponseInfo } from '../request-hooks/events/info'; import ResponseEvent from '../request-hooks/events/response-event'; import { OnResponseEventData } from '../../typings/context'; import ConfigureResponseEventOptions from '../request-hooks/events/configure-response-event-options'; +import requestIsMatchRule from '../request-hooks/request-is-match-rule'; +import { UserScript } from '../../session'; export default abstract class BaseRequestPipelineContext { @@ -19,10 +21,12 @@ export default abstract class BaseRequestPipelineContext { public reqOpts: RequestOptions; public mock: ResponseMock; public onResponseEventData: OnResponseEventData[] = []; + protected injectableUserScripts: string[]; protected constructor (requestId: string) { - this.requestFilterRules = []; - this.requestId = requestId; + this.requestFilterRules = []; + this.requestId = requestId; + this.injectableUserScripts = []; } private async _forEachRequestFilterRule (fn: (rule: RequestFilterRule) => Promise): Promise { @@ -98,4 +102,24 @@ export default abstract class BaseRequestPipelineContext { await eventProvider.callRequestHookErrorHandler(targetRule, this.mock.error as Error); } + + public async prepareInjectableUserScripts (eventFactory: BaseRequestHookEventFactory, userScripts: UserScript[]): Promise { + if (!userScripts.length) + return; + + const requestInfo = eventFactory.createRequestInfo(); + const matchedUserScripts = await Promise.all(userScripts.map(async userScript => { + if (await requestIsMatchRule(userScript.page, requestInfo)) + return userScript; + + return void 0; + } )); + + const injectableUserScripts = matchedUserScripts + .filter(userScript => !!userScript) + .map(userScript => userScript?.url || ''); + + if (injectableUserScripts) + this.injectableUserScripts = injectableUserScripts; + } } diff --git a/src/request-pipeline/context/index.ts b/src/request-pipeline/context/index.ts index 4ab239e72..8b346e99d 100644 --- a/src/request-pipeline/context/index.ts +++ b/src/request-pipeline/context/index.ts @@ -20,14 +20,12 @@ import * as contentTypeUtils from '../../utils/content-type'; import generateUniqueId from '../../utils/generate-unique-id'; import { check as checkSameOriginPolicy } from '../same-origin-policy'; import * as headerTransforms from '../header-transforms'; -import { RequestInfo } from '../request-hooks/events/info'; import SERVICE_ROUTES from '../../proxy/service-routes'; import BUILTIN_HEADERS from '../builtin-header-names'; import logger from '../../utils/logger'; import createSpecialPageResponse from '../create-special-page-response'; import { fetchBody } from '../../utils/http'; import * as requestCache from '../cache'; -import requestIsMatchRule from '../request-hooks/request-is-match-rule'; import { Http2Response } from '../destination-request/http2'; import BaseRequestPipelineContext from './base'; import RequestPipelineRequestHookEventFactory from '../request-hooks/events/factory'; @@ -120,7 +118,6 @@ export default class RequestPipelineContext extends BaseRequestPipelineContext { isSameOriginPolicyFailed = false; windowId?: string; temporaryCacheEntry?: RequestCacheEntry; - _injectableUserScripts: string[] = []; eventFactory: RequestPipelineRequestHookEventFactory; constructor (readonly req: IncomingMessage, @@ -355,23 +352,6 @@ export default class RequestPipelineContext extends BaseRequestPipelineContext { logger.proxy.onContentInfoBuilt(this); } - public async prepareInjectableUserScripts (): Promise { - const requestInfo = RequestInfo.from(this); - const matchedUserScripts = await Promise.all(this.session.injectable.userScripts.map(async userScript => { - if (await requestIsMatchRule(userScript.page, requestInfo)) - return userScript; - - return void 0; - } )); - - const injectableUserScripts = matchedUserScripts - .filter(userScript => !!userScript) - .map(userScript => userScript?.url || ''); - - if (injectableUserScripts) - this._injectableUserScripts = injectableUserScripts; - } - private _handleAttachment (): void { let isOpenedInNewWindow = false; @@ -410,7 +390,7 @@ export default class RequestPipelineContext extends BaseRequestPipelineContext { getInjectableScripts (): string[] { const taskScript = this.isIframe ? SERVICE_ROUTES.iframeTask : SERVICE_ROUTES.task; - const scripts = this.session.injectable.scripts.concat(taskScript, this._injectableUserScripts); + const scripts = this.session.injectable.scripts.concat(taskScript, this.injectableUserScripts); return this._resolveInjectableUrls(scripts); } diff --git a/src/session/index.ts b/src/session/index.ts index 63c5859d0..ba95224dd 100644 --- a/src/session/index.ts +++ b/src/session/index.ts @@ -33,7 +33,7 @@ import { EventEmitter } from 'events'; const TASK_TEMPLATE = read('../client/task.js.mustache'); -interface UserScript { +export interface UserScript { url: string; page: RequestFilterRule; } diff --git a/test/server/data/without-request-pipeline/expected.html b/test/server/data/without-request-pipeline/expected.html index 3a21b2019..bbba23e7f 100644 --- a/test/server/data/without-request-pipeline/expected.html +++ b/test/server/data/without-request-pipeline/expected.html @@ -1,7 +1,7 @@ Title - +

Simple page

diff --git a/test/server/proxy/without-request-pipeline-test.js b/test/server/proxy/without-request-pipeline-test.js index dd8d9e07e..4dbb3caac 100644 --- a/test/server/proxy/without-request-pipeline-test.js +++ b/test/server/proxy/without-request-pipeline-test.js @@ -10,6 +10,7 @@ describe('New API', () => { stylesheets: ['./styles.css'], scripts: ['common/script1.js', './common/script2.js'], embeddedScripts: ['var script1 = 1;', 'var script2 = 2;'], + userScripts: ['/custom-script1.js', '/custom-script2.js'], }; const updatedPageContent = injectResources(pageContent, resources); diff --git a/ts-defs/index.d.ts b/ts-defs/index.d.ts index 36b912e5b..bd1cad4fd 100644 --- a/ts-defs/index.d.ts +++ b/ts-defs/index.d.ts @@ -190,6 +190,7 @@ declare module 'testcafe-hammerhead' { stylesheets: string[]; scripts: string[]; embeddedScripts: string[]; + userScripts?: string[]; } export interface PageRestoreStoragesOptions { @@ -652,6 +653,9 @@ declare module 'testcafe-hammerhead' { /** Information for generating the response events **/ onResponseEventData: OnResponseEventData[]; + /** The target injectable user scripts **/ + injectableUserScripts: string[]; + /** Set request options for the current context **/ setRequestOptions (eventFactory: BaseRequestHookEventFactory): void; @@ -672,5 +676,8 @@ declare module 'testcafe-hammerhead' { /** Get OnResponseEventData depending on specified filter **/ getOnResponseEventData ({ includeBody }: { includeBody: boolean }): OnResponseEventData[]; + + /** Prepare the target injectable user scripts for the current route **/ + prepareInjectableUserScripts (eventFactory: BaseRequestHookEventFactory, userScripts: UserScript[]): Promise; } }