/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { accessibility: "chrome://remote/content/shared/webdriver/Accessibility.sys.mjs", Capabilities: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs", Certificates: "chrome://remote/content/shared/webdriver/Certificates.sys.mjs", error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", Log: "chrome://remote/content/shared/Log.sys.mjs", registerProcessDataActor: "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs", RootMessageHandler: "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs", RootMessageHandlerRegistry: "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs", TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", unregisterProcessDataActor: "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs", WebDriverBiDiConnection: "chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs", WebSocketHandshake: "chrome://remote/content/server/WebSocketHandshake.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); // Global singleton that holds active WebDriver sessions const webDriverSessions = new Map(); /** * @typedef {Set} SessionConfigurationFlags * A set of flags defining the features of a WebDriver session. It can be * empty or contain entries as listed below. External specifications may * define additional flags, or create sessions without the HTTP flag. * *
*
"bidi" (string) *
Flag indicating a WebDriver BiDi session. *
"http" (string) *
Flag indicating a WebDriver classic (HTTP) session. *
*/ /** * Representation of WebDriver session. */ export class WebDriverSession { #bidi; #capabilities; #connections; #http; #id; #messageHandler; #path; static SESSION_FLAG_BIDI = "bidi"; static SESSION_FLAG_HTTP = "http"; /** * Construct a new WebDriver session. * * It is expected that the caller performs the necessary checks on * the requested capabilities to be WebDriver conforming. The WebDriver * service offered by Marionette does not match or negotiate capabilities * beyond type- and bounds checks. * *

Capabilities

* *
*
acceptInsecureCerts (boolean) *
Indicates whether untrusted and self-signed TLS certificates * are implicitly trusted on navigation for the duration of the session. * *
pageLoadStrategy (string) *
(HTTP only) The page load strategy to use for the current session. Must be * one of "none", "eager", and "normal". * *
proxy (Proxy object) *
Defines the proxy configuration. * *
setWindowRect (boolean) *
(HTTP only) Indicates whether the remote end supports all of the resizing * and repositioning commands. * *
strictFileInteractability (boolean) *
(HTTP only) Defines the current session’s strict file interactability. * *
timeouts (Timeouts object) *
(HTTP only) Describes the timeouts imposed on certain session operations. * *
unhandledPromptBehavior (string) *
Describes the current session’s user prompt handler. Must be one of * "accept", "accept and notify", "dismiss", * "dismiss and notify", and "ignore". Defaults to the * "dismiss and notify" state. * *
moz:accessibilityChecks (boolean) *
(HTTP only) Run a11y checks when clicking elements. * *
moz:debuggerAddress (boolean) *
Indicate that the Chrome DevTools Protocol (CDP) has to be enabled. * *
moz:webdriverClick (boolean) *
(HTTP only) Use a WebDriver conforming WebDriver::ElementClick. *
* *

WebAuthn

* *
*
webauthn:virtualAuthenticators (boolean) *
Indicates whether the endpoint node supports all Virtual * Authenticators commands. * *
webauthn:extension:uvm (boolean) *
Indicates whether the endpoint node WebAuthn WebDriver * implementation supports the User Verification Method extension. * *
webauthn:extension:prf (boolean) *
Indicates whether the endpoint node WebAuthn WebDriver * implementation supports the prf extension. * *
webauthn:extension:largeBlob (boolean) *
Indicates whether the endpoint node WebAuthn WebDriver implementation * supports the largeBlob extension. * *
webauthn:extension:credBlob (boolean) *
Indicates whether the endpoint node WebAuthn WebDriver implementation * supports the credBlob extension. *
* *

Timeouts object

* *
*
script (number) *
Determines when to interrupt a script that is being evaluates. * *
pageLoad (number) *
Provides the timeout limit used to interrupt navigation of the * browsing context. * *
implicit (number) *
Gives the timeout of when to abort when locating an element. *
* *

Proxy object

* *
*
proxyType (string) *
Indicates the type of proxy configuration. Must be one * of "pac", "direct", "autodetect", * "system", or "manual". * *
proxyAutoconfigUrl (string) *
Defines the URL for a proxy auto-config file if * proxyType is equal to "pac". * *
httpProxy (string) *
Defines the proxy host for HTTP traffic when the * proxyType is "manual". * *
noProxy (string) *
Lists the address for which the proxy should be bypassed when * the proxyType is "manual". Must be a JSON * List containing any number of any of domains, IPv4 addresses, or IPv6 * addresses. * *
sslProxy (string) *
Defines the proxy host for encrypted TLS traffic when the * proxyType is "manual". * *
socksProxy (string) *
Defines the proxy host for a SOCKS proxy traffic when the * proxyType is "manual". * *
socksVersion (string) *
Defines the SOCKS proxy version when the proxyType is * "manual". It must be any integer between 0 and 255 * inclusive. *
* *

Example

* * Input: * *

   *     {"capabilities": {"acceptInsecureCerts": true}}
   * 
* * @param {Record=} capabilities * JSON Object containing any of the recognized capabilities listed * above. * @param {SessionConfigurationFlags} flags * Session configuration flags. * @param {WebDriverBiDiConnection=} connection * An optional existing WebDriver BiDi connection to associate with the * new session. * * @throws {SessionNotCreatedError} * If, for whatever reason, a session could not be created. */ constructor(capabilities, flags, connection) { // WebSocket connections that use this session. This also accounts for // possible disconnects due to network outages, which require clients // to reconnect. this.#connections = new Set(); this.#id = lazy.generateUUID(); // Flags for WebDriver session features this.#bidi = flags.has(WebDriverSession.SESSION_FLAG_BIDI); this.#http = flags.has(WebDriverSession.SESSION_FLAG_HTTP); if (this.#bidi == this.#http) { // Initially a WebDriver session can either be HTTP or BiDi. An upgrade of a // HTTP session to offer BiDi features is done after the constructor is run. throw new lazy.error.SessionNotCreatedError( `Initially the WebDriver session needs to be either HTTP or BiDi (bidi=${ this.#bidi }, http=${this.#http})` ); } // Define the HTTP path to query this session via WebDriver BiDi this.#path = `/session/${this.#id}`; try { this.#capabilities = lazy.Capabilities.fromJSON(capabilities, this.#bidi); } catch (e) { throw new lazy.error.SessionNotCreatedError(e); } if (this.proxy.init()) { lazy.logger.info( `Proxy settings initialized: ${JSON.stringify(this.proxy)}` ); } if (this.acceptInsecureCerts) { lazy.logger.warn( "TLS certificate errors will be ignored for this session" ); lazy.Certificates.disableSecurityChecks(); } // If we are testing accessibility with marionette, start a11y service in // chrome first. This will ensure that we do not have any content-only // services hanging around. if (this.a11yChecks && lazy.accessibility.service) { lazy.logger.info("Preemptively starting accessibility service in Chrome"); } // If a connection without an associated session has been specified // immediately register the newly created session for it. if (connection) { connection.registerSession(this); this.#connections.add(connection); } // Maps a Navigable (browsing context or content browser for top-level // browsing contexts) to a Set of nodeId's. this.navigableSeenNodes = new WeakMap(); lazy.registerProcessDataActor(); webDriverSessions.set(this.#id, this); } destroy() { webDriverSessions.delete(this.#id); lazy.unregisterProcessDataActor(); this.navigableSeenNodes = null; lazy.Certificates.enableSecurityChecks(); // Close all open connections which unregister themselves. this.#connections.forEach(connection => connection.close()); if (this.#connections.size > 0) { lazy.logger.warn( `Failed to close ${this.#connections.size} WebSocket connections` ); } // Destroy the dedicated MessageHandler instance if we created one. if (this.#messageHandler) { this.#messageHandler.off( "message-handler-protocol-event", this._onMessageHandlerProtocolEvent ); this.#messageHandler.destroy(); } } get a11yChecks() { return this.#capabilities.get("moz:accessibilityChecks"); } get acceptInsecureCerts() { return this.#capabilities.get("acceptInsecureCerts"); } get bidi() { return this.#bidi; } set bidi(value) { this.#bidi = value; } get capabilities() { return this.#capabilities; } get http() { return this.#http; } get id() { return this.#id; } get messageHandler() { if (!this.#messageHandler) { this.#messageHandler = lazy.RootMessageHandlerRegistry.getOrCreateMessageHandler(this.#id); this._onMessageHandlerProtocolEvent = this._onMessageHandlerProtocolEvent.bind(this); this.#messageHandler.on( "message-handler-protocol-event", this._onMessageHandlerProtocolEvent ); } return this.#messageHandler; } get pageLoadStrategy() { return this.#capabilities.get("pageLoadStrategy"); } get path() { return this.#path; } get proxy() { return this.#capabilities.get("proxy"); } get strictFileInteractability() { return this.#capabilities.get("strictFileInteractability"); } get timeouts() { return this.#capabilities.get("timeouts"); } set timeouts(timeouts) { this.#capabilities.set("timeouts", timeouts); } get userPromptHandler() { return this.#capabilities.get("unhandledPromptBehavior"); } get webSocketUrl() { return this.#capabilities.get("webSocketUrl"); } async execute(module, command, params) { // XXX: At the moment, commands do not describe consistently their destination, // so we will need a translation step based on a specific command and its params // in order to extract a destination that can be understood by the MessageHandler. // // For now, an option is to send all commands to ROOT, and all BiDi MessageHandler // modules will therefore need to implement this translation step in the root // implementation of their module. const destination = { type: lazy.RootMessageHandler.type, }; if (!this.messageHandler.supportsCommand(module, command, destination)) { throw new lazy.error.UnknownCommandError(`${module}.${command}`); } return this.messageHandler.handleCommand({ moduleName: module, commandName: command, params, destination, }); } /** * Remove the specified WebDriver BiDi connection. * * @param {WebDriverBiDiConnection} connection */ removeConnection(connection) { if (this.#connections.has(connection)) { this.#connections.delete(connection); } else { lazy.logger.warn("Trying to remove a connection that doesn't exist."); } } toString() { return `[object ${this.constructor.name} ${this.#id}]`; } // nsIHttpRequestHandler /** * Handle new WebSocket connection requests. * * WebSocket clients will attempt to connect to this session at * `/session/:id`. Hereby a WebSocket upgrade will automatically * be performed. * * @param {Request} request * HTTP request (httpd.js) * @param {Response} response * Response to an HTTP request (httpd.js) */ async handle(request, response) { const webSocket = await lazy.WebSocketHandshake.upgrade(request, response); const conn = new lazy.WebDriverBiDiConnection( webSocket, response._connection ); conn.registerSession(this); this.#connections.add(conn); } _onMessageHandlerProtocolEvent(eventName, messageHandlerEvent) { const { name, data } = messageHandlerEvent; this.#connections.forEach(connection => connection.sendEvent(name, data)); } // XPCOM QueryInterface = ChromeUtils.generateQI(["nsIHttpRequestHandler"]); } /** * Get the list of seen nodes for the given browsing context unique to a * WebDriver session. * * @param {string} sessionId * The id of the WebDriver session to use. * @param {BrowsingContext} browsingContext * Browsing context the node is part of. * * @returns {Set} * The list of seen nodes. */ export function getSeenNodesForBrowsingContext(sessionId, browsingContext) { if (!lazy.TabManager.isValidCanonicalBrowsingContext(browsingContext)) { // If browsingContext is not a valid Browsing Context, return an empty set. return new Set(); } const navigable = lazy.TabManager.getNavigableForBrowsingContext(browsingContext); const session = getWebDriverSessionById(sessionId); if (!session.navigableSeenNodes.has(navigable)) { // The navigable hasn't been seen yet. session.navigableSeenNodes.set(navigable, new Set()); } return session.navigableSeenNodes.get(navigable); } /** * * @param {string} sessionId * The ID of the WebDriver session to retrieve. * * @returns {WebDriverSession|undefined} * The WebDriver session or undefined if the id is not known. */ export function getWebDriverSessionById(sessionId) { return webDriverSessions.get(sessionId); }