Bug 1395886 Add support for proxy authentication draft
authorIan MacLeod <rubberdonkeysandwich@gmail.com>
Sun, 18 Mar 2018 21:58:53 -0700
changeset 769423 296a10db52527e01fa880da1a8f1f864a3f0635e
parent 769198 0b997836c7cf258a2b821eaa1c1ee66b9289ab17
push id103122
push userbmo:rubberdonkeysandwich@gmail.com
push dateMon, 19 Mar 2018 15:56:17 +0000
bugs1395886
milestone61.0a1
Bug 1395886 Add support for proxy authentication MozReview-Commit-ID: BDghp5uro35
testing/marionette/client/marionette_driver/geckoinstance.py
testing/marionette/components/marionette.js
testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
testing/marionette/session.js
--- a/testing/marionette/client/marionette_driver/geckoinstance.py
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -117,16 +117,19 @@ class GeckoInstance(object):
 
         # Ensure blocklist updates don't hit the network
         "services.settings.server": "http://%(server)s/dummy/blocklist/",
 
         # Disable password capture, so that tests that include forms aren"t
         # influenced by the presence of the persistent doorhanger notification
         "signon.rememberSignons": False,
 
+        # Prevent popup for proxy authentication when a login is stored
+        "signon.autologin.proxy": True,
+
         # Prevent starting into safe mode after application crashes
         "toolkit.startup.max_resumed_crashes": -1,
 
         # We want to collect telemetry, but we don't want to send in the results
         "toolkit.telemetry.server": "https://%(server)s/dummy/telemetry/",
 
         # Enabling the support for File object creation in the content process.
         "dom.file.createInChild": True,
--- a/testing/marionette/components/marionette.js
+++ b/testing/marionette/components/marionette.js
@@ -253,16 +253,19 @@ const RECOMMENDED_PREFS = new Map([
 
   // Ensure blocklist updates do not hit the network
   ["services.settings.server", "http://%(server)s/dummy/blocklist/"],
 
   // Do not automatically fill sign-in forms with known usernames and
   // passwords
   ["signon.autofillForms", false],
 
+  // Prevent popup for proxy authentication when a login is stored
+  ["signon.autologin.proxy", true],
+
   // Disable password capture, so that tests that include forms are not
   // influenced by the presence of the persistent doorhanger notification
   ["signon.rememberSignons", false],
 
   // Disable first-run welcome page
   ["startup.homepage_welcome_url", "about:blank"],
   ["startup.homepage_welcome_url.additional", ""],
 
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
@@ -63,16 +63,36 @@ class TestProxyCapabilities(MarionetteTe
             "socksProxy": proxy_hostname,
             "socksVersion": 4,
         }}
 
         self.marionette.start_session(capabilities)
         self.assertEqual(self.marionette.session_capabilities["proxy"],
                          capabilities["proxy"])
 
+    def test_proxy_type_manual_with_authentication(self):
+        proxy_hostname = "104.236.137.24"
+        username = "username"
+        password = "password"
+        capabilities = {"proxy": {
+            "proxyType": "manual",
+            "ftpProxy": "{}:{}@{}:3128".format(username, password, proxy_hostname),
+            "httpProxy": "{}:{}@{}:3128".format(username, password, proxy_hostname),
+            "sslProxy": "{}:{}@{}:3128".format(username, password, proxy_hostname),
+        }}
+
+        self.marionette.start_session(capabilities)
+        self.assertEqual(self.marionette.session_capabilities["proxy"],
+                         capabilities["proxy"])
+
+        url = "https://www.google.com/search?q=my+ip+address"
+        self.marionette.navigate(url)
+        self.assertEqual(self.marionette.get_url(), url)
+        self.assertIn("104.236.137.24", self.marionette.page_source)
+
     def test_proxy_type_manual_socks_requires_version(self):
         proxy_port = 4444
         proxy_hostname = "marionette.test"
         proxy_host = "{}:{}".format(proxy_hostname, proxy_port)
         capabilities = {"proxy": {
             "proxyType": "manual",
             "socksProxy": proxy_host,
         }}
--- a/testing/marionette/session.js
+++ b/testing/marionette/session.js
@@ -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/. */
 
 "use strict";
 
 Cu.importGlobalProperties(["URL"]);
 
+ChromeUtils.import("resource://gre/modules/Log.jsm");
+const logger = Log.repository.getLogger("Marionette");
+
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.import("chrome://marionette/content/assert.js");
 const {
   InvalidArgumentError,
 } = ChromeUtils.import("chrome://marionette/content/error.js", {});
 const {
@@ -123,16 +126,31 @@ session.Proxy = class {
    * Sets Firefox proxy settings.
    *
    * @return {boolean}
    *     True if proxy settings were updated as a result of calling this
    *     function, or false indicating that this function acted as
    *     a no-op.
    */
   init() {
+    function addProxyLogin(login) {
+      if (!login) return;
+      let logins = Services.logins.findLogins(
+        {}, login.hostname, "", login.httpRealm, {});
+      if (logins.length) {
+        if (login.username != logins[0].username ||
+            login.password != logins[0].password) {
+          // Throw exception here?
+          logger.debug("Cannot add two different logins for the same host");
+        }
+      } else {
+        Services.logins.addLogin(login);
+      }
+    }
+
     switch (this.proxyType) {
       case "autodetect":
         Preferences.set("network.proxy.type", 4);
         return true;
 
       case "direct":
         Preferences.set("network.proxy.type", 0);
         return true;
@@ -140,30 +158,33 @@ session.Proxy = class {
       case "manual":
         Preferences.set("network.proxy.type", 1);
 
         if (this.ftpProxy) {
           Preferences.set("network.proxy.ftp", this.ftpProxy);
           if (Number.isInteger(this.ftpProxyPort)) {
             Preferences.set("network.proxy.ftp_port", this.ftpProxyPort);
           }
+          addProxyLogin(this.ftpLogin);
         }
 
         if (this.httpProxy) {
           Preferences.set("network.proxy.http", this.httpProxy);
           if (Number.isInteger(this.httpProxyPort)) {
             Preferences.set("network.proxy.http_port", this.httpProxyPort);
           }
+          addProxyLogin(this.httpLogin);
         }
 
         if (this.sslProxy) {
           Preferences.set("network.proxy.ssl", this.sslProxy);
           if (Number.isInteger(this.sslProxyPort)) {
             Preferences.set("network.proxy.ssl_port", this.sslProxyPort);
           }
+          addProxyLogin(this.sslLogin);
         }
 
         if (this.socksProxy) {
           Preferences.set("network.proxy.socks", this.socksProxy);
           if (Number.isInteger(this.socksProxyPort)) {
             Preferences.set("network.proxy.socks_port", this.socksProxyPort);
           }
           if (this.socksVersion) {
@@ -235,26 +256,38 @@ session.Proxy = class {
       if (!Number.isInteger(port)) {
         if (scheme === "socks") {
           port = null;
         } else {
           port = Services.io.getProtocolHandler(scheme).defaultPort;
         }
       }
 
-      if (url.username != "" ||
-          url.password != "" ||
-          url.pathname != "/" ||
+      let login = null;
+      if (url.username !== "") {
+        login = Cc["@mozilla.org/login-manager/loginInfo;1"]
+          .createInstance(Ci.nsILoginInfo);
+        login.init(
+            `moz-proxy://${url.hostname}:${port}`,
+            null,
+            "Squid proxy-caching web server",
+            url.username,
+            url.password,
+            "",
+            "");
+      }
+
+      if (url.pathname != "/" ||
           url.search != "" ||
           url.hash != "") {
         throw new InvalidArgumentError(
-            `${host} was not of the form host[:port]`);
+            `${host} was not of the form [username:password@]host[:port]`);
       }
 
-      return [hostname, port];
+      return [hostname, port, login];
     }
 
     let p = new session.Proxy();
     if (typeof json == "undefined" || json === null) {
       return p;
     }
 
     assert.object(json, pprint`Expected "proxy" to be an object, got ${json}`);
@@ -273,23 +306,23 @@ session.Proxy = class {
       case "pac":
         p.proxyAutoconfigUrl = assert.string(json.proxyAutoconfigUrl,
             `Expected "proxyAutoconfigUrl" to be a string, ` +
             pprint`got ${json.proxyAutoconfigUrl}`);
         break;
 
       case "manual":
         if (typeof json.ftpProxy != "undefined") {
-          [p.ftpProxy, p.ftpProxyPort] = fromHost("ftp", json.ftpProxy);
+          [p.ftpProxy, p.ftpProxyPort, p.ftpLogin] = fromHost("ftp", json.ftpProxy);
         }
         if (typeof json.httpProxy != "undefined") {
-          [p.httpProxy, p.httpProxyPort] = fromHost("http", json.httpProxy);
+          [p.httpProxy, p.httpProxyPort, p.ftpLogin] = fromHost("http", json.httpProxy);
         }
         if (typeof json.sslProxy != "undefined") {
-          [p.sslProxy, p.sslProxyPort] = fromHost("https", json.sslProxy);
+          [p.sslProxy, p.sslProxyPort, p.sslLogin] = fromHost("https", json.sslProxy);
         }
         if (typeof json.socksProxy != "undefined") {
           [p.socksProxy, p.socksProxyPort] = fromHost("socks", json.socksProxy);
           p.socksVersion = assert.positiveInteger(json.socksVersion);
         }
         if (typeof json.noProxy != "undefined") {
           let entries = assert.array(json.noProxy,
               pprint`Expected "noProxy" to be an array, got ${json.noProxy}`);
@@ -322,17 +355,27 @@ session.Proxy = class {
       if (!hostname) {
         return null;
       }
 
       // Add brackets around IPv6 addresses
       hostname = addBracketsToIpv6Hostname(hostname);
 
       if (port != null) {
-        return `${hostname}:${port}`;
+        hostname = `${hostname}:${port}`;
+      }
+
+      let logins = Services.logins.findLogins(
+          {},
+          `moz-proxy://${hostname}`,
+          null,
+          "Squid proxy-caching web server",
+          {});
+      if (logins.length) {
+        hostname = `${logins[0].username}:${logins[0].password}@${hostname}`;
       }
 
       return hostname;
     }
 
     let excludes = this.noProxy;
     if (excludes) {
       excludes = excludes.map(addBracketsToIpv6Hostname);