tor-browser

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

WebSocketHandshake.sys.mjs (8873B)


      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 // This file is an XPCOM service-ified copy of ../devtools/server/socket/websocket-server.js.
      6 
      7 const CC = Components.Constructor;
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
     13  Log: "chrome://remote/content/shared/Log.sys.mjs",
     14  RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
     15 });
     16 
     17 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
     18 
     19 ChromeUtils.defineLazyGetter(lazy, "CryptoHash", () => {
     20  return CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
     21 });
     22 
     23 ChromeUtils.defineLazyGetter(lazy, "threadManager", () => {
     24  return Cc["@mozilla.org/thread-manager;1"].getService();
     25 });
     26 
     27 /**
     28 * Allowed origins are exposed through 2 separate getters because while most
     29 * of the values should be valid URIs, `null` is also a valid origin and cannot
     30 * be converted to a URI. Call sites interested in checking for null should use
     31 * `allowedOrigins`, those interested in URIs should use `allowedOriginURIs`.
     32 */
     33 ChromeUtils.defineLazyGetter(lazy, "allowedOrigins", () =>
     34  lazy.RemoteAgent.allowOrigins !== null ? lazy.RemoteAgent.allowOrigins : []
     35 );
     36 
     37 ChromeUtils.defineLazyGetter(lazy, "allowedOriginURIs", () => {
     38  return lazy.allowedOrigins
     39    .map(origin => {
     40      try {
     41        const originURI = Services.io.newURI(origin);
     42        // Make sure to read host/port/scheme as those getters could throw for
     43        // invalid URIs.
     44        return {
     45          host: originURI.host,
     46          port: originURI.port,
     47          scheme: originURI.scheme,
     48        };
     49      } catch (e) {
     50        return null;
     51      }
     52    })
     53    .filter(uri => uri !== null);
     54 });
     55 
     56 /**
     57 * Write a string of bytes to async output stream
     58 * and return promise that resolves once all data has been written.
     59 * Doesn't do any UTF-16/UTF-8 conversion.
     60 * The string is treated as an array of bytes.
     61 */
     62 function writeString(output, data) {
     63  return new Promise((resolve, reject) => {
     64    const wait = () => {
     65      if (data.length === 0) {
     66        resolve();
     67        return;
     68      }
     69 
     70      output.asyncWait(
     71        () => {
     72          try {
     73            const written = output.write(data, data.length);
     74            data = data.slice(written);
     75            wait();
     76          } catch (ex) {
     77            reject(ex);
     78          }
     79        },
     80        0,
     81        0,
     82        lazy.threadManager.currentThread
     83      );
     84    };
     85 
     86    wait();
     87  });
     88 }
     89 
     90 /**
     91 * Write HTTP response with headers (array of strings) and body
     92 * to async output stream.
     93 */
     94 function writeHttpResponse(output, headers, body = "") {
     95  headers.push(`Content-Length: ${body.length}`);
     96 
     97  const s = headers.join("\r\n") + `\r\n\r\n${body}`;
     98  return writeString(output, s);
     99 }
    100 
    101 /**
    102 * Check if the provided URI's host is an IP address.
    103 *
    104 * @param {nsIURI} uri
    105 *     The URI to check.
    106 * @returns {boolean}
    107 */
    108 function isIPAddress(uri) {
    109  try {
    110    // getBaseDomain throws an explicit error if the uri host is an IP address.
    111    Services.eTLD.getBaseDomain(uri);
    112  } catch (e) {
    113    return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
    114  }
    115  return false;
    116 }
    117 
    118 function isHostValid(hostHeader) {
    119  try {
    120    // Might throw both when calling newURI or when accessing the host/port.
    121    const hostUri = Services.io.newURI(`https://${hostHeader}`);
    122    const { host, port } = hostUri;
    123    const isHostnameValid =
    124      isIPAddress(hostUri) || lazy.RemoteAgent.allowHosts.includes(host);
    125    // For nsIURI a port value of -1 corresponds to the protocol's default port.
    126    const isPortValid = [-1, lazy.RemoteAgent.port].includes(port);
    127    return isHostnameValid && isPortValid;
    128  } catch (e) {
    129    return false;
    130  }
    131 }
    132 
    133 function isOriginValid(originHeader) {
    134  if (originHeader === undefined) {
    135    // Always accept no origin header.
    136    return true;
    137  }
    138 
    139  // Special case "null" origins, used for privacy sensitive or opaque origins.
    140  if (originHeader === "null") {
    141    return lazy.allowedOrigins.includes("null");
    142  }
    143 
    144  try {
    145    // Extract the host, port and scheme from the provided origin header.
    146    const { host, port, scheme } = Services.io.newURI(originHeader);
    147    // Check if any allowed origin matches the provided host, port and scheme.
    148    return lazy.allowedOriginURIs.some(
    149      uri => uri.host === host && uri.port === port && uri.scheme === scheme
    150    );
    151  } catch (e) {
    152    // Reject invalid origin headers
    153    return false;
    154  }
    155 }
    156 
    157 /**
    158 * Process the WebSocket handshake headers and return the key to be sent in
    159 * Sec-WebSocket-Accept response header.
    160 */
    161 function processRequest({ requestLine, headers }) {
    162  if (!isOriginValid(headers.get("origin"))) {
    163    lazy.logger.debug(
    164      `Incorrect Origin header, allowed origins: [${lazy.allowedOrigins}]`
    165    );
    166    throw new Error(
    167      `The handshake request has incorrect Origin header ${headers.get(
    168        "origin"
    169      )}`
    170    );
    171  }
    172 
    173  if (!isHostValid(headers.get("host"))) {
    174    lazy.logger.debug(
    175      `Incorrect Host header, allowed hosts: [${lazy.RemoteAgent.allowHosts}]`
    176    );
    177    throw new Error(
    178      `The handshake request has incorrect Host header ${headers.get("host")}`
    179    );
    180  }
    181 
    182  const method = requestLine.split(" ")[0];
    183  if (method !== "GET") {
    184    throw new Error("The handshake request must use GET method");
    185  }
    186 
    187  const upgrade = headers.get("upgrade");
    188  if (!upgrade || upgrade.toLowerCase() !== "websocket") {
    189    throw new Error(
    190      `The handshake request has incorrect Upgrade header: ${upgrade}`
    191    );
    192  }
    193 
    194  const connection = headers.get("connection");
    195  if (
    196    !connection ||
    197    !connection
    198      .split(",")
    199      .map(t => t.trim().toLowerCase())
    200      .includes("upgrade")
    201  ) {
    202    throw new Error("The handshake request has incorrect Connection header");
    203  }
    204 
    205  const version = headers.get("sec-websocket-version");
    206  if (!version || version !== "13") {
    207    throw new Error(
    208      "The handshake request must have Sec-WebSocket-Version: 13"
    209    );
    210  }
    211 
    212  // Compute the accept key
    213  const key = headers.get("sec-websocket-key");
    214  if (!key) {
    215    throw new Error(
    216      "The handshake request must have a Sec-WebSocket-Key header"
    217    );
    218  }
    219 
    220  return { acceptKey: computeKey(key) };
    221 }
    222 
    223 function computeKey(key) {
    224  const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
    225  const data = Array.from(str, ch => ch.charCodeAt(0));
    226  const hash = new lazy.CryptoHash("sha1");
    227  hash.update(data, data.length);
    228  return hash.finish(true);
    229 }
    230 
    231 /**
    232 * Perform the server part of a WebSocket opening handshake
    233 * on an incoming connection.
    234 */
    235 async function serverHandshake(request, output) {
    236  try {
    237    // Check and extract info from the request
    238    const { acceptKey } = processRequest(request);
    239 
    240    // Send response headers
    241    await writeHttpResponse(output, [
    242      "HTTP/1.1 101 Switching Protocols",
    243      "Server: httpd.js",
    244      "Upgrade: websocket",
    245      "Connection: Upgrade",
    246      `Sec-WebSocket-Accept: ${acceptKey}`,
    247    ]);
    248  } catch (error) {
    249    // Send error response in case of error
    250    await writeHttpResponse(
    251      output,
    252      [
    253        "HTTP/1.1 400 Bad Request",
    254        "Server: httpd.js",
    255        "Content-Type: text/plain",
    256      ],
    257      error.message
    258    );
    259 
    260    throw error;
    261  }
    262 }
    263 
    264 async function createWebSocket(transport, input, output) {
    265  const transportProvider = {
    266    setListener(upgradeListener) {
    267      // onTransportAvailable callback shouldn't be called synchronously
    268      lazy.executeSoon(() => {
    269        upgradeListener.onTransportAvailable(transport, input, output);
    270      });
    271    },
    272  };
    273 
    274  return new Promise((resolve, reject) => {
    275    const socket = WebSocket.createServerWebSocket(
    276      null,
    277      [],
    278      transportProvider,
    279      ""
    280    );
    281    socket.addEventListener("close", () => {
    282      input.close();
    283      output.close();
    284    });
    285 
    286    socket.onopen = () => resolve(socket);
    287    socket.onerror = err => reject(err);
    288  });
    289 }
    290 
    291 /** Upgrade an existing HTTP request from httpd.js to WebSocket. */
    292 async function upgrade(request, response) {
    293  // handle response manually, allowing us to send arbitrary data
    294  response._powerSeized = true;
    295 
    296  const { transport, input, output } = response._connection;
    297 
    298  lazy.logger.info(
    299    `Perform WebSocket upgrade for incoming connection from ${transport.host}:${transport.port}`
    300  );
    301 
    302  const headers = new Map();
    303  for (let [key, values] of Object.entries(request._headers._headers)) {
    304    headers.set(key, values.join("\n"));
    305  }
    306  const convertedRequest = {
    307    requestLine: `${request.method} ${request.path}`,
    308    headers,
    309  };
    310  await serverHandshake(convertedRequest, output);
    311 
    312  return createWebSocket(transport, input, output);
    313 }
    314 
    315 export const WebSocketHandshake = { upgrade };