tor-browser

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

http3_common.js (19969B)


      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 /* import-globals-from head_cache.js */
      8 /* import-globals-from head_cookies.js */
      9 /* import-globals-from head_channels.js */
     10 /* import-globals-from head_http3.js */
     11 
     12 const { HttpServer } = ChromeUtils.importESModule(
     13  "resource://testing-common/httpd.sys.mjs"
     14 );
     15 
     16 function makeChan(uri) {
     17  let chan = NetUtil.newChannel({
     18    uri,
     19    loadUsingSystemPrincipal: true,
     20  }).QueryInterface(Ci.nsIHttpChannel);
     21  chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
     22  return chan;
     23 }
     24 
     25 // Promise-backed Http3CheckListener
     26 class Http3CheckListener {
     27  constructor(
     28    { expectedStatus = Cr.NS_OK, expectedRoute = "" } = {},
     29    resolve,
     30    reject
     31  ) {
     32    this.onDataAvailableFired = false;
     33    this.expectedStatus = expectedStatus;
     34    this.expectedRoute = expectedRoute;
     35    this._resolve = resolve;
     36    this._reject = reject;
     37  }
     38 
     39  onStartRequest(request) {
     40    Assert.ok(request instanceof Ci.nsIHttpChannel);
     41    Assert.equal(request.status, this.expectedStatus);
     42    if (Components.isSuccessCode(this.expectedStatus)) {
     43      Assert.equal(request.responseStatus, 200);
     44    }
     45  }
     46 
     47  onDataAvailable(request, stream, off, cnt) {
     48    this.onDataAvailableFired = true;
     49    read_stream(stream, cnt);
     50  }
     51 
     52  onStopRequest(request, status) {
     53    Assert.equal(status, this.expectedStatus);
     54 
     55    let routed = "NA";
     56    try {
     57      routed = request.getRequestHeader("Alt-Used");
     58    } catch (e) {}
     59    dump("routed is " + routed + "\n");
     60 
     61    Assert.equal(routed, this.expectedRoute);
     62 
     63    if (Components.isSuccessCode(this.expectedStatus)) {
     64      let httpVersion = "";
     65      try {
     66        httpVersion = request.protocolVersion;
     67      } catch (e) {}
     68      Assert.equal(httpVersion, "h3");
     69      Assert.equal(this.onDataAvailableFired, true);
     70      Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3");
     71    }
     72 
     73    this._resolve?.(request);
     74  }
     75 }
     76 
     77 class WaitForHttp3Listener extends Http3CheckListener {
     78  constructor(
     79    {
     80      expectedStatus = Cr.NS_OK,
     81      expectedRoute = "",
     82      uri = "",
     83      h3AltSvc = "",
     84      retry,
     85      delayMs = 500,
     86    } = {},
     87    resolve,
     88    reject
     89  ) {
     90    super({ expectedStatus, expectedRoute }, resolve, reject);
     91    this.uri = uri;
     92    this.h3AltSvc = h3AltSvc;
     93    this._retry = retry; // function to re-open the request
     94    this._delayMs = delayMs; // poll interval
     95  }
     96 
     97  onStopRequest(request, status) {
     98    Assert.equal(status, this.expectedStatus);
     99 
    100    let routed = "NA";
    101    try {
    102      routed = request.getRequestHeader("Alt-Used");
    103    } catch (e) {}
    104    dump(`routed is ${routed}\n`);
    105 
    106    let httpVersion = "";
    107    try {
    108      httpVersion = request.protocolVersion;
    109    } catch (e) {}
    110 
    111    if (routed === this.expectedRoute) {
    112      // (This is where run_next_test() used to be.)
    113      Assert.equal(routed, this.expectedRoute); // useful log/assert
    114      Assert.equal(httpVersion, "h3");
    115      this._resolve?.(request);
    116      return;
    117    }
    118 
    119    // Not routed yet: mirror old behavior (log + supportsHTTP3 check + retry)
    120    dump("poll later for alt-svc mapping\n");
    121    if (httpVersion === "h2") {
    122      request.QueryInterface(Ci.nsIHttpChannelInternal);
    123      Assert.ok(request.supportsHTTP3);
    124    }
    125 
    126    if (typeof this._retry === "function") {
    127      // schedule another attempt (replaces do_test_pending/do_timeout recursion)
    128      do_timeout(this._delayMs, () =>
    129        this._retry(this.uri, this.expectedRoute, this.h3AltSvc)
    130      );
    131    }
    132    // Promise remains pending until a later attempt matches expectedRoute.
    133  }
    134 }
    135 
    136 // Factory to create { listener, promise }
    137 function createHttp3CheckListener(options = {}) {
    138  let resolve, reject;
    139  const promise = new Promise((res, rej) => {
    140    resolve = res;
    141    reject = rej;
    142  });
    143  const listener = new Http3CheckListener(options, resolve, reject);
    144  return { listener, promise };
    145 }
    146 
    147 function createWaitForHttp3Listener(options = {}) {
    148  let resolve, reject;
    149  const promise = new Promise((res, rej) => {
    150    resolve = res;
    151    reject = rej;
    152  });
    153  const listener = new WaitForHttp3Listener(options, resolve, reject);
    154  return { listener, promise };
    155 }
    156 
    157 // --- Async wrapper that does the polling by re-issuing the request ---
    158 async function waitForHttp3Route(
    159  uri,
    160  expectedRoute,
    161  altSvc,
    162  { delayMs = 500 } = {}
    163 ) {
    164  let listenerRef;
    165 
    166  // Function to (re)open the channel using the same listener instance.
    167  const retry = () => {
    168    const chan = makeChan(uri);
    169    if (altSvc) {
    170      chan.setRequestHeader("x-altsvc", altSvc, false);
    171    }
    172    chan.asyncOpen(listenerRef);
    173  };
    174 
    175  const { listener, promise } = createWaitForHttp3Listener({
    176    expectedStatus: Cr.NS_OK,
    177    expectedRoute,
    178    uri,
    179    h3AltSvc: altSvc,
    180    retry,
    181    delayMs,
    182  });
    183  listenerRef = listener;
    184 
    185  // Kick off first attempt; subsequent attempts are scheduled by the listener.
    186  retry();
    187 
    188  // Resolves only when routed === expectedRoute
    189  return promise;
    190 }
    191 
    192 // Promise-backed MultipleListener
    193 class MultipleListener {
    194  constructor(
    195    {
    196      number_of_parallel_requests = 0,
    197      expectedRoute = "",
    198      with_error = Cr.NS_OK, // NS_OK means we expect success for all
    199    } = {},
    200    resolve,
    201    reject
    202  ) {
    203    this.number_of_parallel_requests = number_of_parallel_requests;
    204    this.expectedRoute = expectedRoute;
    205    this.with_error = with_error;
    206 
    207    this.count_of_done_requests = 0;
    208    this.error_found_onstart = false;
    209    this.error_found_onstop = false;
    210    this.need_cancel_found = false;
    211 
    212    this._resolve = resolve;
    213    this._reject = reject;
    214  }
    215 
    216  onStartRequest(request) {
    217    Assert.ok(request instanceof Ci.nsIHttpChannel);
    218 
    219    // Optional cancel behavior via header "CancelMe"
    220    let need_cancel = "";
    221    try {
    222      need_cancel = request.getRequestHeader("CancelMe");
    223    } catch (_) {}
    224    if (need_cancel !== "") {
    225      this.need_cancel_found = true;
    226      request.cancel(Cr.NS_ERROR_ABORT);
    227      return;
    228    }
    229 
    230    // Original logic: either 200 OK for success, or exactly one failure
    231    if (Components.isSuccessCode(request.status)) {
    232      Assert.equal(request.responseStatus, 200);
    233    } else if (this.error_found_onstart) {
    234      // Fail fast: more than one failing request on start
    235      this._reject?.(
    236        new Error("We should have only one request failing (onStart).")
    237      );
    238    } else {
    239      Assert.equal(request.status, this.with_error);
    240      this.error_found_onstart = true;
    241    }
    242  }
    243 
    244  onDataAvailable(request, stream, off, cnt) {
    245    read_stream(stream, cnt);
    246  }
    247 
    248  onStopRequest(request) {
    249    // Check Alt-Used routing matches expectation
    250    let routed = "";
    251    try {
    252      routed = request.getRequestHeader("Alt-Used");
    253    } catch (_) {}
    254    Assert.equal(routed, this.expectedRoute);
    255 
    256    // If success, ensure HTTP/3
    257    if (Components.isSuccessCode(request.status)) {
    258      let httpVersion = "";
    259      try {
    260        httpVersion = request.protocolVersion;
    261      } catch (_) {}
    262      Assert.equal(httpVersion, "h3");
    263    }
    264 
    265    // Track/validate failures (at most one)
    266    if (!Components.isSuccessCode(request.status)) {
    267      if (this.error_found_onstop) {
    268        this._reject?.(
    269          new Error("We should have only one request failing (onStop).")
    270        );
    271        return;
    272      }
    273      Assert.equal(request.status, this.with_error);
    274      this.error_found_onstop = true;
    275    }
    276 
    277    // Count completion and maybe resolve
    278    this.count_of_done_requests++;
    279    if (this.count_of_done_requests === this.number_of_parallel_requests) {
    280      if (Components.isSuccessCode(this.with_error)) {
    281        // All were expected to succeed
    282        Assert.equal(this.error_found_onstart, false);
    283        Assert.equal(this.error_found_onstop, false);
    284      } else {
    285        // One failure was expected OR a cancel path was exercised
    286        Assert.ok(this.error_found_onstart || this.need_cancel_found);
    287        Assert.equal(this.error_found_onstop, true);
    288      }
    289      this._resolve?.();
    290    }
    291  }
    292 }
    293 
    294 // Factory to create { listener, promise }
    295 function createMultipleListener(options = {}) {
    296  let resolve, reject;
    297  const promise = new Promise((res, rej) => {
    298    resolve = res;
    299    reject = rej;
    300  });
    301  const listener = new MultipleListener(options, resolve, reject);
    302  return { listener, promise };
    303 }
    304 
    305 async function do_test_multiple_requests(
    306  number_of_parallel_requests,
    307  h3Route,
    308  httpsOrigin
    309 ) {
    310  dump("test_multiple_requests()\n");
    311 
    312  const { listener, promise } = createMultipleListener({
    313    number_of_parallel_requests,
    314    expectedRoute: h3Route,
    315    with_error: Cr.NS_OK,
    316  });
    317 
    318  for (let i = 0; i < number_of_parallel_requests; i++) {
    319    const chan = makeChan(httpsOrigin + "20000");
    320    chan.asyncOpen(listener);
    321  }
    322 
    323  await promise;
    324 }
    325 
    326 async function do_test_request_cancelled_by_server(h3Route, httpsOrigin) {
    327  dump("do_test_request_cancelled_by_server()\n");
    328 
    329  const { listener, promise } = createHttp3CheckListener({
    330    expectedStatus: Cr.NS_ERROR_NET_INTERRUPT,
    331    expectedRoute: h3Route,
    332  });
    333 
    334  const chan = makeChan(httpsOrigin + "RequestCancelled");
    335  chan.asyncOpen(listener);
    336 
    337  // Resolves at the point where run_next_test() used to be called
    338  await promise;
    339 }
    340 
    341 // Promise-backed Http3CheckListener must already exist:
    342 //   createHttp3CheckListener({ expectedStatus, expectedRoute })
    343 
    344 class CancelRequestListener extends Http3CheckListener {
    345  constructor({ expectedRoute = "" } = {}, resolve, reject) {
    346    super(
    347      { expectedStatus: Cr.NS_ERROR_ABORT, expectedRoute },
    348      resolve,
    349      reject
    350    );
    351  }
    352 
    353  onStartRequest(request) {
    354    Assert.ok(request instanceof Ci.nsIHttpChannel);
    355    Assert.equal(Components.isSuccessCode(request.status), true);
    356    // Cancel the request immediately (simulate Necko cancelling)
    357    request.cancel(Cr.NS_ERROR_ABORT);
    358  }
    359 }
    360 
    361 function createCancelRequestListener(options = {}) {
    362  let resolve, reject;
    363  const promise = new Promise((res, rej) => {
    364    resolve = res;
    365    reject = rej;
    366  });
    367  const listener = new CancelRequestListener(options, resolve, reject);
    368  return { listener, promise };
    369 }
    370 
    371 // Cancel stream after OnStartRequest.
    372 async function do_test_stream_cancelled_by_necko(h3Route, httpsOrigin) {
    373  dump("do_test_stream_cancelled_by_necko()\n");
    374 
    375  const { listener, promise } = createCancelRequestListener({
    376    expectedRoute: h3Route,
    377  });
    378 
    379  const chan = makeChan(httpsOrigin + "20000");
    380  chan.asyncOpen(listener);
    381 
    382  // Resolves at the end of onStopRequest (where run_next_test() used to be)
    383  await promise;
    384 }
    385 
    386 async function do_test_multiple_request_one_is_cancelled(
    387  number_of_parallel_requests,
    388  h3Route,
    389  httpsOrigin
    390 ) {
    391  dump("do_test_multiple_request_one_is_cancelled()\n");
    392 
    393  const { listener, promise } = createMultipleListener({
    394    number_of_parallel_requests,
    395    expectedRoute: h3Route,
    396    with_error: Cr.NS_ERROR_NET_INTERRUPT, // one request is expected to fail (server-cancelled)
    397  });
    398 
    399  for (let i = 0; i < number_of_parallel_requests; i++) {
    400    let uri = httpsOrigin + "20000";
    401    if (i === 4) {
    402      // Add a request that will be cancelled by the server.
    403      uri = httpsOrigin + "RequestCancelled";
    404    }
    405    const chan = makeChan(uri);
    406    chan.asyncOpen(listener);
    407  }
    408 
    409  // Resolves when all parallel requests complete and invariants are checked
    410  await promise;
    411 }
    412 
    413 async function do_test_multiple_request_one_is_cancelled_by_necko(
    414  number_of_parallel_requests,
    415  h3Route,
    416  httpsOrigin
    417 ) {
    418  dump("do_test_multiple_request_one_is_cancelled_by_necko()\n");
    419 
    420  const { listener, promise } = createMultipleListener({
    421    number_of_parallel_requests,
    422    expectedRoute: h3Route,
    423    with_error: Cr.NS_ERROR_ABORT,
    424  });
    425 
    426  for (let i = 0; i < number_of_parallel_requests; i++) {
    427    let chan = makeChan(httpsOrigin + "20000");
    428    if (i === 4) {
    429      // MultipleListener will cancel request with this header.
    430      chan.setRequestHeader("CancelMe", "true", false);
    431    }
    432    chan.asyncOpen(listener);
    433  }
    434 
    435  // Resolves when all parallel requests complete and invariants are checked
    436  await promise;
    437 }
    438 
    439 // Promise-backed Http3CheckListener assumed available:
    440 // function createHttp3CheckListener({ expectedStatus, expectedRoute })
    441 
    442 class PostListener extends Http3CheckListener {
    443  constructor(opts = {}, resolve, reject) {
    444    super(opts, resolve, reject);
    445  }
    446  onDataAvailable(request, stream, off, cnt) {
    447    this.onDataAvailableFired = true;
    448    read_stream(stream, cnt);
    449  }
    450 }
    451 
    452 // Factory for PostListener
    453 function createPostListener(options = {}) {
    454  let resolve, reject;
    455  const promise = new Promise((res, rej) => {
    456    resolve = res;
    457    reject = rej;
    458  });
    459  const listener = new PostListener(options, resolve, reject);
    460  return { listener, promise };
    461 }
    462 
    463 // Helper to perform a POST (or any method with a body)
    464 function openWithBody(
    465  content,
    466  chan,
    467  method = "POST",
    468  contentType = "text/plain"
    469 ) {
    470  const stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    471    Ci.nsIStringInputStream
    472  );
    473  stream.setByteStringData(content);
    474 
    475  const uchan = chan.QueryInterface(Ci.nsIUploadChannel);
    476  uchan.setUploadStream(stream, contentType, stream.available());
    477  chan.requestMethod = method;
    478  return chan;
    479 }
    480 
    481 // Generate a post with known pre-calculated md5 sum.
    482 function generateContent(size) {
    483  let content = "";
    484  for (let i = 0; i < size; i++) {
    485    content += "0";
    486  }
    487  return content;
    488 }
    489 
    490 let post = generateContent(10);
    491 
    492 // Test a simple POST (async)
    493 async function do_test_post(httpsOrigin, h3Route) {
    494  dump("do_test_post()\n");
    495 
    496  const chan = makeChan(httpsOrigin + "post");
    497  openWithBody(post, chan, "POST");
    498 
    499  const { listener, promise } = createPostListener({
    500    expectedStatus: Cr.NS_OK,
    501    expectedRoute: h3Route,
    502  });
    503 
    504  chan.asyncOpen(listener);
    505  await promise; // resolves at end of onStopRequest in Http3CheckListener
    506 }
    507 
    508 // Test a simple PATCH
    509 async function do_test_patch(httpsOrigin, h3Route) {
    510  dump("do_test_post()\n");
    511 
    512  const chan = makeChan(httpsOrigin + "patch");
    513  openWithBody(post, chan, "PATCH");
    514 
    515  const { listener, promise } = createPostListener({
    516    expectedStatus: Cr.NS_OK,
    517    expectedRoute: h3Route,
    518  });
    519 
    520  chan.asyncOpen(listener);
    521  await promise;
    522 }
    523 
    524 let h1Server = null;
    525 let altsvcHost = "";
    526 let httpOrigin = "";
    527 
    528 function h1Response(metadata, response) {
    529  response.setStatusLine(metadata.httpVersion, 200, "OK");
    530  response.setHeader("Content-Type", "text/plain", false);
    531  response.setHeader("Connection", "close", false);
    532  response.setHeader("Cache-Control", "no-cache", false);
    533  response.setHeader("Access-Control-Allow-Origin", "*", false);
    534  response.setHeader("Access-Control-Allow-Method", "GET", false);
    535  response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
    536 
    537  try {
    538    let hval = "h3=" + metadata.getHeader("x-altsvc");
    539    response.setHeader("Alt-Svc", hval, false);
    540  } catch (e) {}
    541 
    542  let body = "Q: What did 0 say to 8? A: Nice Belt!\n";
    543  response.bodyOutputStream.write(body, body.length);
    544 }
    545 
    546 function h1ServerWK(metadata, response) {
    547  response.setStatusLine(metadata.httpVersion, 200, "OK");
    548  response.setHeader("Content-Type", "application/json", false);
    549  response.setHeader("Connection", "close", false);
    550  response.setHeader("Cache-Control", "no-cache", false);
    551  response.setHeader("Access-Control-Allow-Origin", "*", false);
    552  response.setHeader("Access-Control-Allow-Method", "GET", false);
    553  response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
    554 
    555  let body = `["http://${altsvcHost}:${h1Server.identity.primaryPort}"]`;
    556  response.bodyOutputStream.write(body, body.length);
    557 }
    558 
    559 function setup_h1_server(host) {
    560  altsvcHost = host;
    561  h1Server = new HttpServer();
    562  h1Server.registerPathHandler("/http3-test", h1Response);
    563  h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
    564  h1Server.registerPathHandler("/VersionFallback", h1Response);
    565  h1Server.start(-1);
    566  h1Server.identity.setPrimary(
    567    "http",
    568    altsvcHost,
    569    h1Server.identity.primaryPort
    570  );
    571  httpOrigin = `http://${altsvcHost}:${h1Server.identity.primaryPort}/`;
    572  registerCleanupFunction(() => {
    573    h1Server.stop();
    574  });
    575 }
    576 
    577 // Promise-backed base assumed available:
    578 //   class Http3CheckListener { ... }
    579 //   function createHttp3CheckListener(opts) { return { listener, promise }; }
    580 
    581 class SlowReceiverListener extends Http3CheckListener {
    582  constructor(
    583    {
    584      expectedStatus = Cr.NS_OK,
    585      expectedRoute = "",
    586      expectedBytes = 10_000_000,
    587    } = {},
    588    resolve,
    589    reject
    590  ) {
    591    super({ expectedStatus, expectedRoute }, resolve, reject);
    592    this.count = 0;
    593    this.expectedBytes = expectedBytes;
    594  }
    595 
    596  onDataAvailable(request, stream, off, cnt) {
    597    this.onDataAvailableFired = true;
    598    this.count += cnt;
    599    read_stream(stream, cnt);
    600  }
    601 
    602  onStopRequest(request, status) {
    603    Assert.equal(status, this.expectedStatus);
    604    Assert.equal(this.count, this.expectedBytes);
    605 
    606    let routed = "NA";
    607    try {
    608      routed = request.getRequestHeader("Alt-Used");
    609    } catch (e) {}
    610    dump(`routed is ${routed}\n`);
    611    Assert.equal(routed, this.expectedRoute);
    612 
    613    if (Components.isSuccessCode(this.expectedStatus)) {
    614      let httpVersion = "";
    615      try {
    616        httpVersion = request.protocolVersion;
    617      } catch (e) {}
    618      Assert.equal(httpVersion, "h3");
    619      Assert.equal(this.onDataAvailableFired, true);
    620    }
    621 
    622    // Resolve where run_next_test() used to be
    623    this._resolve?.(request);
    624  }
    625 }
    626 
    627 function createSlowReceiverListener(options = {}) {
    628  let resolve, reject;
    629  const promise = new Promise((res, rej) => {
    630    resolve = res;
    631    reject = rej;
    632  });
    633  const listener = new SlowReceiverListener(options, resolve, reject);
    634  return { listener, promise };
    635 }
    636 
    637 // Test: slow receiver (suspend, then resume)
    638 async function do_test_slow_receiver(httpsOrigin, h3Route) {
    639  dump("do_test_slow_receiver()\n");
    640 
    641  const chan = makeChan(httpsOrigin + "10000000");
    642 
    643  const { listener, promise } = createSlowReceiverListener({
    644    expectedStatus: Cr.NS_OK,
    645    expectedRoute: h3Route,
    646    expectedBytes: 10_000_000,
    647  });
    648 
    649  chan.asyncOpen(listener);
    650 
    651  // Suspend immediately, then resume after 1s (replaces do_test_pending/do_timeout)
    652  chan.suspend();
    653  await new Promise(r => do_timeout(1000, r));
    654  chan.resume();
    655 
    656  // Wait for completion (used to be run_next_test/do_test_finished)
    657  await promise;
    658 }
    659 
    660 // Promise-backed listener for version fallback checks
    661 class CheckFallbackListener {
    662  constructor(resolve, reject) {
    663    this._resolve = resolve;
    664    this._reject = reject;
    665  }
    666 
    667  onStartRequest(request) {
    668    Assert.ok(request instanceof Ci.nsIHttpChannel);
    669    Assert.equal(request.status, Cr.NS_OK);
    670    Assert.equal(request.responseStatus, 200);
    671  }
    672 
    673  onDataAvailable(request, stream, off, cnt) {
    674    read_stream(stream, cnt);
    675  }
    676 
    677  onStopRequest(request, status) {
    678    Assert.equal(status, Cr.NS_OK);
    679 
    680    let routed = "NA";
    681    try {
    682      routed = request.getRequestHeader("Alt-Used");
    683    } catch (e) {}
    684    dump(`routed is ${routed}\n`);
    685    Assert.equal(routed, "0");
    686 
    687    let httpVersion = "";
    688    try {
    689      httpVersion = request.protocolVersion;
    690    } catch (e) {}
    691    Assert.equal(httpVersion, "http/1.1");
    692 
    693    // Resolve where run_next_test() used to be called
    694    this._resolve?.(request);
    695  }
    696 }
    697 
    698 // Factory to create { listener, promise }
    699 function createCheckFallbackListener() {
    700  let resolve, reject;
    701  const promise = new Promise((res, rej) => {
    702    resolve = res;
    703    reject = rej;
    704  });
    705  const listener = new CheckFallbackListener(resolve, reject);
    706  return { listener, promise };
    707 }
    708 
    709 // Server cancels request with VersionFallback.
    710 async function do_test_version_fallback(httpsOrigin) {
    711  dump("do_test_version_fallback()\n");
    712 
    713  const chan = makeChan(httpsOrigin + "VersionFallback");
    714  const { listener, promise } = createCheckFallbackListener();
    715 
    716  chan.asyncOpen(listener);
    717 
    718  await promise;
    719 }