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 });