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