tor-browser

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

test_resumable_channel.js (14037B)


      1 /* Tests various aspects of nsIResumableChannel in combination with HTTP */
      2 "use strict";
      3 
      4 const { HttpServer } = ChromeUtils.importESModule(
      5  "resource://testing-common/httpd.sys.mjs"
      6 );
      7 
      8 ChromeUtils.defineLazyGetter(this, "URL", function () {
      9  return "http://localhost:" + httpserver.identity.primaryPort;
     10 });
     11 
     12 var httpserver = null;
     13 
     14 const NS_ERROR_ENTITY_CHANGED = 0x804b0020;
     15 const NS_ERROR_NOT_RESUMABLE = 0x804b0019;
     16 
     17 const rangeBody = "Body of the range request handler.\r\n";
     18 
     19 function make_channel(url) {
     20  return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
     21 }
     22 
     23 function AuthPrompt2() {}
     24 
     25 AuthPrompt2.prototype = {
     26  user: "guest",
     27  pass: "guest",
     28 
     29  QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
     30 
     31  promptAuth: function ap2_promptAuth(channel, level, authInfo) {
     32    authInfo.username = this.user;
     33    authInfo.password = this.pass;
     34    return true;
     35  },
     36 
     37  asyncPromptAuth: function ap2_async() {
     38    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
     39  },
     40 };
     41 
     42 function Requestor() {}
     43 
     44 Requestor.prototype = {
     45  QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
     46 
     47  getInterface: function requestor_gi(iid) {
     48    if (iid.equals(Ci.nsIAuthPrompt2)) {
     49      // Allow the prompt to store state by caching it here
     50      if (!this.prompt2) {
     51        this.prompt2 = new AuthPrompt2();
     52      }
     53      return this.prompt2;
     54    }
     55 
     56    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
     57  },
     58 
     59  prompt2: null,
     60 };
     61 
     62 function run_test() {
     63  dump("*** run_test\n");
     64  httpserver = new HttpServer();
     65  httpserver.registerPathHandler("/auth", authHandler);
     66  httpserver.registerPathHandler("/range", rangeHandler);
     67  httpserver.registerPathHandler("/acceptranges", acceptRangesHandler);
     68  httpserver.registerPathHandler("/redir", redirHandler);
     69 
     70  var entityID;
     71 
     72  function get_entity_id(request) {
     73    dump("*** get_entity_id()\n");
     74    Assert.ok(
     75      request instanceof Ci.nsIResumableChannel,
     76      "must be a resumable channel"
     77    );
     78    entityID = request.entityID;
     79    dump("*** entity id = " + entityID + "\n");
     80 
     81    // Try a non-resumable URL (responds with 200)
     82    var chan = make_channel(URL);
     83    chan.nsIResumableChannel.resumeAt(1, entityID);
     84    chan.asyncOpen(new ChannelListener(try_resume, null, CL_EXPECT_FAILURE));
     85  }
     86 
     87  function try_resume(request) {
     88    dump("*** try_resume()\n");
     89    Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
     90 
     91    // Try a successful resume
     92    var chan = make_channel(URL + "/range");
     93    chan.nsIResumableChannel.resumeAt(1, entityID);
     94    chan.asyncOpen(new ChannelListener(try_resume_zero, null));
     95  }
     96 
     97  function try_resume_zero(request, data) {
     98    dump("*** try_resume_zero()\n");
     99    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    100    Assert.equal(data, rangeBody.substring(1));
    101 
    102    // Try a server which doesn't support range requests
    103    var chan = make_channel(URL + "/acceptranges");
    104    chan.nsIResumableChannel.resumeAt(0, entityID);
    105    chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "none", false);
    106    chan.asyncOpen(new ChannelListener(try_no_range, null, CL_EXPECT_FAILURE));
    107  }
    108 
    109  function try_no_range(request) {
    110    dump("*** try_no_range()\n");
    111    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    112    Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
    113 
    114    // Try a server which supports "bytes" range requests
    115    var chan = make_channel(URL + "/acceptranges");
    116    chan.nsIResumableChannel.resumeAt(0, entityID);
    117    chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes", false);
    118    chan.asyncOpen(new ChannelListener(try_bytes_range, null));
    119  }
    120 
    121  function try_bytes_range(request, data) {
    122    dump("*** try_bytes_range()\n");
    123    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    124    Assert.equal(data, rangeBody);
    125 
    126    // Try a server which supports "foo" and "bar" range requests
    127    var chan = make_channel(URL + "/acceptranges");
    128    chan.nsIResumableChannel.resumeAt(0, entityID);
    129    chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foo, bar", false);
    130    chan.asyncOpen(
    131      new ChannelListener(try_foo_bar_range, null, CL_EXPECT_FAILURE)
    132    );
    133  }
    134 
    135  function try_foo_bar_range(request) {
    136    dump("*** try_foo_bar_range()\n");
    137    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    138    Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
    139 
    140    // Try a server which supports "foobar" range requests
    141    var chan = make_channel(URL + "/acceptranges");
    142    chan.nsIResumableChannel.resumeAt(0, entityID);
    143    chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foobar", false);
    144    chan.asyncOpen(
    145      new ChannelListener(try_foobar_range, null, CL_EXPECT_FAILURE)
    146    );
    147  }
    148 
    149  function try_foobar_range(request) {
    150    dump("*** try_foobar_range()\n");
    151    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    152    Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
    153 
    154    // Try a server which supports "bytes" and "foobar" range requests
    155    var chan = make_channel(URL + "/acceptranges");
    156    chan.nsIResumableChannel.resumeAt(0, entityID);
    157    chan.nsIHttpChannel.setRequestHeader(
    158      "X-Range-Type",
    159      "bytes, foobar",
    160      false
    161    );
    162    chan.asyncOpen(new ChannelListener(try_bytes_foobar_range, null));
    163  }
    164 
    165  function try_bytes_foobar_range(request, data) {
    166    dump("*** try_bytes_foobar_range()\n");
    167    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    168    Assert.equal(data, rangeBody);
    169 
    170    // Try a server which supports "bytesfoo" and "bar" range requests
    171    var chan = make_channel(URL + "/acceptranges");
    172    chan.nsIResumableChannel.resumeAt(0, entityID);
    173    chan.nsIHttpChannel.setRequestHeader(
    174      "X-Range-Type",
    175      "bytesfoo, bar",
    176      false
    177    );
    178    chan.asyncOpen(
    179      new ChannelListener(try_bytesfoo_bar_range, null, CL_EXPECT_FAILURE)
    180    );
    181  }
    182 
    183  function try_bytesfoo_bar_range(request) {
    184    dump("*** try_bytesfoo_bar_range()\n");
    185    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    186    Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
    187 
    188    // Try a server which doesn't send Accept-Ranges header at all
    189    var chan = make_channel(URL + "/acceptranges");
    190    chan.nsIResumableChannel.resumeAt(0, entityID);
    191    chan.asyncOpen(new ChannelListener(try_no_accept_ranges, null));
    192  }
    193 
    194  function try_no_accept_ranges(request, data) {
    195    dump("*** try_no_accept_ranges()\n");
    196    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    197    Assert.equal(data, rangeBody);
    198 
    199    // Try a successful suspend/resume from 0
    200    var chan = make_channel(URL + "/range");
    201    chan.nsIResumableChannel.resumeAt(0, entityID);
    202    chan.asyncOpen(
    203      new ChannelListener(
    204        try_suspend_resume,
    205        null,
    206        CL_SUSPEND | CL_EXPECT_3S_DELAY
    207      )
    208    );
    209  }
    210 
    211  function try_suspend_resume(request, data) {
    212    dump("*** try_suspend_resume()\n");
    213    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    214    Assert.equal(data, rangeBody);
    215 
    216    // Try a successful resume from 0
    217    var chan = make_channel(URL + "/range");
    218    chan.nsIResumableChannel.resumeAt(0, entityID);
    219    chan.asyncOpen(new ChannelListener(success, null));
    220  }
    221 
    222  function success(request, data) {
    223    dump("*** success()\n");
    224    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    225    Assert.equal(data, rangeBody);
    226 
    227    // Authentication (no password; working resume)
    228    // (should not give us any data)
    229    var chan = make_channel(URL + "/range");
    230    chan.nsIResumableChannel.resumeAt(1, entityID);
    231    chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false);
    232    chan.asyncOpen(
    233      new ChannelListener(test_auth_nopw, null, CL_EXPECT_FAILURE)
    234    );
    235  }
    236 
    237  function test_auth_nopw(request) {
    238    dump("*** test_auth_nopw()\n");
    239    Assert.ok(!request.nsIHttpChannel.requestSucceeded);
    240    Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
    241 
    242    // Authentication + not working resume
    243    var chan = make_channel(
    244      "http://guest:guest@localhost:" +
    245        httpserver.identity.primaryPort +
    246        "/auth"
    247    );
    248    chan.nsIResumableChannel.resumeAt(1, entityID);
    249    chan.notificationCallbacks = new Requestor();
    250    chan.asyncOpen(new ChannelListener(test_auth, null, CL_EXPECT_FAILURE));
    251  }
    252  function test_auth(request) {
    253    dump("*** test_auth()\n");
    254    Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
    255    Assert.less(request.nsIHttpChannel.responseStatus, 300);
    256 
    257    // Authentication + working resume
    258    var chan = make_channel(
    259      "http://guest:guest@localhost:" +
    260        httpserver.identity.primaryPort +
    261        "/range"
    262    );
    263    chan.nsIResumableChannel.resumeAt(1, entityID);
    264    chan.notificationCallbacks = new Requestor();
    265    chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false);
    266    chan.asyncOpen(new ChannelListener(test_auth_resume, null));
    267  }
    268 
    269  function test_auth_resume(request, data) {
    270    dump("*** test_auth_resume()\n");
    271    Assert.equal(data, rangeBody.substring(1));
    272    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    273 
    274    // 404 page (same content length as real content)
    275    var chan = make_channel(URL + "/range");
    276    chan.nsIResumableChannel.resumeAt(1, entityID);
    277    chan.nsIHttpChannel.setRequestHeader("X-Want-404", "true", false);
    278    chan.asyncOpen(new ChannelListener(test_404, null, CL_EXPECT_FAILURE));
    279  }
    280 
    281  function test_404(request) {
    282    dump("*** test_404()\n");
    283    Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
    284    Assert.equal(request.nsIHttpChannel.responseStatus, 404);
    285 
    286    // 416 Requested Range Not Satisfiable
    287    var chan = make_channel(URL + "/range");
    288    chan.nsIResumableChannel.resumeAt(1000, entityID);
    289    chan.asyncOpen(new ChannelListener(test_416, null, CL_EXPECT_FAILURE));
    290  }
    291 
    292  function test_416(request) {
    293    dump("*** test_416()\n");
    294    Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
    295    Assert.equal(request.nsIHttpChannel.responseStatus, 416);
    296 
    297    // Redirect + successful resume
    298    var chan = make_channel(URL + "/redir");
    299    chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/range", false);
    300    chan.nsIResumableChannel.resumeAt(1, entityID);
    301    chan.asyncOpen(new ChannelListener(test_redir_resume, null));
    302  }
    303 
    304  function test_redir_resume(request, data) {
    305    dump("*** test_redir_resume()\n");
    306    Assert.ok(request.nsIHttpChannel.requestSucceeded);
    307    Assert.equal(data, rangeBody.substring(1));
    308    Assert.equal(request.nsIHttpChannel.responseStatus, 206);
    309 
    310    // Redirect + failed resume
    311    var chan = make_channel(URL + "/redir");
    312    chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/", false);
    313    chan.nsIResumableChannel.resumeAt(1, entityID);
    314    chan.asyncOpen(
    315      new ChannelListener(test_redir_noresume, null, CL_EXPECT_FAILURE)
    316    );
    317  }
    318 
    319  function test_redir_noresume(request) {
    320    dump("*** test_redir_noresume()\n");
    321    Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
    322 
    323    httpserver.stop(do_test_finished);
    324  }
    325 
    326  httpserver.start(-1);
    327  var chan = make_channel(URL + "/range");
    328  chan.asyncOpen(new ChannelListener(get_entity_id, null));
    329  do_test_pending();
    330 }
    331 
    332 // HANDLERS
    333 
    334 function handleAuth(metadata, response) {
    335  // btoa("guest:guest"), but that function is not available here
    336  var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
    337 
    338  if (
    339    metadata.hasHeader("Authorization") &&
    340    metadata.getHeader("Authorization") == expectedHeader
    341  ) {
    342    response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
    343    response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
    344 
    345    return true;
    346  }
    347  // didn't know guest:guest, failure
    348  response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    349  response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
    350  return false;
    351 }
    352 
    353 // /auth
    354 function authHandler(metadata, response) {
    355  response.setHeader("Content-Type", "text/html", false);
    356  var body = handleAuth(metadata, response) ? "success" : "failure";
    357  response.bodyOutputStream.write(body, body.length);
    358 }
    359 
    360 // /range
    361 function rangeHandler(metadata, response) {
    362  response.setHeader("Content-Type", "text/html", false);
    363 
    364  if (metadata.hasHeader("X-Need-Auth")) {
    365    if (!handleAuth(metadata, response)) {
    366      body = "auth failed";
    367      response.bodyOutputStream.write(body, body.length);
    368      return;
    369    }
    370  }
    371 
    372  if (metadata.hasHeader("X-Want-404")) {
    373    response.setStatusLine(metadata.httpVersion, 404, "Not Found");
    374    body = rangeBody;
    375    response.bodyOutputStream.write(body, body.length);
    376    return;
    377  }
    378 
    379  var body = rangeBody;
    380 
    381  if (metadata.hasHeader("Range")) {
    382    // Syntax: bytes=[from]-[to] (we don't support multiple ranges)
    383    var matches = metadata
    384      .getHeader("Range")
    385      .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
    386    var from = matches[1] === undefined ? 0 : matches[1];
    387    var to = matches[2] === undefined ? rangeBody.length - 1 : matches[2];
    388    if (from >= rangeBody.length) {
    389      response.setStatusLine(metadata.httpVersion, 416, "Start pos too high");
    390      response.setHeader("Content-Range", "*/" + rangeBody.length, false);
    391      return;
    392    }
    393    body = body.substring(from, to + 1);
    394    // always respond to successful range requests with 206
    395    response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
    396    response.setHeader(
    397      "Content-Range",
    398      from + "-" + to + "/" + rangeBody.length,
    399      false
    400    );
    401  }
    402 
    403  response.bodyOutputStream.write(body, body.length);
    404 }
    405 
    406 // /acceptranges
    407 function acceptRangesHandler(metadata, response) {
    408  response.setHeader("Content-Type", "text/html", false);
    409  if (metadata.hasHeader("X-Range-Type")) {
    410    response.setHeader(
    411      "Accept-Ranges",
    412      metadata.getHeader("X-Range-Type"),
    413      false
    414    );
    415  }
    416  response.bodyOutputStream.write(rangeBody, rangeBody.length);
    417 }
    418 
    419 // /redir
    420 function redirHandler(metadata, response) {
    421  response.setStatusLine(metadata.httpVersion, 302, "Found");
    422  response.setHeader("Content-Type", "text/html", false);
    423  response.setHeader("Location", metadata.getHeader("X-Redir-To"), false);
    424  var body = "redirect\r\n";
    425  response.bodyOutputStream.write(body, body.length);
    426 }