tor-browser

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

websocket-server.js (6553B)


      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 "use strict";
      6 
      7 const { executeSoon } = require("resource://devtools/shared/DevToolsUtils.js");
      8 const {
      9  delimitedRead,
     10 } = require("resource://devtools/shared/transport/stream-utils.js");
     11 const CryptoHash = Components.Constructor(
     12  "@mozilla.org/security/hash;1",
     13  "nsICryptoHash",
     14  "initWithString"
     15 );
     16 const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
     17 
     18 // Limit the header size to put an upper bound on allocated memory
     19 const HEADER_MAX_LEN = 8000;
     20 
     21 /**
     22 * Read a line from async input stream and return promise that resolves to the line once
     23 * it has been read. If the line is longer than HEADER_MAX_LEN, will throw error.
     24 */
     25 function readLine(input) {
     26  return new Promise((resolve, reject) => {
     27    let line = "";
     28    const wait = () => {
     29      input.asyncWait(
     30        () => {
     31          try {
     32            const amountToRead = HEADER_MAX_LEN - line.length;
     33            line += delimitedRead(input, "\n", amountToRead);
     34 
     35            if (line.endsWith("\n")) {
     36              resolve(line.trimRight());
     37              return;
     38            }
     39 
     40            if (line.length >= HEADER_MAX_LEN) {
     41              throw new Error(
     42                `Failed to read HTTP header longer than ${HEADER_MAX_LEN} bytes`
     43              );
     44            }
     45 
     46            wait();
     47          } catch (ex) {
     48            reject(ex);
     49          }
     50        },
     51        0,
     52        0,
     53        threadManager.currentThread
     54      );
     55    };
     56 
     57    wait();
     58  });
     59 }
     60 
     61 /**
     62 * Write a string of bytes to async output stream and return promise that resolves once
     63 * all data has been written. Doesn't do any utf-16/utf-8 conversion - the string is
     64 * treated as an array of bytes.
     65 */
     66 function writeString(output, data) {
     67  return new Promise((resolve, reject) => {
     68    const wait = () => {
     69      if (data.length === 0) {
     70        resolve();
     71        return;
     72      }
     73 
     74      output.asyncWait(
     75        () => {
     76          try {
     77            const written = output.write(data, data.length);
     78            data = data.slice(written);
     79            wait();
     80          } catch (ex) {
     81            reject(ex);
     82          }
     83        },
     84        0,
     85        0,
     86        threadManager.currentThread
     87      );
     88    };
     89 
     90    wait();
     91  });
     92 }
     93 
     94 /**
     95 * Read HTTP request from async input stream.
     96 *
     97 * @return Request line (string) and Map of header names and values.
     98 */
     99 const readHttpRequest = async function (input) {
    100  let requestLine = "";
    101  const headers = new Map();
    102 
    103  while (true) {
    104    const line = await readLine(input);
    105    if (!line.length) {
    106      break;
    107    }
    108 
    109    if (!requestLine) {
    110      requestLine = line;
    111    } else {
    112      const colon = line.indexOf(":");
    113      if (colon == -1) {
    114        throw new Error(`Malformed HTTP header: ${line}`);
    115      }
    116 
    117      const name = line.slice(0, colon).toLowerCase();
    118      const value = line.slice(colon + 1).trim();
    119      headers.set(name, value);
    120    }
    121  }
    122 
    123  return { requestLine, headers };
    124 };
    125 
    126 /**
    127 * Write HTTP response (array of strings) to async output stream.
    128 */
    129 function writeHttpResponse(output, response) {
    130  const responseString = response.join("\r\n") + "\r\n\r\n";
    131  return writeString(output, responseString);
    132 }
    133 
    134 /**
    135 * Process the WebSocket handshake headers and return the key to be sent in
    136 * Sec-WebSocket-Accept response header.
    137 */
    138 function processRequest({ requestLine, headers }) {
    139  const [method, path] = requestLine.split(" ");
    140  if (method !== "GET") {
    141    throw new Error("The handshake request must use GET method");
    142  }
    143 
    144  if (path !== "/") {
    145    throw new Error("The handshake request has unknown path");
    146  }
    147 
    148  const upgrade = headers.get("upgrade");
    149  if (!upgrade || upgrade !== "websocket") {
    150    throw new Error("The handshake request has incorrect Upgrade header");
    151  }
    152 
    153  const connection = headers.get("connection");
    154  if (
    155    !connection ||
    156    !connection
    157      .split(",")
    158      .map(t => t.trim())
    159      .includes("Upgrade")
    160  ) {
    161    throw new Error("The handshake request has incorrect Connection header");
    162  }
    163 
    164  const version = headers.get("sec-websocket-version");
    165  if (!version || version !== "13") {
    166    throw new Error(
    167      "The handshake request must have Sec-WebSocket-Version: 13"
    168    );
    169  }
    170 
    171  // Compute the accept key
    172  const key = headers.get("sec-websocket-key");
    173  if (!key) {
    174    throw new Error(
    175      "The handshake request must have a Sec-WebSocket-Key header"
    176    );
    177  }
    178 
    179  return { acceptKey: computeKey(key) };
    180 }
    181 
    182 function computeKey(key) {
    183  const str = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    184 
    185  const data = Array.from(str, ch => ch.charCodeAt(0));
    186  const hash = new CryptoHash("sha1");
    187  hash.update(data, data.length);
    188  return hash.finish(true);
    189 }
    190 
    191 /**
    192 * Perform the server part of a WebSocket opening handshake on an incoming connection.
    193 */
    194 const serverHandshake = async function (input, output) {
    195  // Read the request
    196  const request = await readHttpRequest(input);
    197 
    198  try {
    199    // Check and extract info from the request
    200    const { acceptKey } = processRequest(request);
    201 
    202    // Send response headers
    203    await writeHttpResponse(output, [
    204      "HTTP/1.1 101 Switching Protocols",
    205      "Upgrade: websocket",
    206      "Connection: Upgrade",
    207      `Sec-WebSocket-Accept: ${acceptKey}`,
    208    ]);
    209  } catch (error) {
    210    // Send error response in case of error
    211    await writeHttpResponse(output, ["HTTP/1.1 400 Bad Request"]);
    212    throw error;
    213  }
    214 };
    215 
    216 /**
    217 * Accept an incoming WebSocket server connection.
    218 * Takes an established nsISocketTransport in the parameters.
    219 * Performs the WebSocket handshake and waits for the WebSocket to open.
    220 * Returns Promise with a WebSocket ready to send and receive messages.
    221 */
    222 const accept = async function (transport, input, output) {
    223  await serverHandshake(input, output);
    224 
    225  const transportProvider = {
    226    setListener(upgradeListener) {
    227      // The onTransportAvailable callback shouldn't be called synchronously.
    228      executeSoon(() => {
    229        upgradeListener.onTransportAvailable(transport, input, output);
    230      });
    231    },
    232  };
    233 
    234  return new Promise((resolve, reject) => {
    235    const socket = WebSocket.createServerWebSocket(
    236      null,
    237      [],
    238      transportProvider,
    239      ""
    240    );
    241    socket.addEventListener("close", () => {
    242      input.close();
    243      output.close();
    244    });
    245 
    246    socket.onopen = () => resolve(socket);
    247    socket.onerror = err => reject(err);
    248  });
    249 };
    250 
    251 exports.accept = accept;