1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
9 "chrome://remote/content/shared/webdriver/Accessibility.sys.mjs",
10 allowAllCerts: "chrome://remote/content/marionette/cert.sys.mjs",
11 Capabilities: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
12 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
13 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
14 Log: "chrome://remote/content/shared/Log.sys.mjs",
15 registerProcessDataActor:
16 "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
18 "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
19 RootMessageHandlerRegistry:
20 "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs",
21 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
22 unregisterProcessDataActor:
23 "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
24 WebDriverBiDiConnection:
25 "chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs",
27 "chrome://remote/content/server/WebSocketHandshake.sys.mjs",
30 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
32 // Global singleton that holds active WebDriver sessions
33 const webDriverSessions = new Map();
36 * @typedef {Set} SessionConfigurationFlags
37 * A set of flags defining the features of a WebDriver session. It can be
38 * empty or contain entries as listed below. External specifications may
39 * define additional flags, or create sessions without the HTTP flag.
42 * <dt><code>"bidi"</code> (string)
43 * <dd>Flag indicating a WebDriver BiDi session.
44 * <dt><code>"http"</code> (string)
45 * <dd>Flag indicating a WebDriver classic (HTTP) session.
50 * Representation of WebDriver session.
52 export class WebDriverSession {
61 static SESSION_FLAG_BIDI = "bidi";
62 static SESSION_FLAG_HTTP = "http";
65 * Construct a new WebDriver session.
67 * It is expected that the caller performs the necessary checks on
68 * the requested capabilities to be WebDriver conforming. The WebDriver
69 * service offered by Marionette does not match or negotiate capabilities
70 * beyond type- and bounds checks.
72 * <h3>Capabilities</h3>
75 * <dt><code>acceptInsecureCerts</code> (boolean)
76 * <dd>Indicates whether untrusted and self-signed TLS certificates
77 * are implicitly trusted on navigation for the duration of the session.
79 * <dt><code>pageLoadStrategy</code> (string)
80 * <dd>(HTTP only) The page load strategy to use for the current session. Must be
81 * one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>".
83 * <dt><code>proxy</code> (Proxy object)
84 * <dd>Defines the proxy configuration.
86 * <dt><code>setWindowRect</code> (boolean)
87 * <dd>(HTTP only) Indicates whether the remote end supports all of the resizing
88 * and repositioning commands.
90 * <dt><code>strictFileInteractability</code> (boolean)
91 * <dd>(HTTP only) Defines the current session’s strict file interactability.
93 * <dt><code>timeouts</code> (Timeouts object)
94 * <dd>(HTTP only) Describes the timeouts imposed on certain session operations.
96 * <dt><code>unhandledPromptBehavior</code> (string)
97 * <dd>Describes the current session’s user prompt handler. Must be one of
98 * "<tt>accept</tt>", "<tt>accept and notify</tt>", "<tt>dismiss</tt>",
99 * "<tt>dismiss and notify</tt>", and "<tt>ignore</tt>". Defaults to the
100 * "<tt>dismiss and notify</tt>" state.
102 * <dt><code>moz:accessibilityChecks</code> (boolean)
103 * <dd>(HTTP only) Run a11y checks when clicking elements.
105 * <dt><code>moz:debuggerAddress</code> (boolean)
106 * <dd>Indicate that the Chrome DevTools Protocol (CDP) has to be enabled.
108 * <dt><code>moz:webdriverClick</code> (boolean)
109 * <dd>(HTTP only) Use a WebDriver conforming <i>WebDriver::ElementClick</i>.
115 * <dt><code>webauthn:virtualAuthenticators</code> (boolean)
116 * <dd>Indicates whether the endpoint node supports all Virtual
117 * Authenticators commands.
119 * <dt><code>webauthn:extension:uvm</code> (boolean)
120 * <dd>Indicates whether the endpoint node WebAuthn WebDriver
121 * implementation supports the User Verification Method extension.
123 * <dt><code>webauthn:extension:prf</code> (boolean)
124 * <dd>Indicates whether the endpoint node WebAuthn WebDriver
125 * implementation supports the prf extension.
127 * <dt><code>webauthn:extension:largeBlob</code> (boolean)
128 * <dd>Indicates whether the endpoint node WebAuthn WebDriver implementation
129 * supports the largeBlob extension.
131 * <dt><code>webauthn:extension:credBlob</code> (boolean)
132 * <dd>Indicates whether the endpoint node WebAuthn WebDriver implementation
133 * supports the credBlob extension.
136 * <h4>Timeouts object</h4>
139 * <dt><code>script</code> (number)
140 * <dd>Determines when to interrupt a script that is being evaluates.
142 * <dt><code>pageLoad</code> (number)
143 * <dd>Provides the timeout limit used to interrupt navigation of the
146 * <dt><code>implicit</code> (number)
147 * <dd>Gives the timeout of when to abort when locating an element.
150 * <h4>Proxy object</h4>
153 * <dt><code>proxyType</code> (string)
154 * <dd>Indicates the type of proxy configuration. Must be one
155 * of "<tt>pac</tt>", "<tt>direct</tt>", "<tt>autodetect</tt>",
156 * "<tt>system</tt>", or "<tt>manual</tt>".
158 * <dt><code>proxyAutoconfigUrl</code> (string)
159 * <dd>Defines the URL for a proxy auto-config file if
160 * <code>proxyType</code> is equal to "<tt>pac</tt>".
162 * <dt><code>httpProxy</code> (string)
163 * <dd>Defines the proxy host for HTTP traffic when the
164 * <code>proxyType</code> is "<tt>manual</tt>".
166 * <dt><code>noProxy</code> (string)
167 * <dd>Lists the address for which the proxy should be bypassed when
168 * the <code>proxyType</code> is "<tt>manual</tt>". Must be a JSON
169 * List containing any number of any of domains, IPv4 addresses, or IPv6
172 * <dt><code>sslProxy</code> (string)
173 * <dd>Defines the proxy host for encrypted TLS traffic when the
174 * <code>proxyType</code> is "<tt>manual</tt>".
176 * <dt><code>socksProxy</code> (string)
177 * <dd>Defines the proxy host for a SOCKS proxy traffic when the
178 * <code>proxyType</code> is "<tt>manual</tt>".
180 * <dt><code>socksVersion</code> (string)
181 * <dd>Defines the SOCKS proxy version when the <code>proxyType</code> is
182 * "<tt>manual</tt>". It must be any integer between 0 and 255
191 * {"capabilities": {"acceptInsecureCerts": true}}
194 * @param {Record<string, *>=} capabilities
195 * JSON Object containing any of the recognized capabilities listed
197 * @param {SessionConfigurationFlags} flags
198 * Session configuration flags.
199 * @param {WebDriverBiDiConnection=} connection
200 * An optional existing WebDriver BiDi connection to associate with the
203 * @throws {SessionNotCreatedError}
204 * If, for whatever reason, a session could not be created.
206 constructor(capabilities, flags, connection) {
207 // WebSocket connections that use this session. This also accounts for
208 // possible disconnects due to network outages, which require clients
210 this.#connections = new Set();
212 this.#id = lazy.generateUUID();
214 // Flags for WebDriver session features
215 this.#bidi = flags.has(WebDriverSession.SESSION_FLAG_BIDI);
216 this.#http = flags.has(WebDriverSession.SESSION_FLAG_HTTP);
218 if (this.#bidi == this.#http) {
219 // Initially a WebDriver session can either be HTTP or BiDi. An upgrade of a
220 // HTTP session to offer BiDi features is done after the constructor is run.
221 throw new lazy.error.SessionNotCreatedError(
222 `Initially the WebDriver session needs to be either HTTP or BiDi (bidi=${
224 }, http=${this.#http})`
228 // Define the HTTP path to query this session via WebDriver BiDi
229 this.#path = `/session/${this.#id}`;
232 this.#capabilities = lazy.Capabilities.fromJSON(capabilities, this.#bidi);
234 throw new lazy.error.SessionNotCreatedError(e);
237 if (this.proxy.init()) {
239 `Proxy settings initialized: ${JSON.stringify(this.proxy)}`
243 if (this.acceptInsecureCerts) {
245 "TLS certificate errors will be ignored for this session"
247 lazy.allowAllCerts.enable();
250 // If we are testing accessibility with marionette, start a11y service in
251 // chrome first. This will ensure that we do not have any content-only
252 // services hanging around.
253 if (this.a11yChecks && lazy.accessibility.service) {
254 lazy.logger.info("Preemptively starting accessibility service in Chrome");
257 // If a connection without an associated session has been specified
258 // immediately register the newly created session for it.
260 connection.registerSession(this);
261 this.#connections.add(connection);
264 // Maps a Navigable (browsing context or content browser for top-level
265 // browsing contexts) to a Set of nodeId's.
266 this.navigableSeenNodes = new WeakMap();
268 lazy.registerProcessDataActor();
270 webDriverSessions.set(this.#id, this);
274 webDriverSessions.delete(this.#id);
276 lazy.unregisterProcessDataActor();
278 this.navigableSeenNodes = null;
280 lazy.allowAllCerts.disable();
282 // Close all open connections which unregister themselves.
283 this.#connections.forEach(connection => connection.close());
284 if (this.#connections.size > 0) {
286 `Failed to close ${this.#connections.size} WebSocket connections`
290 // Destroy the dedicated MessageHandler instance if we created one.
291 if (this.#messageHandler) {
292 this.#messageHandler.off(
293 "message-handler-protocol-event",
294 this._onMessageHandlerProtocolEvent
296 this.#messageHandler.destroy();
301 return this.#capabilities.get("moz:accessibilityChecks");
304 get acceptInsecureCerts() {
305 return this.#capabilities.get("acceptInsecureCerts");
317 return this.#capabilities;
328 get messageHandler() {
329 if (!this.#messageHandler) {
330 this.#messageHandler =
331 lazy.RootMessageHandlerRegistry.getOrCreateMessageHandler(this.#id);
332 this._onMessageHandlerProtocolEvent =
333 this._onMessageHandlerProtocolEvent.bind(this);
334 this.#messageHandler.on(
335 "message-handler-protocol-event",
336 this._onMessageHandlerProtocolEvent
340 return this.#messageHandler;
343 get pageLoadStrategy() {
344 return this.#capabilities.get("pageLoadStrategy");
352 return this.#capabilities.get("proxy");
355 get strictFileInteractability() {
356 return this.#capabilities.get("strictFileInteractability");
360 return this.#capabilities.get("timeouts");
363 set timeouts(timeouts) {
364 this.#capabilities.set("timeouts", timeouts);
367 get userPromptHandler() {
368 return this.#capabilities.get("unhandledPromptBehavior");
372 return this.#capabilities.get("webSocketUrl");
375 async execute(module, command, params) {
376 // XXX: At the moment, commands do not describe consistently their destination,
377 // so we will need a translation step based on a specific command and its params
378 // in order to extract a destination that can be understood by the MessageHandler.
380 // For now, an option is to send all commands to ROOT, and all BiDi MessageHandler
381 // modules will therefore need to implement this translation step in the root
382 // implementation of their module.
383 const destination = {
384 type: lazy.RootMessageHandler.type,
386 if (!this.messageHandler.supportsCommand(module, command, destination)) {
387 throw new lazy.error.UnknownCommandError(`${module}.${command}`);
390 return this.messageHandler.handleCommand({
392 commandName: command,
399 * Remove the specified WebDriver BiDi connection.
401 * @param {WebDriverBiDiConnection} connection
403 removeConnection(connection) {
404 if (this.#connections.has(connection)) {
405 this.#connections.delete(connection);
407 lazy.logger.warn("Trying to remove a connection that doesn't exist.");
412 return `[object ${this.constructor.name} ${this.#id}]`;
415 // nsIHttpRequestHandler
418 * Handle new WebSocket connection requests.
420 * WebSocket clients will attempt to connect to this session at
421 * `/session/:id`. Hereby a WebSocket upgrade will automatically
424 * @param {Request} request
425 * HTTP request (httpd.js)
426 * @param {Response} response
427 * Response to an HTTP request (httpd.js)
429 async handle(request, response) {
430 const webSocket = await lazy.WebSocketHandshake.upgrade(request, response);
431 const conn = new lazy.WebDriverBiDiConnection(
435 conn.registerSession(this);
436 this.#connections.add(conn);
439 _onMessageHandlerProtocolEvent(eventName, messageHandlerEvent) {
440 const { name, data } = messageHandlerEvent;
441 this.#connections.forEach(connection => connection.sendEvent(name, data));
446 get QueryInterface() {
447 return ChromeUtils.generateQI(["nsIHttpRequestHandler"]);
452 * Get the list of seen nodes for the given browsing context unique to a
455 * @param {string} sessionId
456 * The id of the WebDriver session to use.
457 * @param {BrowsingContext} browsingContext
458 * Browsing context the node is part of.
461 * The list of seen nodes.
463 export function getSeenNodesForBrowsingContext(sessionId, browsingContext) {
464 if (!lazy.TabManager.isValidCanonicalBrowsingContext(browsingContext)) {
465 // If browsingContext is not a valid Browsing Context, return an empty set.
470 lazy.TabManager.getNavigableForBrowsingContext(browsingContext);
471 const session = getWebDriverSessionById(sessionId);
473 if (!session.navigableSeenNodes.has(navigable)) {
474 // The navigable hasn't been seen yet.
475 session.navigableSeenNodes.set(navigable, new Set());
478 return session.navigableSeenNodes.get(navigable);
483 * @param {string} sessionId
484 * The ID of the WebDriver session to retrieve.
486 * @returns {WebDriverSession|undefined}
487 * The WebDriver session or undefined if the id is not known.
489 export function getWebDriverSessionById(sessionId) {
490 return webDriverSessions.get(sessionId);