tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WebDriverBiDiConnection.sys.mjs (9103B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { WebSocketConnection } from "chrome://remote/content/shared/WebSocketConnection.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
     11  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     12  Log: "chrome://remote/content/shared/Log.sys.mjs",
     13  pprint: "chrome://remote/content/shared/Format.sys.mjs",
     14  processCapabilities:
     15    "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
     16  quit: "chrome://remote/content/shared/Browser.sys.mjs",
     17  RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
     18  WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
     19 });
     20 
     21 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     22  lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
     23 );
     24 
     25 export class WebDriverBiDiConnection extends WebSocketConnection {
     26  #sessionConfigFlags;
     27 
     28  /**
     29   * @param {WebSocket} webSocket
     30   *     The WebSocket server connection to wrap.
     31   * @param {Connection} httpdConnection
     32   *     Reference to the httpd.js's connection needed for clean-up.
     33   */
     34  constructor(webSocket, httpdConnection) {
     35    super(webSocket, httpdConnection);
     36 
     37    // Each connection has only a single associated WebDriver session.
     38    this.session = null;
     39 
     40    this.#sessionConfigFlags = new Set([
     41      lazy.WebDriverSession.SESSION_FLAG_BIDI,
     42    ]);
     43  }
     44 
     45  /**
     46   * Perform required steps to end the session.
     47   */
     48  endSession() {
     49    // TODO Bug 1838269. Implement session ending logic
     50    // for the case of classic + bidi session.
     51    // We currently only support one session, see Bug 1720707.
     52    lazy.RemoteAgent.webDriverBiDi.deleteSession();
     53  }
     54 
     55  /**
     56   * Register a new WebDriver Session to forward the messages to.
     57   *
     58   * @param {Session} session
     59   *     The WebDriverSession to register.
     60   */
     61  registerSession(session) {
     62    if (this.session) {
     63      throw new lazy.error.UnknownError(
     64        "A WebDriver session has already been set"
     65      );
     66    }
     67 
     68    this.session = session;
     69    lazy.logger.debug(
     70      `Connection ${this.id} attached to session ${session.id}`
     71    );
     72  }
     73 
     74  /**
     75   * Unregister the already set WebDriver session.
     76   */
     77  unregisterSession() {
     78    if (!this.session) {
     79      return;
     80    }
     81 
     82    this.session.removeConnection(this);
     83    this.session = null;
     84  }
     85 
     86  /**
     87   * Send an error back to the WebDriver BiDi client.
     88   *
     89   * @param {number} id
     90   *     Id of the packet which lead to an error.
     91   * @param {Error} err
     92   *     Error object with `status`, `message` and `stack` attributes.
     93   */
     94  sendError(id, err) {
     95    // Convert specific MessageHandler errors to WebDriver errors
     96    let webDriverError;
     97    switch (err.name) {
     98      case "DiscardedBrowsingContextError":
     99        webDriverError = lazy.error.wrap(err, lazy.error.NoSuchFrameError);
    100        break;
    101      default:
    102        webDriverError = lazy.error.wrap(err);
    103    }
    104 
    105    this.send({
    106      type: "error",
    107      id,
    108      error: webDriverError.status,
    109      message: webDriverError.message,
    110      stacktrace: webDriverError.stack,
    111    });
    112  }
    113 
    114  /**
    115   * Send an event coming from a module to the WebDriver BiDi client.
    116   *
    117   * @param {string} method
    118   *     The event name. This is composed by a module name, a dot character
    119   *     followed by the event name, e.g. `log.entryAdded`.
    120   * @param {object} params
    121   *     A JSON-serializable object, which is the payload of this event.
    122   */
    123  sendEvent(method, params) {
    124    this.send({ type: "event", method, params });
    125 
    126    if (Services.profiler?.IsActive()) {
    127      ChromeUtils.addProfilerMarker(
    128        "BiDi: Event",
    129        { category: "Remote-Protocol" },
    130        method
    131      );
    132    }
    133  }
    134 
    135  /**
    136   * Send the result of a call to a module's method back to the
    137   * WebDriver BiDi client.
    138   *
    139   * @param {number} id
    140   *     The request id being sent by the client to call the module's method.
    141   * @param {object} result
    142   *     A JSON-serializable object, which is the actual result.
    143   */
    144  sendResult(id, result) {
    145    result = typeof result !== "undefined" ? result : {};
    146    this.send({ type: "success", id, result });
    147  }
    148 
    149  observe(subject, topic) {
    150    switch (topic) {
    151      case "quit-application-requested":
    152        this.endSession();
    153        break;
    154    }
    155  }
    156 
    157  // Transport hooks
    158 
    159  /**
    160   * Called by the `transport` when the connection is closed.
    161   */
    162  onConnectionClose() {
    163    this.unregisterSession();
    164 
    165    super.onConnectionClose();
    166  }
    167 
    168  /**
    169   * Receive a packet from the WebSocket layer.
    170   *
    171   * This packet is sent by a WebDriver BiDi client and is meant to execute
    172   * a particular method on a given module.
    173   *
    174   * @param {object} packet
    175   *     JSON-serializable object sent by the client
    176   */
    177  async onPacket(packet) {
    178    super.onPacket(packet);
    179 
    180    const { id, method, params } = packet;
    181    const startTime = ChromeUtils.now();
    182 
    183    try {
    184      // First check for mandatory field in the command packet
    185      lazy.assert.positiveInteger(
    186        id,
    187        lazy.pprint`Expected "id" to be a positive integer, got ${id}`
    188      );
    189      lazy.assert.string(
    190        method,
    191        lazy.pprint`Expected "method" to be a string, got ${method}`
    192      );
    193      lazy.assert.object(
    194        params,
    195        lazy.pprint`Expected "params" to be an object, got ${params}`
    196      );
    197 
    198      // Extract the module and the command name out of `method` attribute
    199      const { module, command } = splitMethod(method);
    200      let result;
    201 
    202      // Handle static commands first
    203      if (module === "session" && command === "new") {
    204        const processedCapabilities = lazy.processCapabilities(params);
    205 
    206        result = await lazy.RemoteAgent.webDriverBiDi.createSession(
    207          processedCapabilities,
    208          this.#sessionConfigFlags,
    209          this
    210        );
    211 
    212        // Until the spec (see: https://github.com/w3c/webdriver/issues/1834)
    213        // is updated to specify what should be the default value for bidi session,
    214        // remove this capability from the return value when it's not provided by a client.
    215        if (!("unhandledPromptBehavior" in processedCapabilities)) {
    216          // We don't want to modify the original `capabilities` field
    217          // because it points to an original Capabilities object used by the session.
    218          // Since before the result is sent to a client we're going anyway to call
    219          // `JSON.stringify` on `result` which will call `toJSON` method recursively,
    220          // we can call it already here for the `capabilities` property
    221          // to update only the command response object.
    222          result.capabilities = result.capabilities.toJSON();
    223          delete result.capabilities.unhandledPromptBehavior;
    224        }
    225      } else if (module === "session" && command === "status") {
    226        result = lazy.RemoteAgent.webDriverBiDi.getSessionReadinessStatus();
    227      } else {
    228        lazy.assert.session(this.session);
    229 
    230        // Bug 1741854 - Workaround to deny internal methods to be called
    231        if (command.startsWith("_")) {
    232          throw new lazy.error.UnknownCommandError(method);
    233        }
    234 
    235        // Finally, instruct the session to execute the command
    236        result = await this.session.execute(module, command, params);
    237      }
    238 
    239      this.sendResult(id, result);
    240 
    241      // Session clean up.
    242      if (module === "session" && command === "end") {
    243        this.endSession();
    244      }
    245      // Close the browser.
    246      // TODO Bug 1842018. Refactor this part to return the response
    247      // when the quitting of the browser is finished.
    248      else if (module === "browser" && command === "close") {
    249        // Register handler to run WebDriver BiDi specific shutdown code.
    250        Services.obs.addObserver(this, "quit-application-requested");
    251 
    252        // TODO Bug 1836282. Add as the third argument "moz:windowless" capability
    253        // from the session, when this capability is supported by Webdriver BiDi.
    254        await lazy.quit(["eForceQuit"], false);
    255 
    256        Services.obs.removeObserver(this, "quit-application-requested");
    257      }
    258    } catch (e) {
    259      this.sendError(id, e);
    260    }
    261 
    262    if (Services.profiler?.IsActive()) {
    263      ChromeUtils.addProfilerMarker(
    264        "BiDi: Command",
    265        { startTime, category: "Remote-Protocol" },
    266        `${method} (${id})`
    267      );
    268    }
    269  }
    270 }
    271 
    272 /**
    273 * Splits a WebDriver BiDi method into module and command components.
    274 *
    275 * @param {string} method
    276 *     Name of the method to split, e.g. "session.subscribe".
    277 *
    278 * @returns {Record<string, string>}
    279 *     Object with the module ("session") and command ("subscribe")
    280 *     as properties.
    281 */
    282 export function splitMethod(method) {
    283  const parts = method.split(".");
    284 
    285  if (parts.length != 2 || !parts[0].length || !parts[1].length) {
    286    throw new TypeError(`Invalid method format: '${method}'`);
    287  }
    288 
    289  return {
    290    module: parts[0],
    291    command: parts[1],
    292  };
    293 }