489 lines
16 KiB
JavaScript
489 lines
16 KiB
JavaScript
/* 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.
|
||
*
|
||
* <dl>
|
||
* <dt><code>"bidi"</code> (string)
|
||
* <dd>Flag indicating a WebDriver BiDi session.
|
||
* <dt><code>"http"</code> (string)
|
||
* <dd>Flag indicating a WebDriver classic (HTTP) session.
|
||
* </dl>
|
||
*/
|
||
|
||
/**
|
||
* 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.
|
||
*
|
||
* <h3>Capabilities</h3>
|
||
*
|
||
* <dl>
|
||
* <dt><code>acceptInsecureCerts</code> (boolean)
|
||
* <dd>Indicates whether untrusted and self-signed TLS certificates
|
||
* are implicitly trusted on navigation for the duration of the session.
|
||
*
|
||
* <dt><code>pageLoadStrategy</code> (string)
|
||
* <dd>(HTTP only) The page load strategy to use for the current session. Must be
|
||
* one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>".
|
||
*
|
||
* <dt><code>proxy</code> (Proxy object)
|
||
* <dd>Defines the proxy configuration.
|
||
*
|
||
* <dt><code>setWindowRect</code> (boolean)
|
||
* <dd>(HTTP only) Indicates whether the remote end supports all of the resizing
|
||
* and repositioning commands.
|
||
*
|
||
* <dt><code>strictFileInteractability</code> (boolean)
|
||
* <dd>(HTTP only) Defines the current session’s strict file interactability.
|
||
*
|
||
* <dt><code>timeouts</code> (Timeouts object)
|
||
* <dd>(HTTP only) Describes the timeouts imposed on certain session operations.
|
||
*
|
||
* <dt><code>unhandledPromptBehavior</code> (string)
|
||
* <dd>Describes the current session’s user prompt handler. Must be one of
|
||
* "<tt>accept</tt>", "<tt>accept and notify</tt>", "<tt>dismiss</tt>",
|
||
* "<tt>dismiss and notify</tt>", and "<tt>ignore</tt>". Defaults to the
|
||
* "<tt>dismiss and notify</tt>" state.
|
||
*
|
||
* <dt><code>moz:accessibilityChecks</code> (boolean)
|
||
* <dd>(HTTP only) Run a11y checks when clicking elements.
|
||
*
|
||
* <dt><code>moz:debuggerAddress</code> (boolean)
|
||
* <dd>Indicate that the Chrome DevTools Protocol (CDP) has to be enabled.
|
||
*
|
||
* <dt><code>moz:webdriverClick</code> (boolean)
|
||
* <dd>(HTTP only) Use a WebDriver conforming <i>WebDriver::ElementClick</i>.
|
||
* </dl>
|
||
*
|
||
* <h4>WebAuthn</h4>
|
||
*
|
||
* <dl>
|
||
* <dt><code>webauthn:virtualAuthenticators</code> (boolean)
|
||
* <dd>Indicates whether the endpoint node supports all Virtual
|
||
* Authenticators commands.
|
||
*
|
||
* <dt><code>webauthn:extension:uvm</code> (boolean)
|
||
* <dd>Indicates whether the endpoint node WebAuthn WebDriver
|
||
* implementation supports the User Verification Method extension.
|
||
*
|
||
* <dt><code>webauthn:extension:prf</code> (boolean)
|
||
* <dd>Indicates whether the endpoint node WebAuthn WebDriver
|
||
* implementation supports the prf extension.
|
||
*
|
||
* <dt><code>webauthn:extension:largeBlob</code> (boolean)
|
||
* <dd>Indicates whether the endpoint node WebAuthn WebDriver implementation
|
||
* supports the largeBlob extension.
|
||
*
|
||
* <dt><code>webauthn:extension:credBlob</code> (boolean)
|
||
* <dd>Indicates whether the endpoint node WebAuthn WebDriver implementation
|
||
* supports the credBlob extension.
|
||
* </dl>
|
||
*
|
||
* <h4>Timeouts object</h4>
|
||
*
|
||
* <dl>
|
||
* <dt><code>script</code> (number)
|
||
* <dd>Determines when to interrupt a script that is being evaluates.
|
||
*
|
||
* <dt><code>pageLoad</code> (number)
|
||
* <dd>Provides the timeout limit used to interrupt navigation of the
|
||
* browsing context.
|
||
*
|
||
* <dt><code>implicit</code> (number)
|
||
* <dd>Gives the timeout of when to abort when locating an element.
|
||
* </dl>
|
||
*
|
||
* <h4>Proxy object</h4>
|
||
*
|
||
* <dl>
|
||
* <dt><code>proxyType</code> (string)
|
||
* <dd>Indicates the type of proxy configuration. Must be one
|
||
* of "<tt>pac</tt>", "<tt>direct</tt>", "<tt>autodetect</tt>",
|
||
* "<tt>system</tt>", or "<tt>manual</tt>".
|
||
*
|
||
* <dt><code>proxyAutoconfigUrl</code> (string)
|
||
* <dd>Defines the URL for a proxy auto-config file if
|
||
* <code>proxyType</code> is equal to "<tt>pac</tt>".
|
||
*
|
||
* <dt><code>httpProxy</code> (string)
|
||
* <dd>Defines the proxy host for HTTP traffic when the
|
||
* <code>proxyType</code> is "<tt>manual</tt>".
|
||
*
|
||
* <dt><code>noProxy</code> (string)
|
||
* <dd>Lists the address for which the proxy should be bypassed when
|
||
* the <code>proxyType</code> is "<tt>manual</tt>". Must be a JSON
|
||
* List containing any number of any of domains, IPv4 addresses, or IPv6
|
||
* addresses.
|
||
*
|
||
* <dt><code>sslProxy</code> (string)
|
||
* <dd>Defines the proxy host for encrypted TLS traffic when the
|
||
* <code>proxyType</code> is "<tt>manual</tt>".
|
||
*
|
||
* <dt><code>socksProxy</code> (string)
|
||
* <dd>Defines the proxy host for a SOCKS proxy traffic when the
|
||
* <code>proxyType</code> is "<tt>manual</tt>".
|
||
*
|
||
* <dt><code>socksVersion</code> (string)
|
||
* <dd>Defines the SOCKS proxy version when the <code>proxyType</code> is
|
||
* "<tt>manual</tt>". It must be any integer between 0 and 255
|
||
* inclusive.
|
||
* </dl>
|
||
*
|
||
* <h3>Example</h3>
|
||
*
|
||
* Input:
|
||
*
|
||
* <pre><code>
|
||
* {"capabilities": {"acceptInsecureCerts": true}}
|
||
* </code></pre>
|
||
*
|
||
* @param {Record<string, *>=} 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);
|
||
}
|