tor-browser

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

WebDriverBiDi.sys.mjs (8289B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  cleanupCacheBypassState:
      9    "chrome://remote/content/shared/NetworkCacheManager.sys.mjs",
     10  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     11  Log: "chrome://remote/content/shared/Log.sys.mjs",
     12  RecommendedPreferences:
     13    "chrome://remote/content/shared/RecommendedPreferences.sys.mjs",
     14  WebDriverNewSessionHandler:
     15    "chrome://remote/content/webdriver-bidi/NewSessionHandler.sys.mjs",
     16  WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
     17 });
     18 
     19 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     20  lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
     21 );
     22 ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder());
     23 
     24 const RECOMMENDED_PREFS = new Map([
     25  // Enables permission isolation by user context.
     26  // It should be enabled by default in Nightly in the scope of the bug 1641584.
     27  ["permissions.isolateBy.userContext", true],
     28  // Enables race-cache-with-network, which avoids issues with requests
     29  // intercepted in the responseStarted phase. Without this preference, any
     30  // subsequent request to the same URL as a suspended request hangs as well.
     31  // Bug 1966494: should allow to unblock subsequent request, but might do so
     32  // with a timer, slowing down tests. Should be reconsidered once fixed.
     33  ["network.http.rcwn.enabled", true],
     34 ]);
     35 
     36 /**
     37 * Entry class for the WebDriver BiDi support.
     38 *
     39 * @see https://w3c.github.io/webdriver-bidi
     40 */
     41 export class WebDriverBiDi {
     42  #agent;
     43  #bidiServerPath;
     44  #running;
     45  #session;
     46  #sessionlessConnections;
     47 
     48  /**
     49   * Creates a new instance of the WebDriverBiDi class.
     50   *
     51   * @param {RemoteAgent} agent
     52   *     Reference to the Remote Agent instance.
     53   */
     54  constructor(agent) {
     55    this.#agent = agent;
     56    this.#running = false;
     57 
     58    this.#bidiServerPath;
     59    this.#session = null;
     60    this.#sessionlessConnections = new Set();
     61  }
     62 
     63  get address() {
     64    return `ws://${this.#agent.host}:${this.#agent.port}`;
     65  }
     66 
     67  get session() {
     68    return this.#session;
     69  }
     70 
     71  #newSessionAlgorithm(session, flags) {
     72    if (!this.#agent.running) {
     73      // With the Remote Agent not running WebDriver BiDi is not supported.
     74      return;
     75    }
     76 
     77    if (flags.has(lazy.WebDriverSession.SESSION_FLAG_BIDI)) {
     78      // It's already a WebDriver BiDi session.
     79      return;
     80    }
     81 
     82    const webSocketUrl = session.capabilities.get("webSocketUrl");
     83    if (webSocketUrl === undefined) {
     84      return;
     85    }
     86 
     87    // Start listening for BiDi connections.
     88    this.#agent.server.registerPathHandler(session.path, session);
     89    lazy.logger.debug(`Registered session handler: ${session.path}`);
     90 
     91    session.capabilities.set("webSocketUrl", `${this.address}${session.path}`);
     92 
     93    session.bidi = true;
     94    flags.add("bidi");
     95  }
     96 
     97  /**
     98   * Add a new connection that is not yet attached to a WebDriver session.
     99   *
    100   * @param {WebDriverBiDiConnection} connection
    101   *     The connection without an associated WebDriver session.
    102   */
    103  addSessionlessConnection(connection) {
    104    this.#sessionlessConnections.add(connection);
    105  }
    106 
    107  /**
    108   * Create a new WebDriver session.
    109   *
    110   * @param {Record<string, *>=} capabilities
    111   *     JSON Object containing any of the recognised capabilities as listed
    112   *     on the `WebDriverSession` class.
    113   * @param {Set} flags
    114   *     Session configuration flags.
    115   * @param {WebDriverBiDiConnection=} sessionlessConnection
    116   *     Optional connection that is not yet associated with a WebDriver
    117   *     session, and has to be associated with the new WebDriver session.
    118   *
    119   * @returns {Record<string, Capabilities>}
    120   *     Object containing the current session ID, and all its capabilities.
    121   *
    122   * @throws {SessionNotCreatedError}
    123   *     If, for whatever reason, a session could not be created.
    124   */
    125  async createSession(capabilities, flags, sessionlessConnection) {
    126    if (this.#session) {
    127      throw new lazy.error.SessionNotCreatedError(
    128        "Maximum number of active sessions"
    129      );
    130    }
    131 
    132    this.#session = new lazy.WebDriverSession(
    133      capabilities,
    134      flags,
    135      sessionlessConnection
    136    );
    137 
    138    // Run new session steps for WebDriver BiDi.
    139    this.#newSessionAlgorithm(this.#session, flags);
    140 
    141    if (sessionlessConnection) {
    142      // Connection is now registered with a WebDriver session
    143      this.#sessionlessConnections.delete(sessionlessConnection);
    144    }
    145 
    146    if (this.#session.bidi) {
    147      // Creating a WebDriver BiDi session too early can cause issues with
    148      // clients in not being able to find any available browsing context.
    149      // Also when closing the application while it's still starting up can
    150      // cause shutdown hangs. As such WebDriver BiDi will return a new session
    151      // once the initial application window has finished initializing.
    152      lazy.logger.debug(`Waiting for initial application window`);
    153      await this.#agent.browserStartupFinished;
    154    }
    155 
    156    return {
    157      sessionId: this.#session.id,
    158      capabilities: this.#session.capabilities,
    159    };
    160  }
    161 
    162  /**
    163   * Delete the current WebDriver session.
    164   */
    165  deleteSession() {
    166    if (!this.#session) {
    167      return;
    168    }
    169 
    170    // When the Remote Agent is listening, and a BiDi WebSocket is active,
    171    // unregister the path handler for the session.
    172    if (this.#agent.running && this.#session.capabilities.get("webSocketUrl")) {
    173      this.#agent.server.registerPathHandler(this.#session.path, null);
    174      lazy.logger.debug(`Unregistered session handler: ${this.#session.path}`);
    175    }
    176 
    177    // For multiple session check first if the last session was closed.
    178    lazy.cleanupCacheBypassState();
    179 
    180    this.#session.destroy();
    181    this.#session = null;
    182  }
    183 
    184  /**
    185   * Retrieve the readiness state of the remote end, regarding the creation of
    186   * new WebDriverBiDi sessions.
    187   *
    188   * See https://w3c.github.io/webdriver-bidi/#command-session-status
    189   *
    190   * @returns {object}
    191   *     The readiness state.
    192   */
    193  getSessionReadinessStatus() {
    194    if (this.#session) {
    195      // We currently only support one session, see Bug 1720707.
    196      return {
    197        ready: false,
    198        message: "Session already started",
    199      };
    200    }
    201 
    202    return {
    203      ready: true,
    204      message: "",
    205    };
    206  }
    207 
    208  /**
    209   * Starts the WebDriver BiDi support.
    210   */
    211  async start() {
    212    if (this.#running) {
    213      return;
    214    }
    215 
    216    this.#running = true;
    217 
    218    lazy.RecommendedPreferences.applyPreferences(RECOMMENDED_PREFS);
    219 
    220    // Install a HTTP handler for direct WebDriver BiDi connection requests.
    221    this.#agent.server.registerPathHandler(
    222      "/session",
    223      new lazy.WebDriverNewSessionHandler(this)
    224    );
    225 
    226    Cu.printStderr(`WebDriver BiDi listening on ${this.address}\n`);
    227 
    228    try {
    229      // Write WebSocket connection details to the WebDriverBiDiServer.json file
    230      // located within the application's profile.
    231      this.#bidiServerPath = PathUtils.join(
    232        PathUtils.profileDir,
    233        "WebDriverBiDiServer.json"
    234      );
    235 
    236      const data = {
    237        ws_host: this.#agent.host,
    238        ws_port: this.#agent.port,
    239      };
    240 
    241      await IOUtils.write(
    242        this.#bidiServerPath,
    243        lazy.textEncoder.encode(JSON.stringify(data, undefined, "  "))
    244      );
    245    } catch (e) {
    246      lazy.logger.warn(
    247        `Failed to create ${this.#bidiServerPath} (${e.message})`
    248      );
    249    }
    250  }
    251 
    252  /**
    253   * Stops the WebDriver BiDi support.
    254   */
    255  async stop() {
    256    if (!this.#running) {
    257      return;
    258    }
    259 
    260    try {
    261      await IOUtils.remove(this.#bidiServerPath);
    262    } catch (e) {
    263      lazy.logger.warn(
    264        `Failed to remove ${this.#bidiServerPath} (${e.message})`
    265      );
    266    }
    267 
    268    try {
    269      // Close open session
    270      this.deleteSession();
    271      this.#agent.server.registerPathHandler("/session", null);
    272 
    273      // Close all open session-less connections
    274      this.#sessionlessConnections.forEach(connection => connection.close());
    275      this.#sessionlessConnections.clear();
    276    } catch (e) {
    277      lazy.logger.error("Failed to stop protocol", e);
    278    } finally {
    279      this.#running = false;
    280    }
    281  }
    282 }