tor-browser

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

NodeServer.sys.mjs (36606B)


      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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs";
      7 
      8 /* globals require, __dirname, global, Buffer, process */
      9 
     10 class BaseNodeHTTPServerCode {
     11  static globalHandler(req, resp) {
     12    let path = new URL(req.url, "https://example.com").pathname;
     13    let handler = global.path_handlers[path];
     14    if (handler) {
     15      return handler(req, resp);
     16    }
     17 
     18    // Didn't find a handler for this path.
     19    let response = `<h1> 404 Path not found: ${path}</h1>`;
     20    resp.setHeader("Content-Type", "text/html");
     21    resp.setHeader("Content-Length", response.length);
     22    resp.writeHead(404);
     23    resp.end(response);
     24    return undefined;
     25  }
     26 }
     27 
     28 class ADB {
     29  static async stopForwarding(port) {
     30    return this.forwardPort(port, true);
     31  }
     32 
     33  static async forwardPort(port, remove = false) {
     34    if (!process.env.MOZ_ANDROID_DATA_DIR) {
     35      // Not android, or we don't know how to do the forwarding
     36      return true;
     37    }
     38    // When creating a server on Android we must make sure that the port
     39    // is forwarded from the host machine to the emulator.
     40    let adb_path = "adb";
     41    if (process.env.MOZ_FETCHES_DIR) {
     42      adb_path = `${process.env.MOZ_FETCHES_DIR}/android-sdk-linux/platform-tools/adb`;
     43    }
     44 
     45    let command = `${adb_path} reverse tcp:${port} tcp:${port}`;
     46    if (remove) {
     47      command = `${adb_path} reverse --remove tcp:${port}`;
     48      return true;
     49    }
     50 
     51    try {
     52      await new Promise((resolve, reject) => {
     53        const { exec } = require("child_process");
     54        exec(command, (error, stdout, stderr) => {
     55          if (error) {
     56            console.log(`error: ${error.message}`);
     57            reject(error);
     58          } else if (stderr) {
     59            console.log(`stderr: ${stderr}`);
     60            reject(stderr);
     61          } else {
     62            // console.log(`stdout: ${stdout}`);
     63            resolve();
     64          }
     65        });
     66      });
     67    } catch (error) {
     68      console.log(`Command failed: ${error}`);
     69      return false;
     70    }
     71 
     72    return true;
     73  }
     74 
     75  static async listenAndForwardPort(server, port) {
     76    let retryCount = 0;
     77    const maxRetries = 10;
     78 
     79    while (retryCount < maxRetries) {
     80      await server.listen(port);
     81      let serverPort = server.address().port;
     82      let res = await ADB.forwardPort(serverPort);
     83 
     84      if (res) {
     85        return serverPort;
     86      }
     87 
     88      retryCount++;
     89      console.log(
     90        `Port forwarding failed. Retrying (${retryCount}/${maxRetries})...`
     91      );
     92      server.close();
     93      // eslint-disable-next-line no-undef
     94      await new Promise(resolve => setTimeout(resolve, 500));
     95    }
     96 
     97    return -1;
     98  }
     99 }
    100 
    101 class BaseNodeServer {
    102  protocol() {
    103    return this._protocol;
    104  }
    105  version() {
    106    return this._version;
    107  }
    108  origin() {
    109    return `${this.protocol()}://localhost:${this.port()}`;
    110  }
    111  port() {
    112    return this._port;
    113  }
    114  domain() {
    115    return `localhost`;
    116  }
    117 
    118  static async installCert(filename) {
    119    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
    120      // Can't install cert from content process.
    121      return;
    122    }
    123    let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    124      Ci.nsIX509CertDB
    125    );
    126 
    127    function readFile(file) {
    128      let fstream = Cc[
    129        "@mozilla.org/network/file-input-stream;1"
    130      ].createInstance(Ci.nsIFileInputStream);
    131      fstream.init(file, -1, 0, 0);
    132      let data = NetUtil.readInputStreamToString(fstream, fstream.available());
    133      fstream.close();
    134      return data;
    135    }
    136 
    137    // Find the root directory that contains netwerk/
    138    let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
    139    let rootDir = currentDir.clone();
    140 
    141    // XXX(valentin) The certs are stored in netwerk/test/unit
    142    // Walk up until the dir contains netwerk/
    143    // This is hacky, but the alternative would also require
    144    // us to walk up the path to the root dir.
    145    while (rootDir) {
    146      let netwerkDir = rootDir.clone();
    147      netwerkDir.append("netwerk");
    148      if (netwerkDir.exists() && netwerkDir.isDirectory()) {
    149        break;
    150      }
    151      let parent = rootDir.parent;
    152      if (!parent || parent.equals(rootDir)) {
    153        // Reached filesystem root, fallback to current directory
    154        rootDir = currentDir;
    155        break;
    156      }
    157      rootDir = parent;
    158    }
    159 
    160    let certFile = rootDir.clone();
    161    certFile.append("netwerk");
    162    certFile.append("test");
    163    certFile.append("unit");
    164    certFile.append(filename);
    165 
    166    try {
    167      let pem = readFile(certFile)
    168        .replace(/-----BEGIN CERTIFICATE-----/, "")
    169        .replace(/-----END CERTIFICATE-----/, "")
    170        .replace(/[\r\n]/g, "");
    171      certdb.addCertFromBase64(pem, "CTu,u,u");
    172    } catch (e) {
    173      let errStr = e.toString();
    174      console.log(`Error installing cert ${errStr}`);
    175      if (errStr.includes("0x805a1fe8")) {
    176        // Can't install the cert without a profile
    177        // Let's show an error, otherwise this will be difficult to diagnose.
    178        console.log(
    179          `!!! BaseNodeServer.installCert > Make sure your unit test calls do_get_profile()`
    180        );
    181      }
    182    }
    183  }
    184 
    185  /// Stops the server
    186  async stop() {
    187    if (this.processId) {
    188      await this.execute(`ADB.stopForwarding(${this.port()})`);
    189      await NodeServer.kill(this.processId);
    190      this.processId = undefined;
    191    }
    192  }
    193 
    194  /// Executes a command in the context of the node server
    195  async execute(command) {
    196    return NodeServer.execute(this.processId, command);
    197  }
    198 
    199  /// @path : string - the path on the server that we're handling. ex: /path
    200  /// @handler : function(req, resp, url) - function that processes request and
    201  ///     emits a response.
    202  async registerPathHandler(path, handler) {
    203    return this.execute(
    204      `global.path_handlers["${path}"] = ${handler.toString()}`
    205    );
    206  }
    207 }
    208 
    209 // HTTP
    210 
    211 class NodeHTTPServerCode extends BaseNodeHTTPServerCode {
    212  static async startServer(port) {
    213    const http = require("http");
    214    global.server = http.createServer(BaseNodeHTTPServerCode.globalHandler);
    215 
    216    let serverPort = await ADB.listenAndForwardPort(global.server, port);
    217    return serverPort;
    218  }
    219 }
    220 
    221 export class NodeHTTPServer extends BaseNodeServer {
    222  _protocol = "http";
    223  _version = "http/1.1";
    224  /// Starts the server
    225  /// @port - default 0
    226  ///    when provided, will attempt to listen on that port.
    227  async start(port = 0) {
    228    this.processId = await NodeServer.fork();
    229 
    230    await this.execute(BaseNodeHTTPServerCode);
    231    await this.execute(NodeHTTPServerCode);
    232    await this.execute(ADB);
    233    this._port = await this.execute(`NodeHTTPServerCode.startServer(${port})`);
    234    await this.execute(`global.path_handlers = {};`);
    235  }
    236 }
    237 
    238 // HTTPS
    239 
    240 class NodeHTTPSServerCode extends BaseNodeHTTPServerCode {
    241  static async startServer(port) {
    242    const fs = require("fs");
    243    const options = {
    244      key: fs.readFileSync(__dirname + "/http2-cert.key"),
    245      cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
    246      maxHeaderSize: 128 * 1024,
    247    };
    248    const https = require("https");
    249    global.server = https.createServer(
    250      options,
    251      BaseNodeHTTPServerCode.globalHandler
    252    );
    253 
    254    let serverPort = await ADB.listenAndForwardPort(global.server, port);
    255    return serverPort;
    256  }
    257 }
    258 
    259 export class NodeHTTPSServer extends BaseNodeServer {
    260  _protocol = "https";
    261  _version = "http/1.1";
    262  /// Starts the server
    263  /// @port - default 0
    264  ///    when provided, will attempt to listen on that port.
    265  async start(port = 0) {
    266    if (!this._skipCert) {
    267      await BaseNodeServer.installCert("http2-ca.pem");
    268    }
    269    this.processId = await NodeServer.fork();
    270 
    271    await this.execute(BaseNodeHTTPServerCode);
    272    await this.execute(NodeHTTPSServerCode);
    273    await this.execute(ADB);
    274    this._port = await this.execute(`NodeHTTPSServerCode.startServer(${port})`);
    275    await this.execute(`global.path_handlers = {};`);
    276  }
    277 }
    278 
    279 // HTTP2
    280 
    281 class NodeHTTP2ServerCode extends BaseNodeHTTPServerCode {
    282  static async startServer(port) {
    283    const fs = require("fs");
    284    const options = {
    285      key: fs.readFileSync(__dirname + "/http2-cert.key"),
    286      cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
    287    };
    288    const http2 = require("http2");
    289    global.server = http2.createSecureServer(
    290      options,
    291      BaseNodeHTTPServerCode.globalHandler
    292    );
    293 
    294    global.sessionCount = 0;
    295    global.sessions = new Set();
    296    global.server.on("session", session => {
    297      global.sessions.add(session);
    298      session.on("close", () => {
    299        global.sessions.delete(session);
    300      });
    301      global.sessionCount++;
    302    });
    303 
    304    let serverPort = await ADB.listenAndForwardPort(global.server, port);
    305    return serverPort;
    306  }
    307 
    308  static sessionCount() {
    309    return global.sessionCount;
    310  }
    311 }
    312 
    313 export class NodeHTTP2Server extends BaseNodeServer {
    314  _protocol = "https";
    315  _version = "h2";
    316  /// Starts the server
    317  /// @port - default 0
    318  ///    when provided, will attempt to listen on that port.
    319  async start(port = 0) {
    320    if (!this._skipCert) {
    321      await BaseNodeServer.installCert("http2-ca.pem");
    322    }
    323    this.processId = await NodeServer.fork();
    324 
    325    await this.execute(BaseNodeHTTPServerCode);
    326    await this.execute(NodeHTTP2ServerCode);
    327    await this.execute(ADB);
    328    this._port = await this.execute(`NodeHTTP2ServerCode.startServer(${port})`);
    329    await this.execute(`global.path_handlers = {};`);
    330  }
    331 
    332  async sessionCount() {
    333    let count = this.execute(`NodeHTTP2ServerCode.sessionCount()`);
    334    return count;
    335  }
    336 }
    337 
    338 // Base HTTP proxy
    339 
    340 class BaseProxyCode {
    341  static proxyHandler(req, res) {
    342    if (req.url.startsWith("/")) {
    343      res.writeHead(405);
    344      res.end();
    345      return;
    346    }
    347 
    348    let url = new URL(req.url);
    349    const http = require("http");
    350    let preq = http
    351      .request(
    352        {
    353          method: req.method,
    354          path: url.pathname,
    355          port: url.port,
    356          host: url.hostname,
    357          protocol: url.protocol,
    358        },
    359        proxyresp => {
    360          res.writeHead(
    361            proxyresp.statusCode,
    362            proxyresp.statusMessage,
    363            proxyresp.headers
    364          );
    365          proxyresp.on("data", chunk => {
    366            if (!res.writableEnded) {
    367              res.write(chunk);
    368            }
    369          });
    370          proxyresp.on("end", () => {
    371            res.end();
    372          });
    373        }
    374      )
    375      .on("error", e => {
    376        console.log(`sock err: ${e}`);
    377      });
    378    if (req.method != "POST") {
    379      preq.end();
    380    } else {
    381      req.on("data", chunk => {
    382        if (!preq.writableEnded) {
    383          preq.write(chunk);
    384        }
    385      });
    386      req.on("end", () => preq.end());
    387    }
    388  }
    389 
    390  static onConnect(req, clientSocket, head) {
    391    if (global.connect_handler) {
    392      global.connect_handler(req, clientSocket, head);
    393      return;
    394    }
    395    const net = require("net");
    396    // Connect to an origin server
    397    const { port, hostname } = new URL(`https://${req.url}`);
    398    const serverSocket = net
    399      .connect(
    400        {
    401          port: port || 443,
    402          host: hostname,
    403          family: 4, // Specifies to use IPv4
    404        },
    405        () => {
    406          clientSocket.write(
    407            "HTTP/1.1 200 Connection Established\r\n" +
    408              "Proxy-agent: Node.js-Proxy\r\n" +
    409              "\r\n"
    410          );
    411          serverSocket.write(head);
    412          serverSocket.pipe(clientSocket);
    413          clientSocket.pipe(serverSocket);
    414        }
    415      )
    416      .on("error", e => {
    417        console.log("error" + e);
    418        // The socket will error out when we kill the connection
    419        // just ignore it.
    420      });
    421 
    422    clientSocket.on("error", e => {
    423      console.log("client error" + e);
    424      // Sometimes we got ECONNRESET error on windows platform.
    425      // Ignore it for now.
    426    });
    427  }
    428 }
    429 
    430 class BaseHTTPProxy extends BaseNodeServer {
    431  registerFilter() {
    432    const pps =
    433      Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
    434    this.filter = new NodeProxyFilter(
    435      this.protocol(),
    436      "localhost",
    437      this.port(),
    438      0
    439    );
    440    pps.registerFilter(this.filter, 10);
    441  }
    442 
    443  unregisterFilter() {
    444    const pps =
    445      Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
    446    if (this.filter) {
    447      pps.unregisterFilter(this.filter);
    448      this.filter = undefined;
    449    }
    450  }
    451 
    452  /// Stops the server
    453  async stop() {
    454    this.unregisterFilter();
    455    await super.stop();
    456  }
    457 
    458  async registerConnectHandler(handler) {
    459    return this.execute(`global.connect_handler = ${handler.toString()}`);
    460  }
    461 }
    462 
    463 // HTTP1 Proxy
    464 
    465 export class NodeProxyFilter {
    466  constructor(type, host, port, flags) {
    467    this._type = type;
    468    this._host = host;
    469    this._port = port;
    470    this._flags = flags;
    471    this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]);
    472  }
    473  applyFilter(uri, pi, cb) {
    474    const pps =
    475      Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
    476    cb.onProxyFilterResult(
    477      pps.newProxyInfo(
    478        this._type,
    479        this._host,
    480        this._port,
    481        "",
    482        "",
    483        this._flags,
    484        1000,
    485        null
    486      )
    487    );
    488  }
    489 }
    490 
    491 export class Http3ProxyFilter {
    492  constructor(host, port, flags, masqueTemplate, auth) {
    493    this._host = host;
    494    this._port = port;
    495    this._flags = flags;
    496    this._masqueTemplate = masqueTemplate;
    497    this._auth = auth;
    498    this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]);
    499  }
    500  applyFilter(uri, pi, cb) {
    501    const pps =
    502      Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
    503    cb.onProxyFilterResult(
    504      pps.newMASQUEProxyInfo(
    505        this._host,
    506        this._port,
    507        this._masqueTemplate,
    508        this._auth,
    509        "",
    510        this._flags,
    511        1000,
    512        null
    513      )
    514    );
    515  }
    516 }
    517 
    518 class HTTPProxyCode {
    519  static async startServer(port) {
    520    const http = require("http");
    521    global.proxy = http.createServer(BaseProxyCode.proxyHandler);
    522    global.proxy.on("connect", BaseProxyCode.onConnect);
    523 
    524    let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
    525    return proxyPort;
    526  }
    527 }
    528 
    529 export class NodeHTTPProxyServer extends BaseHTTPProxy {
    530  _protocol = "http";
    531  /// Starts the server
    532  /// @port - default 0
    533  ///    when provided, will attempt to listen on that port.
    534  async start(port = 0) {
    535    this.processId = await NodeServer.fork();
    536 
    537    await this.execute(BaseProxyCode);
    538    await this.execute(HTTPProxyCode);
    539    await this.execute(ADB);
    540    await this.execute(`global.connect_handler = null;`);
    541    this._port = await this.execute(`HTTPProxyCode.startServer(${port})`);
    542 
    543    this.registerFilter();
    544  }
    545 }
    546 
    547 // HTTPS proxy
    548 
    549 class HTTPSProxyCode {
    550  static async startServer(port) {
    551    const fs = require("fs");
    552    const options = {
    553      key: fs.readFileSync(__dirname + "/proxy-cert.key"),
    554      cert: fs.readFileSync(__dirname + "/proxy-cert.pem"),
    555    };
    556    const https = require("https");
    557    global.proxy = https.createServer(options, BaseProxyCode.proxyHandler);
    558    global.proxy.on("connect", BaseProxyCode.onConnect);
    559 
    560    let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
    561    return proxyPort;
    562  }
    563 }
    564 
    565 export class NodeHTTPSProxyServer extends BaseHTTPProxy {
    566  _protocol = "https";
    567  /// Starts the server
    568  /// @port - default 0
    569  ///    when provided, will attempt to listen on that port.
    570  async start(port = 0) {
    571    if (!this._skipCert) {
    572      await BaseNodeServer.installCert("proxy-ca.pem");
    573    }
    574    this.processId = await NodeServer.fork();
    575 
    576    await this.execute(BaseProxyCode);
    577    await this.execute(HTTPSProxyCode);
    578    await this.execute(ADB);
    579    await this.execute(`global.connect_handler = null;`);
    580    this._port = await this.execute(`HTTPSProxyCode.startServer(${port})`);
    581 
    582    this.registerFilter();
    583  }
    584 }
    585 
    586 // HTTP2 proxy
    587 
    588 class HTTP2ProxyCode {
    589  static async startServer(port, auth, maxConcurrentStreams) {
    590    const fs = require("fs");
    591    const options = {
    592      key: fs.readFileSync(__dirname + "/proxy-cert.key"),
    593      cert: fs.readFileSync(__dirname + "/proxy-cert.pem"),
    594      settings: {
    595        maxConcurrentStreams,
    596      },
    597    };
    598    const http2 = require("http2");
    599    global.proxy = http2.createSecureServer(options);
    600    global.socketCounts = {};
    601    this.setupProxy(auth);
    602 
    603    let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
    604    return proxyPort;
    605  }
    606 
    607  static setupProxy(auth) {
    608    if (!global.proxy) {
    609      throw new Error("proxy is null");
    610    }
    611 
    612    global.proxy.on("stream", (stream, headers) => {
    613      if (headers[":scheme"] === "http") {
    614        const http = require("http");
    615        let url = new URL(
    616          `${headers[":scheme"]}://${headers[":authority"]}${headers[":path"]}`
    617        );
    618        let req = http
    619          .request(
    620            {
    621              method: headers[":method"],
    622              path: headers[":path"],
    623              port: url.port,
    624              host: url.hostname,
    625              protocol: url.protocol,
    626            },
    627            proxyresp => {
    628              let proxyheaders = Object.assign({}, proxyresp.headers);
    629              // Filter out some prohibited headers.
    630              ["connection", "transfer-encoding", "keep-alive"].forEach(
    631                prop => {
    632                  delete proxyheaders[prop];
    633                }
    634              );
    635              try {
    636                stream.respond(
    637                  Object.assign(
    638                    { ":status": proxyresp.statusCode },
    639                    proxyheaders
    640                  )
    641                );
    642              } catch (e) {
    643                // The channel may have been closed already.
    644                if (
    645                  e.code !== "ERR_HTTP2_INVALID_STREAM" &&
    646                  !e.message.includes("The stream has been destroyed")
    647                ) {
    648                  throw e;
    649                }
    650              }
    651              proxyresp.on("data", chunk => {
    652                if (stream.writable) {
    653                  stream.write(chunk);
    654                }
    655              });
    656              proxyresp.on("end", () => {
    657                stream.end();
    658              });
    659            }
    660          )
    661          .on("error", e => {
    662            console.log(`sock err: ${e}`);
    663          });
    664 
    665        if (headers[":method"] != "POST") {
    666          req.end();
    667        } else {
    668          stream.on("data", chunk => {
    669            if (!req.writableEnded) {
    670              req.write(chunk);
    671            }
    672          });
    673          stream.on("end", () => req.end());
    674        }
    675        return;
    676      }
    677      if (headers[":method"] !== "CONNECT") {
    678        // Only accept CONNECT requests
    679        try {
    680          stream.respond({ ":status": 405 });
    681        } catch (e) {
    682          if (
    683            e.code !== "ERR_HTTP2_INVALID_STREAM" &&
    684            !e.message.includes("The stream has been destroyed")
    685          ) {
    686            throw e;
    687          }
    688        }
    689        stream.end();
    690        return;
    691      }
    692 
    693      const authorization_token = headers["proxy-authorization"];
    694      if (auth && !authorization_token) {
    695        try {
    696          stream.respond({
    697            ":status": 407,
    698            "proxy-authenticate": "Basic realm='foo'",
    699          });
    700        } catch (e) {
    701          if (
    702            e.code !== "ERR_HTTP2_INVALID_STREAM" &&
    703            !e.message.includes("The stream has been destroyed")
    704          ) {
    705            throw e;
    706          }
    707        }
    708        stream.end();
    709        return;
    710      }
    711 
    712      const target = headers[":authority"];
    713      const { port } = new URL(`https://${target}`);
    714      const net = require("net");
    715      const socket = net.connect(port, "127.0.0.1", () => {
    716        try {
    717          global.socketCounts[socket.remotePort] =
    718            (global.socketCounts[socket.remotePort] || 0) + 1;
    719          try {
    720            stream.respond({ ":status": 200 });
    721          } catch (e) {
    722            if (
    723              e.code !== "ERR_HTTP2_INVALID_STREAM" &&
    724              !e.message.includes("The stream has been destroyed")
    725            ) {
    726              throw e;
    727            }
    728          }
    729          socket.pipe(stream);
    730          stream.pipe(socket);
    731        } catch (exception) {
    732          console.log(exception);
    733          stream.close();
    734        }
    735      });
    736      const http2 = require("http2");
    737      socket.on("error", error => {
    738        const status = error.errno == "ENOTFOUND" ? 404 : 502;
    739        try {
    740          // If we already sent headers when the socket connected
    741          // then sending the status again would throw.
    742          if (!stream.sentHeaders) {
    743            try {
    744              stream.respond({ ":status": status });
    745            } catch (e) {
    746              if (
    747                e.code !== "ERR_HTTP2_INVALID_STREAM" &&
    748                !e.message.includes("The stream has been destroyed")
    749              ) {
    750                throw e;
    751              }
    752            }
    753          }
    754          stream.end();
    755        } catch (exception) {
    756          stream.close(http2.constants.NGHTTP2_CONNECT_ERROR);
    757        }
    758      });
    759      stream.on("close", () => {
    760        socket.end();
    761      });
    762      socket.on("close", () => {
    763        stream.close();
    764      });
    765      stream.on("end", () => {
    766        socket.end();
    767      });
    768      stream.on("aborted", () => {
    769        socket.end();
    770      });
    771      stream.on("error", error => {
    772        console.log("RESPONSE STREAM ERROR", error);
    773      });
    774    });
    775  }
    776 
    777  static socketCount(port) {
    778    return global.socketCounts[port];
    779  }
    780 }
    781 
    782 export class NodeHTTP2ProxyServer extends BaseHTTPProxy {
    783  _protocol = "https";
    784  /// Starts the server
    785  /// @port - default 0
    786  ///    when provided, will attempt to listen on that port.
    787  async start(port = 0, auth, maxConcurrentStreams = 100) {
    788    await this.startWithoutProxyFilter(port, auth, maxConcurrentStreams);
    789    this.registerFilter();
    790  }
    791 
    792  async startWithoutProxyFilter(port = 0, auth, maxConcurrentStreams = 100) {
    793    if (!this._skipCert) {
    794      await BaseNodeServer.installCert("proxy-ca.pem");
    795    }
    796    this.processId = await NodeServer.fork();
    797 
    798    await this.execute(BaseProxyCode);
    799    await this.execute(HTTP2ProxyCode);
    800    await this.execute(ADB);
    801    await this.execute(`global.connect_handler = null;`);
    802    this._port = await this.execute(
    803      `HTTP2ProxyCode.startServer(${port}, ${auth}, ${maxConcurrentStreams})`
    804    );
    805  }
    806 
    807  async socketCount(port) {
    808    let count = await this.execute(`HTTP2ProxyCode.socketCount(${port})`);
    809    return count;
    810  }
    811 }
    812 
    813 // websocket server
    814 
    815 class NodeWebSocketServerCode extends BaseNodeHTTPServerCode {
    816  static messageHandler(data, ws) {
    817    if (global.wsInputHandler) {
    818      global.wsInputHandler(data, ws);
    819      return;
    820    }
    821 
    822    ws.send("test");
    823  }
    824 
    825  static async startServer(port) {
    826    const fs = require("fs");
    827    const options = {
    828      key: fs.readFileSync(__dirname + "/http2-cert.key"),
    829      cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
    830    };
    831    const https = require("https");
    832    global.server = https.createServer(
    833      options,
    834      BaseNodeHTTPServerCode.globalHandler
    835    );
    836 
    837    let node_ws_root = `${__dirname}/../node-ws`;
    838    const WS = require(`${node_ws_root}/lib/websocket`);
    839    WS.Server = require(`${node_ws_root}/lib/websocket-server`);
    840    global.webSocketServer = new WS.Server({ server: global.server });
    841    global.webSocketServer.on("connection", function connection(ws) {
    842      ws.on("message", data =>
    843        NodeWebSocketServerCode.messageHandler(data, ws)
    844      );
    845    });
    846 
    847    let serverPort = await ADB.listenAndForwardPort(global.server, port);
    848    return serverPort;
    849  }
    850 }
    851 
    852 export class NodeWebSocketServer extends BaseNodeServer {
    853  _protocol = "wss";
    854  /// Starts the server
    855  /// @port - default 0
    856  ///    when provided, will attempt to listen on that port.
    857  async start(port = 0) {
    858    if (!this._skipCert) {
    859      await BaseNodeServer.installCert("http2-ca.pem");
    860    }
    861    this.processId = await NodeServer.fork();
    862 
    863    await this.execute(BaseNodeHTTPServerCode);
    864    await this.execute(NodeWebSocketServerCode);
    865    await this.execute(ADB);
    866    this._port = await this.execute(
    867      `NodeWebSocketServerCode.startServer(${port})`
    868    );
    869    await this.execute(`global.path_handlers = {};`);
    870    await this.execute(`global.wsInputHandler = null;`);
    871  }
    872 
    873  async registerMessageHandler(handler) {
    874    return this.execute(`global.wsInputHandler = ${handler.toString()}`);
    875  }
    876 }
    877 
    878 // unencrypted websocket server
    879 
    880 class NodeWebSocketPlainServerCode extends BaseNodeHTTPServerCode {
    881  static async startServer(port) {
    882    const http = require("http");
    883    global.server = http.createServer(BaseNodeHTTPServerCode.globalHandler);
    884 
    885    let node_ws_root = `${__dirname}/../node-ws`;
    886    const WS = require(`${node_ws_root}/lib/websocket`);
    887    WS.Server = require(`${node_ws_root}/lib/websocket-server`);
    888    global.webSocketServer = new WS.Server({ server: global.server });
    889    global.webSocketServer.on("connection", function connection(ws) {
    890      ws.on("message", data =>
    891        NodeWebSocketServerCode.messageHandler(data, ws)
    892      );
    893    });
    894 
    895    let serverPort = await ADB.listenAndForwardPort(global.server, port);
    896    return serverPort;
    897  }
    898 }
    899 
    900 export class NodeWebSocketPlainServer extends BaseNodeServer {
    901  _protocol = "ws";
    902  /// Starts the server
    903  /// @port - default 0
    904  ///    when provided, will attempt to listen on that port.
    905  async start(port = 0) {
    906    this.processId = await NodeServer.fork();
    907 
    908    await this.execute(BaseNodeHTTPServerCode);
    909    await this.execute(NodeWebSocketServerCode);
    910    await this.execute(NodeWebSocketPlainServerCode);
    911    await this.execute(ADB);
    912    this._port = await this.execute(
    913      `NodeWebSocketPlainServerCode.startServer(${port})`
    914    );
    915    await this.execute(`global.path_handlers = {};`);
    916    await this.execute(`global.wsInputHandler = null;`);
    917  }
    918 
    919  async registerMessageHandler(handler) {
    920    return this.execute(`global.wsInputHandler = ${handler.toString()}`);
    921  }
    922 }
    923 
    924 // websocket http2 server
    925 // This code is inspired by
    926 // https://github.com/szmarczak/http2-wrapper/blob/master/examples/ws/server.js
    927 class NodeWebSocketHttp2ServerCode extends BaseNodeHTTPServerCode {
    928  static async startServer(port, fallbackToH1) {
    929    const fs = require("fs");
    930    const options = {
    931      key: fs.readFileSync(__dirname + "/http2-cert.key"),
    932      cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
    933      settings: {
    934        enableConnectProtocol: !fallbackToH1,
    935        allowHTTP1: fallbackToH1,
    936      },
    937    };
    938    const http2 = require("http2");
    939    global.h2Server = http2.createSecureServer(options);
    940 
    941    let node_ws_root = `${__dirname}/../node-ws`;
    942    const WS = require(`${node_ws_root}/lib/websocket`);
    943 
    944    global.h2Server.on("stream", (stream, headers) => {
    945      if (headers[":method"] === "CONNECT") {
    946        try {
    947          stream.respond();
    948        } catch (e) {
    949          if (
    950            e.code !== "ERR_HTTP2_INVALID_STREAM" &&
    951            !e.message.includes("The stream has been destroyed")
    952          ) {
    953            throw e;
    954          }
    955        }
    956 
    957        const ws = new WS(null);
    958        stream.setNoDelay = () => {};
    959        ws.setSocket(stream, Buffer.from(""), 100 * 1024 * 1024);
    960 
    961        ws.on("message", data => {
    962          if (global.wsInputHandler) {
    963            global.wsInputHandler(data, ws);
    964            return;
    965          }
    966 
    967          ws.send("test");
    968        });
    969      } else {
    970        try {
    971          stream.respond();
    972        } catch (e) {
    973          if (
    974            e.code !== "ERR_HTTP2_INVALID_STREAM" &&
    975            !e.message.includes("The stream has been destroyed")
    976          ) {
    977            throw e;
    978          }
    979        }
    980        stream.end("ok");
    981      }
    982    });
    983 
    984    let serverPort = await ADB.listenAndForwardPort(global.h2Server, port);
    985    return serverPort;
    986  }
    987 }
    988 
    989 export class NodeWebSocketHttp2Server extends BaseNodeServer {
    990  _protocol = "wss";
    991  /// Starts the server
    992  /// @port - default 0
    993  ///    when provided, will attempt to listen on that port.
    994  async start(port = 0, fallbackToH1 = false) {
    995    if (!this._skipCert) {
    996      await BaseNodeServer.installCert("http2-ca.pem");
    997    }
    998    this.processId = await NodeServer.fork();
    999 
   1000    await this.execute(BaseNodeHTTPServerCode);
   1001    await this.execute(NodeWebSocketHttp2ServerCode);
   1002    await this.execute(ADB);
   1003    this._port = await this.execute(
   1004      `NodeWebSocketHttp2ServerCode.startServer(${port}, ${fallbackToH1})`
   1005    );
   1006    await this.execute(`global.path_handlers = {};`);
   1007    await this.execute(`global.wsInputHandler = null;`);
   1008  }
   1009 
   1010  async registerMessageHandler(handler) {
   1011    return this.execute(`global.wsInputHandler = ${handler.toString()}`);
   1012  }
   1013 }
   1014 
   1015 // Helper functions
   1016 
   1017 export async function with_node_servers(arrayOfClasses, asyncClosure) {
   1018  for (let s of arrayOfClasses) {
   1019    let server = new s();
   1020    await server.start();
   1021    await asyncClosure(server);
   1022    await server.stop();
   1023  }
   1024 }
   1025 
   1026 export class WebSocketConnection {
   1027  constructor() {
   1028    this._openPromise = new Promise(resolve => {
   1029      this._openCallback = resolve;
   1030    });
   1031 
   1032    this._stopPromise = new Promise(resolve => {
   1033      this._stopCallback = resolve;
   1034    });
   1035 
   1036    this._msgPromise = new Promise(resolve => {
   1037      this._msgCallback = resolve;
   1038    });
   1039 
   1040    this._proxyAvailablePromise = new Promise(resolve => {
   1041      this._proxyAvailCallback = resolve;
   1042    });
   1043 
   1044    this._messages = [];
   1045    this._ws = null;
   1046  }
   1047 
   1048  get QueryInterface() {
   1049    return ChromeUtils.generateQI([
   1050      "nsIWebSocketListener",
   1051      "nsIProtocolProxyCallback",
   1052    ]);
   1053  }
   1054 
   1055  onAcknowledge() {}
   1056  onBinaryMessageAvailable(aContext, aMsg) {
   1057    this._messages.push(aMsg);
   1058    this._msgCallback();
   1059  }
   1060  onMessageAvailable() {}
   1061  onServerClose() {}
   1062  onWebSocketListenerStart() {}
   1063  onStart() {
   1064    this._openCallback();
   1065  }
   1066  onStop(aContext, aStatusCode) {
   1067    this._stopCallback({ status: aStatusCode });
   1068    this._ws = null;
   1069  }
   1070  onProxyAvailable(req, chan, proxyInfo) {
   1071    if (proxyInfo) {
   1072      this._proxyAvailCallback({ type: proxyInfo.type });
   1073    } else {
   1074      this._proxyAvailCallback({});
   1075    }
   1076  }
   1077 
   1078  static makeWebSocketChan(url) {
   1079    let protocol = url.startsWith("wss:") ? "wss" : "ws";
   1080    let chan = Cc[
   1081      `@mozilla.org/network/protocol;1?name=${protocol}`
   1082    ].createInstance(Ci.nsIWebSocketChannel);
   1083    chan.initLoadInfo(
   1084      null, // aLoadingNode
   1085      Services.scriptSecurityManager.getSystemPrincipal(),
   1086      null, // aTriggeringPrincipal
   1087      Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
   1088      Ci.nsIContentPolicy.TYPE_WEBSOCKET
   1089    );
   1090    return chan;
   1091  }
   1092  // Returns a promise that resolves when the websocket channel is opened.
   1093  open(url) {
   1094    this._ws = WebSocketConnection.makeWebSocketChan(url);
   1095    let uri = Services.io.newURI(url);
   1096    this._ws.asyncOpen(uri, url, {}, 0, this, null);
   1097    return this._openPromise;
   1098  }
   1099  // Closes the inner websocket. code and reason arguments are optional.
   1100  close(code, reason) {
   1101    this._ws.close(code || Ci.nsIWebSocketChannel.CLOSE_NORMAL, reason || "");
   1102  }
   1103  // Sends a message to the server.
   1104  send(msg) {
   1105    this._ws.sendMsg(msg);
   1106  }
   1107  // Returns a promise that resolves when the channel's onStop is called.
   1108  // Promise resolves with an `{status}` object, where status is the
   1109  // result passed to onStop.
   1110  finished() {
   1111    return this._stopPromise;
   1112  }
   1113  getProxyInfo() {
   1114    return this._proxyAvailablePromise;
   1115  }
   1116 
   1117  // Returned promise resolves with an array of received messages
   1118  // If messages have been received in the the past before calling
   1119  // receiveMessages, the promise will immediately resolve. Otherwise
   1120  // it will resolve when the first message is received.
   1121  async receiveMessages() {
   1122    await this._msgPromise;
   1123    this._msgPromise = new Promise(resolve => {
   1124      this._msgCallback = resolve;
   1125    });
   1126    let messages = this._messages;
   1127    this._messages = [];
   1128    return messages;
   1129  }
   1130 }
   1131 
   1132 export class HTTP3Server {
   1133  protocol() {
   1134    return "https";
   1135  }
   1136  version() {
   1137    return "h3";
   1138  }
   1139  origin() {
   1140    return `${this.protocol()}://localhost:${this.port()}`;
   1141  }
   1142  port() {
   1143    return this._port;
   1144  }
   1145  masque_proxy_port() {
   1146    return this._masque_proxy_port;
   1147  }
   1148  no_response_port() {
   1149    return this._no_response_port;
   1150  }
   1151  domain() {
   1152    return `localhost`;
   1153  }
   1154 
   1155  /// Stops the server
   1156  async stop() {
   1157    if (this.processId) {
   1158      await NodeServer.kill(this.processId);
   1159      this.processId = undefined;
   1160    }
   1161  }
   1162 
   1163  async start(path, dbPath) {
   1164    let result = await NodeServer.sendCommand(
   1165      "",
   1166      `/forkH3Server?path=${path}&dbPath=${dbPath}`
   1167    );
   1168    this.processId = result.id;
   1169 
   1170    /* eslint-disable no-control-regex */
   1171    const regex =
   1172      /HTTP3 server listening on ports (\d+), (\d+), (\d+), (\d+), (\d+) and (\d+). EchConfig is @([\x00-\x7F]+)@/;
   1173 
   1174    // Execute the regex on the input string
   1175    let match = regex.exec(result.output);
   1176 
   1177    if (match) {
   1178      // Extract the ports as an array of numbers
   1179      let ports = match.slice(1, 7).map(Number);
   1180      this._port = ports[0];
   1181      this._no_response_port = ports[4];
   1182      this._masque_proxy_port = ports[5];
   1183      return ports[0];
   1184    }
   1185 
   1186    return undefined;
   1187  }
   1188 }
   1189 
   1190 export class NodeServer {
   1191  // Executes command in the context of a node server.
   1192  // See handler in moz-http2.js
   1193  //
   1194  // Example use:
   1195  // let id = NodeServer.fork(); // id is a random string
   1196  // await NodeServer.execute(id, `"hello"`)
   1197  // > "hello"
   1198  // await NodeServer.execute(id, `(() => "hello")()`)
   1199  // > "hello"
   1200  // await NodeServer.execute(id, `(() => var_defined_on_server)()`)
   1201  // > "0"
   1202  // await NodeServer.execute(id, `var_defined_on_server`)
   1203  // > "0"
   1204  // function f(param) { if (param) return param; return "bla"; }
   1205  // await NodeServer.execute(id, f); // Defines the function on the server
   1206  // await NodeServer.execute(id, `f()`) // executes defined function
   1207  // > "bla"
   1208  // let result = await NodeServer.execute(id, `f("test")`);
   1209  // > "test"
   1210  // await NodeServer.kill(id); // shuts down the server
   1211 
   1212  // Forks a new node server using moz-http2-child.js as a starting point
   1213  static fork() {
   1214    return this.sendCommand("", "/fork");
   1215  }
   1216  // Executes command in the context of the node server indicated by `id`
   1217  static execute(id, command) {
   1218    return this.sendCommand(command, `/execute/${id}`);
   1219  }
   1220  // Shuts down the server
   1221  static kill(id) {
   1222    return this.sendCommand("", `/kill/${id}`);
   1223  }
   1224 
   1225  // Issues a request to the node server (handler defined in moz-http2.js)
   1226  // This method should not be called directly.
   1227  static sendCommand(command, path) {
   1228    let h2Port = Services.env.get("MOZNODE_EXEC_PORT");
   1229    if (!h2Port) {
   1230      throw new Error("Could not find MOZNODE_EXEC_PORT");
   1231    }
   1232 
   1233    let req = new XMLHttpRequest({ mozAnon: true, mozSystem: true });
   1234    const serverIP =
   1235      AppConstants.platform == "android" ? "10.0.2.2" : "127.0.0.1";
   1236    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
   1237    req.open("POST", `http://${serverIP}:${h2Port}${path}`);
   1238    req.channel.QueryInterface(Ci.nsIHttpChannelInternal).bypassProxy = true;
   1239    req.channel.loadFlags |= Ci.nsIChannel.LOAD_BYPASS_URL_CLASSIFIER;
   1240    // Prevent HTTPS-Only Mode from upgrading the request.
   1241    req.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT;
   1242    // Allow deprecated HTTP request from SystemPrincipal
   1243    req.channel.loadInfo.allowDeprecatedSystemRequests = true;
   1244 
   1245    // Passing a function to NodeServer.execute will define that function
   1246    // in node. It can be called in a later execute command.
   1247    let isFunction = function (obj) {
   1248      return !!(obj && obj.constructor && obj.call && obj.apply);
   1249    };
   1250    let payload = command;
   1251    if (isFunction(command)) {
   1252      payload = `${command.name} = ${command.toString()};`;
   1253    }
   1254 
   1255    return new Promise((resolve, reject) => {
   1256      req.onload = () => {
   1257        let x = null;
   1258 
   1259        if (req.statusText != "OK") {
   1260          reject(`XHR request failed: ${req.statusText}`);
   1261          return;
   1262        }
   1263 
   1264        try {
   1265          x = JSON.parse(req.responseText);
   1266        } catch (e) {
   1267          reject(`Failed to parse ${req.responseText} - ${e}`);
   1268          return;
   1269        }
   1270 
   1271        if (x.error) {
   1272          let e = new Error(x.error, "", 0);
   1273          e.stack = x.errorStack;
   1274          reject(e);
   1275          return;
   1276        }
   1277        resolve(x.result);
   1278      };
   1279      req.onerror = e => {
   1280        reject(e);
   1281      };
   1282 
   1283      req.send(payload.toString());
   1284    });
   1285  }
   1286 }