diff --git a/src/bidiMapper/BidiServer.ts b/src/bidiMapper/BidiServer.ts index 02d60454f9..9ca8436e68 100644 --- a/src/bidiMapper/BidiServer.ts +++ b/src/bidiMapper/BidiServer.ts @@ -80,7 +80,6 @@ export class BidiServer extends EventEmitter { browserCdpClient: CdpClient, selfTargetId: string, defaultUserContextId: Browser.UserContext, - options?: MapperOptions, parser?: BidiCommandParameterParser, logger?: LoggerFn ) { @@ -99,19 +98,6 @@ export class BidiServer extends EventEmitter { browserCdpClient, logger ); - new CdpTargetManager( - cdpConnection, - browserCdpClient, - selfTargetId, - this.#eventManager, - this.#browsingContextStorage, - this.#realmStorage, - networkStorage, - this.#preloadScriptStorage, - defaultUserContextId, - options?.unhandledPromptBehavior, - logger - ); this.#commandProcessor = new CommandProcessor( cdpConnection, browserCdpClient, @@ -121,6 +107,42 @@ export class BidiServer extends EventEmitter { this.#preloadScriptStorage, networkStorage, parser, + async (options: MapperOptions) => { + // This is required to ignore certificate errors when service worker is fetched. + await browserCdpClient.sendCommand( + 'Security.setIgnoreCertificateErrors', + { + ignore: options.acceptInsecureCerts ?? false, + } + ); + new CdpTargetManager( + cdpConnection, + browserCdpClient, + selfTargetId, + this.#eventManager, + this.#browsingContextStorage, + this.#realmStorage, + networkStorage, + this.#preloadScriptStorage, + defaultUserContextId, + options?.unhandledPromptBehavior, + logger + ); + + // Needed to get events about new targets. + await browserCdpClient.sendCommand('Target.setDiscoverTargets', { + discover: true, + }); + + // Needed to automatically attach to new targets. + await browserCdpClient.sendCommand('Target.setAutoAttach', { + autoAttach: true, + waitForDebuggerOnStart: true, + flatten: true, + }); + + await this.#topLevelContextsLoaded(); + }, this.#logger ); this.#eventManager.on(EventManagerEvents.Event, ({message, event}) => { @@ -142,7 +164,6 @@ export class BidiServer extends EventEmitter { cdpConnection: CdpConnection, browserCdpClient: CdpClient, selfTargetId: string, - options?: MapperOptions, parser?: BidiCommandParameterParser, logger?: LoggerFn ): Promise { @@ -153,10 +174,6 @@ export class BidiServer extends EventEmitter { const [{browserContextIds}, {targetInfos}] = await Promise.all([ browserCdpClient.sendCommand('Target.getBrowserContexts'), browserCdpClient.sendCommand('Target.getTargets'), - // This is required to ignore certificate errors when service worker is fetched. - browserCdpClient.sendCommand('Security.setIgnoreCertificateErrors', { - ignore: options?.acceptInsecureCerts ?? false, - }), ]); let defaultUserContextId = 'default'; for (const info of targetInfos) { @@ -175,23 +192,10 @@ export class BidiServer extends EventEmitter { browserCdpClient, selfTargetId, defaultUserContextId, - options, parser, logger ); - // Needed to get events about new targets. - await browserCdpClient.sendCommand('Target.setDiscoverTargets', { - discover: true, - }); - - // Needed to automatically attach to new targets. - await browserCdpClient.sendCommand('Target.setAutoAttach', { - autoAttach: true, - waitForDebuggerOnStart: true, - flatten: true, - }); - await server.#topLevelContextsLoaded(); return server; } diff --git a/src/bidiMapper/CommandProcessor.ts b/src/bidiMapper/CommandProcessor.ts index 15f787525c..987051e197 100644 --- a/src/bidiMapper/CommandProcessor.ts +++ b/src/bidiMapper/CommandProcessor.ts @@ -30,6 +30,7 @@ import type {Result} from '../utils/result.js'; import {BidiNoOpParser} from './BidiNoOpParser.js'; import type {BidiCommandParameterParser} from './BidiParser.js'; +import type {MapperOptions} from './BidiServer.js'; import {BrowserProcessor} from './modules/browser/BrowserProcessor.js'; import {CdpProcessor} from './modules/cdp/CdpProcessor.js'; import {BrowsingContextProcessor} from './modules/context/BrowsingContextProcessor.js'; @@ -82,6 +83,7 @@ export class CommandProcessor extends EventEmitter { preloadScriptStorage: PreloadScriptStorage, networkStorage: NetworkStorage, parser: BidiCommandParameterParser = new BidiNoOpParser(), + initConnection: (options: MapperOptions) => Promise, logger?: LoggerFn ) { super(); @@ -118,7 +120,8 @@ export class CommandProcessor extends EventEmitter { ); this.#sessionProcessor = new SessionProcessor( eventManager, - browserCdpClient + browserCdpClient, + initConnection ); this.#storageProcessor = new StorageProcessor( browserCdpClient, diff --git a/src/bidiMapper/modules/session/SessionProcessor.ts b/src/bidiMapper/modules/session/SessionProcessor.ts index 1e1ea4a972..6aef5e917a 100644 --- a/src/bidiMapper/modules/session/SessionProcessor.ts +++ b/src/bidiMapper/modules/session/SessionProcessor.ts @@ -17,38 +17,97 @@ import type {CdpClient} from '../../../cdp/CdpClient.js'; import type {BidiPlusChannel} from '../../../protocol/chromium-bidi.js'; -import type { - ChromiumBidi, - EmptyResult, +import { + InvalidArgumentException, Session, + type ChromiumBidi, + type EmptyResult, } from '../../../protocol/protocol.js'; +import type {MapperOptions} from '../../BidiServer.js'; import type {EventManager} from './EventManager.js'; export class SessionProcessor { #eventManager: EventManager; #browserCdpClient: CdpClient; + #initConnection: (opts: MapperOptions) => Promise; + #created = false; - constructor(eventManager: EventManager, browserCdpClient: CdpClient) { + constructor( + eventManager: EventManager, + browserCdpClient: CdpClient, + initConnection: (opts: MapperOptions) => Promise + ) { this.#eventManager = eventManager; this.#browserCdpClient = browserCdpClient; + this.#initConnection = initConnection; } status(): Session.StatusResult { return {ready: false, message: 'already connected'}; } - async new(_params: Session.NewParameters): Promise { + #getMapperOptions(capabilities: any): MapperOptions { + const acceptInsecureCerts = + capabilities?.alwaysMatch?.acceptInsecureCerts ?? false; + const unhandledPromptBehavior = this.#getUnhandledPromptBehavior( + capabilities?.alwaysMatch?.unhandledPromptBehavior + ); + + return {acceptInsecureCerts, unhandledPromptBehavior}; + } + + #getUnhandledPromptBehavior( + capabilityValue: unknown + ): Session.UserPromptHandler | undefined { + if (capabilityValue === undefined) { + return undefined; + } + if (typeof capabilityValue === 'object') { + // Do not validate capabilities. Incorrect ones will be ignored by Mapper. + return capabilityValue as Session.UserPromptHandler; + } + if (typeof capabilityValue !== 'string') { + throw new InvalidArgumentException( + `Unexpected 'unhandledPromptBehavior' type: ${typeof capabilityValue}` + ); + } + switch (capabilityValue) { + case 'accept': + case 'accept and notify': + return {default: Session.UserPromptHandlerType.Accept}; + case 'dismiss': + case 'dismiss and notify': + return {default: Session.UserPromptHandlerType.Dismiss}; + case 'ignore': + return {default: Session.UserPromptHandlerType.Ignore}; + default: + throw new InvalidArgumentException( + `Unexpected 'unhandledPromptBehavior' value: ${capabilityValue}` + ); + } + } + + async new(params: Session.NewParameters): Promise { + if (this.#created) { + throw new Error('Session has been already created.'); + } + this.#created = true; // Since mapper exists, there is a session already. // Still the mapper can handle capabilities for us. // Currently, only Puppeteer calls here but, eventually, every client // should delegrate capability processing here. + await this.#initConnection(this.#getMapperOptions(params.capabilities)); + const version = await this.#browserCdpClient.sendCommand('Browser.getVersion'); + return { sessionId: 'unknown', capabilities: { - acceptInsecureCerts: false, + acceptInsecureCerts: Boolean( + params.capabilities.alwaysMatch?.acceptInsecureCerts + ), browserName: version.product, browserVersion: version.revision, platformName: '', diff --git a/src/bidiServer/BrowserInstance.ts b/src/bidiServer/BrowserInstance.ts index d21859360f..fdd64a1eb3 100644 --- a/src/bidiServer/BrowserInstance.ts +++ b/src/bidiServer/BrowserInstance.ts @@ -28,7 +28,6 @@ import { import debug from 'debug'; import WebSocket from 'ws'; -import type {MapperOptions} from '../bidiMapper/BidiServer.js'; import {MapperCdpConnection} from '../cdp/CdpConnection.js'; import {WebSocketTransport} from '../utils/WebsocketTransport.js'; @@ -58,7 +57,6 @@ export class BrowserInstance { static async run( chromeOptions: ChromeOptions, - mapperOptions: MapperOptions, verbose: boolean ): Promise { const profileDir = await mkdtemp( @@ -121,8 +119,7 @@ export class BrowserInstance { const mapperCdpConnection = await MapperServerCdpConnection.create( cdpConnection, mapperTabSource, - verbose, - mapperOptions + verbose ); return new BrowserInstance(mapperCdpConnection, browserProcess); diff --git a/src/bidiServer/MapperCdpConnection.ts b/src/bidiServer/MapperCdpConnection.ts index 190a38cfb5..1617c7e1b7 100644 --- a/src/bidiServer/MapperCdpConnection.ts +++ b/src/bidiServer/MapperCdpConnection.ts @@ -18,7 +18,6 @@ import debug, {type Debugger} from 'debug'; import type {Protocol} from 'devtools-protocol'; -import type {MapperOptions} from '../bidiMapper/BidiServer.js'; import type {MapperCdpClient} from '../cdp/CdpClient.js'; import type {MapperCdpConnection} from '../cdp/CdpConnection.js'; import type {LogPrefix, LogType} from '../utils/log.js'; @@ -47,15 +46,13 @@ export class MapperServerCdpConnection { static async create( cdpConnection: MapperCdpConnection, mapperTabSource: string, - verbose: boolean, - mapperOptions: MapperOptions + verbose: boolean ): Promise { try { const bidiSession = await this.#initMapper( cdpConnection, mapperTabSource, - verbose, - mapperOptions + verbose ); return new MapperServerCdpConnection(cdpConnection, bidiSession); } catch (e) { @@ -140,10 +137,9 @@ export class MapperServerCdpConnection { static async #initMapper( cdpConnection: MapperCdpConnection, mapperTabSource: string, - verbose: boolean, - mapperOptions: MapperOptions + verbose: boolean ): Promise { - debugInternal('Initializing Mapper.', mapperOptions); + debugInternal('Initializing Mapper.'); const browserClient = await cdpConnection.createBrowserSession(); @@ -214,10 +210,9 @@ export class MapperServerCdpConnection { expression: mapperTabSource, }); + // TODO: handle errors in all these evaluate calls! await mapperCdpClient.sendCommand('Runtime.evaluate', { - expression: `window.runMapperInstance('${mapperTabTargetId}', ${JSON.stringify( - mapperOptions - )})`, + expression: `window.runMapperInstance('${mapperTabTargetId}')`, awaitPromise: true, }); diff --git a/src/bidiServer/WebSocketServer.ts b/src/bidiServer/WebSocketServer.ts index e038a5a70f..ff9cc457e9 100644 --- a/src/bidiServer/WebSocketServer.ts +++ b/src/bidiServer/WebSocketServer.ts @@ -19,12 +19,8 @@ import http from 'http'; import debug from 'debug'; import * as websocket from 'websocket'; -import type {MapperOptions} from '../bidiMapper/BidiServer.js'; -import { - ErrorCode, - InvalidArgumentException, - Session, -} from '../protocol/protocol.js'; +import {ErrorCode, type Session} from '../protocol/protocol.js'; +import {Deferred} from '../utils/Deferred.js'; import {uuidv4} from '../utils/uuid.js'; import {BrowserInstance, type ChromeOptions} from './BrowserInstance.js'; @@ -45,9 +41,9 @@ type Session = { }; type SessionOptions = { - readonly mapperOptions: MapperOptions; readonly chromeOptions: ChromeOptions; readonly verbose: boolean; + readonly sessionNewBody: string; }; export class WebSocketServer { @@ -143,8 +139,8 @@ export class WebSocketServer { browserInstancePromise: undefined, sessionOptions: { chromeOptions: this.#getChromeOptions(jsonBody.capabilities), - mapperOptions: this.#getMapperOptions(jsonBody.capabilities), verbose: this.#verbose, + sessionNewBody: `{"id":0,"method":"session.new","params":${body.toString()}}`, }, }; this.#sessions.set(sessionId, session); @@ -303,15 +299,14 @@ export class WebSocketServer { chromeOptions: this.#getChromeOptions( parsedCommandData.params?.capabilities ), - mapperOptions: this.#getMapperOptions( - parsedCommandData.params?.capabilities - ), verbose: this.#verbose, + sessionNewBody: plainCommandData, }; const browserInstance = await this.#launchBrowserInstance( connection, - sessionOptions + sessionOptions, + true ); const sessionId = uuidv4(); @@ -332,19 +327,6 @@ export class WebSocketServer { ); return; } - - // TODO: extend with capabilities. - this.#sendClientMessage( - { - id: parsedCommandData.id, - type: 'success', - result: { - sessionId: session.sessionId, - capabilities: {}, - }, - }, - connection - ); return; } @@ -450,47 +432,6 @@ export class WebSocketServer { void browserInstance.close(); } - #getMapperOptions(capabilities: any): MapperOptions { - const acceptInsecureCerts = - capabilities?.alwaysMatch?.acceptInsecureCerts ?? false; - const unhandledPromptBehavior = this.#getUnhandledPromptBehavior( - capabilities?.alwaysMatch?.unhandledPromptBehavior - ); - - return {acceptInsecureCerts, unhandledPromptBehavior}; - } - - #getUnhandledPromptBehavior( - capabilityValue: unknown - ): Session.UserPromptHandler | undefined { - if (capabilityValue === undefined) { - return undefined; - } - if (typeof capabilityValue === 'object') { - // Do not validate capabilities. Incorrect ones will be ignored by Mapper. - return capabilityValue as Session.UserPromptHandler; - } - if (typeof capabilityValue !== 'string') { - throw new InvalidArgumentException( - `Unexpected 'unhandledPromptBehavior' type: ${typeof capabilityValue}` - ); - } - switch (capabilityValue) { - case 'accept': - case 'accept and notify': - return {default: Session.UserPromptHandlerType.Accept}; - case 'dismiss': - case 'dismiss and notify': - return {default: Session.UserPromptHandlerType.Dismiss}; - case 'ignore': - return {default: Session.UserPromptHandlerType.Ignore}; - default: - throw new InvalidArgumentException( - `Unexpected 'unhandledPromptBehavior' value: ${capabilityValue}` - ); - } - } - #getChromeOptions(capabilities: any): ChromeOptions { const chromeCapabilities = capabilities?.alwaysMatch?.['goog:chromeOptions']; @@ -502,21 +443,44 @@ export class WebSocketServer { async #launchBrowserInstance( connection: websocket.connection, - sessionOptions: SessionOptions + sessionOptions: SessionOptions, + passSessionNewThrough = false ): Promise { debugInfo('Scheduling browser launch...'); const browserInstance = await BrowserInstance.run( sessionOptions.chromeOptions, - sessionOptions.mapperOptions, sessionOptions.verbose ); + const body = JSON.parse(sessionOptions.sessionNewBody); + const id = body.id; + const sessionCreated = new Deferred(); + const sessionResponseListener = (message: string) => { + const jsonMessage = JSON.parse(message); + if (jsonMessage['id'] === id) { + debugInfo('Receiving session.new response from mapper', message); + sessionCreated.resolve(); + if (passSessionNewThrough) { + this.#sendClientMessageString(message, connection); + } + } + }; + + browserInstance.bidiSession().on('message', sessionResponseListener); + debugInfo('Sending session.new to mapper', sessionOptions.sessionNewBody); + await browserInstance + .bidiSession() + .sendCommand(sessionOptions.sessionNewBody); + await sessionCreated; + browserInstance.bidiSession().off('message', sessionResponseListener); + // Forward messages from BiDi Mapper to the client unconditionally. browserInstance.bidiSession().on('message', (message) => { this.#sendClientMessageString(message, connection); }); debugInfo('Browser is launched!'); + return browserInstance; } diff --git a/src/bidiTab/bidiTab.ts b/src/bidiTab/bidiTab.ts index bea60d4e7e..771fedb747 100644 --- a/src/bidiTab/bidiTab.ts +++ b/src/bidiTab/bidiTab.ts @@ -18,7 +18,6 @@ */ import {BidiServer} from '../bidiMapper/BidiMapper.js'; -import type {MapperOptions} from '../bidiMapper/BidiServer'; import {MapperCdpConnection} from '../cdp/CdpConnection.js'; import {LogType} from '../utils/log.js'; @@ -70,10 +69,7 @@ const cdpConnection = new MapperCdpConnection(cdpTransport, log); * @param {string} selfTargetId * @param options Mapper options. E.g. `acceptInsecureCerts`. */ -async function runMapperInstance( - selfTargetId: string, - options?: MapperOptions -) { +async function runMapperInstance(selfTargetId: string) { // eslint-disable-next-line no-console console.log('Launching Mapper instance with selfTargetId:', selfTargetId); @@ -85,7 +81,6 @@ async function runMapperInstance( */ await cdpConnection.createBrowserSession(), selfTargetId, - options, new BidiParser(), log ); @@ -98,7 +93,7 @@ async function runMapperInstance( /** * Set `window.runMapper` to a function which launches the BiDi mapper instance. * @param selfTargetId Needed to filter out info related to BiDi target. - * @param options Mapper options. E.g. `acceptInsecureCerts`. */ -window.runMapperInstance = async (selfTargetId, options?: MapperOptions) => { - await runMapperInstance(selfTargetId, options); + */ +window.runMapperInstance = async (selfTargetId) => { + await runMapperInstance(selfTargetId); }; diff --git a/wpt-metadata/chromedriver/headful/webdriver/tests/interop/beforeunload_prompt.py.ini b/wpt-metadata/chromedriver/headful/webdriver/tests/interop/beforeunload_prompt.py.ini index 5bcbf116df..e6e5c12fd7 100644 --- a/wpt-metadata/chromedriver/headful/webdriver/tests/interop/beforeunload_prompt.py.ini +++ b/wpt-metadata/chromedriver/headful/webdriver/tests/interop/beforeunload_prompt.py.ini @@ -22,9 +22,3 @@ [test_dismiss_and_notify[capabilities0-True\]] expected: FAIL - - [test_ignore[capabilities0-False\]] - expected: FAIL - - [test_ignore[capabilities0-True\]] - expected: FAIL diff --git a/wpt-metadata/chromedriver/headless/webdriver/tests/interop/beforeunload_prompt.py.ini b/wpt-metadata/chromedriver/headless/webdriver/tests/interop/beforeunload_prompt.py.ini index 5bcbf116df..e6e5c12fd7 100644 --- a/wpt-metadata/chromedriver/headless/webdriver/tests/interop/beforeunload_prompt.py.ini +++ b/wpt-metadata/chromedriver/headless/webdriver/tests/interop/beforeunload_prompt.py.ini @@ -22,9 +22,3 @@ [test_dismiss_and_notify[capabilities0-True\]] expected: FAIL - - [test_ignore[capabilities0-False\]] - expected: FAIL - - [test_ignore[capabilities0-True\]] - expected: FAIL