Bug 1395886 Add support for proxy authentication draft
authorIan MacLeod <rubberdonkeysandwich@gmail.com>
Tue, 30 Jan 2018 00:17:08 -0800
changeset 748785 17ad51c59f5b83c05f0bcb9ff114a6fd988fcbd2
parent 748520 9a3b6d64a64b328ed0de3d6503b99f20d1c94cfb
push id97229
push userbmo:rubberdonkeysandwich@gmail.com
push dateTue, 30 Jan 2018 10:21:17 +0000
bugs1395886
milestone60.0a1
Bug 1395886 Add support for proxy authentication MozReview-Commit-ID: 45TseZ2wozB
testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
testing/marionette/server.js
testing/marionette/session.js
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
@@ -3,16 +3,18 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 from marionette_driver import errors
 
 from marionette_harness import MarionetteTestCase
 
+from time import sleep
+
 
 class TestProxyCapabilities(MarionetteTestCase):
 
     def setUp(self):
         super(TestProxyCapabilities, self).setUp()
 
         self.marionette.delete_session()
 
@@ -63,16 +65,40 @@ 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)
+        sleep(3)
+        self.marionette.switch_to_alert().accept()
+        sleep(3)
+        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/server.js
+++ b/testing/marionette/server.js
@@ -265,16 +265,23 @@ const RECOMMENDED_PREFS = new Map([
   ["security.fileuri.strict_origin_policy", false],
 
   // Tests do not wait for the notification button security delay
   ["security.notification_enable_delay", 0],
 
   // Ensure blocklist updates do not hit the network
   ["services.settings.server", "http://%(server)s/dummy/blocklist/"],
 
+  // Prevent popup for proxy authentication when a login is stored
+  ["signon.autologin.proxy", true],
+  ["network.negotiate-auth.allow-proxies", false],
+  ["network.proxy.share_proxy_settings", false],
+  ["network.automatic-ntlm-auth.allow-proxies", false],
+  ["network.auth.use-sspi", false],
+
   // Do not automatically fill sign-in forms with known usernames and
   // passwords
   ["signon.autofillForms", false],
 
   // Disable password capture, so that tests that include forms are not
   // influenced by the presence of the persistent doorhanger notification
   ["signon.rememberSignons", false],
 
--- a/testing/marionette/session.js
+++ b/testing/marionette/session.js
@@ -3,16 +3,19 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.importGlobalProperties(["URL"]);
 
+Cu.import("resource://gre/modules/Log.jsm");
+const logger = Log.repository.getLogger("Marionette");
+
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.import("chrome://marionette/content/assert.js");
 const {
   InvalidArgumentError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 const {
@@ -125,16 +128,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) {
+        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;
@@ -142,30 +160,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) {
@@ -224,39 +245,44 @@ session.Proxy = class {
         url = new URL("http://" + host);
         if (url.port == "") {
           url = new URL("https://" + host);
         }
       } catch (e) {
         throw new InvalidArgumentError(e.message);
       }
 
-      let hostname = stripBracketsFromIpv6Hostname(url.hostname);
-
       // If the port hasn't been set, use the default port of
       // the selected scheme (except for socks which doesn't have one).
       let port = parseInt(url.port);
       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, "", "");
+      }
+
+      let hostname = stripBracketsFromIpv6Hostname(url.hostname);
+
+      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}`);
@@ -275,23 +301,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.httpLogin] = 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}`);
@@ -324,17 +350,22 @@ 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);