tor-browser

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

head_channels.js (17537B)


      1 /**
      2 * Read count bytes from stream and return as a String object
      3 */
      4 
      5 /* import-globals-from head_cache.js */
      6 /* import-globals-from head_cookies.js */
      7 
      8 function read_stream(stream, count) {
      9  /* assume stream has non-ASCII data */
     10  var wrapper = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
     11    Ci.nsIBinaryInputStream
     12  );
     13  wrapper.setInputStream(stream);
     14  /* JS methods can be called with a maximum of 65535 arguments, and input
     15     streams don't have to return all the data they make .available() when
     16     asked to .read() that number of bytes. */
     17  var data = [];
     18  while (count > 0) {
     19    var bytes = wrapper.readByteArray(Math.min(65535, count));
     20    data.push(String.fromCharCode.apply(null, bytes));
     21    count -= bytes.length;
     22    if (!bytes.length) {
     23      do_throw("Nothing read from input stream!");
     24    }
     25  }
     26  return data.join("");
     27 }
     28 
     29 // CL_ stands for ChannelListener
     30 const CL_EXPECT_FAILURE = 0x1;
     31 const CL_EXPECT_GZIP = 0x2;
     32 const CL_EXPECT_3S_DELAY = 0x4;
     33 const CL_SUSPEND = 0x8;
     34 const CL_ALLOW_UNKNOWN_CL = 0x10; // Response can contain no or invalid content-length header
     35 const CL_EXPECT_LATE_FAILURE = 0x20;
     36 const CL_FROM_CACHE = 0x40; // Response must be from the cache
     37 const CL_NOT_FROM_CACHE = 0x80; // Response must NOT be from the cache
     38 const CL_IGNORE_CL = 0x100; // don't bother to verify the content-length
     39 const CL_IGNORE_DELAYS = 0x200; // don't throw if channel returns after a long delay
     40 
     41 const SUSPEND_DELAY = 3000;
     42 
     43 /**
     44 * A stream listener that calls a callback function with a specified
     45 * context and the received data when the channel is loaded.
     46 *
     47 * Signature of the closure:
     48 *   void closure(in nsIRequest request, in ACString data, in JSObject context);
     49 *
     50 * This listener makes sure that various parts of the channel API are
     51 * implemented correctly and that the channel's status is a success code
     52 * (you can pass CL_EXPECT_FAILURE or CL_EXPECT_LATE_FAILURE as flags
     53 * to allow a failure code)
     54 *
     55 * Note that it also requires a valid content length on the channel and
     56 * is thus not fully generic.
     57 */
     58 function ChannelListener(closure, ctx, flags) {
     59  this._closure = closure;
     60  this._closurectx = ctx;
     61  this._flags = flags;
     62  this._isFromCache = false;
     63  this._hasCacheEntry = false;
     64  this._cacheEntryId = undefined;
     65 }
     66 ChannelListener.prototype = {
     67  _closure: null,
     68  _closurectx: null,
     69  _buffer: "",
     70  _got_onstartrequest: false,
     71  _got_onstoprequest: false,
     72  _contentLen: -1,
     73  _lastEvent: 0,
     74 
     75  QueryInterface: ChromeUtils.generateQI([
     76    "nsIStreamListener",
     77    "nsIRequestObserver",
     78  ]),
     79 
     80  onStartRequest(request) {
     81    try {
     82      if (this._got_onstartrequest) {
     83        do_throw("Got second onStartRequest event!");
     84      }
     85      this._got_onstartrequest = true;
     86      this._lastEvent = Date.now();
     87 
     88      try {
     89        this._isFromCache = request
     90          .QueryInterface(Ci.nsICacheInfoChannel)
     91          .isFromCache();
     92      } catch (e) {}
     93 
     94      try {
     95        this._hasCacheEntry = request
     96          .QueryInterface(Ci.nsICacheInfoChannel)
     97          .hasCacheEntry();
     98      } catch (e) {}
     99 
    100      var thrown = false;
    101      try {
    102        this._cacheEntryId = request
    103          .QueryInterface(Ci.nsICacheInfoChannel)
    104          .getCacheEntryId();
    105      } catch (e) {
    106        thrown = true;
    107      }
    108      if (this._hasCacheEntry && thrown) {
    109        do_throw("Should get a CacheEntryId");
    110      } else if (!this._hasCacheEntry && !thrown) {
    111        do_throw("Shouldn't get a CacheEntryId");
    112      }
    113 
    114      request.QueryInterface(Ci.nsIChannel);
    115      try {
    116        this._contentLen = request.contentLength;
    117      } catch (ex) {
    118        if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))) {
    119          do_throw("Could not get contentLength");
    120        }
    121      }
    122      if (!request.isPending()) {
    123        do_throw("request reports itself as not pending from onStartRequest!");
    124      }
    125      if (
    126        this._contentLen == -1 &&
    127        !(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))
    128      ) {
    129        do_throw("Content length is unknown in onStartRequest!");
    130      }
    131 
    132      if (this._flags & CL_FROM_CACHE) {
    133        request.QueryInterface(Ci.nsICachingChannel);
    134        if (!request.isFromCache()) {
    135          do_throw("Response is not from the cache (CL_FROM_CACHE)");
    136        }
    137      }
    138      if (this._flags & CL_NOT_FROM_CACHE) {
    139        request.QueryInterface(Ci.nsICachingChannel);
    140        if (request.isFromCache()) {
    141          do_throw("Response is from the cache (CL_NOT_FROM_CACHE)");
    142        }
    143      }
    144 
    145      if (this._flags & CL_SUSPEND) {
    146        request.suspend();
    147        do_timeout(SUSPEND_DELAY, function () {
    148          request.resume();
    149        });
    150      }
    151    } catch (ex) {
    152      do_throw("Error in onStartRequest: " + ex);
    153    }
    154  },
    155 
    156  onDataAvailable(request, stream, offset, count) {
    157    try {
    158      let current = Date.now();
    159 
    160      if (!this._got_onstartrequest) {
    161        do_throw("onDataAvailable without onStartRequest event!");
    162      }
    163      if (this._got_onstoprequest) {
    164        do_throw("onDataAvailable after onStopRequest event!");
    165      }
    166      if (!request.isPending()) {
    167        do_throw("request reports itself as not pending from onDataAvailable!");
    168      }
    169      if (this._flags & CL_EXPECT_FAILURE) {
    170        do_throw("Got data despite expecting a failure");
    171      }
    172 
    173      if (
    174        !(this._flags & CL_IGNORE_DELAYS) &&
    175        current - this._lastEvent >= SUSPEND_DELAY &&
    176        !(this._flags & CL_EXPECT_3S_DELAY)
    177      ) {
    178        do_throw("Data received after significant unexpected delay");
    179      } else if (
    180        current - this._lastEvent < SUSPEND_DELAY &&
    181        this._flags & CL_EXPECT_3S_DELAY
    182      ) {
    183        do_throw("Data received sooner than expected");
    184      } else if (
    185        current - this._lastEvent >= SUSPEND_DELAY &&
    186        this._flags & CL_EXPECT_3S_DELAY
    187      ) {
    188        this._flags &= ~CL_EXPECT_3S_DELAY;
    189      } // No more delays expected
    190 
    191      this._buffer = this._buffer.concat(read_stream(stream, count));
    192      this._lastEvent = current;
    193    } catch (ex) {
    194      do_throw("Error in onDataAvailable: " + ex);
    195    }
    196  },
    197 
    198  onStopRequest(request, status) {
    199    try {
    200      var success = Components.isSuccessCode(status);
    201      if (!this._got_onstartrequest) {
    202        do_throw("onStopRequest without onStartRequest event!");
    203      }
    204      if (this._got_onstoprequest) {
    205        do_throw("Got second onStopRequest event!");
    206      }
    207      this._got_onstoprequest = true;
    208      if (
    209        this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE) &&
    210        success
    211      ) {
    212        do_throw(
    213          "Should have failed to load URL (status is " +
    214            status.toString(16) +
    215            ")"
    216        );
    217      } else if (
    218        !(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) &&
    219        !success
    220      ) {
    221        do_throw("Failed to load URL: " + status.toString(16));
    222      }
    223      if (status != request.status) {
    224        do_throw("request.status does not match status arg to onStopRequest!");
    225      }
    226      if (request.isPending()) {
    227        do_throw("request reports itself as pending from onStopRequest!");
    228      }
    229      if (
    230        !(
    231          this._flags &
    232          (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL)
    233        ) &&
    234        !(this._flags & CL_EXPECT_GZIP) &&
    235        this._contentLen != -1
    236      ) {
    237        Assert.equal(this._buffer.length, this._contentLen);
    238      }
    239    } catch (ex) {
    240      do_throw("Error in onStopRequest: " + ex);
    241    }
    242    try {
    243      this._closure(
    244        request,
    245        this._buffer,
    246        this._closurectx,
    247        this._isFromCache,
    248        this._cacheEntryId
    249      );
    250      this._closurectx = null;
    251    } catch (ex) {
    252      do_throw("Error in closure function: " + ex);
    253    }
    254  },
    255 };
    256 
    257 var ES_ABORT_REDIRECT = 0x01;
    258 
    259 function ChannelEventSink(flags) {
    260  this._flags = flags;
    261 }
    262 
    263 ChannelEventSink.prototype = {
    264  QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
    265 
    266  getInterface(iid) {
    267    if (iid.equals(Ci.nsIChannelEventSink)) {
    268      return this;
    269    }
    270    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
    271  },
    272 
    273  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
    274    if (this._flags & ES_ABORT_REDIRECT) {
    275      throw Components.Exception("", Cr.NS_BINDING_ABORTED);
    276    }
    277 
    278    callback.onRedirectVerifyCallback(Cr.NS_OK);
    279  },
    280 };
    281 
    282 /**
    283 * A helper class to construct origin attributes.
    284 */
    285 function OriginAttributes(privateId) {
    286  this.privateBrowsingId = privateId;
    287 }
    288 OriginAttributes.prototype = {
    289  privateBrowsingId: 0,
    290 };
    291 
    292 function readFile(file) {
    293  let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
    294    Ci.nsIFileInputStream
    295  );
    296  fstream.init(file, -1, 0, 0);
    297  let data = NetUtil.readInputStreamToString(fstream, fstream.available());
    298  fstream.close();
    299  return data;
    300 }
    301 
    302 function addCertFromFile(certdb, filename, trustString) {
    303  let certFile = do_get_file(filename, false);
    304  let pem = readFile(certFile)
    305    .replace(/-----BEGIN CERTIFICATE-----/, "")
    306    .replace(/-----END CERTIFICATE-----/, "")
    307    .replace(/[\r\n]/g, "");
    308  certdb.addCertFromBase64(pem, trustString);
    309 }
    310 
    311 // Helper code to test nsISerializable
    312 function serialize_to_escaped_string(obj) {
    313  let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
    314    Ci.nsIObjectOutputStream
    315  );
    316  let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
    317  pipe.init(false, false, 0, 0xffffffff, null);
    318  objectOutStream.setOutputStream(pipe.outputStream);
    319  objectOutStream.writeCompoundObject(obj, Ci.nsISupports, true);
    320  objectOutStream.close();
    321 
    322  let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
    323    Ci.nsIObjectInputStream
    324  );
    325  objectInStream.setInputStream(pipe.inputStream);
    326  let data = [];
    327  // This reads all the data from the stream until an error occurs.
    328  while (true) {
    329    try {
    330      let bytes = objectInStream.readByteArray(1);
    331      data.push(String.fromCharCode.apply(null, bytes));
    332    } catch (e) {
    333      break;
    334    }
    335  }
    336  return escape(data.join(""));
    337 }
    338 
    339 function deserialize_from_escaped_string(str) {
    340  let payload = unescape(str);
    341  let data = [];
    342  let i = 0;
    343  while (i < payload.length) {
    344    data.push(payload.charCodeAt(i++));
    345  }
    346 
    347  let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
    348    Ci.nsIObjectOutputStream
    349  );
    350  let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
    351  pipe.init(false, false, 0, 0xffffffff, null);
    352  objectOutStream.setOutputStream(pipe.outputStream);
    353  objectOutStream.writeByteArray(data);
    354  objectOutStream.close();
    355 
    356  let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
    357    Ci.nsIObjectInputStream
    358  );
    359  objectInStream.setInputStream(pipe.inputStream);
    360  return objectInStream.readObject(true);
    361 }
    362 
    363 async function asyncStartTLSTestServer(
    364  serverBinName,
    365  certsPath,
    366  addDefaultRoot = true
    367 ) {
    368  const { HttpServer } = ChromeUtils.importESModule(
    369    "resource://testing-common/httpd.sys.mjs"
    370  );
    371  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    372    Ci.nsIX509CertDB
    373  );
    374  // The trusted CA that is typically used for "good" certificates.
    375  if (addDefaultRoot) {
    376    addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u");
    377  }
    378 
    379  const CALLBACK_PORT = 8444;
    380 
    381  let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile);
    382  Services.env.set("DYLD_LIBRARY_PATH", greBinDir.path);
    383  // TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD"
    384  // does not return this path on Android, so hard code it here.
    385  Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
    386  Services.env.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3");
    387  Services.env.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT);
    388  Services.env.set("MOZ_TLS_ECH_ALPN_FLAG", "1");
    389 
    390  let httpServer = new HttpServer();
    391  let serverReady = new Promise(resolve => {
    392    httpServer.registerPathHandler(
    393      "/",
    394      function handleServerCallback(aRequest, aResponse) {
    395        aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
    396        aResponse.setHeader("Content-Type", "text/plain");
    397        let responseBody = "OK!";
    398        aResponse.bodyOutputStream.write(responseBody, responseBody.length);
    399        executeSoon(function () {
    400          httpServer.stop(resolve);
    401        });
    402      }
    403    );
    404    httpServer.start(CALLBACK_PORT);
    405  });
    406 
    407  let serverBin = _getBinaryUtil(serverBinName);
    408  let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
    409  process.init(serverBin);
    410  let certDir = do_get_file(certsPath, false);
    411  Assert.ok(certDir.exists(), `certificate folder (${certsPath}) should exist`);
    412  // Using "sql:" causes the SQL DB to be used so we can run tests on Android.
    413  process.run(false, ["sql:" + certDir.path, Services.appinfo.processID], 2);
    414 
    415  registerCleanupFunction(function () {
    416    process.kill();
    417  });
    418 
    419  await serverReady;
    420 }
    421 
    422 function _getBinaryUtil(binaryUtilName) {
    423  let utilBin = Services.dirsvc.get("GreD", Ci.nsIFile);
    424  // On macOS, GreD is .../Contents/Resources, and most binary utilities
    425  // are located there, but certutil is in GreBinD (or .../Contents/MacOS),
    426  // so we have to change the path accordingly.
    427  if (binaryUtilName === "certutil") {
    428    utilBin = Services.dirsvc.get("GreBinD", Ci.nsIFile);
    429  }
    430  utilBin.append(binaryUtilName + mozinfo.bin_suffix);
    431  // If we're testing locally, the above works. If not, the server executable
    432  // is in another location.
    433  if (!utilBin.exists()) {
    434    utilBin = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
    435    while (utilBin.path.includes("xpcshell")) {
    436      utilBin = utilBin.parent;
    437    }
    438    utilBin.append("bin");
    439    utilBin.append(binaryUtilName + mozinfo.bin_suffix);
    440  }
    441  // But maybe we're on Android, where binaries are in /data/local/xpcb.
    442  if (!utilBin.exists()) {
    443    utilBin.initWithPath("/data/local/xpcb/");
    444    utilBin.append(binaryUtilName);
    445  }
    446  Assert.ok(utilBin.exists(), `Binary util ${binaryUtilName} should exist`);
    447  return utilBin;
    448 }
    449 
    450 function promiseAsyncOpen(chan) {
    451  return new Promise(resolve => {
    452    chan.asyncOpen(
    453      new ChannelListener((req, buf, ctx, isCache, cacheId) => {
    454        resolve({ req, buf, ctx, isCache, cacheId });
    455      })
    456    );
    457  });
    458 }
    459 
    460 function hexStringToBytes(hex) {
    461  let bytes = [];
    462  for (let hexByteStr of hex.split(/(..)/)) {
    463    if (hexByteStr.length) {
    464      bytes.push(parseInt(hexByteStr, 16));
    465    }
    466  }
    467  return bytes;
    468 }
    469 
    470 function stringToBytes(str) {
    471  return Array.from(str, chr => chr.charCodeAt(0));
    472 }
    473 
    474 function BinaryHttpResponse(status, headerNames, headerValues, content) {
    475  this.status = status;
    476  this.headerNames = headerNames;
    477  this.headerValues = headerValues;
    478  this.content = content;
    479 }
    480 
    481 BinaryHttpResponse.prototype = {
    482  QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]),
    483 };
    484 
    485 function bytesToString(bytes) {
    486  return String.fromCharCode.apply(null, bytes);
    487 }
    488 
    489 function check_http_info(request, expected_httpVersion, expected_proxy) {
    490  let httpVersion = "";
    491  try {
    492    httpVersion = request.QueryInterface(Ci.nsIHttpChannel).protocolVersion;
    493  } catch (e) {}
    494 
    495  request.QueryInterface(Ci.nsIProxiedChannel);
    496  var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
    497 
    498  Assert.equal(expected_httpVersion, httpVersion);
    499  if (expected_proxy) {
    500    Assert.equal(httpProxyConnectResponseCode, 200);
    501  } else {
    502    Assert.equal(httpProxyConnectResponseCode, -1);
    503  }
    504 }
    505 
    506 function makeHTTPChannel(url, with_proxy) {
    507  function createPrincipal(uri) {
    508    var ssm = Services.scriptSecurityManager;
    509    try {
    510      return ssm.createContentPrincipal(Services.io.newURI(uri), {});
    511    } catch (e) {
    512      return null;
    513    }
    514  }
    515 
    516  if (with_proxy) {
    517    return Services.io
    518      .newChannelFromURIWithProxyFlags(
    519        Services.io.newURI(url),
    520        null,
    521        Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL,
    522        null,
    523        createPrincipal(url),
    524        createPrincipal(url),
    525        Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
    526        Ci.nsIContentPolicy.TYPE_OTHER
    527      )
    528      .QueryInterface(Ci.nsIHttpChannel);
    529  }
    530  return NetUtil.newChannel({
    531    uri: url,
    532    loadUsingSystemPrincipal: true,
    533  }).QueryInterface(Ci.nsIHttpChannel);
    534 }
    535 
    536 // Like ChannelListener but does not throw an exception if something
    537 // goes wrong. Callback is supposed to do all the work.
    538 class SimpleChannelListener {
    539  constructor(callback) {
    540    this._onStopCallback = callback;
    541    this._buffer = "";
    542  }
    543  get QueryInterface() {
    544    return ChromeUtils.generateQI(["nsIStreamListener", "nsIRequestObserver"]);
    545  }
    546 
    547  onStartRequest() {}
    548 
    549  onDataAvailable(request, stream, offset, count) {
    550    this._buffer = this._buffer.concat(read_stream(stream, count));
    551  }
    552 
    553  onStopRequest(request) {
    554    if (this._onStopCallback) {
    555      this._onStopCallback(request, this._buffer);
    556    }
    557  }
    558 }
    559 
    560 // nsITLSServerSocket needs a certificate with a corresponding private key
    561 // available. xpcshell tests can import the test file "client-cert.p12" using
    562 // the password "password", resulting in a certificate with the common name
    563 // "Test End-entity" being available with a corresponding private key.
    564 function getTestServerCertificate() {
    565  const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
    566    Ci.nsIX509CertDB
    567  );
    568  const certFile = do_get_file("client-cert.p12");
    569  certDB.importPKCS12File(certFile, "password");
    570  for (const cert of certDB.getCerts()) {
    571    if (cert.commonName == "Test End-entity") {
    572      return cert;
    573    }
    574  }
    575  return null;
    576 }