tor-browser

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

test_lna_captive_portal.js (10592B)


      1 "use strict";
      2 
      3 const { HttpServer } = ChromeUtils.importESModule(
      4  "resource://testing-common/httpd.sys.mjs"
      5 );
      6 
      7 let httpserver = null;
      8 let lnaServer = null;
      9 
     10 ChromeUtils.defineLazyGetter(this, "cpURI", function () {
     11  return (
     12    "http://localhost:" + httpserver.identity.primaryPort + "/captive.html"
     13  );
     14 });
     15 
     16 ChromeUtils.defineLazyGetter(this, "LNA_URL", function () {
     17  return "http://localhost:" + lnaServer.identity.primaryPort + "/test";
     18 });
     19 
     20 const SUCCESS_STRING =
     21  '<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>';
     22 let cpResponse = SUCCESS_STRING;
     23 
     24 function captivePortalHandler(metadata, response) {
     25  response.setHeader("Content-Type", "text/html");
     26  response.bodyOutputStream.write(cpResponse, cpResponse.length);
     27 }
     28 
     29 function lnaHandler(metadata, response) {
     30  response.setStatusLine(metadata.httpVersion, 200, "OK");
     31  let body = "success";
     32  response.bodyOutputStream.write(body, body.length);
     33 }
     34 
     35 const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled";
     36 const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode";
     37 const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL";
     38 const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval";
     39 const PREF_CAPTIVE_MAXTIME = "network.captive-portal-service.maxInterval";
     40 const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost";
     41 
     42 const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
     43  Ci.nsICaptivePortalService
     44 );
     45 
     46 function makeChannel(url, triggeringPrincipalURI = null) {
     47  let uri = NetUtil.newURI(url);
     48  var principal = Services.scriptSecurityManager.createContentPrincipal(
     49    uri,
     50    {}
     51  );
     52 
     53  var triggeringPrincipal;
     54  if (triggeringPrincipalURI) {
     55    let triggeringURI = NetUtil.newURI(triggeringPrincipalURI);
     56    triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal(
     57      triggeringURI,
     58      {}
     59    );
     60  } else {
     61    let triggeringURI = NetUtil.newURI("https://public.example.com");
     62    triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal(
     63      triggeringURI,
     64      {}
     65    );
     66  }
     67 
     68  return NetUtil.newChannel({
     69    uri: url,
     70    loadingPrincipal: principal,
     71    triggeringPrincipal,
     72    securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
     73    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
     74  }).QueryInterface(Ci.nsIHttpChannel);
     75 }
     76 
     77 add_setup(async function () {
     78  // Setup captive portal detection server
     79  httpserver = new HttpServer();
     80  httpserver.registerPathHandler("/captive.html", captivePortalHandler);
     81  httpserver.start(-1);
     82 
     83  // Setup LNA target server
     84  lnaServer = new HttpServer();
     85  lnaServer.registerPathHandler("/test", lnaHandler);
     86  lnaServer.start(-1);
     87 
     88  // Configure captive portal service
     89  Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, cpURI);
     90  Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 50);
     91  Services.prefs.setIntPref(PREF_CAPTIVE_MAXTIME, 100);
     92  Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true);
     93  Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true);
     94 
     95  // Configure LNA blocking
     96  Services.prefs.setBoolPref("network.lna.blocking", true);
     97  Services.prefs.setBoolPref("network.localhost.prompt.testing", true);
     98  Services.prefs.setBoolPref("network.localnetwork.prompt.testing", true);
     99 
    100  registerCleanupFunction(async () => {
    101    Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED);
    102    Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE);
    103    Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT);
    104    Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME);
    105    Services.prefs.clearUserPref(PREF_CAPTIVE_MAXTIME);
    106    Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST);
    107    Services.prefs.clearUserPref("network.lna.blocking");
    108    Services.prefs.clearUserPref("network.localhost.prompt.testing");
    109    Services.prefs.clearUserPref("network.localnetwork.prompt.testing");
    110    Services.prefs.clearUserPref("network.localhost.prompt.testing.allow");
    111    Services.prefs.clearUserPref("network.localnetwork.prompt.testing.allow");
    112    Services.prefs.clearUserPref("network.lna.address_space.private.override");
    113 
    114    await new Promise(resolve => {
    115      httpserver.stop(resolve);
    116    });
    117    await new Promise(resolve => {
    118      lnaServer.stop(resolve);
    119    });
    120  });
    121 });
    122 
    123 function observerPromise(topic) {
    124  return new Promise(resolve => {
    125    let observer = {
    126      QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
    127      observe(aSubject, aTopic, aData) {
    128        if (aTopic == topic) {
    129          Services.obs.removeObserver(observer, topic);
    130          resolve(aData);
    131        }
    132      },
    133    };
    134    Services.obs.addObserver(observer, topic);
    135  });
    136 }
    137 
    138 add_task(async function test_localnetwork_blocked_without_captive_portal() {
    139  // Override address space to treat this localhost:port as Private (local network)
    140  Services.prefs.setCharPref(
    141    "network.lna.address_space.private.override",
    142    "127.0.0.1:" + lnaServer.identity.primaryPort
    143  );
    144 
    145  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    146  Services.prefs.setBoolPref(
    147    "network.localnetwork.prompt.testing.allow",
    148    false
    149  );
    150 
    151  let chan = makeChannel(LNA_URL);
    152  chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
    153 
    154  await new Promise(resolve => {
    155    chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
    156  });
    157 
    158  Assert.equal(
    159    chan.status,
    160    Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    161    "Request should be blocked when captive portal is not active"
    162  );
    163  Services.prefs.clearUserPref("network.lna.address_space.private.override");
    164 });
    165 
    166 add_task(async function test_localnetwork_allowed_with_captive_portal() {
    167  // Override address space to treat this localhost:port as Private (local network)
    168  Services.prefs.setCharPref(
    169    "network.lna.address_space.private.override",
    170    "127.0.0.1:" + lnaServer.identity.primaryPort
    171  );
    172  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    173  Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    174 
    175  // Start captive portal service and wait for it to detect "no captive portal"
    176  let notification = observerPromise("network:captive-portal-connectivity");
    177  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    178  await notification;
    179  Assert.equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
    180 
    181  // Trigger captive portal detection (locked state)
    182  cpResponse = "captive portal page";
    183  notification = observerPromise("captive-portal-login");
    184  cps.recheckCaptivePortal();
    185  await notification;
    186  Assert.equal(
    187    cps.state,
    188    Ci.nsICaptivePortalService.LOCKED_PORTAL,
    189    "Captive portal should be in LOCKED_PORTAL state"
    190  );
    191 
    192  // Set prompt to deny - but it should still succeed because captive portal is active
    193  Services.prefs.setBoolPref(
    194    "network.localnetwork.prompt.testing.allow",
    195    false
    196  );
    197 
    198  let chan = makeChannel(LNA_URL);
    199  chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
    200 
    201  await new Promise(resolve => {
    202    chan.asyncOpen(new ChannelListener(resolve, null, 0));
    203  });
    204 
    205  Assert.equal(
    206    chan.status,
    207    Cr.NS_OK,
    208    "Request should succeed when captive portal is active (locked)"
    209  );
    210 
    211  // Cleanup: unlock the captive portal
    212  cpResponse = SUCCESS_STRING;
    213  notification = observerPromise("captive-portal-login-success");
    214  cps.recheckCaptivePortal();
    215  await notification;
    216  Assert.equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
    217 
    218  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    219  Services.prefs.clearUserPref("network.lna.address_space.private.override");
    220 });
    221 
    222 add_task(async function test_localhost_blocked_during_captive_portal() {
    223  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    224  Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    225 
    226  // Start captive portal service and wait for it to detect "no captive portal"
    227  let notification = observerPromise("network:captive-portal-connectivity");
    228  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    229  await notification;
    230  Assert.equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
    231 
    232  // Trigger captive portal detection (locked state)
    233  cpResponse = "captive portal page";
    234  notification = observerPromise("captive-portal-login");
    235  cps.recheckCaptivePortal();
    236  await notification;
    237  Assert.equal(
    238    cps.state,
    239    Ci.nsICaptivePortalService.LOCKED_PORTAL,
    240    "Captive portal should be in LOCKED_PORTAL state"
    241  );
    242 
    243  // Set prompt to deny localhost access
    244  Services.prefs.setBoolPref("network.localhost.prompt.testing.allow", false);
    245 
    246  // Create a separate localhost server (without private override)
    247  // This will be treated as Local address space, not Private
    248  let localhostServer = new HttpServer();
    249  localhostServer.registerPathHandler("/test", lnaHandler);
    250  localhostServer.start(-1);
    251 
    252  let localhostURL =
    253    "http://localhost:" + localhostServer.identity.primaryPort + "/test";
    254 
    255  let chan = makeChannel(localhostURL);
    256  chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
    257 
    258  await new Promise(resolve => {
    259    chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
    260  });
    261 
    262  Assert.equal(
    263    chan.status,
    264    Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    265    "Localhost access should be blocked even when captive portal is active"
    266  );
    267 
    268  // Cleanup
    269  await new Promise(resolve => {
    270    localhostServer.stop(resolve);
    271  });
    272 
    273  // Unlock the captive portal
    274  cpResponse = SUCCESS_STRING;
    275  notification = observerPromise("captive-portal-login-success");
    276  cps.recheckCaptivePortal();
    277  await notification;
    278  Assert.equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
    279 
    280  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    281 });
    282 
    283 add_task(
    284  async function test_localnetwork_blocked_after_captive_portal_unlocked() {
    285    Services.prefs.setCharPref(
    286      "network.lna.address_space.private.override",
    287      "127.0.0.1:" + lnaServer.identity.primaryPort
    288    );
    289 
    290    Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    291    Services.prefs.setBoolPref(
    292      "network.localnetwork.prompt.testing.allow",
    293      false
    294    );
    295 
    296    Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    297 
    298    let chan = makeChannel(LNA_URL);
    299    chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
    300 
    301    await new Promise(resolve => {
    302      chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
    303    });
    304 
    305    Assert.equal(
    306      chan.status,
    307      Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    308      "Request should be blocked again when captive portal is no longer active"
    309    );
    310    Services.prefs.clearUserPref("network.lna.address_space.private.override");
    311  }
    312 );