tor-browser

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

ObliviousHTTP.sys.mjs (7251B)


      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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 ChromeUtils.defineESModuleGetters(lazy, {
      9  HPKEConfigManager: "resource://gre/modules/HPKEConfigManager.sys.mjs",
     10 });
     11 ChromeUtils.defineLazyGetter(lazy, "decoder", () => new TextDecoder());
     12 XPCOMUtils.defineLazyServiceGetters(lazy, {
     13  ohttpService: [
     14    "@mozilla.org/network/oblivious-http-service;1",
     15    Ci.nsIObliviousHttpService,
     16  ],
     17 });
     18 
     19 const BinaryInputStream = Components.Constructor(
     20  "@mozilla.org/binaryinputstream;1",
     21  "nsIBinaryInputStream",
     22  "setInputStream"
     23 );
     24 
     25 const StringInputStream = Components.Constructor(
     26  "@mozilla.org/io/string-input-stream;1",
     27  "nsIStringInputStream",
     28  "setByteStringData"
     29 );
     30 
     31 const ArrayBufferInputStream = Components.Constructor(
     32  "@mozilla.org/io/arraybuffer-input-stream;1",
     33  "nsIArrayBufferInputStream",
     34  "setData"
     35 );
     36 
     37 function readFromStream(stream, count) {
     38  let binaryStream = new BinaryInputStream(stream);
     39  let arrayBuffer = new ArrayBuffer(count);
     40  while (count > 0) {
     41    let actuallyRead = binaryStream.readArrayBuffer(count, arrayBuffer);
     42    if (!actuallyRead) {
     43      throw new Error("Nothing read from input stream!");
     44    }
     45    count -= actuallyRead;
     46  }
     47  return arrayBuffer;
     48 }
     49 
     50 /**
     51 * @typedef {object} OHTTPResponse
     52 *   An object with properties mimicking that of a Response.
     53 * @property {boolean} ok
     54 *   Indicates whether the request was successful.
     55 * @property {number} status
     56 *   Representation of the HTTP status code.
     57 * @property {?Headers} headers
     58 *   Representing the response headers.
     59 * @property {() => ?JSON} json
     60 *   Returns the parsed JSON response body.
     61 * @property {() => ?Blob} blob
     62 *   Returns a Blob response body.
     63 */
     64 
     65 export class ObliviousHTTP {
     66  /**
     67   * Get a cached, or fetch a copy of, an OHTTP config from a given URL.
     68   *
     69   * @param {string} gatewayConfigURL
     70   *   The URL for the config that needs to be fetched.
     71   *   The URL should be complete (i.e. include the full path to the config).
     72   * @returns {Promise<Uint8Array>}
     73   *   The config bytes.
     74   */
     75  static async getOHTTPConfig(gatewayConfigURL) {
     76    return lazy.HPKEConfigManager.get(gatewayConfigURL);
     77  }
     78 
     79  /**
     80   * Make a request over OHTTP.
     81   *
     82   * @param {string} obliviousHTTPRelay
     83   *   The URL of the OHTTP relay to use.
     84   * @param {Uint8Array} config
     85   *   A byte array representing the OHTTP config.
     86   * @param {URL|string} requestURL
     87   *   The URL of the request we want to make over the relay.
     88   * @param {object} options
     89   * @param {string} [options.method]
     90   *   The HTTP method to use for the inner request. Only GET, POST, and PUT are
     91   *   supported right now.
     92   * @param {string|ArrayBuffer} [options.body]
     93   *   The body content to send over the request.
     94   * @param {object} options.headers
     95   *   The request headers to set. Each property of the object represents
     96   *   a header, with the key the header name and the value the header value.
     97   * @param {AbortSignal} options.signal
     98   *   If the consumer passes an AbortSignal object, aborting the signal
     99   *   will abort the request.
    100   * @param {Function} [options.abortCallback]
    101   *   Called if the abort signal is triggered before the request completes
    102   *   fully.
    103   *
    104   * @returns {Promise<OHTTPResponse>}
    105   */
    106  static async ohttpRequest(
    107    obliviousHTTPRelay,
    108    config,
    109    requestURL,
    110    { method = "GET", body, headers, signal, abortCallback }
    111  ) {
    112    let relayURI = Services.io.newURI(obliviousHTTPRelay);
    113    let requestURI = URL.isInstance(requestURL)
    114      ? requestURL.URI
    115      : Services.io.newURI(requestURL);
    116    let obliviousHttpChannel = lazy.ohttpService
    117      .newChannel(relayURI, requestURI, config)
    118      .QueryInterface(Ci.nsIHttpChannel);
    119 
    120    if (method == "POST" || method == "PUT" || method == "DELETE") {
    121      let uploadChannel = obliviousHttpChannel.QueryInterface(
    122        Ci.nsIUploadChannel2
    123      );
    124      let bodyStream;
    125      if (typeof body === "string") {
    126        bodyStream = new StringInputStream(body);
    127      } else if (body instanceof ArrayBuffer) {
    128        bodyStream = new ArrayBufferInputStream(body, 0, body.byteLength);
    129      } else {
    130        throw new Error("ohttpRequest got unexpected body payload type.");
    131      }
    132      uploadChannel.explicitSetUploadStream(
    133        bodyStream,
    134        null,
    135        -1,
    136        method,
    137        false
    138      );
    139    } else if (method != "GET") {
    140      throw new Error(`Unsupported HTTP verb ${method}`);
    141    }
    142 
    143    for (let headerName of Object.keys(headers)) {
    144      obliviousHttpChannel.setRequestHeader(
    145        headerName,
    146        headers[headerName],
    147        false
    148      );
    149    }
    150    let abortHandler = () => {
    151      abortCallback?.();
    152      obliviousHttpChannel.cancel(Cr.NS_BINDING_ABORTED);
    153    };
    154    signal.addEventListener("abort", abortHandler);
    155    return new Promise((resolve, reject) => {
    156      let listener = {
    157        _buffer: [],
    158        _headers: null,
    159        QueryInterface: ChromeUtils.generateQI([
    160          "nsIStreamListener",
    161          "nsIRequestObserver",
    162        ]),
    163        onStartRequest(request) {
    164          this._headers = new Headers();
    165          try {
    166            request
    167              .QueryInterface(Ci.nsIHttpChannel)
    168              .visitResponseHeaders((header, value) => {
    169                this._headers.append(header, value);
    170              });
    171          } catch (error) {
    172            this._headers = null;
    173          }
    174        },
    175        onDataAvailable(request, stream, offset, count) {
    176          this._buffer.push(readFromStream(stream, count));
    177        },
    178        onStopRequest(request, requestStatus) {
    179          signal.removeEventListener("abort", abortHandler);
    180          let result = this._buffer;
    181          try {
    182            let ohttpStatus = request.QueryInterface(Ci.nsIObliviousHttpChannel)
    183              .relayChannel.responseStatus;
    184            if (ohttpStatus == 200) {
    185              let httpStatus = request.QueryInterface(
    186                Ci.nsIHttpChannel
    187              ).responseStatus;
    188              resolve({
    189                ok: requestStatus == Cr.NS_OK && httpStatus == 200,
    190                status: httpStatus,
    191                headers: this._headers,
    192                json() {
    193                  let decodedBuffer = result.reduce((accumulator, currVal) => {
    194                    return accumulator + lazy.decoder.decode(currVal);
    195                  }, "");
    196                  return JSON.parse(decodedBuffer);
    197                },
    198                blob() {
    199                  return new Blob(result, { type: "image/jpeg" });
    200                },
    201              });
    202            } else {
    203              resolve({
    204                ok: false,
    205                status: ohttpStatus,
    206                headers: null,
    207                json() {
    208                  return null;
    209                },
    210                blob() {
    211                  return null;
    212                },
    213              });
    214            }
    215          } catch (error) {
    216            reject(error);
    217          }
    218        },
    219      };
    220      obliviousHttpChannel.asyncOpen(listener);
    221    });
    222  }
    223 }