Bug 1545159 implement captivePortal api r=rpl,valentin
authorShane Caraveo <scaraveo@mozilla.com>
Wed, 08 May 2019 18:40:08 +0000
changeset 534989 57598d1d033f5086f6f53def5960761757fa28fe
parent 534988 23e432bdaf62c88b10c45a4953a8ea6206f5c9bf
child 534990 eb384663078998538a5af4dfa7f45972fe29da49
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrpl, valentin
bugs1545159
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1545159 implement captivePortal api r=rpl,valentin Differential Revision: https://phabricator.services.mozilla.com/D29602
browser/components/extensions/ext-browser.json
toolkit/components/extensions/jar.mn
toolkit/components/extensions/parent/ext-captivePortal.js
toolkit/components/extensions/schemas/captive_portal.json
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/test/xpcshell/test_ext_captivePortal.js
toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -19,16 +19,24 @@
   "browsingData": {
     "url": "chrome://browser/content/parent/ext-browsingData.js",
     "schema": "chrome://browser/content/schemas/browsing_data.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["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": [],
     "events": ["update", "uninstall"],
     "schema": "chrome://browser/content/schemas/chrome_settings_overrides.json",
     "manifest": ["chrome_settings_overrides"]
   },
   "commands": {
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -5,16 +5,19 @@
 toolkit.jar:
 % content extensions %content/extensions/
     content/extensions/dummy.xul
     content/extensions/ext-browser-content.js
     content/extensions/ext-toolkit.json
     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)
     content/extensions/parent/ext-cookies.js (parent/ext-cookies.js)
     content/extensions/parent/ext-dns.js (parent/ext-dns.js)
     content/extensions/parent/ext-downloads.js (parent/ext-downloads.js)
     content/extensions/parent/ext-extension.js (parent/ext-extension.js)
 #ifndef ANDROID
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-captivePortal.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(),
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/captive_portal.json
@@ -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"
+          }
+        ]
+      }
+    ]
+  }
+]
\ No newline at end of file
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -1,16 +1,19 @@
 # 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/.
 
 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
     content/extensions/schemas/cookies.json
     content/extensions/schemas/dns.json
     content/extensions/schemas/downloads.json
     content/extensions/schemas/events.json
     content/extensions/schemas/experiments.json
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_captivePortal.js
@@ -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();
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -424,16 +424,17 @@ add_task(async function test_alreadyGran
   await extension.unload();
 });
 
 // IMPORTANT: Do not change this list without review from a Web Extensions peer!
 
 const GRANTED_WITHOUT_USER_PROMPT = [
   "activeTab",
   "alarms",
+  "captivePortal",
   "contextMenus",
   "contextualIdentities",
   "cookies",
   "geckoProfiler",
   "identity",
   "idle",
   "menus",
   "menus.overrideContext",
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -16,16 +16,20 @@ skip-if = os == "android" # Android does
 [test_ext_background_sub_windows.js]
 [test_ext_background_teardown.js]
 [test_ext_background_telemetry.js]
 [test_ext_background_window_properties.js]
 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]
 skip-if = (os == "win" && debug) #Bug 1485567
 [test_ext_contentscript_api_injection.js]
 [test_ext_contentscript_async_loading.js]
 skip-if = os == 'android' && debug # The generated script takes too long to load on Android debug
 [test_ext_contentscript_context.js]