Bug 1429177 - Policy: Set network proxy settings. r=mixedpuppy draft
authorFelipe Gomes <felipc@gmail.com>
Thu, 15 Mar 2018 17:45:35 -0300
changeset 768266 16bf9fa7787dade1fa3d0c7d474f4ced56f679b8
parent 767005 deb7714a7bcd3448952440e92d0209abec6b886d
push id102825
push userfelipc@gmail.com
push dateThu, 15 Mar 2018 20:46:12 +0000
reviewersmixedpuppy
bugs1429177
milestone61.0a1
Bug 1429177 - Policy: Set network proxy settings. r=mixedpuppy MozReview-Commit-ID: KPiz6fdwKc0
browser/components/enterprisepolicies/Policies.jsm
browser/components/enterprisepolicies/helpers/ProxyPolicies.jsm
browser/components/enterprisepolicies/helpers/moz.build
browser/components/enterprisepolicies/helpers/sample_proxy.json
browser/components/enterprisepolicies/schemas/policies-schema.json
browser/components/enterprisepolicies/tests/browser/browser.ini
browser/components/enterprisepolicies/tests/browser/browser_policy_proxy.js
browser/components/preferences/connection.js
toolkit/components/extensions/ext-browserSettings.js
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -7,16 +7,17 @@
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "gXulStore",
                                    "@mozilla.org/xul/xulstore;1",
                                    "nsIXULStore");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BookmarksPolicies: "resource:///modules/policies/BookmarksPolicies.jsm",
+  ProxyPolicies: "resource:///modules/policies/ProxyPolicies.jsm",
 });
 
 const PREF_LOGLEVEL           = "browser.policies.loglevel";
 const BROWSER_DOCUMENT_URL    = "chrome://browser/content/browser.xul";
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
   return new ConsoleAPI({
@@ -276,16 +277,23 @@ var Policies = {
   },
 
   "Popups": {
     onBeforeUIStartup(manager, param) {
       addAllowDenyPermissions("popup", param.Allow, null);
     }
   },
 
+  "Proxy": {
+    onBeforeAddons(manager, param) {
+      manager.disallowFeature("changeProxySettings");
+      ProxyPolicies.configureProxySettings(param, setAndLockPref);
+    }
+  },
+
   "RememberPasswords": {
     onBeforeUIStartup(manager, param) {
       setAndLockPref("signon.rememberSignons", param);
     }
   },
 };
 
 /*
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/helpers/ProxyPolicies.jsm
@@ -0,0 +1,110 @@
+/* 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";
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Cu.importGlobalProperties(["URL"]);
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
+  return new ConsoleAPI({
+    prefix: "ProxyPolicies.jsm",
+    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+    // messages during development. See LOG_LEVELS in Console.jsm for details.
+    maxLogLevel: "error",
+    maxLogLevelPref: PREF_LOGLEVEL,
+  });
+});
+
+// Don't use const here because this is acessed by
+// tests through the BackstagePass object.
+var PROXY_TYPES_MAP = new Map([
+  ["none", Ci.nsIProtocolProxyService.PROXYCONFIG_DIRECT],
+  ["system", Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM],
+  ["manual", Ci.nsIProtocolProxyService.PROXYCONFIG_MANUAL],
+  ["autoDetect", Ci.nsIProtocolProxyService.PROXYCONFIG_WPAD],
+  ["autoConfig", Ci.nsIProtocolProxyService.PROXYCONFIG_PAC],
+]);
+
+var EXPORTED_SYMBOLS = [ "ProxyPolicies" ];
+
+var ProxyPolicies = {
+  configureProxySettings(param, setAndLockPref) {
+    if (param.Mode) {
+      setAndLockPref("network.proxy.type", PROXY_TYPES_MAP.get(param.Mode));
+    }
+
+    if (param.AutoConfigURL) {
+      setAndLockPref("network.proxy.autoconfig_url", param.AutoConfigURL.spec);
+    }
+
+    if (param.UseProxyForDNS !== undefined) {
+      setAndLockPref("network.proxy.socks_remote_dns", param.UseProxyForDNS);
+    }
+
+    if (param.AutoLogin !== undefined) {
+      setAndLockPref("signon.autologin.proxy", param.AutoLogin);
+    }
+
+    if (param.SOCKSVersion !== undefined) {
+      if (param.SOCKSVersion != 4 && param.SOCKSVersion != 5) {
+        log.error("Invalid SOCKS version");
+      } else {
+        setAndLockPref("network.proxy.socks_version", param.SOCKSVersion);
+      }
+    }
+
+    if (param.Passthrough !== undefined) {
+      setAndLockPref("network.proxy.no_proxies_on", param.Passthrough);
+    }
+
+    if (param.UseHTTPProxyForAllProtocols !== undefined) {
+      setAndLockPref("network.proxy.share_proxy_settings", param.UseHTTPProxyForAllProtocols);
+    }
+
+    function setProxyHostAndPort(type, address) {
+      let url;
+      try {
+        // Prepend https just so we can use the URL parser
+        // instead of parsing manually.
+        url = new URL(`https://${address}`);
+      } catch (e) {
+        log.error(`Invalid address for ${type} proxy: ${address}`);
+        return;
+      }
+
+      setAndLockPref(`network.proxy.${type}`, url.hostname);
+      if (url.port) {
+        setAndLockPref(`network.proxy.${type}_port`, Number(url.port));
+      }
+    }
+
+    if (param.HTTPProxy) {
+      setProxyHostAndPort("http", param.HTTPProxy);
+
+      // network.proxy.share_proxy_settings is a UI feature, not handled by the
+      // network code. That pref only controls if the checkbox is checked, and
+      // then we must manually set the other values.
+      if (param.UseHTTPProxyForAllProtocols) {
+        param.FTPProxy = param.SSLProxy = param.SOCKSProxy = param.HTTPProxy;
+      }
+    }
+
+    if (param.FTPProxy) {
+      setProxyHostAndPort("ftp", param.FTPProxy);
+    }
+
+    if (param.SSLProxy) {
+      setProxyHostAndPort("ssl", param.SSLProxy);
+    }
+
+    if (param.SOCKSProxy) {
+      setProxyHostAndPort("socks", param.SOCKSProxy);
+    }
+  }
+};
--- a/browser/components/enterprisepolicies/helpers/moz.build
+++ b/browser/components/enterprisepolicies/helpers/moz.build
@@ -4,9 +4,10 @@
 # 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/.
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "Enterprise Policies")
 
 EXTRA_JS_MODULES.policies += [
     'BookmarksPolicies.jsm',
+    'ProxyPolicies.jsm',
 ]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/helpers/sample_proxy.json
@@ -0,0 +1,12 @@
+{
+  "policies": {
+    "Proxy": {
+      "Mode": "manual",
+      "HTTPProxy": "www.example.com:42",
+      "UseHTTPProxyForAllProtocols": true,
+      "Passthrough": "foo, bar, baz",
+      "SOCKSVersion": 4,
+      "UseProxyForDNS": true
+    }
+  }
+}
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -256,16 +256,70 @@
           "type": "array",
           "items": {
             "type": "origin"
           }
         }
       }
     },
 
+    "Proxy": {
+      "description": "Configure Proxy settings.",
+      "first_available": "60.0",
+
+      "type": "object",
+      "properties": {
+        "Mode": {
+          "type": "string",
+          "enum": ["none", "system", "manual", "autoDetect", "autoConfig"]
+        },
+
+        "AutoConfigURL": {
+          "type": "URLorEmpty"
+        },
+
+        "FTPProxy": {
+          "type": "string"
+        },
+
+        "HTTPProxy": {
+          "type": "string"
+        },
+
+        "SSLProxy": {
+          "type": "string"
+        },
+
+        "SOCKSProxy": {
+          "type": "string"
+        },
+
+        "SOCKSVersion": {
+          "type": "number",
+          "enum": [4, 5]
+        },
+
+        "UseHTTPProxyForAllProtocols": {
+          "type": "boolean"
+        },
+
+        "Passthrough": {
+          "type": "string"
+        },
+
+        "UseProxyForDNS": {
+          "type": "boolean"
+        },
+
+        "AutoLogin": {
+          "type": "boolean"
+        }
+      }
+    },
+
     "RememberPasswords": {
       "description": "Enforces the setting to allow Firefox to remember saved logins and passwords. Both true and false values are accepted.",
       "first_available": "60.0",
 
       "type": "boolean"
     }
   }
 }
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -28,10 +28,11 @@ support-files =
 [browser_policy_disable_fxaccounts.js]
 [browser_policy_disable_fxscreenshots.js]
 [browser_policy_disable_masterpassword.js]
 [browser_policy_disable_pocket.js]
 [browser_policy_disable_privatebrowsing.js]
 [browser_policy_disable_shield.js]
 [browser_policy_display_bookmarks.js]
 [browser_policy_display_menu.js]
+[browser_policy_proxy.js]
 [browser_policy_remember_passwords.js]
 [browser_policy_set_homepage.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_proxy.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function checkPref(prefName, expectedValue) {
+  let prefType, prefValue;
+  switch (typeof(expectedValue)) {
+    case "boolean":
+      prefType = Services.prefs.PREF_BOOL;
+      prefValue = Services.prefs.getBoolPref(prefName);
+      break;
+
+    case "number":
+      prefType = Services.prefs.PREF_INT;
+      prefValue = Services.prefs.getIntPref(prefName);
+      break;
+
+    case "string":
+      prefType = Services.prefs.PREF_STRING;
+      prefValue = Services.prefs.getStringPref(prefName);
+      break;
+  }
+
+  ok(Services.prefs.prefIsLocked(prefName), `Pref ${prefName} is correctly locked`);
+  is(Services.prefs.getPrefType(prefName), prefType, `Pref ${prefName} has the correct type`);
+  is(prefValue, expectedValue, `Pref ${prefName} has the correct value`);
+}
+
+add_task(async function test_proxy_modes() {
+  // Checks that every Mode value translates correctly to the expected pref value
+  let { PROXY_TYPES_MAP } = ChromeUtils.import("resource:///modules/policies/ProxyPolicies.jsm", {});
+  for (let [mode, expectedValue] of PROXY_TYPES_MAP) {
+    await setupPolicyEngineWithJson({
+      "policies": {
+        "Proxy": {
+          "Mode": mode
+        }
+      }
+    });
+    checkPref("network.proxy.type", expectedValue);
+  }
+});
+
+add_task(async function test_proxy_boolean_settings() {
+  // Tests that both false and true values are correctly set and locked
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "UseProxyForDNS": false,
+        "AutoLogin": false,
+      }
+    }
+  });
+
+  checkPref("network.proxy.socks_remote_dns", false);
+  checkPref("signon.autologin.proxy", false);
+
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "UseProxyForDNS": true,
+        "AutoLogin": true,
+      }
+    }
+  });
+
+  checkPref("network.proxy.socks_remote_dns", true);
+  checkPref("signon.autologin.proxy", true);
+});
+
+add_task(async function test_proxy_socks_and_passthrough() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "SOCKSVersion": 4,
+        "Passthrough": "a, b, c"
+      }
+    }
+  });
+
+  checkPref("network.proxy.socks_version", 4);
+  checkPref("network.proxy.no_proxies_on", "a, b, c");
+});
+
+add_task(async function test_proxy_addresses() {
+  function checkProxyPref(proxytype, address, port) {
+    checkPref(`network.proxy.${proxytype}`, address);
+    checkPref(`network.proxy.${proxytype}_port`, port);
+  }
+
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "HTTPProxy": "http.proxy.example.com:10",
+        "FTPProxy": "ftp.proxy.example.com:20",
+        "SSLProxy": "ssl.proxy.example.com:30",
+        "SOCKSProxy": "socks.proxy.example.com:40",
+      }
+    }
+  });
+
+  checkProxyPref("http", "http.proxy.example.com", 10);
+  checkProxyPref("ftp", "ftp.proxy.example.com", 20);
+  checkProxyPref("ssl", "ssl.proxy.example.com", 30);
+  checkProxyPref("socks", "socks.proxy.example.com", 40);
+
+  // Do the same, but now use the UseHTTPProxyForAllProtocols option
+  // and check that it takes effect.
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "HTTPProxy": "http.proxy.example.com:10",
+        "FTPProxy": "ftp.proxy.example.com:20",
+        "SSLProxy": "ssl.proxy.example.com:30",
+        "SOCKSProxy": "socks.proxy.example.com:40",
+        "UseHTTPProxyForAllProtocols": true
+      }
+    }
+  });
+
+
+  checkProxyPref("http", "http.proxy.example.com", 10);
+  checkProxyPref("ftp", "http.proxy.example.com", 10);
+  checkProxyPref("ssl", "http.proxy.example.com", 10);
+  checkProxyPref("socks", "http.proxy.example.com", 10);
+});
--- a/browser/components/preferences/connection.js
+++ b/browser/components/preferences/connection.js
@@ -256,17 +256,17 @@ var gConnectionsDialog = {
     let isLocked = API_PROXY_PREFS.some(
       pref => Services.prefs.prefIsLocked(pref));
 
     function setInputsDisabledState(isControlled) {
       let disabled = isLocked || isControlled;
       for (let element of gConnectionsDialog.getProxyControls()) {
         element.disabled = disabled;
       }
-      if (!isControlled) {
+      if (!isLocked) {
         gConnectionsDialog.proxyTypeChanged();
       }
     }
 
     if (isLocked) {
       // An extension can't control this setting if any pref is locked.
       hideControllingExtension(PROXY_KEY);
       setInputsDisabledState(false);
--- a/toolkit/components/extensions/ext-browserSettings.js
+++ b/toolkit/components/extensions/ext-browserSettings.js
@@ -305,16 +305,21 @@ this.browserSettings = class extends Ext
           ),
           {
             set: details => {
               if (AppConstants.platform === "android") {
                 throw new ExtensionError(
                   "proxyConfig is not supported on android.");
               }
 
+              if (!Services.policies.isAllowed("changeProxySettings")) {
+                throw new ExtensionError(
+                  "Proxy settings are being managed by the Policies manager.");
+              }
+
               let value = details.value;
 
               if (!PROXY_TYPES_MAP.has(value.proxyType)) {
                 throw new ExtensionError(
                   `${value.proxyType} is not a valid value for proxyType.`);
               }
 
               for (let prop of ["http", "ftp", "ssl", "socks"]) {