Bug 1545159 implement captivePortal api r=rpl,valentin

Differential Revision: https://phabricator.services.mozilla.com/D29602

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Shane Caraveo 2019-05-08 18:40:08 +00:00
Родитель 97f51aaa75
Коммит 2cbc937bf2
8 изменённых файлов: 269 добавлений и 0 удалений

Просмотреть файл

@ -24,6 +24,14 @@
["browsingData"]
]
},
"captivePortal": {
"url": "chrome://extensions/content/parent/ext-captivePortal.js",
"schema": "chrome://extensions/content/schemas/captive_portal.json",
"scopes": ["addon_parent"],
"paths": [
["captivePortal"]
]
},
"chrome_settings_overrides": {
"url": "chrome://browser/content/parent/ext-chrome-settings-overrides.js",
"scopes": [],

Просмотреть файл

@ -10,6 +10,9 @@ toolkit.jar:
content/extensions/parent/ext-alarms.js (parent/ext-alarms.js)
content/extensions/parent/ext-backgroundPage.js (parent/ext-backgroundPage.js)
content/extensions/parent/ext-browserSettings.js (parent/ext-browserSettings.js)
#ifndef ANDROID
content/extensions/parent/ext-captivePortal.js (parent/ext-captivePortal.js)
#endif
content/extensions/parent/ext-contentScripts.js (parent/ext-contentScripts.js)
content/extensions/parent/ext-contextualIdentities.js (parent/ext-contextualIdentities.js)
content/extensions/parent/ext-clipboard.js (parent/ext-clipboard.js)

Просмотреть файл

@ -0,0 +1,83 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
XPCOMUtils.defineLazyServiceGetter(this, "gCPS",
"@mozilla.org/network/captive-portal-service;1",
"nsICaptivePortalService");
XPCOMUtils.defineLazyPreferenceGetter(this, "gCaptivePortalEnabled",
"network.captive-portal-service.enabled",
false);
function nameForCPSState(state) {
switch (state) {
case gCPS.UNKNOWN: return "unknown";
case gCPS.NOT_CAPTIVE: return "not_captive";
case gCPS.UNLOCKED_PORTAL: return "unlocked_portal";
case gCPS.LOCKED_PORTAL: return "locked_portal";
default: return "unknown";
}
}
var {
ExtensionError,
} = ExtensionUtils;
this.captivePortal = class extends ExtensionAPI {
getAPI(context) {
function checkEnabled() {
if (!gCaptivePortalEnabled) {
throw new ExtensionError("Captive Portal detection is not enabled");
}
}
return {
captivePortal: {
getState() {
checkEnabled();
return nameForCPSState(gCPS.state);
},
getLastChecked() {
checkEnabled();
return gCPS.lastChecked;
},
onStateChanged: new EventManager({
context,
name: "captivePortal.onStateChanged",
register: fire => {
checkEnabled();
let observer = (subject, topic) => {
fire.async({state: nameForCPSState(gCPS.state)});
};
Services.obs.addObserver(observer, "ipc:network:captive-portal-set-state");
return () => {
Services.obs.removeObserver(observer, "ipc:network:captive-portal-set-state");
};
},
}).api(),
onConnectivityAvailable: new EventManager({
context,
name: "captivePortal.onConnectivityAvailable",
register: fire => {
checkEnabled();
let observer = (subject, topic, data) => {
fire.async({status: data});
};
Services.obs.addObserver(observer, "network:captive-portal-connectivity");
return () => {
Services.obs.removeObserver(observer, "network:captive-portal-connectivity");
};
},
}).api(),
},
};
}
};

Просмотреть файл

@ -0,0 +1,69 @@
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": [
"captivePortal"
]
}]
}
]
},
{
"namespace": "captivePortal",
"description": "This API provides the ability detect the captive portal state of the users connection.",
"permissions": ["captivePortal"],
"functions": [
{
"name": "getState",
"type": "function",
"description": "Returns the current portal state, one of `unknown`, `not_captive`, `unlocked_portal`, `locked_portal`.",
"async": true,
"parameters": []
},
{
"name": "getLastChecked",
"type": "function",
"description": "Returns the time difference between NOW and the last time a request was completed in milliseconds.",
"async": true,
"parameters": []
}
],
"events": [
{
"name": "onStateChanged",
"type": "function",
"description": "Fired when the captive portal state changes.",
"parameters": [
{
"type": "object",
"name": "details",
"properties": {
"state": {
"type": "string",
"enum": ["unknown", "not_captive", "unlocked_portal", "locked_portal"],
"description": "The current captive portal state."
}
}
}
]
},
{
"name": "onConnectivityAvailable",
"type": "function",
"description": "This notification will be emitted when the captive portal service has determined that we can connect to the internet. The service will pass either `captive` if there is an unlocked captive portal present, or `clear` if no captive portal was detected.",
"parameters": [
{
"name": "status",
"enum": ["captive", "clear"],
"type": "string"
}
]
}
]
}
]

Просмотреть файл

@ -6,6 +6,9 @@ toolkit.jar:
% content extensions %content/extensions/
content/extensions/schemas/alarms.json
content/extensions/schemas/browser_settings.json
#ifndef ANDROID
content/extensions/schemas/captive_portal.json
#endif
content/extensions/schemas/clipboard.json
content/extensions/schemas/content_scripts.json
content/extensions/schemas/contextual_identities.json

Просмотреть файл

@ -0,0 +1,98 @@
"use strict";
/**
* This duplicates the test from netwerk/test/unit/test_captive_portal_service.js
* however using an extension to gather the captive portal information.
*/
PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled";
const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode";
const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval";
const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL";
const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost";
const SUCCESS_STRING = "success\n";
let cpResponse = SUCCESS_STRING;
const httpserver = createHttpServer();
httpserver.registerPathHandler("/captive.txt", (request, response) => {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "text/plain");
response.write(cpResponse);
});
registerCleanupFunction(() => {
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_DNS_NATIVE_IS_LOCALHOST);
});
add_task(function setup() {
Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, `http://localhost:${httpserver.identity.primaryPort}/captive.txt`);
Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true);
Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 0);
Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true);
});
add_task(async function test_captivePortal_basic() {
let cps = Cc["@mozilla.org/network/captive-portal-service;1"]
.getService(Ci.nsICaptivePortalService);
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["captivePortal"],
},
isPrivileged: true,
async background() {
browser.captivePortal.onConnectivityAvailable.addListener(details => {
browser.test.log(`onConnectivityAvailable received ${JSON.stringify(details)}`);
browser.test.sendMessage("connectivity", details);
});
browser.captivePortal.onStateChanged.addListener(details => {
browser.test.log(`onStateChanged received ${JSON.stringify(details)}`);
browser.test.sendMessage("state", details);
});
browser.test.onMessage.addListener(async msg => {
if (msg == "getstate") {
browser.test.sendMessage("getstate", await browser.captivePortal.getState());
}
});
browser.test.assertEq("unknown", await browser.captivePortal.getState(), "initial state unknown");
},
});
await extension.startup();
// The captive portal service is started by nsIOService when the pref becomes true, so we
// toggle the pref. We cannot set to false before the extension loads above.
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
let details = await extension.awaitMessage("connectivity");
equal(details.status, "clear", "initial connectivity");
extension.sendMessage("getstate");
details = await extension.awaitMessage("getstate");
equal(details, "not_captive", "initial state");
info("REFRESH to other");
cpResponse = "other";
cps.recheckCaptivePortal();
details = await extension.awaitMessage("state");
equal(details.state, "locked_portal", "state in portal");
info("REFRESH to success");
cpResponse = SUCCESS_STRING;
cps.recheckCaptivePortal();
details = await extension.awaitMessage("connectivity");
equal(details.status, "captive", "final connectivity");
details = await extension.awaitMessage("state");
equal(details.state, "unlocked_portal", "state after unlocking portal");
await extension.unload();
});

Просмотреть файл

@ -429,6 +429,7 @@ add_task(async function test_alreadyGranted() {
const GRANTED_WITHOUT_USER_PROMPT = [
"activeTab",
"alarms",
"captivePortal",
"contextMenus",
"contextualIdentities",
"cookies",

Просмотреть файл

@ -21,6 +21,10 @@ skip-if = os == "android"
[test_ext_browserSettings.js]
[test_ext_browserSettings_homepage.js]
skip-if = appname == "thunderbird" || os == "android"
[test_ext_captivePortal.js]
# As with test_captive_portal_service.js, we use the same limits here.
skip-if = os == "android" # CP service is disabled on Android
run-sequentially = node server exceptions dont replay well
[test_ext_cookieBehaviors.js]
[test_ext_cookies_samesite.js]
[test_ext_content_security_policy.js]