Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

"use strict";
const { HttpServer } = ChromeUtils.importESModule(
);
let httpserver = null;
let lnaServer = null;
ChromeUtils.defineLazyGetter(this, "cpURI", function () {
return (
"http://localhost:" + httpserver.identity.primaryPort + "/captive.html"
);
});
ChromeUtils.defineLazyGetter(this, "LNA_URL", function () {
return "http://localhost:" + lnaServer.identity.primaryPort + "/test";
});
const SUCCESS_STRING =
'<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>';
let cpResponse = SUCCESS_STRING;
function captivePortalHandler(metadata, response) {
response.setHeader("Content-Type", "text/html");
response.bodyOutputStream.write(cpResponse, cpResponse.length);
}
function lnaHandler(metadata, response) {
response.setStatusLine(metadata.httpVersion, 200, "OK");
let body = "success";
response.bodyOutputStream.write(body, body.length);
}
const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled";
const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode";
const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL";
const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval";
const PREF_CAPTIVE_MAXTIME = "network.captive-portal-service.maxInterval";
const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost";
const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
Ci.nsICaptivePortalService
);
function makeChannel(url, triggeringPrincipalURI = null) {
let uri = NetUtil.newURI(url);
var principal = Services.scriptSecurityManager.createContentPrincipal(
uri,
{}
);
var triggeringPrincipal;
if (triggeringPrincipalURI) {
let triggeringURI = NetUtil.newURI(triggeringPrincipalURI);
triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal(
triggeringURI,
{}
);
} else {
let triggeringURI = NetUtil.newURI("https://public.example.com");
triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal(
triggeringURI,
{}
);
}
return NetUtil.newChannel({
uri: url,
loadingPrincipal: principal,
triggeringPrincipal,
securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
}).QueryInterface(Ci.nsIHttpChannel);
}
add_setup(async function () {
// Setup captive portal detection server
httpserver = new HttpServer();
httpserver.registerPathHandler("/captive.html", captivePortalHandler);
httpserver.start(-1);
// Setup LNA target server
lnaServer = new HttpServer();
lnaServer.registerPathHandler("/test", lnaHandler);
lnaServer.start(-1);
// Configure captive portal service
Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, cpURI);
Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 50);
Services.prefs.setIntPref(PREF_CAPTIVE_MAXTIME, 100);
Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true);
Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true);
// Configure LNA blocking
Services.prefs.setBoolPref("network.lna.blocking", true);
Services.prefs.setBoolPref("network.localhost.prompt.testing", true);
Services.prefs.setBoolPref("network.localnetwork.prompt.testing", true);
registerCleanupFunction(async () => {
Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED);
Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE);
Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT);
Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME);
Services.prefs.clearUserPref(PREF_CAPTIVE_MAXTIME);
Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST);
Services.prefs.clearUserPref("network.lna.blocking");
Services.prefs.clearUserPref("network.localhost.prompt.testing");
Services.prefs.clearUserPref("network.localnetwork.prompt.testing");
Services.prefs.clearUserPref("network.localhost.prompt.testing.allow");
Services.prefs.clearUserPref("network.localnetwork.prompt.testing.allow");
Services.prefs.clearUserPref("network.lna.address_space.private.override");
await new Promise(resolve => {
httpserver.stop(resolve);
});
await new Promise(resolve => {
lnaServer.stop(resolve);
});
});
});
function observerPromise(topic) {
return new Promise(resolve => {
let observer = {
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
observe(aSubject, aTopic, aData) {
if (aTopic == topic) {
Services.obs.removeObserver(observer, topic);
resolve(aData);
}
},
};
Services.obs.addObserver(observer, topic);
});
}
add_task(async function test_localnetwork_blocked_without_captive_portal() {
// Override address space to treat this localhost:port as Private (local network)
Services.prefs.setCharPref(
"network.lna.address_space.private.override",
"127.0.0.1:" + lnaServer.identity.primaryPort
);
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
Services.prefs.setBoolPref(
"network.localnetwork.prompt.testing.allow",
false
);
let chan = makeChannel(LNA_URL);
chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
await new Promise(resolve => {
chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
});
Assert.equal(
chan.status,
Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
"Request should be blocked when captive portal is not active"
);
Services.prefs.clearUserPref("network.lna.address_space.private.override");
});
add_task(async function test_localnetwork_allowed_with_captive_portal() {
// Override address space to treat this localhost:port as Private (local network)
Services.prefs.setCharPref(
"network.lna.address_space.private.override",
"127.0.0.1:" + lnaServer.identity.primaryPort
);
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
// Start captive portal service and wait for it to detect "no captive portal"
let notification = observerPromise("network:captive-portal-connectivity");
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
await notification;
Assert.equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
// Trigger captive portal detection (locked state)
cpResponse = "captive portal page";
notification = observerPromise("captive-portal-login");
cps.recheckCaptivePortal();
await notification;
Assert.equal(
cps.state,
Ci.nsICaptivePortalService.LOCKED_PORTAL,
"Captive portal should be in LOCKED_PORTAL state"
);
// Set prompt to deny - but it should still succeed because captive portal is active
Services.prefs.setBoolPref(
"network.localnetwork.prompt.testing.allow",
false
);
let chan = makeChannel(LNA_URL);
chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
await new Promise(resolve => {
chan.asyncOpen(new ChannelListener(resolve, null, 0));
});
Assert.equal(
chan.status,
Cr.NS_OK,
"Request should succeed when captive portal is active (locked)"
);
// Cleanup: unlock the captive portal
cpResponse = SUCCESS_STRING;
notification = observerPromise("captive-portal-login-success");
cps.recheckCaptivePortal();
await notification;
Assert.equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
Services.prefs.clearUserPref("network.lna.address_space.private.override");
});
add_task(async function test_localhost_blocked_during_captive_portal() {
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
// Start captive portal service and wait for it to detect "no captive portal"
let notification = observerPromise("network:captive-portal-connectivity");
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
await notification;
Assert.equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
// Trigger captive portal detection (locked state)
cpResponse = "captive portal page";
notification = observerPromise("captive-portal-login");
cps.recheckCaptivePortal();
await notification;
Assert.equal(
cps.state,
Ci.nsICaptivePortalService.LOCKED_PORTAL,
"Captive portal should be in LOCKED_PORTAL state"
);
// Set prompt to deny localhost access
Services.prefs.setBoolPref("network.localhost.prompt.testing.allow", false);
// Create a separate localhost server (without private override)
// This will be treated as Local address space, not Private
let localhostServer = new HttpServer();
localhostServer.registerPathHandler("/test", lnaHandler);
localhostServer.start(-1);
let localhostURL =
"http://localhost:" + localhostServer.identity.primaryPort + "/test";
let chan = makeChannel(localhostURL);
chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
await new Promise(resolve => {
chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
});
Assert.equal(
chan.status,
Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
"Localhost access should be blocked even when captive portal is active"
);
// Cleanup
await new Promise(resolve => {
localhostServer.stop(resolve);
});
// Unlock the captive portal
cpResponse = SUCCESS_STRING;
notification = observerPromise("captive-portal-login-success");
cps.recheckCaptivePortal();
await notification;
Assert.equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
});
add_task(
async function test_localnetwork_blocked_after_captive_portal_unlocked() {
Services.prefs.setCharPref(
"network.lna.address_space.private.override",
"127.0.0.1:" + lnaServer.identity.primaryPort
);
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
Services.prefs.setBoolPref(
"network.localnetwork.prompt.testing.allow",
false
);
Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
let chan = makeChannel(LNA_URL);
chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
await new Promise(resolve => {
chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
});
Assert.equal(
chan.status,
Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
"Request should be blocked again when captive portal is no longer active"
);
Services.prefs.clearUserPref("network.lna.address_space.private.override");
}
);