Session.sys.mjs (20166B)
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/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 11 12 accessibility: 13 "chrome://remote/content/shared/webdriver/Accessibility.sys.mjs", 14 Capabilities: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs", 15 Certificates: "chrome://remote/content/shared/webdriver/Certificates.sys.mjs", 16 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 17 FilePickerHandler: 18 "chrome://remote/content/shared/webdriver/FilePickerHandler.sys.mjs", 19 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", 20 Log: "chrome://remote/content/shared/Log.sys.mjs", 21 NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", 22 registerProcessDataActor: 23 "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs", 24 RootMessageHandler: 25 "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs", 26 RootMessageHandlerRegistry: 27 "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs", 28 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", 29 unregisterProcessDataActor: 30 "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs", 31 WebDriverBiDiConnection: 32 "chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs", 33 WebSocketHandshake: 34 "chrome://remote/content/server/WebSocketHandshake.sys.mjs", 35 windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs", 36 }); 37 38 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 39 40 // Bug 1999693: This preference is a temporary workaround until clients can use 41 // the unhandledPromptBehavior capability to decide if file pickers should be 42 // dismissed or not. 43 XPCOMUtils.defineLazyPreferenceGetter( 44 lazy, 45 "dismissFilePickersEnabled", 46 "remote.bidi.dismiss_file_pickers.enabled", 47 false 48 ); 49 50 XPCOMUtils.defineLazyServiceGetter( 51 lazy, 52 "aomStartup", 53 "@mozilla.org/addons/addon-manager-startup;1", 54 Ci.amIAddonManagerStartup 55 ); 56 57 // Global singleton that holds active WebDriver sessions 58 const webDriverSessions = new Map(); 59 60 /** 61 * @typedef {Set} SessionConfigurationFlags 62 * A set of flags defining the features of a WebDriver session. It can be 63 * empty or contain entries as listed below. External specifications may 64 * define additional flags, or create sessions without the HTTP flag. 65 * 66 * <dl> 67 * <dt><code>"bidi"</code> (string) 68 * <dd>Flag indicating a WebDriver BiDi session. 69 * <dt><code>"http"</code> (string) 70 * <dd>Flag indicating a WebDriver classic (HTTP) session. 71 * </dl> 72 */ 73 74 /** 75 * Representation of WebDriver session. 76 */ 77 export class WebDriverSession { 78 #bidi; 79 #capabilities; 80 #chromeProtocolHandles; 81 #connections; 82 #http; 83 #id; 84 #messageHandler; 85 #navigableSeenNodes; 86 #path; 87 88 static SESSION_FLAG_BIDI = "bidi"; 89 static SESSION_FLAG_HTTP = "http"; 90 91 /** 92 * Construct a new WebDriver session. 93 * 94 * It is expected that the caller performs the necessary checks on 95 * the requested capabilities to be WebDriver conforming. The WebDriver 96 * service offered by Marionette does not match or negotiate capabilities 97 * beyond type- and bounds checks. 98 * 99 * <h3>Capabilities</h3> 100 * 101 * <dl> 102 * <dt><code>acceptInsecureCerts</code> (boolean) 103 * <dd>Indicates whether untrusted and self-signed TLS certificates 104 * are implicitly trusted on navigation for the duration of the session. 105 * 106 * <dt><code>pageLoadStrategy</code> (string) 107 * <dd>(HTTP only) The page load strategy to use for the current session. Must be 108 * one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>". 109 * 110 * <dt><code>proxy</code> (Proxy object) 111 * <dd>Defines the proxy configuration. 112 * 113 * <dt><code>setWindowRect</code> (boolean) 114 * <dd>(HTTP only) Indicates whether the remote end supports all of the resizing 115 * and repositioning commands. 116 * 117 * <dt><code>strictFileInteractability</code> (boolean) 118 * <dd>(HTTP only) Defines the current session’s strict file interactability. 119 * 120 * <dt><code>timeouts</code> (Timeouts object) 121 * <dd>(HTTP only) Describes the timeouts imposed on certain session operations. 122 * 123 * <dt><code>unhandledPromptBehavior</code> (string) 124 * <dd>Describes the current session’s user prompt handler. Must be one of 125 * "<tt>accept</tt>", "<tt>accept and notify</tt>", "<tt>dismiss</tt>", 126 * "<tt>dismiss and notify</tt>", and "<tt>ignore</tt>". Defaults to the 127 * "<tt>dismiss and notify</tt>" state. 128 * 129 * <dt><code>moz:accessibilityChecks</code> (boolean) 130 * <dd>(HTTP only) Run a11y checks when clicking elements. 131 * 132 * <dt><code>moz:webdriverClick</code> (boolean) 133 * <dd>(HTTP only) Use a WebDriver conforming <i>WebDriver::ElementClick</i>. 134 * </dl> 135 * 136 * <h4>WebAuthn</h4> 137 * 138 * <dl> 139 * <dt><code>webauthn:virtualAuthenticators</code> (boolean) 140 * <dd>Indicates whether the endpoint node supports all Virtual 141 * Authenticators commands. 142 * 143 * <dt><code>webauthn:extension:uvm</code> (boolean) 144 * <dd>Indicates whether the endpoint node WebAuthn WebDriver 145 * implementation supports the User Verification Method extension. 146 * 147 * <dt><code>webauthn:extension:prf</code> (boolean) 148 * <dd>Indicates whether the endpoint node WebAuthn WebDriver 149 * implementation supports the prf extension. 150 * 151 * <dt><code>webauthn:extension:largeBlob</code> (boolean) 152 * <dd>Indicates whether the endpoint node WebAuthn WebDriver implementation 153 * supports the largeBlob extension. 154 * 155 * <dt><code>webauthn:extension:credBlob</code> (boolean) 156 * <dd>Indicates whether the endpoint node WebAuthn WebDriver implementation 157 * supports the credBlob extension. 158 * </dl> 159 * 160 * <h4>Timeouts object</h4> 161 * 162 * <dl> 163 * <dt><code>script</code> (number) 164 * <dd>Determines when to interrupt a script that is being evaluates. 165 * 166 * <dt><code>pageLoad</code> (number) 167 * <dd>Provides the timeout limit used to interrupt navigation of the 168 * browsing context. 169 * 170 * <dt><code>implicit</code> (number) 171 * <dd>Gives the timeout of when to abort when locating an element. 172 * </dl> 173 * 174 * <h4>Proxy object</h4> 175 * 176 * <dl> 177 * <dt><code>proxyType</code> (string) 178 * <dd>Indicates the type of proxy configuration. Must be one 179 * of "<tt>pac</tt>", "<tt>direct</tt>", "<tt>autodetect</tt>", 180 * "<tt>system</tt>", or "<tt>manual</tt>". 181 * 182 * <dt><code>proxyAutoconfigUrl</code> (string) 183 * <dd>Defines the URL for a proxy auto-config file if 184 * <code>proxyType</code> is equal to "<tt>pac</tt>". 185 * 186 * <dt><code>httpProxy</code> (string) 187 * <dd>Defines the proxy host for HTTP traffic when the 188 * <code>proxyType</code> is "<tt>manual</tt>". 189 * 190 * <dt><code>noProxy</code> (string) 191 * <dd>Lists the address for which the proxy should be bypassed when 192 * the <code>proxyType</code> is "<tt>manual</tt>". Must be a JSON 193 * List containing any number of any of domains, IPv4 addresses, or IPv6 194 * addresses. 195 * 196 * <dt><code>sslProxy</code> (string) 197 * <dd>Defines the proxy host for encrypted TLS traffic when the 198 * <code>proxyType</code> is "<tt>manual</tt>". 199 * 200 * <dt><code>socksProxy</code> (string) 201 * <dd>Defines the proxy host for a SOCKS proxy traffic when the 202 * <code>proxyType</code> is "<tt>manual</tt>". 203 * 204 * <dt><code>socksVersion</code> (string) 205 * <dd>Defines the SOCKS proxy version when the <code>proxyType</code> is 206 * "<tt>manual</tt>". It must be any integer between 0 and 255 207 * inclusive. 208 * </dl> 209 * 210 * <h3>Example</h3> 211 * 212 * Input: 213 * 214 * <pre><code> 215 * {"capabilities": {"acceptInsecureCerts": true}} 216 * </code></pre> 217 * 218 * @param {Record<string, *>=} capabilities 219 * JSON Object containing any of the recognized capabilities listed 220 * above. 221 * @param {SessionConfigurationFlags} flags 222 * Session configuration flags. 223 * @param {WebDriverBiDiConnection=} connection 224 * An optional existing WebDriver BiDi connection to associate with the 225 * new session. 226 * 227 * @throws {SessionNotCreatedError} 228 * If, for whatever reason, a session could not be created. 229 */ 230 constructor(capabilities, flags, connection) { 231 // List of handles for registered chrome:// URLs 232 this.#chromeProtocolHandles = new Map(); 233 234 // WebSocket connections that use this session. This also accounts for 235 // possible disconnects due to network outages, which require clients 236 // to reconnect. 237 this.#connections = new Set(); 238 239 this.#id = lazy.generateUUID(); 240 241 // Flags for WebDriver session features 242 this.#bidi = flags.has(WebDriverSession.SESSION_FLAG_BIDI); 243 this.#http = flags.has(WebDriverSession.SESSION_FLAG_HTTP); 244 245 if (this.#bidi == this.#http) { 246 // Initially a WebDriver session can either be HTTP or BiDi. An upgrade of a 247 // HTTP session to offer BiDi features is done after the constructor is run. 248 throw new lazy.error.SessionNotCreatedError( 249 `Initially the WebDriver session needs to be either HTTP or BiDi (bidi=${ 250 this.#bidi 251 }, http=${this.#http})` 252 ); 253 } 254 255 // Define the HTTP path to query this session via WebDriver BiDi 256 this.#path = `/session/${this.#id}`; 257 258 try { 259 this.#capabilities = lazy.Capabilities.fromJSON(capabilities, this.#bidi); 260 } catch (e) { 261 throw new lazy.error.SessionNotCreatedError(e); 262 } 263 264 if (this.proxy.init()) { 265 lazy.logger.info( 266 `Proxy settings initialized: ${JSON.stringify(this.proxy)}` 267 ); 268 } 269 270 if (this.acceptInsecureCerts) { 271 lazy.logger.warn( 272 "TLS certificate errors will be ignored for this session" 273 ); 274 lazy.Certificates.disableSecurityChecks(); 275 } 276 277 // If we are testing accessibility with marionette, start a11y service in 278 // chrome first. This will ensure that we do not have any content-only 279 // services hanging around. 280 if (this.a11yChecks && lazy.accessibility.service) { 281 lazy.logger.info("Preemptively starting accessibility service in Chrome"); 282 } 283 284 // If a connection without an associated session has been specified 285 // immediately register the newly created session for it. 286 if (connection) { 287 connection.registerSession(this); 288 this.#connections.add(connection); 289 } 290 291 // Maps a Navigable (browsing context or content browser for top-level 292 // browsing contexts) to a Set of nodeId's. 293 this.#navigableSeenNodes = new WeakMap(); 294 295 lazy.registerProcessDataActor(); 296 297 // Start the tracking of browsing contexts to create Navigable ids. 298 lazy.NavigableManager.startTracking(); 299 lazy.windowManager.startTracking(); 300 301 webDriverSessions.set(this.#id, this); 302 } 303 304 destroy() { 305 webDriverSessions.delete(this.#id); 306 307 // Stop the tracking of browsing contexts when no WebDriver 308 // session exists anymore. 309 lazy.NavigableManager.stopTracking(); 310 lazy.windowManager.stopTracking(); 311 312 lazy.unregisterProcessDataActor(); 313 314 this.#navigableSeenNodes = null; 315 316 lazy.Certificates.enableSecurityChecks(); 317 318 // Close all open connections which unregister themselves. 319 this.#connections.forEach(connection => connection.close()); 320 if (this.#connections.size > 0) { 321 lazy.logger.warn( 322 `Failed to close ${this.#connections.size} WebSocket connections` 323 ); 324 } 325 326 // For the WebDriver BiDi session cleanup, the root network module is 327 // responsible for resuming requests in the blocked request map. 328 // See root NetworkModule.destroy(). 329 330 // Destroy the dedicated MessageHandler instance if we created one. 331 if (this.#messageHandler) { 332 this.#messageHandler.off( 333 "message-handler-protocol-event", 334 this._onMessageHandlerProtocolEvent 335 ); 336 this.#messageHandler.destroy(); 337 338 // Note: do not check lazy.dismissFilePickersEnabled, the preference might 339 // have been updated at runtime. allowFilePickers(this) is safe to call, 340 // if there was no corresponding dismissFilePickers(this), it will be a 341 // no-op. 342 lazy.FilePickerHandler.allowFilePickers(this); 343 } 344 345 for (const id of this.#chromeProtocolHandles.keys()) { 346 this.unregisterChromeHandler(id); 347 } 348 } 349 350 get a11yChecks() { 351 return this.#capabilities.get("moz:accessibilityChecks"); 352 } 353 354 get acceptInsecureCerts() { 355 return this.#capabilities.get("acceptInsecureCerts"); 356 } 357 358 get bidi() { 359 return this.#bidi; 360 } 361 362 set bidi(value) { 363 this.#bidi = value; 364 } 365 366 get capabilities() { 367 return this.#capabilities; 368 } 369 370 get http() { 371 return this.#http; 372 } 373 374 get id() { 375 return this.#id; 376 } 377 378 get messageHandler() { 379 if (!this.#messageHandler) { 380 this.#messageHandler = 381 lazy.RootMessageHandlerRegistry.getOrCreateMessageHandler(this.#id); 382 this._onMessageHandlerProtocolEvent = 383 this._onMessageHandlerProtocolEvent.bind(this); 384 this.#messageHandler.on( 385 "message-handler-protocol-event", 386 this._onMessageHandlerProtocolEvent 387 ); 388 389 // Bug 2005673: Only enable dismissing file pickers lazily if the session 390 // explicitly starts handling BiDi commands. 391 if (lazy.dismissFilePickersEnabled) { 392 // Temporarily dismiss all file pickers. 393 // Bug 1999693: File pickers should only be dismissed when the unhandled 394 // prompt behaviour for type "file" is not set to "ignore". 395 lazy.FilePickerHandler.dismissFilePickers(this); 396 } 397 } 398 399 return this.#messageHandler; 400 } 401 402 get navigableSeenNodes() { 403 return this.#navigableSeenNodes; 404 } 405 406 get pageLoadStrategy() { 407 return this.#capabilities.get("pageLoadStrategy"); 408 } 409 410 get path() { 411 return this.#path; 412 } 413 414 get proxy() { 415 return this.#capabilities.get("proxy"); 416 } 417 418 get strictFileInteractability() { 419 return this.#capabilities.get("strictFileInteractability"); 420 } 421 422 get timeouts() { 423 return this.#capabilities.get("timeouts"); 424 } 425 426 set timeouts(timeouts) { 427 this.#capabilities.set("timeouts", timeouts); 428 } 429 430 get userPromptHandler() { 431 return this.#capabilities.get("unhandledPromptBehavior"); 432 } 433 434 get webSocketUrl() { 435 return this.#capabilities.get("webSocketUrl"); 436 } 437 438 async execute(module, command, params) { 439 // XXX: At the moment, commands do not describe consistently their destination, 440 // so we will need a translation step based on a specific command and its params 441 // in order to extract a destination that can be understood by the MessageHandler. 442 // 443 // For now, an option is to send all commands to ROOT, and all BiDi MessageHandler 444 // modules will therefore need to implement this translation step in the root 445 // implementation of their module. 446 const destination = { 447 type: lazy.RootMessageHandler.type, 448 }; 449 if (!this.messageHandler.supportsCommand(module, command, destination)) { 450 throw new lazy.error.UnknownCommandError(`${module}.${command}`); 451 } 452 453 return this.messageHandler.handleCommand({ 454 moduleName: module, 455 commandName: command, 456 params, 457 destination, 458 }); 459 } 460 461 /** 462 * Register a chrome protocol handler for a directory containing XHTML or XUL 463 * files, allowing them to be loaded via the chrome:// protocol. 464 * 465 * @param {string} manifestPath 466 * The base manifest path for the entries. URL values are resolved 467 * relative to this path. 468 * @param {Array<Array<string, string, string>>} entries 469 * An array of arrays, each containing a registry entry (type, namespace, 470 * path, options) as it would appear in a chrome.manifest file. Only the 471 * following entry types are currently accepted: 472 * 473 * - "content" A URL entry. Must be a 3-element array. 474 * - "override" A URL override entry. Must be a 3-element array. 475 * - "locale" A locale package entry. Must be a 4-element array. 476 * 477 * @returns {string} id 478 * The identifier for the registered chrome protocol handler. 479 */ 480 registerChromeHandler(manifestPath, entries) { 481 const manifest = new lazy.FileUtils.File(manifestPath); 482 const rootURI = Services.io.newFileURI(manifest.parent); 483 const manifestURI = Services.io.newURI(manifest.leafName, null, rootURI); 484 485 const handle = lazy.aomStartup.registerChrome(manifestURI, entries); 486 const id = lazy.generateUUID(); 487 488 this.#chromeProtocolHandles.set(id, handle); 489 490 return id; 491 } 492 493 /** 494 * Unregister a previously registered chrome protocol handler. 495 * 496 * @param {string} id 497 * The identifier returned when the chrome protocol handler was registered. 498 * 499 * @throws {UnknownError} 500 * If there is no such registered chrome protocol handler. 501 */ 502 unregisterChromeHandler(id) { 503 if (!this.#chromeProtocolHandles.has(id)) { 504 throw new lazy.error.UnknownError( 505 `Id ${id} is not a known chrome protocol handler` 506 ); 507 } 508 509 const handle = this.#chromeProtocolHandles.get(id); 510 this.#chromeProtocolHandles.delete(id); 511 handle.destruct(); 512 } 513 514 /** 515 * Remove the specified WebDriver BiDi connection. 516 * 517 * @param {WebDriverBiDiConnection} connection 518 */ 519 removeConnection(connection) { 520 if (this.#connections.has(connection)) { 521 this.#connections.delete(connection); 522 } else { 523 lazy.logger.warn("Trying to remove a connection that doesn't exist."); 524 } 525 } 526 527 toString() { 528 return `[object ${this.constructor.name} ${this.#id}]`; 529 } 530 531 // nsIHttpRequestHandler 532 533 /** 534 * Handle new WebSocket connection requests. 535 * 536 * WebSocket clients will attempt to connect to this session at 537 * `/session/:id`. Hereby a WebSocket upgrade will automatically 538 * be performed. 539 * 540 * @param {Request} request 541 * HTTP request (httpd.js) 542 * @param {Response} response 543 * Response to an HTTP request (httpd.js) 544 */ 545 async handle(request, response) { 546 const webSocket = await lazy.WebSocketHandshake.upgrade(request, response); 547 const conn = new lazy.WebDriverBiDiConnection( 548 webSocket, 549 response._connection 550 ); 551 conn.registerSession(this); 552 this.#connections.add(conn); 553 } 554 555 _onMessageHandlerProtocolEvent(eventName, messageHandlerEvent) { 556 const { name, data } = messageHandlerEvent; 557 this.#connections.forEach(connection => connection.sendEvent(name, data)); 558 } 559 560 // XPCOM 561 562 QueryInterface = ChromeUtils.generateQI(["nsIHttpRequestHandler"]); 563 } 564 565 /** 566 * Get the list of seen nodes for the given browsing context unique to a 567 * WebDriver session. 568 * 569 * @param {string} sessionId 570 * The id of the WebDriver session to use. 571 * @param {BrowsingContext} browsingContext 572 * Browsing context the node is part of. 573 * 574 * @returns {Set} 575 * The list of seen nodes. 576 */ 577 export function getSeenNodesForBrowsingContext(sessionId, browsingContext) { 578 if (!lazy.TabManager.isValidCanonicalBrowsingContext(browsingContext)) { 579 // If browsingContext is not a valid Browsing Context, return an empty set. 580 return new Set(); 581 } 582 583 const navigable = 584 lazy.NavigableManager.getNavigableForBrowsingContext(browsingContext); 585 const session = getWebDriverSessionById(sessionId); 586 587 if (!session.navigableSeenNodes.has(navigable)) { 588 // The navigable hasn't been seen yet. 589 session.navigableSeenNodes.set(navigable, new Set()); 590 } 591 592 return session.navigableSeenNodes.get(navigable); 593 } 594 595 /** 596 * 597 * @param {string} sessionId 598 * The ID of the WebDriver session to retrieve. 599 * 600 * @returns {WebDriverSession|undefined} 601 * The WebDriver session or undefined if the id is not known. 602 */ 603 export function getWebDriverSessionById(sessionId) { 604 return webDriverSessions.get(sessionId); 605 }