tor-browser

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

test_captive_portal_service.js (11157B)


      1 "use strict";
      2 
      3 const { HttpServer } = ChromeUtils.importESModule(
      4  "resource://testing-common/httpd.sys.mjs"
      5 );
      6 
      7 let httpserver = null;
      8 ChromeUtils.defineLazyGetter(this, "cpURI", function () {
      9  return (
     10    "http://localhost:" + httpserver.identity.primaryPort + "/captive.html"
     11  );
     12 });
     13 
     14 const SUCCESS_STRING =
     15  '<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>';
     16 let cpResponse = SUCCESS_STRING;
     17 function contentHandler(metadata, response) {
     18  response.setHeader("Content-Type", "text/html");
     19  response.bodyOutputStream.write(cpResponse, cpResponse.length);
     20 }
     21 
     22 const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled";
     23 const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode";
     24 const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL";
     25 const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval";
     26 const PREF_CAPTIVE_MAXTIME = "network.captive-portal-service.maxInterval";
     27 const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost";
     28 
     29 const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
     30  Ci.nsICaptivePortalService
     31 );
     32 
     33 registerCleanupFunction(async () => {
     34  Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED);
     35  Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE);
     36  Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT);
     37  Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME);
     38  Services.prefs.clearUserPref(PREF_CAPTIVE_MAXTIME);
     39  Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST);
     40 
     41  await new Promise(resolve => {
     42    httpserver.stop(resolve);
     43  });
     44 });
     45 
     46 function observerPromise(topic) {
     47  return new Promise(resolve => {
     48    let observer = {
     49      QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
     50      observe(aSubject, aTopic, aData) {
     51        if (aTopic == topic) {
     52          Services.obs.removeObserver(observer, topic);
     53          resolve(aData);
     54        }
     55      },
     56    };
     57    Services.obs.addObserver(observer, topic);
     58  });
     59 }
     60 
     61 add_task(function setup() {
     62  httpserver = new HttpServer();
     63  httpserver.registerPathHandler("/captive.html", contentHandler);
     64  httpserver.start(-1);
     65 
     66  Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, cpURI);
     67  Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 50);
     68  Services.prefs.setIntPref(PREF_CAPTIVE_MAXTIME, 100);
     69  Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true);
     70  Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true);
     71 });
     72 
     73 add_task(async function test_simple() {
     74  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
     75 
     76  equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
     77 
     78  let notification = observerPromise("network:captive-portal-connectivity");
     79  // The service is started by nsIOService when the pref becomes true.
     80  // We might want to add a method to do this in the future.
     81  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
     82 
     83  let observerPayload = await notification;
     84  equal(observerPayload, "clear");
     85  equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
     86 
     87  cpResponse = "other";
     88  notification = observerPromise("captive-portal-login");
     89  cps.recheckCaptivePortal();
     90  await notification;
     91  equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
     92 
     93  cpResponse = SUCCESS_STRING;
     94  notification = observerPromise("captive-portal-login-success");
     95  cps.recheckCaptivePortal();
     96  await notification;
     97  equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
     98 });
     99 
    100 // This test redirects to another URL which returns the same content.
    101 // It should still be interpreted as a captive portal.
    102 add_task(async function test_redirect_success() {
    103  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    104  equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    105 
    106  httpserver.registerPathHandler("/succ.txt", (metadata, response) => {
    107    response.setHeader("Content-Type", "text/html");
    108    response.bodyOutputStream.write(cpResponse, cpResponse.length);
    109  });
    110  httpserver.registerPathHandler("/captive.html", (metadata, response) => {
    111    response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
    112    response.setHeader(
    113      "Location",
    114      `http://localhost:${httpserver.identity.primaryPort}/succ.txt`
    115    );
    116  });
    117 
    118  let notification = observerPromise("captive-portal-login").then(
    119    () => "login"
    120  );
    121  let succNotif = observerPromise("network:captive-portal-connectivity").then(
    122    () => "connectivity"
    123  );
    124  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    125 
    126  let winner = await Promise.race([notification, succNotif]);
    127  equal(winner, "login", "This should have been a login, not a success");
    128  equal(
    129    cps.state,
    130    Ci.nsICaptivePortalService.LOCKED_PORTAL,
    131    "Should be locked after redirect to same text"
    132  );
    133 });
    134 
    135 // This redirects to another URI with a different content.
    136 // We check that it triggers a captive portal login
    137 add_task(async function test_redirect_bad() {
    138  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    139  equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    140 
    141  httpserver.registerPathHandler("/bad.txt", (metadata, response) => {
    142    response.setHeader("Content-Type", "text/html");
    143    response.bodyOutputStream.write("bad", "bad".length);
    144  });
    145 
    146  httpserver.registerPathHandler("/captive.html", (metadata, response) => {
    147    response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
    148    response.setHeader(
    149      "Location",
    150      `http://localhost:${httpserver.identity.primaryPort}/bad.txt`
    151    );
    152  });
    153 
    154  let notification = observerPromise("captive-portal-login");
    155  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    156 
    157  await notification;
    158  equal(
    159    cps.state,
    160    Ci.nsICaptivePortalService.LOCKED_PORTAL,
    161    "Should be locked after redirect to bad text"
    162  );
    163 });
    164 
    165 // This redirects to the same URI.
    166 // We check that it triggers a captive portal login
    167 add_task(async function test_redirect_loop() {
    168  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    169  equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    170 
    171  // This is actually a redirect loop
    172  httpserver.registerPathHandler("/captive.html", (metadata, response) => {
    173    response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
    174    response.setHeader("Location", cpURI);
    175  });
    176 
    177  let notification = observerPromise("captive-portal-login");
    178  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    179 
    180  await notification;
    181  equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
    182 });
    183 
    184 // This redirects to a https URI.
    185 // We check that it triggers a captive portal login
    186 add_task(async function test_redirect_https() {
    187  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    188  equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    189 
    190  let h2Port = Services.env.get("MOZHTTP2_PORT");
    191  Assert.notEqual(h2Port, null);
    192  Assert.notEqual(h2Port, "");
    193 
    194  // Any kind of redirection should trigger the captive portal login.
    195  httpserver.registerPathHandler("/captive.html", (metadata, response) => {
    196    response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
    197    response.setHeader("Location", `https://foo.example.com:${h2Port}/exit`);
    198  });
    199 
    200  let notification = observerPromise("captive-portal-login");
    201  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    202 
    203  await notification;
    204  equal(
    205    cps.state,
    206    Ci.nsICaptivePortalService.LOCKED_PORTAL,
    207    "Should be locked after redirect to https"
    208  );
    209  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    210 });
    211 
    212 // This test uses a 511 status code to request a captive portal login
    213 // We check that it triggers a captive portal login
    214 // See RFC 6585 for details
    215 add_task(async function test_511_error() {
    216  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    217  equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    218 
    219  httpserver.registerPathHandler("/captive.html", (metadata, response) => {
    220    response.setStatusLine(
    221      metadata.httpVersion,
    222      511,
    223      "Network Authentication Required"
    224    );
    225    cpResponse = '<meta http-equiv="refresh" content="0;url=/login">';
    226    contentHandler(metadata, response);
    227  });
    228 
    229  let notification = observerPromise("captive-portal-login");
    230  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    231 
    232  await notification;
    233  equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
    234 });
    235 
    236 // Any other 5xx HTTP error, is assumed to be an issue with the
    237 // canonical web server, and should not trigger a captive portal login
    238 add_task(async function test_generic_5xx_error() {
    239  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    240  equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    241 
    242  let requests = 0;
    243  httpserver.registerPathHandler("/captive.html", (metadata, response) => {
    244    if (requests++ === 0) {
    245      // on first attempt, send 503 error
    246      response.setStatusLine(
    247        metadata.httpVersion,
    248        503,
    249        "Internal Server Error"
    250      );
    251      cpResponse = "<h1>Internal Server Error</h1>";
    252    } else {
    253      // on retry, send canonical reply
    254      cpResponse = SUCCESS_STRING;
    255    }
    256    contentHandler(metadata, response);
    257  });
    258 
    259  let notification = observerPromise("network:captive-portal-connectivity");
    260  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    261 
    262  await notification;
    263  equal(requests, 2);
    264  equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
    265 });
    266 
    267 add_task(async function test_changed_notification() {
    268  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    269  equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
    270 
    271  httpserver.registerPathHandler("/captive.html", contentHandler);
    272  cpResponse = SUCCESS_STRING;
    273 
    274  let changedNotificationCount = 0;
    275  let observer = {
    276    QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
    277    observe() {
    278      changedNotificationCount += 1;
    279    },
    280  };
    281  Services.obs.addObserver(
    282    observer,
    283    "network:captive-portal-connectivity-changed"
    284  );
    285 
    286  let notification = observerPromise(
    287    "network:captive-portal-connectivity-changed"
    288  );
    289  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
    290  await notification;
    291  equal(changedNotificationCount, 1);
    292  equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
    293 
    294  notification = observerPromise("network:captive-portal-connectivity");
    295  cps.recheckCaptivePortal();
    296  await notification;
    297  equal(changedNotificationCount, 1);
    298  equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
    299 
    300  notification = observerPromise("captive-portal-login");
    301  cpResponse = "you are captive";
    302  cps.recheckCaptivePortal();
    303  await notification;
    304  equal(changedNotificationCount, 1);
    305  equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
    306 
    307  notification = observerPromise("captive-portal-login-success");
    308  cpResponse = SUCCESS_STRING;
    309  cps.recheckCaptivePortal();
    310  await notification;
    311  equal(changedNotificationCount, 2);
    312  equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
    313 
    314  notification = observerPromise("captive-portal-login");
    315  cpResponse = "you are captive";
    316  cps.recheckCaptivePortal();
    317  await notification;
    318  equal(changedNotificationCount, 2);
    319  equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
    320 
    321  Services.obs.removeObserver(
    322    observer,
    323    "network:captive-portal-connectivity-changed"
    324  );
    325 });