tor-browser

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

http3_proxy_common.js (16840B)


      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 {
     13  Http3ProxyFilter,
     14  with_node_servers,
     15  NodeHTTPServer,
     16  NodeHTTPSServer,
     17  NodeHTTP2Server,
     18  NodeHTTP2ProxyServer,
     19  NodeWebSocketHttp2Server,
     20  WebSocketConnection,
     21 } = ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs");
     22 
     23 function makeChan(uri) {
     24  let chan = NetUtil.newChannel({
     25    uri,
     26    loadUsingSystemPrincipal: true,
     27  }).QueryInterface(Ci.nsIHttpChannel);
     28  chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
     29  return chan;
     30 }
     31 
     32 function channelOpenPromise(chan, flags) {
     33  return new Promise(resolve => {
     34    function finish(req, buffer) {
     35      resolve([req, buffer]);
     36    }
     37    chan.asyncOpen(new ChannelListener(finish, null, flags));
     38  });
     39 }
     40 
     41 let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
     42 let proxyHost;
     43 let proxyPort;
     44 let noResponsePort;
     45 let proxyAuth;
     46 let proxyFilter;
     47 
     48 /**
     49 * Sets up proxy filter to MASQUE H3 proxy
     50 */
     51 async function setup_http3_proxy() {
     52  Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
     53  Services.prefs.setBoolPref("network.dns.disableIPv6", true);
     54  Services.prefs.setIntPref("network.webtransport.datagram_size", 1500);
     55  Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
     56  Services.prefs.setIntPref("network.http.http3.max_gso_segments", 1); // TODO: fix underflow
     57  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
     58    Ci.nsIX509CertDB
     59  );
     60  addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
     61  addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
     62 
     63  proxyHost = "foo.example.com";
     64  ({ masqueProxyPort: proxyPort, noResponsePort } =
     65    await create_masque_proxy_server());
     66  proxyAuth = "";
     67 
     68  Assert.notEqual(proxyPort, null);
     69  Assert.notEqual(proxyPort, "");
     70 
     71  // A dummy request to make sure AltSvcCache::mStorage is ready.
     72  let chan = makeChan(`https://localhost`);
     73  await channelOpenPromise(chan, CL_EXPECT_FAILURE);
     74 
     75  proxyFilter = new Http3ProxyFilter(
     76    proxyHost,
     77    proxyPort,
     78    0,
     79    "/.well-known/masque/udp/{target_host}/{target_port}/",
     80    proxyAuth
     81  );
     82  pps.registerFilter(proxyFilter, 10);
     83 
     84  registerCleanupFunction(() => {
     85    Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
     86  });
     87 }
     88 
     89 /**
     90 * Tests HTTP connect through H3 proxy to HTTP, HTTPS and H2 servers
     91 * Makes multiple requests. Expects success.
     92 */
     93 async function test_http_connect() {
     94  info("Running test_http_connect");
     95  await with_node_servers(
     96    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
     97    async server => {
     98      info(`Proxying to ${server.constructor.name} server`);
     99      await server.registerPathHandler("/first", (req, resp) => {
    100        resp.writeHead(200);
    101        resp.end("first");
    102      });
    103      await server.registerPathHandler("/second", (req, resp) => {
    104        resp.writeHead(200);
    105        resp.end("second");
    106      });
    107      await server.registerPathHandler("/third", (req, resp) => {
    108        resp.writeHead(200);
    109        resp.end("third");
    110      });
    111      let chan = makeChan(
    112        `${server.protocol()}://alt1.example.com:${server.port()}/first`
    113      );
    114      let [req, buf] = await channelOpenPromise(
    115        chan,
    116        CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
    117      );
    118      Assert.equal(req.status, Cr.NS_OK);
    119      Assert.equal(buf, "first");
    120      chan = makeChan(
    121        `${server.protocol()}://alt1.example.com:${server.port()}/second`
    122      );
    123      [req, buf] = await channelOpenPromise(
    124        chan,
    125        CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
    126      );
    127      Assert.equal(req.status, Cr.NS_OK);
    128      Assert.equal(buf, "second");
    129 
    130      chan = makeChan(
    131        `${server.protocol()}://alt1.example.com:${server.port()}/third`
    132      );
    133      [req, buf] = await channelOpenPromise(
    134        chan,
    135        CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
    136      );
    137      Assert.equal(req.status, Cr.NS_OK);
    138      Assert.equal(buf, "third");
    139    }
    140  );
    141 }
    142 
    143 /**
    144 * Test HTTP CONNECT authentication failure - tests behavior when proxy
    145 * authentication is required but not provided or incorrect
    146 */
    147 async function test_http_connect_auth_failure() {
    148  info("Running test_http_connect_auth_failure");
    149  await with_node_servers(
    150    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    151    async server => {
    152      info(`Testing auth failure with ${server.constructor.name} server`);
    153      // Register a handler that requires authentication
    154      await server.registerPathHandler("/auth-required", (req, resp) => {
    155        const auth = req.headers.authorization;
    156        if (!auth || auth !== "Basic dGVzdDp0ZXN0") {
    157          resp.writeHead(401, {
    158            "WWW-Authenticate": 'Basic realm="Test Realm"',
    159            "Content-Type": "text/plain",
    160          });
    161          resp.end("");
    162        } else {
    163          resp.writeHead(200);
    164          resp.end("Authenticated");
    165        }
    166      });
    167 
    168      let chan = makeChan(
    169        `${server.protocol()}://alt1.example.com:${server.port()}/auth-required`
    170      );
    171      let [req] = await channelOpenPromise(
    172        chan,
    173        CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
    174      );
    175 
    176      // Should receive 401 Unauthorized through the tunnel
    177      Assert.equal(req.status, Cr.NS_OK);
    178      Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 401);
    179    }
    180  );
    181 }
    182 
    183 /**
    184 * Test HTTP CONNECT with large request/response data - ensures the tunnel
    185 * can handle substantial data transfer without corruption or truncation
    186 */
    187 async function test_http_connect_large_data() {
    188  info("Running test_http_connect_large_data");
    189  await with_node_servers(
    190    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    191    async server => {
    192      info(
    193        `Testing large data transfer with ${server.constructor.name} server`
    194      );
    195      // Create a large response payload (1MB of data)
    196      const largeData = "x".repeat(1024 * 1024);
    197      await server.registerPathHandler("/large", (req, resp) => {
    198        const largeData = "x".repeat(1024 * 1024);
    199        resp.writeHead(200, { "Content-Type": "text/plain" });
    200        resp.end(largeData);
    201      });
    202 
    203      let chan = makeChan(
    204        `${server.protocol()}://alt1.example.com:${server.port()}/large`
    205      );
    206      let [req, buf] = await channelOpenPromise(
    207        chan,
    208        CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
    209      );
    210 
    211      Assert.equal(req.status, Cr.NS_OK);
    212      Assert.equal(buf.length, largeData.length);
    213      Assert.equal(buf, largeData);
    214    }
    215  );
    216 }
    217 
    218 /**
    219 * Test HTTP CONNECT tunnel connection refused - simulates target server
    220 * being unreachable or refusing connections
    221 */
    222 async function test_http_connect_connection_refused() {
    223  info("Running test_http_connect_connection_refused");
    224  // Test connecting to a port that's definitely not in use
    225  let chan = makeChan(`http://alt1.example.com:667/refused`);
    226  let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE);
    227 
    228  // Should fail to establish tunnel connection
    229  Assert.notEqual(req.status, Cr.NS_OK);
    230  info(`Connection refused status: ${req.status}`);
    231 }
    232 
    233 /**
    234 * Test HTTP CONNECT with invalid target host - verifies proper error handling
    235 * when trying to tunnel to a non-existent hostname
    236 */
    237 async function test_http_connect_invalid_host() {
    238  info("Running test_http_connect_invalid_host");
    239  let chan = makeChan(`http://nonexistent.invalid.example/test`);
    240  let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE);
    241 
    242  // Should fail DNS resolution for invalid hostname
    243  Assert.notEqual(req.status, Cr.NS_OK);
    244  info(`Invalid host status: ${req.status}`);
    245 }
    246 
    247 /**
    248 * Test concurrent HTTP CONNECT tunnels - ensures multiple simultaneous
    249 * requests can be established and used independently through the same H3 proxy
    250 */
    251 async function test_concurrent_http_connect_tunnels() {
    252  info("Running test_concurrent_http_connect_tunnels");
    253  await with_node_servers(
    254    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    255    async server => {
    256      info(`Testing concurrent tunnels with ${server.constructor.name} server`);
    257 
    258      // Register multiple endpoints
    259      await server.registerPathHandler("/concurrent1", (req, resp) => {
    260        resp.writeHead(200);
    261        resp.end("response1");
    262      });
    263      await server.registerPathHandler("/concurrent2", (req, resp) => {
    264        resp.writeHead(200);
    265        resp.end("response2");
    266      });
    267      await server.registerPathHandler("/concurrent3", (req, resp) => {
    268        resp.writeHead(200);
    269        resp.end("response3");
    270      });
    271 
    272      // Create multiple concurrent requests through the tunnel
    273      const promises = [];
    274      for (let i = 1; i <= 3; i++) {
    275        let chan = makeChan(
    276          `${server.protocol()}://alt1.example.com:${server.port()}/concurrent${i}`
    277        );
    278        promises.push(
    279          channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)
    280        );
    281      }
    282 
    283      const results = await Promise.all(promises);
    284 
    285      // Verify all requests succeeded with correct responses
    286      for (let i = 0; i < 3; i++) {
    287        const [req, buf] = results[i];
    288        Assert.equal(req.status, Cr.NS_OK);
    289        Assert.equal(buf, `response${i + 1}`);
    290      }
    291      info("All concurrent tunnels completed successfully");
    292    }
    293  );
    294 }
    295 
    296 /**
    297 * Test HTTP CONNECT tunnel stream closure handling - verifies proper cleanup
    298 * when the tunnel connection is closed unexpectedly
    299 */
    300 // eslint-disable-next-line no-unused-vars
    301 async function test_http_connect_stream_closure() {
    302  info("Running test_http_connect_stream_closure");
    303  await with_node_servers([NodeHTTPServer], async server => {
    304    info(`Testing stream closure with ${server.constructor.name} server`);
    305 
    306    await server.registerPathHandler("/close", (req, resp) => {
    307      // Send partial response then close connection abruptly
    308      resp.writeHead(200, { "Content-Type": "text/plain" });
    309      resp.write("partial");
    310      // Simulate connection closure
    311      resp.destroy();
    312    });
    313 
    314    let chan = makeChan(
    315      `${server.protocol()}://alt1.example.com:${server.port()}/close`
    316    );
    317    let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE);
    318 
    319    // Should handle connection closure gracefully
    320    Assert.notEqual(req.status, Cr.NS_OK);
    321    info(`Stream closure status: ${req.status}`);
    322  });
    323 }
    324 
    325 /**
    326 * Test connect-udp - SUCCESS case.
    327 * Will use h3 proxy to connect to h3 server.
    328 */
    329 async function test_connect_udp() {
    330  info("Running test_connect_udp");
    331  let h3Port = Services.env.get("MOZHTTP3_PORT");
    332  info(`h3Port = ${h3Port}`);
    333 
    334  Services.prefs.setCharPref(
    335    "network.http.http3.alt-svc-mapping-for-testing",
    336    `alt1.example.com;h3=:${h3Port}`
    337  );
    338 
    339  {
    340    let chan = makeChan(`https://alt1.example.com:${h3Port}/no_body`);
    341    let [req] = await channelOpenPromise(
    342      chan,
    343      CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
    344    );
    345    Assert.equal(req.protocolVersion, "h3");
    346    Assert.equal(req.status, Cr.NS_OK);
    347    Assert.equal(req.responseStatus, 200);
    348  }
    349 }
    350 
    351 async function test_http_connect_fallback() {
    352  info("Running test_http_connect_fallback");
    353  pps.unregisterFilter(proxyFilter);
    354 
    355  Services.prefs.setCharPref(
    356    "network.http.http3.alt-svc-mapping-for-testing",
    357    ""
    358  );
    359 
    360  let proxyPort = noResponsePort;
    361  let proxy = new NodeHTTP2ProxyServer();
    362  await proxy.startWithoutProxyFilter(proxyPort);
    363  Assert.equal(proxyPort, proxy.port());
    364  dump(`proxy port=${proxy.port()}\n`);
    365 
    366  let server = new NodeHTTP2Server();
    367  await server.start();
    368 
    369  // Register multiple endpoints
    370  await server.registerPathHandler("/concurrent1", (req, resp) => {
    371    resp.writeHead(200);
    372    resp.end("response1");
    373  });
    374  await server.registerPathHandler("/concurrent2", (req, resp) => {
    375    resp.writeHead(200);
    376    resp.end("response2");
    377  });
    378  await server.registerPathHandler("/concurrent3", (req, resp) => {
    379    resp.writeHead(200);
    380    resp.end("response3");
    381  });
    382 
    383  let filter = new Http3ProxyFilter(
    384    proxyHost,
    385    proxy.port(),
    386    0,
    387    "/.well-known/masque/udp/{target_host}/{target_port}/",
    388    proxyAuth
    389  );
    390  pps.registerFilter(filter, 10);
    391 
    392  registerCleanupFunction(async () => {
    393    await proxy.stop();
    394    await server.stop();
    395  });
    396 
    397  // Create multiple concurrent requests through the tunnel
    398  const promises = [];
    399  for (let i = 1; i <= 3; i++) {
    400    let chan = makeChan(
    401      `${server.protocol()}://alt1.example.com:${server.port()}/concurrent${i}`
    402    );
    403    promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL));
    404  }
    405 
    406  const results = await Promise.all(promises);
    407 
    408  // Verify all requests succeeded with correct responses
    409  for (let i = 0; i < 3; i++) {
    410    const [req, buf] = results[i];
    411    Assert.equal(req.status, Cr.NS_OK);
    412    Assert.equal(buf, `response${i + 1}`);
    413  }
    414 
    415  let h3Port = server.port();
    416  console.log(`h3Port = ${h3Port}`);
    417 
    418  Services.prefs.setCharPref(
    419    "network.http.http3.alt-svc-mapping-for-testing",
    420    `alt1.example.com;h3=:${h3Port}`
    421  );
    422 
    423  let chan = makeChan(`https://alt1.example.com:${h3Port}/concurrent1`);
    424  let [req] = await channelOpenPromise(
    425    chan,
    426    CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
    427  );
    428  Assert.equal(req.status, Cr.NS_OK);
    429  Assert.equal(req.responseStatus, 200);
    430 
    431  await proxy.stop();
    432  pps.unregisterFilter(filter);
    433  await server.stop();
    434 }
    435 
    436 /**
    437 * Helper function to open a WebSocket connection
    438 */
    439 async function wsChannelOpenPromise(url, msg) {
    440  let conn = new WebSocketConnection();
    441  let statusObj = await Promise.race([conn.open(url), conn.finished()]);
    442  if (statusObj && statusObj.status != Cr.NS_OK) {
    443    return [statusObj.status, ""];
    444  }
    445  let finalStatusPromise = conn.finished();
    446  conn.send(msg);
    447  let res = await conn.receiveMessages();
    448  conn.close();
    449  let finalStatus = await finalStatusPromise;
    450  return [finalStatus.status, res];
    451 }
    452 
    453 /**
    454 * Test WebSocket through H3 proxy using H2 WebSocket server
    455 */
    456 async function test_http_connect_websocket() {
    457  info("Running test_http_connect_websocket");
    458  Services.prefs.setBoolPref("network.http.http2.websockets", true);
    459 
    460  let wss = new NodeWebSocketHttp2Server();
    461  await wss.start();
    462 
    463  registerCleanupFunction(async () => {
    464    await wss.stop();
    465  });
    466 
    467  Assert.notEqual(wss.port(), null);
    468  await wss.registerMessageHandler((data, ws) => {
    469    ws.send(data);
    470  });
    471 
    472  let url = `wss://alt1.example.com:${wss.port()}`;
    473  const msg = "test h2 websocket through h3 proxy";
    474  let [status, res] = await wsChannelOpenPromise(url, msg);
    475  Assert.equal(status, Cr.NS_OK);
    476  Assert.deepEqual(res, [msg]);
    477 
    478  // Test multiple messages
    479  let conn = new WebSocketConnection();
    480  await conn.open(url);
    481  conn.send("message1");
    482  let mess1 = await conn.receiveMessages();
    483  Assert.deepEqual(mess1, ["message1"]);
    484 
    485  conn.send("message2");
    486  conn.send("message3");
    487  let mess2 = [];
    488  while (mess2.length < 2) {
    489    mess2 = mess2.concat(await conn.receiveMessages());
    490  }
    491  Assert.deepEqual(mess2, ["message2", "message3"]);
    492 
    493  conn.close();
    494  let { status: finalStatus } = await conn.finished();
    495  Assert.equal(finalStatus, Cr.NS_OK);
    496 
    497  await wss.stop();
    498 }
    499 
    500 async function test_inner_connection_fallback() {
    501  info("Running test_inner_connection_fallback");
    502  let h3Port = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE");
    503  info(`h3Port = ${h3Port}`);
    504 
    505  // Register the connect-udp proxy.
    506  pps.registerFilter(proxyFilter, 10);
    507 
    508  let server = new NodeHTTPSServer();
    509  await server.start(h3Port);
    510 
    511  // Register multiple endpoints
    512  await server.registerPathHandler("/concurrent1", (req, resp) => {
    513    resp.writeHead(200);
    514    resp.end("fallback1");
    515  });
    516  await server.registerPathHandler("/concurrent2", (req, resp) => {
    517    resp.writeHead(200);
    518    resp.end("fallback2");
    519  });
    520  await server.registerPathHandler("/concurrent3", (req, resp) => {
    521    resp.writeHead(200);
    522    resp.end("fallback3");
    523  });
    524  registerCleanupFunction(async () => {
    525    await server.stop();
    526  });
    527 
    528  Services.prefs.setCharPref(
    529    "network.http.http3.alt-svc-mapping-for-testing",
    530    `alt1.example.com;h3=:${h3Port}`
    531  );
    532 
    533  // Create multiple concurrent requests through the tunnel
    534  const promises = [];
    535  for (let i = 1; i <= 3; i++) {
    536    let chan = makeChan(
    537      `${server.protocol()}://alt1.example.com:${h3Port}/concurrent${i}`
    538    );
    539    promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL));
    540  }
    541 
    542  const results = await Promise.all(promises);
    543 
    544  // Verify all requests succeeded with correct responses
    545  for (let i = 0; i < 3; i++) {
    546    const [req, buf] = results[i];
    547    Assert.equal(req.status, Cr.NS_OK);
    548    Assert.equal(buf, `fallback${i + 1}`);
    549  }
    550  await server.stop();
    551 }