tor-browser

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

server.sys.mjs (13179B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
      9  Command: "chrome://remote/content/marionette/message.sys.mjs",
     10  DebuggerTransport: "chrome://remote/content/marionette/transport.sys.mjs",
     11  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     12  GeckoDriver: "chrome://remote/content/marionette/driver.sys.mjs",
     13  Log: "chrome://remote/content/shared/Log.sys.mjs",
     14  MarionettePrefs: "chrome://remote/content/marionette/prefs.sys.mjs",
     15  Message: "chrome://remote/content/marionette/message.sys.mjs",
     16  PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
     17  Response: "chrome://remote/content/marionette/message.sys.mjs",
     18 });
     19 
     20 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     21  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
     22 );
     23 ChromeUtils.defineLazyGetter(lazy, "ServerSocket", () => {
     24  return Components.Constructor(
     25    "@mozilla.org/network/server-socket;1",
     26    "nsIServerSocket",
     27    "initSpecialConnection"
     28  );
     29 });
     30 
     31 const { KeepWhenOffline, LoopbackOnly } = Ci.nsIServerSocket;
     32 
     33 const PROTOCOL_VERSION = 3;
     34 
     35 /**
     36 * Bootstraps Marionette and handles incoming client connections.
     37 *
     38 * Starting the Marionette server will open a TCP socket sporting the
     39 * debugger transport interface on the provided `port`.  For every
     40 * new connection, a {@link TCPConnection} is created.
     41 */
     42 export class TCPListener {
     43  /**
     44   * @param {number} port
     45   *     Port for server to listen to.
     46   */
     47  constructor(port) {
     48    this.port = port;
     49    this.socket = null;
     50    this.conns = new Set();
     51    this.nextConnID = 0;
     52    this.alive = false;
     53  }
     54 
     55  /**
     56   * Function produces a {@link GeckoDriver}.
     57   *
     58   * Determines the application to initialise the driver with.
     59   *
     60   * @returns {GeckoDriver}
     61   *     A driver instance.
     62   */
     63  driverFactory() {
     64    return new lazy.GeckoDriver(this);
     65  }
     66 
     67  async setAcceptConnections(value) {
     68    if (value) {
     69      if (!this.socket) {
     70        await lazy.PollPromise(
     71          (resolve, reject) => {
     72            try {
     73              const flags = KeepWhenOffline | LoopbackOnly;
     74              const backlog = 1;
     75              this.socket = new lazy.ServerSocket(this.port, flags, backlog);
     76              resolve();
     77            } catch (e) {
     78              lazy.logger.debug(
     79                `Could not bind to port ${this.port} (${e.name})`
     80              );
     81              reject();
     82            }
     83          },
     84          { interval: 250, timeout: 5000 }
     85        );
     86 
     87        // Since PollPromise doesn't throw when timeout expires,
     88        // we can end up in the situation when the socket is undefined.
     89        if (!this.socket) {
     90          throw new Error(`Could not bind to port ${this.port}`);
     91        }
     92 
     93        this.port = this.socket.port;
     94 
     95        this.socket.asyncListen(this);
     96        lazy.logger.info(`Listening on port ${this.port}`);
     97      }
     98    } else if (this.socket) {
     99      // Note that closing the server socket will not close currently active
    100      // connections.
    101      this.socket.close();
    102      this.socket = null;
    103      lazy.logger.info(`Stopped listening on port ${this.port}`);
    104    }
    105  }
    106 
    107  /**
    108   * Bind this listener to {@link #port} and start accepting incoming
    109   * socket connections on {@link #onSocketAccepted}.
    110   *
    111   * The marionette.port preference will be populated with the value
    112   * of {@link #port}.
    113   */
    114  async start() {
    115    if (this.alive) {
    116      return;
    117    }
    118 
    119    // Start socket server and listening for connection attempts
    120    await this.setAcceptConnections(true);
    121    lazy.MarionettePrefs.port = this.port;
    122    this.alive = true;
    123  }
    124 
    125  async stop() {
    126    if (!this.alive) {
    127      return;
    128    }
    129 
    130    // Shutdown server socket, and no longer listen for new connections
    131    await this.setAcceptConnections(false);
    132    this.alive = false;
    133  }
    134 
    135  onSocketAccepted(serverSocket, clientSocket) {
    136    let input = clientSocket.openInputStream(0, 0, 0);
    137    let output = clientSocket.openOutputStream(0, 0, 0);
    138    let transport = new lazy.DebuggerTransport(input, output);
    139 
    140    // Only allow a single active WebDriver session at a time
    141    const hasActiveSession = [...this.conns].find(
    142      conn => !!conn.driver.currentSession
    143    );
    144    if (hasActiveSession) {
    145      lazy.logger.warn(
    146        "Connection attempt denied because an active session has been found"
    147      );
    148 
    149      // Ideally we should stop the server to listen for new connection
    150      // attempts, but the current architecture doesn't allow us to do that.
    151      // As such just close the transport if no further connections are allowed.
    152      transport.close();
    153      return;
    154    }
    155 
    156    let conn = new TCPConnection(
    157      this.nextConnID++,
    158      transport,
    159      this.driverFactory.bind(this)
    160    );
    161    conn.onclose = this.onConnectionClosed.bind(this);
    162    this.conns.add(conn);
    163 
    164    lazy.logger.debug(
    165      `Accepted connection ${conn.id} ` +
    166        `from ${clientSocket.host}:${clientSocket.port}`
    167    );
    168    conn.sayHello();
    169    transport.ready();
    170  }
    171 
    172  onConnectionClosed(conn) {
    173    lazy.logger.debug(`Closed connection ${conn.id}`);
    174    this.conns.delete(conn);
    175  }
    176 }
    177 
    178 /**
    179 * Marionette client connection.
    180 *
    181 * Dispatches packets received to their correct service destinations
    182 * and sends back the service endpoint's return values.
    183 *
    184 * @param {number} connID
    185 *     Unique identifier of the connection this dispatcher should handle.
    186 * @param {DebuggerTransport} transport
    187 *     Debugger transport connection to the client.
    188 * @param {function(): GeckoDriver} driverFactory
    189 *     Factory function that produces a {@link GeckoDriver}.
    190 */
    191 export class TCPConnection {
    192  constructor(connID, transport, driverFactory) {
    193    this.id = connID;
    194    this.conn = transport;
    195 
    196    // transport hooks are TCPConnection#onPacket
    197    // and TCPConnection#onClosed
    198    this.conn.hooks = this;
    199 
    200    // callback for when connection is closed
    201    this.onclose = null;
    202 
    203    // last received/sent message ID
    204    this.lastID = 0;
    205 
    206    this.driver = driverFactory();
    207  }
    208 
    209  #log(msg) {
    210    let dir = msg.origin == lazy.Message.Origin.Client ? "->" : "<-";
    211    lazy.logger.debug(`${this.id} ${dir} ${msg.toString()}`);
    212  }
    213 
    214  /**
    215   * Debugger transport callback that cleans up
    216   * after a connection is closed.
    217   */
    218  onClosed() {
    219    this.driver.deleteSession();
    220    if (this.onclose) {
    221      this.onclose(this);
    222    }
    223  }
    224 
    225  /**
    226   * Callback that receives data packets from the client.
    227   *
    228   * If the message is a Response, we look up the command previously
    229   * issued to the client and run its callback, if any.  In case of
    230   * a Command, the corresponding is executed.
    231   *
    232   * @param {Array.<number, number, ?, ?>} data
    233   *     A four element array where the elements, in sequence, signifies
    234   *     message type, message ID, method name or error, and parameters
    235   *     or result.
    236   */
    237  onPacket(data) {
    238    // unable to determine how to respond
    239    if (!Array.isArray(data)) {
    240      let e = new TypeError(
    241        "Unable to unmarshal packet data: " + JSON.stringify(data)
    242      );
    243      lazy.error.report(e);
    244      return;
    245    }
    246 
    247    // return immediately with any error trying to unmarshal message
    248    let msg;
    249    try {
    250      msg = lazy.Message.fromPacket(data);
    251      msg.origin = lazy.Message.Origin.Client;
    252      this.#log(msg);
    253    } catch (e) {
    254      let resp = this.createResponse(data[1]);
    255      resp.sendError(e);
    256      return;
    257    }
    258 
    259    // execute new command
    260    if (msg instanceof lazy.Command) {
    261      (async () => {
    262        await this.execute(msg);
    263      })();
    264    } else {
    265      lazy.logger.fatal("Cannot process messages other than Command");
    266    }
    267  }
    268 
    269  /**
    270   * Executes a Marionette command and sends back a response when it
    271   * has finished executing.
    272   *
    273   * If the command implementation sends the response itself by calling
    274   * <code>resp.send()</code>, the response is guaranteed to not be
    275   * sent twice.
    276   *
    277   * Errors thrown in commands are marshaled and sent back, and if they
    278   * are not {@link WebDriverError} instances, they are additionally
    279   * propagated and reported to {@link Components.utils.reportError}.
    280   *
    281   * @param {Command} cmd
    282   *     Command to execute.
    283   */
    284  async execute(cmd) {
    285    let resp = this.createResponse(cmd.id);
    286    let sendResponse = () => resp.sendConditionally(resp => !resp.sent);
    287    let sendError = resp.sendError.bind(resp);
    288 
    289    await this.dispatch(cmd, resp)
    290      .then(sendResponse, sendError)
    291      .catch(lazy.error.report);
    292  }
    293 
    294  /**
    295   * Dispatches command to appropriate Marionette service.
    296   *
    297   * @param {Command} cmd
    298   *     Command to run.
    299   * @param {Response} resp
    300   *     Mutable response where the command's return value will be
    301   *     assigned.
    302   *
    303   * @throws {Error}
    304   *     A command's implementation may throw at any time.
    305   */
    306  async dispatch(cmd, resp) {
    307    const startTime = ChromeUtils.now();
    308 
    309    let fn = this.driver.commands[cmd.name];
    310    if (typeof fn == "undefined") {
    311      throw new lazy.error.UnknownCommandError(cmd.name);
    312    }
    313 
    314    if (cmd.name != "WebDriver:NewSession") {
    315      lazy.assert.session(this.driver.currentSession);
    316    }
    317 
    318    let rv = await fn.bind(this.driver)(cmd);
    319 
    320    // Bug 1819029: Some older commands cannot return a response wrapped within
    321    // a value field because it would break compatibility with geckodriver and
    322    // Marionette client. It's unlikely that we are going to fix that.
    323    //
    324    // Warning: No more commands should be added to this list!
    325    const commandsNoValueResponse = [
    326      "Marionette:Quit",
    327      "WebDriver:FindElements",
    328      "WebDriver:FindElementsFromShadowRoot",
    329      "WebDriver:CloseChromeWindow",
    330      "WebDriver:CloseWindow",
    331      "WebDriver:FullscreenWindow",
    332      "WebDriver:GetCookies",
    333      "WebDriver:GetElementRect",
    334      "WebDriver:GetTimeouts",
    335      "WebDriver:GetWindowHandles",
    336      "WebDriver:GetWindowRect",
    337      "WebDriver:MaximizeWindow",
    338      "WebDriver:MinimizeWindow",
    339      "WebDriver:NewSession",
    340      "WebDriver:NewWindow",
    341      "WebDriver:SetWindowRect",
    342    ];
    343 
    344    if (rv != null) {
    345      // By default the Response' constructor sets the body to `{ value: null }`.
    346      // As such we only want to override the value if it's neither `null` nor
    347      // `undefined`.
    348      if (commandsNoValueResponse.includes(cmd.name)) {
    349        resp.body = rv;
    350      } else {
    351        resp.body.value = rv;
    352      }
    353    }
    354 
    355    if (Services.profiler?.IsActive()) {
    356      ChromeUtils.addProfilerMarker(
    357        "Marionette: Command",
    358        { startTime, category: "Remote-Protocol" },
    359        `${cmd.name} (${cmd.id})`
    360      );
    361    }
    362  }
    363 
    364  /**
    365   * Fail-safe creation of a new instance of {@link Response}.
    366   *
    367   * @param {number} msgID
    368   *     Message ID to respond to.  If it is not a number, -1 is used.
    369   *
    370   * @returns {Response}
    371   *     Response to the message with `msgID`.
    372   */
    373  createResponse(msgID) {
    374    if (typeof msgID != "number") {
    375      msgID = -1;
    376    }
    377    return new lazy.Response(msgID, this.send.bind(this));
    378  }
    379 
    380  sendError(err, cmdID) {
    381    let resp = new lazy.Response(cmdID, this.send.bind(this));
    382    resp.sendError(err);
    383  }
    384 
    385  /**
    386   * When a client connects we send across a JSON Object defining the
    387   * protocol level.
    388   *
    389   * This is the only message sent by Marionette that does not follow
    390   * the regular message format.
    391   */
    392  sayHello() {
    393    let whatHo = {
    394      applicationType: "gecko",
    395      marionetteProtocol: PROTOCOL_VERSION,
    396    };
    397    this.sendRaw(whatHo);
    398  }
    399 
    400  /**
    401   * Delegates message to client based on the provided `cmdID`.
    402   * The message is sent over the debugger transport socket.
    403   *
    404   * The command ID is a unique identifier assigned to the client's request
    405   * that is used to distinguish the asynchronous responses.
    406   *
    407   * Whilst responses to commands are synchronous and must be sent in the
    408   * correct order.
    409   *
    410   * @param {Message} msg
    411   *     The command or response to send.
    412   */
    413  send(msg) {
    414    msg.origin = lazy.Message.Origin.Server;
    415    if (msg instanceof lazy.Response) {
    416      this.sendToClient(msg);
    417    } else {
    418      lazy.logger.fatal("Cannot send messages other than Response");
    419    }
    420  }
    421 
    422  // Low-level methods:
    423 
    424  /**
    425   * Send given response to the client over the debugger transport socket.
    426   *
    427   * @param {Response} resp
    428   *     The response to send back to the client.
    429   */
    430  sendToClient(resp) {
    431    this.sendMessage(resp);
    432  }
    433 
    434  /**
    435   * Marshal message to the Marionette message format and send it.
    436   *
    437   * @param {Message} msg
    438   *     The message to send.
    439   */
    440  sendMessage(msg) {
    441    this.#log(msg);
    442    let payload = msg.toPacket();
    443    this.sendRaw(payload);
    444  }
    445 
    446  /**
    447   * Send the given payload over the debugger transport socket to the
    448   * connected client.
    449   *
    450   * @param {Record<string, ?>} payload
    451   *     The payload to ship.
    452   */
    453  sendRaw(payload) {
    454    this.conn.send(payload);
    455  }
    456 
    457  toString() {
    458    return `[object TCPConnection ${this.id}]`;
    459  }
    460 }