/* 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);
}