396 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			396 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use strict";
 | |
| Object.defineProperty(exports, "__esModule", { value: true });
 | |
| exports.CdpTarget = void 0;
 | |
| const chromium_bidi_js_1 = require("../../../protocol/chromium-bidi.js");
 | |
| const Deferred_js_1 = require("../../../utils/Deferred.js");
 | |
| const EventEmitter_js_1 = require("../../../utils/EventEmitter.js");
 | |
| const log_js_1 = require("../../../utils/log.js");
 | |
| const BrowsingContextImpl_js_1 = require("../context/BrowsingContextImpl.js");
 | |
| const LogManager_js_1 = require("../log/LogManager.js");
 | |
| class CdpTarget extends EventEmitter_js_1.EventEmitter {
 | |
|     #id;
 | |
|     #cdpClient;
 | |
|     #browserCdpClient;
 | |
|     #parentCdpClient;
 | |
|     #realmStorage;
 | |
|     #eventManager;
 | |
|     #preloadScriptStorage;
 | |
|     #browsingContextStorage;
 | |
|     #prerenderingDisabled;
 | |
|     #networkStorage;
 | |
|     #unblocked = new Deferred_js_1.Deferred();
 | |
|     #unhandledPromptBehavior;
 | |
|     #logger;
 | |
|     #deviceAccessEnabled = false;
 | |
|     #cacheDisableState = false;
 | |
|     #fetchDomainStages = {
 | |
|         request: false,
 | |
|         response: false,
 | |
|         auth: false,
 | |
|     };
 | |
|     static create(targetId, cdpClient, browserCdpClient, parentCdpClient, realmStorage, eventManager, preloadScriptStorage, browsingContextStorage, networkStorage, prerenderingDisabled, unhandledPromptBehavior, logger) {
 | |
|         const cdpTarget = new CdpTarget(targetId, cdpClient, browserCdpClient, parentCdpClient, eventManager, realmStorage, preloadScriptStorage, browsingContextStorage, networkStorage, prerenderingDisabled, unhandledPromptBehavior, logger);
 | |
|         LogManager_js_1.LogManager.create(cdpTarget, realmStorage, eventManager, logger);
 | |
|         cdpTarget.#setEventListeners();
 | |
|         // No need to await.
 | |
|         // Deferred will be resolved when the target is unblocked.
 | |
|         void cdpTarget.#unblock();
 | |
|         return cdpTarget;
 | |
|     }
 | |
|     constructor(targetId, cdpClient, browserCdpClient, parentCdpClient, eventManager, realmStorage, preloadScriptStorage, browsingContextStorage, networkStorage, prerenderingDisabled, unhandledPromptBehavior, logger) {
 | |
|         super();
 | |
|         this.#id = targetId;
 | |
|         this.#cdpClient = cdpClient;
 | |
|         this.#browserCdpClient = browserCdpClient;
 | |
|         this.#parentCdpClient = parentCdpClient;
 | |
|         this.#eventManager = eventManager;
 | |
|         this.#realmStorage = realmStorage;
 | |
|         this.#preloadScriptStorage = preloadScriptStorage;
 | |
|         this.#networkStorage = networkStorage;
 | |
|         this.#browsingContextStorage = browsingContextStorage;
 | |
|         this.#prerenderingDisabled = prerenderingDisabled;
 | |
|         this.#unhandledPromptBehavior = unhandledPromptBehavior;
 | |
|         this.#logger = logger;
 | |
|     }
 | |
|     /** Returns a deferred that resolves when the target is unblocked. */
 | |
|     get unblocked() {
 | |
|         return this.#unblocked;
 | |
|     }
 | |
|     get id() {
 | |
|         return this.#id;
 | |
|     }
 | |
|     get cdpClient() {
 | |
|         return this.#cdpClient;
 | |
|     }
 | |
|     get parentCdpClient() {
 | |
|         return this.#parentCdpClient;
 | |
|     }
 | |
|     get browserCdpClient() {
 | |
|         return this.#browserCdpClient;
 | |
|     }
 | |
|     /** Needed for CDP escape path. */
 | |
|     get cdpSessionId() {
 | |
|         // SAFETY we got the client by it's id for creating
 | |
|         return this.#cdpClient.sessionId;
 | |
|     }
 | |
|     /**
 | |
|      * Enables all the required CDP domains and unblocks the target.
 | |
|      */
 | |
|     async #unblock() {
 | |
|         try {
 | |
|             await Promise.all([
 | |
|                 this.#cdpClient.sendCommand('Page.enable'),
 | |
|                 // There can be some existing frames in the target, if reconnecting to an
 | |
|                 // existing browser instance, e.g. via Puppeteer. Need to restore the browsing
 | |
|                 // contexts for the frames to correctly handle further events, like
 | |
|                 // `Runtime.executionContextCreated`.
 | |
|                 // It's important to schedule this task together with enabling domains commands to
 | |
|                 // prepare the tree before the events (e.g. Runtime.executionContextCreated) start
 | |
|                 // coming.
 | |
|                 // https://github.com/GoogleChromeLabs/chromium-bidi/issues/2282
 | |
|                 this.#cdpClient
 | |
|                     .sendCommand('Page.getFrameTree')
 | |
|                     .then((frameTree) => this.#restoreFrameTreeState(frameTree.frameTree)),
 | |
|                 this.#cdpClient.sendCommand('Runtime.enable'),
 | |
|                 this.#cdpClient.sendCommand('Page.setLifecycleEventsEnabled', {
 | |
|                     enabled: true,
 | |
|                 }),
 | |
|                 this.#cdpClient
 | |
|                     .sendCommand('Page.setPrerenderingAllowed', {
 | |
|                     isAllowed: !this.#prerenderingDisabled,
 | |
|                 })
 | |
|                     .catch(() => {
 | |
|                     // Ignore CDP errors, as the command is not supported by iframe targets or
 | |
|                     // prerendered pages. Generic catch, as the error can vary between CdpClient
 | |
|                     // implementations: Tab vs Puppeteer.
 | |
|                 }),
 | |
|                 // Enabling CDP Network domain is required for navigation detection:
 | |
|                 // https://github.com/GoogleChromeLabs/chromium-bidi/issues/2856.
 | |
|                 this.#cdpClient
 | |
|                     .sendCommand('Network.enable')
 | |
|                     .then(() => this.toggleNetworkIfNeeded()),
 | |
|                 this.#cdpClient.sendCommand('Target.setAutoAttach', {
 | |
|                     autoAttach: true,
 | |
|                     waitForDebuggerOnStart: true,
 | |
|                     flatten: true,
 | |
|                 }),
 | |
|                 this.#initAndEvaluatePreloadScripts(),
 | |
|                 this.#cdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
 | |
|                 // Resume tab execution as well if it was paused by the debugger.
 | |
|                 this.#parentCdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
 | |
|                 this.toggleDeviceAccessIfNeeded(),
 | |
|             ]);
 | |
|         }
 | |
|         catch (error) {
 | |
|             this.#logger?.(log_js_1.LogType.debugError, 'Failed to unblock target', error);
 | |
|             // The target might have been closed before the initialization finished.
 | |
|             if (!this.#cdpClient.isCloseError(error)) {
 | |
|                 this.#unblocked.resolve({
 | |
|                     kind: 'error',
 | |
|                     error,
 | |
|                 });
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
|         this.#unblocked.resolve({
 | |
|             kind: 'success',
 | |
|             value: undefined,
 | |
|         });
 | |
|     }
 | |
|     #restoreFrameTreeState(frameTree) {
 | |
|         const frame = frameTree.frame;
 | |
|         const maybeContext = this.#browsingContextStorage.findContext(frame.id);
 | |
|         if (maybeContext !== undefined) {
 | |
|             // Restoring parent of already known browsing context. This means the target is
 | |
|             // OOPiF and the BiDi session was connected to already existing browser instance.
 | |
|             if (maybeContext.parentId === null &&
 | |
|                 frame.parentId !== null &&
 | |
|                 frame.parentId !== undefined) {
 | |
|                 maybeContext.parentId = frame.parentId;
 | |
|             }
 | |
|         }
 | |
|         if (maybeContext === undefined && frame.parentId !== undefined) {
 | |
|             // Restore not yet known nested frames. The top-level frame is created when the
 | |
|             // target is attached.
 | |
|             const parentBrowsingContext = this.#browsingContextStorage.getContext(frame.parentId);
 | |
|             BrowsingContextImpl_js_1.BrowsingContextImpl.create(frame.id, frame.parentId, parentBrowsingContext.userContext, parentBrowsingContext.cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, frame.url, undefined, this.#unhandledPromptBehavior, this.#logger);
 | |
|         }
 | |
|         frameTree.childFrames?.map((frameTree) => this.#restoreFrameTreeState(frameTree));
 | |
|     }
 | |
|     async toggleFetchIfNeeded() {
 | |
|         const stages = this.#networkStorage.getInterceptionStages(this.topLevelId);
 | |
|         if (this.#fetchDomainStages.request === stages.request &&
 | |
|             this.#fetchDomainStages.response === stages.response &&
 | |
|             this.#fetchDomainStages.auth === stages.auth) {
 | |
|             return;
 | |
|         }
 | |
|         const patterns = [];
 | |
|         this.#fetchDomainStages = stages;
 | |
|         if (stages.request || stages.auth) {
 | |
|             // CDP quirk we need request interception when we intercept auth
 | |
|             patterns.push({
 | |
|                 urlPattern: '*',
 | |
|                 requestStage: 'Request',
 | |
|             });
 | |
|         }
 | |
|         if (stages.response) {
 | |
|             patterns.push({
 | |
|                 urlPattern: '*',
 | |
|                 requestStage: 'Response',
 | |
|             });
 | |
|         }
 | |
|         if (patterns.length) {
 | |
|             await this.#cdpClient.sendCommand('Fetch.enable', {
 | |
|                 patterns,
 | |
|                 handleAuthRequests: stages.auth,
 | |
|             });
 | |
|         }
 | |
|         else {
 | |
|             const blockedRequest = this.#networkStorage
 | |
|                 .getRequestsByTarget(this)
 | |
|                 .filter((request) => request.interceptPhase);
 | |
|             void Promise.allSettled(blockedRequest.map((request) => request.waitNextPhase))
 | |
|                 .then(async () => {
 | |
|                 const blockedRequest = this.#networkStorage
 | |
|                     .getRequestsByTarget(this)
 | |
|                     .filter((request) => request.interceptPhase);
 | |
|                 if (blockedRequest.length) {
 | |
|                     return await this.toggleFetchIfNeeded();
 | |
|                 }
 | |
|                 return await this.#cdpClient.sendCommand('Fetch.disable');
 | |
|             })
 | |
|                 .catch((error) => {
 | |
|                 this.#logger?.(log_js_1.LogType.bidi, 'Disable failed', error);
 | |
|             });
 | |
|         }
 | |
|     }
 | |
|     /**
 | |
|      * Toggles CDP "Fetch" domain and enable/disable network cache.
 | |
|      */
 | |
|     async toggleNetworkIfNeeded() {
 | |
|         // Although the Network domain remains active, Fetch domain activation and caching
 | |
|         // settings should be managed dynamically.
 | |
|         try {
 | |
|             await Promise.all([
 | |
|                 this.toggleSetCacheDisabled(),
 | |
|                 this.toggleFetchIfNeeded(),
 | |
|             ]);
 | |
|         }
 | |
|         catch (err) {
 | |
|             this.#logger?.(log_js_1.LogType.debugError, err);
 | |
|             if (!this.#isExpectedError(err)) {
 | |
|                 throw err;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     async toggleSetCacheDisabled(disable) {
 | |
|         const defaultCacheDisabled = this.#networkStorage.defaultCacheBehavior === 'bypass';
 | |
|         const cacheDisabled = disable ?? defaultCacheDisabled;
 | |
|         if (this.#cacheDisableState === cacheDisabled) {
 | |
|             return;
 | |
|         }
 | |
|         this.#cacheDisableState = cacheDisabled;
 | |
|         try {
 | |
|             await this.#cdpClient.sendCommand('Network.setCacheDisabled', {
 | |
|                 cacheDisabled,
 | |
|             });
 | |
|         }
 | |
|         catch (err) {
 | |
|             this.#logger?.(log_js_1.LogType.debugError, err);
 | |
|             this.#cacheDisableState = !cacheDisabled;
 | |
|             if (!this.#isExpectedError(err)) {
 | |
|                 throw err;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     async toggleDeviceAccessIfNeeded() {
 | |
|         const enabled = this.isSubscribedTo(chromium_bidi_js_1.BiDiModule.Bluetooth);
 | |
|         if (this.#deviceAccessEnabled === enabled) {
 | |
|             return;
 | |
|         }
 | |
|         this.#deviceAccessEnabled = enabled;
 | |
|         try {
 | |
|             await this.#cdpClient.sendCommand(enabled ? 'DeviceAccess.enable' : 'DeviceAccess.disable');
 | |
|         }
 | |
|         catch (err) {
 | |
|             this.#logger?.(log_js_1.LogType.debugError, err);
 | |
|             this.#deviceAccessEnabled = !enabled;
 | |
|             if (!this.#isExpectedError(err)) {
 | |
|                 throw err;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     /**
 | |
|      * Heuristic checking if the error is due to the session being closed. If so, ignore the
 | |
|      * error.
 | |
|      */
 | |
|     #isExpectedError(err) {
 | |
|         const error = err;
 | |
|         return ((error.code === -32001 &&
 | |
|             error.message === 'Session with given id not found.') ||
 | |
|             this.#cdpClient.isCloseError(err));
 | |
|     }
 | |
|     #setEventListeners() {
 | |
|         this.#cdpClient.on('Network.requestWillBeSent', (eventParams) => {
 | |
|             if (eventParams.loaderId === eventParams.requestId) {
 | |
|                 this.emit("frameStartedNavigating" /* TargetEvents.FrameStartedNavigating */, {
 | |
|                     loaderId: eventParams.loaderId,
 | |
|                     url: eventParams.request.url,
 | |
|                     frameId: eventParams.frameId,
 | |
|                 });
 | |
|             }
 | |
|         });
 | |
|         this.#cdpClient.on('*', (event, params) => {
 | |
|             // We may encounter uses for EventEmitter other than CDP events,
 | |
|             // which we want to skip.
 | |
|             if (typeof event !== 'string') {
 | |
|                 return;
 | |
|             }
 | |
|             this.#eventManager.registerEvent({
 | |
|                 type: 'event',
 | |
|                 method: `goog:cdp.${event}`,
 | |
|                 params: {
 | |
|                     event,
 | |
|                     params,
 | |
|                     session: this.cdpSessionId,
 | |
|                 },
 | |
|             }, this.id);
 | |
|             // Duplicate the event to the deprecated event name.
 | |
|             // https://github.com/GoogleChromeLabs/chromium-bidi/issues/2844
 | |
|             this.#eventManager.registerEvent({
 | |
|                 type: 'event',
 | |
|                 method: `cdp.${event}`,
 | |
|                 params: {
 | |
|                     event,
 | |
|                     params,
 | |
|                     session: this.cdpSessionId,
 | |
|                 },
 | |
|             }, this.id);
 | |
|         });
 | |
|     }
 | |
|     async #enableFetch(stages) {
 | |
|         const patterns = [];
 | |
|         if (stages.request || stages.auth) {
 | |
|             // CDP quirk we need request interception when we intercept auth
 | |
|             patterns.push({
 | |
|                 urlPattern: '*',
 | |
|                 requestStage: 'Request',
 | |
|             });
 | |
|         }
 | |
|         if (stages.response) {
 | |
|             patterns.push({
 | |
|                 urlPattern: '*',
 | |
|                 requestStage: 'Response',
 | |
|             });
 | |
|         }
 | |
|         if (patterns.length) {
 | |
|             const oldStages = this.#fetchDomainStages;
 | |
|             this.#fetchDomainStages = stages;
 | |
|             try {
 | |
|                 await this.#cdpClient.sendCommand('Fetch.enable', {
 | |
|                     patterns,
 | |
|                     handleAuthRequests: stages.auth,
 | |
|                 });
 | |
|             }
 | |
|             catch {
 | |
|                 this.#fetchDomainStages = oldStages;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     async #disableFetch() {
 | |
|         const blockedRequest = this.#networkStorage
 | |
|             .getRequestsByTarget(this)
 | |
|             .filter((request) => request.interceptPhase);
 | |
|         if (blockedRequest.length === 0) {
 | |
|             this.#fetchDomainStages = {
 | |
|                 request: false,
 | |
|                 response: false,
 | |
|                 auth: false,
 | |
|             };
 | |
|             await this.#cdpClient.sendCommand('Fetch.disable');
 | |
|         }
 | |
|     }
 | |
|     async toggleNetwork() {
 | |
|         const stages = this.#networkStorage.getInterceptionStages(this.topLevelId);
 | |
|         const fetchEnable = Object.values(stages).some((value) => value);
 | |
|         const fetchChanged = this.#fetchDomainStages.request !== stages.request ||
 | |
|             this.#fetchDomainStages.response !== stages.response ||
 | |
|             this.#fetchDomainStages.auth !== stages.auth;
 | |
|         this.#logger?.(log_js_1.LogType.debugInfo, 'Toggle Network', `Fetch (${fetchEnable}) ${fetchChanged}`);
 | |
|         if (fetchEnable && fetchChanged) {
 | |
|             await this.#enableFetch(stages);
 | |
|         }
 | |
|         if (!fetchEnable && fetchChanged) {
 | |
|             await this.#disableFetch();
 | |
|         }
 | |
|     }
 | |
|     /**
 | |
|      * All the ProxyChannels from all the preload scripts of the given
 | |
|      * BrowsingContext.
 | |
|      */
 | |
|     getChannels() {
 | |
|         return this.#preloadScriptStorage
 | |
|             .find()
 | |
|             .flatMap((script) => script.channels);
 | |
|     }
 | |
|     /** Loads all top-level preload scripts. */
 | |
|     async #initAndEvaluatePreloadScripts() {
 | |
|         await Promise.all(this.#preloadScriptStorage
 | |
|             .find({
 | |
|             // Needed for OOPIF
 | |
|             targetId: this.topLevelId,
 | |
|             global: true,
 | |
|         })
 | |
|             .map((script) => {
 | |
|             return script.initInTarget(this, true);
 | |
|         }));
 | |
|     }
 | |
|     get topLevelId() {
 | |
|         return (this.#browsingContextStorage.findTopLevelContextId(this.id) ?? this.id);
 | |
|     }
 | |
|     isSubscribedTo(moduleOrEvent) {
 | |
|         return this.#eventManager.subscriptionManager.isSubscribedTo(moduleOrEvent, this.topLevelId);
 | |
|     }
 | |
| }
 | |
| exports.CdpTarget = CdpTarget;
 | |
| //# sourceMappingURL=CdpTarget.js.map
 | 
