Bug 1369827 - Make proxy port an optional suffix for the host. draft
authorHenrik Skupin <mail@hskupin.info>
Fri, 18 Aug 2017 14:49:00 +0200
changeset 655951 35c7af22ff9332b1de6fac660efe74531ac73df2
parent 654393 c4e6d6364c2a050b4a580827d6d7f695b1c7608c
child 655952 eebb247b1e36ca0c673ec0c73a7700c28d35ecdd
push id77013
push userbmo:hskupin@gmail.com
push dateWed, 30 Aug 2017 15:59:42 +0000
bugs1369827
milestone57.0a1
Bug 1369827 - Make proxy port an optional suffix for the host. The WebDriver spec has been changed a while ago in regard of how proxy capabilities are getting specified. It means that the port is no longer its own key but an optional suffix for each of the ftpProxy, httpProxy, sslProxy, and socksProxy keys. MozReview-Commit-ID: zdYnVZSf09
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py
testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
testing/marionette/session.js
testing/marionette/test_session.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -767,32 +767,33 @@ GeckoDriver.prototype.listeningPromise =
  *     If, for whatever reason, a session could not be created.
  */
 GeckoDriver.prototype.newSession = async function(cmd, resp) {
   if (this.sessionID) {
     throw new SessionNotCreatedError("Maximum number of active sessions");
   }
   this.sessionID = element.generateUUID();
   this.newSessionCommandId = cmd.id;
+
   try {
     this.capabilities = session.Capabilities.fromJSON(cmd.parameters);
+
+    if (!this.secureTLS) {
+      logger.warn("TLS certificate errors will be ignored for this session");
+      let acceptAllCerts = new cert.InsecureSweepingOverride();
+      cert.installOverride(acceptAllCerts);
+    }
+
+    if (this.proxy.init()) {
+      logger.info("Proxy settings initialised: " + JSON.stringify(this.proxy));
+    }
   } catch (e) {
     throw new SessionNotCreatedError(e);
   }
 
-  if (!this.secureTLS) {
-    logger.warn("TLS certificate errors will be ignored for this session");
-    let acceptAllCerts = new cert.InsecureSweepingOverride();
-    cert.installOverride(acceptAllCerts);
-  }
-
-  if (this.proxy.init()) {
-    logger.info("Proxy settings initialised: " + JSON.stringify(this.proxy));
-  }
-
   // If we are testing accessibility with marionette, start a11y service in
   // chrome first. This will ensure that we do not have any content-only
   // services hanging around.
   if (this.a11yChecks && accessibility.service) {
     logger.info("Preemptively starting accessibility service in Chrome");
   }
 
   let registerBrowsers = this.registerPromise();
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py
@@ -114,46 +114,15 @@ class TestCapabilityMatching(MarionetteT
         self.marionette.start_session({"pageLoadStrategy": None})
         self.assertEqual(self.marionette.session_capabilities["pageLoadStrategy"], "normal")
 
         for value in ["", "EAGER", True, 42, {}, []]:
             print("invalid strategy {}".format(value))
             with self.assertRaisesRegexp(SessionNotCreatedException, "InvalidArgumentError"):
                 self.marionette.start_session({"pageLoadStrategy": value})
 
-    def test_proxy_none_by_default(self):
-        self.marionette.start_session()
-        self.assertNotIn("proxy", self.marionette.session_capabilities)
-
-    def test_invalid_proxy_type(self):
-        with self.assertRaises(SessionNotCreatedException):
-            self.marionette.start_session({"proxy": {"proxyAutoconfigUrl": None}})
-
-        with self.assertRaises(SessionNotCreatedException):
-            self.marionette.start_session({"proxy": {"proxyType": None}})
-
-    def test_proxy_type_pac_invalid_url(self):
-        with self.assertRaises(SessionNotCreatedException):
-            self.marionette.start_session({"proxy": {"proxyType": "pac"}})
-
-        with self.assertRaises(SessionNotCreatedException):
-            self.marionette.start_session({"proxy": {"proxyType": "pac",
-                                                     "proxyAutoconfigUrl": None}})
-
-    def test_proxy_type_direct(self):
-        self.marionette.start_session({"proxy": {"proxyType": "direct"}})
-        self.assertIn("proxy", self.marionette.session_capabilities)
-        self.assertEqual(self.marionette.session_capabilities["proxy"]["proxyType"], "direct")
-        self.assertEqual(self.marionette.get_pref("network.proxy.type"), 0)
-
-    def test_proxy_type_manual(self):
-        self.marionette.start_session({"proxy": {"proxyType": "manual"}})
-        self.assertIn("proxy", self.marionette.session_capabilities)
-        self.assertEqual(self.marionette.session_capabilities["proxy"]["proxyType"], "manual")
-        self.assertEqual(self.marionette.get_pref("network.proxy.type"), 1)
-
     def test_timeouts(self):
         timeouts = {u"implicit": 123, u"pageLoad": 456, u"script": 789}
         caps = {"timeouts": timeouts}
         self.marionette.start_session(caps)
         self.assertIn("timeouts", self.marionette.session_capabilities)
         self.assertDictEqual(self.marionette.session_capabilities["timeouts"], timeouts)
         self.assertDictEqual(self.marionette._send_message("getTimeouts"), timeouts)
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py
@@ -1,252 +1,98 @@
 # 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/.
 
-from marionette_driver.errors import InvalidArgumentException
+from marionette_driver import errors
 
 from marionette_harness import MarionetteTestCase
 
 
-class TestProxy(MarionetteTestCase):
+class TestProxyCapabilities(MarionetteTestCase):
 
     def setUp(self):
-        super(TestProxy, self).setUp()
-        self.marionette.delete_session()
-
-    def test_that_we_can_set_a_autodetect_proxy(self):
-        capabilities = {"requiredCapabilities":
-                            {
-                                "proxy":{
-                                    "proxyType": "autodetect",
-                                }
-                            }
-                        }
-        self.marionette.start_session(capabilities)
-        result = None
-        with self.marionette.using_context('chrome'):
-            result = self.marionette.execute_script("""return {
-                "proxyType" : Services.prefs.getIntPref('network.proxy.type'),
-                }
-            """)
-
-        self.assertEqual(result["proxyType"], 4)
-
-    def test_that_capabilities_returned_have_proxy_details(self):
-        capabilities = {"requiredCapabilities":
-                            {
-                                "proxy":{
-                                    "proxyType": "autodetect",
-                                    }
-                            }
-                        }
-        self.marionette.start_session(capabilities)
-        result = self.marionette.session_capabilities
-
-        self.assertEqual(result["proxy"]["proxyType"], "autodetect")
-
-    def test_that_we_can_set_a_system_proxy(self):
-        capabilities = {"requiredCapabilities":
-                            {
-                                "proxy":{
-                                    "proxyType": "system",
-                                    }
-                            }
-                       }
-        self.marionette.start_session(capabilities)
-        result = None
-        with self.marionette.using_context('chrome'):
-            result = self.marionette.execute_script("""return {
-                "proxyType" : Services.prefs.getIntPref('network.proxy.type'),
-                }
-            """)
-
-        self.assertEqual(result["proxyType"], 5)
-
-    def test_we_can_set_a_pac_proxy(self):
-        url = "http://marionette.test"
-        capabilities = {"requiredCapabilities":
-                            {
-                                "proxy":{
-                                    "proxyType": "pac",
-                                    "proxyAutoconfigUrl": url,
-                                }
-                            }
-                        }
-        self.marionette.start_session(capabilities)
-        result = None
-        with self.marionette.using_context('chrome'):
-            result = self.marionette.execute_script("""return {
-                "proxyType" : Services.prefs.getIntPref('network.proxy.type'),
-                "proxyAutoconfigUrl" : Services.prefs.getCharPref('network.proxy.autoconfig_url'),
-                }
-            """)
-
-        self.assertEqual(result["proxyType"], 2)
-        self.assertEqual(result["proxyAutoconfigUrl"], url, 'proxyAutoconfigUrl was not set')
-
-    def test_that_we_can_set_a_manual_proxy(self):
-        port = 4444
-        url = "http://marionette.test"
-        capabilities = {"requiredCapabilities":
-                            {
-                                "proxy":{
-                                    "proxyType": "manual",
-                                    "ftpProxy": url,
-                                    "ftpProxyPort": port,
-                                    "httpProxy": url,
-                                    "httpProxyPort": port,
-                                    "sslProxy": url,
-                                    "sslProxyPort": port,
-                                }
-                            }
-                        }
-        self.marionette.start_session(capabilities)
-        result = None
-        with self.marionette.using_context('chrome'):
-            result = self.marionette.execute_script("""return {
-                "proxyType" : Services.prefs.getIntPref('network.proxy.type'),
-                "httpProxy" : Services.prefs.getCharPref('network.proxy.http'),
-                "httpProxyPort": Services.prefs.getIntPref('network.proxy.http_port'),
-                "sslProxy": Services.prefs.getCharPref('network.proxy.ssl'),
-                "sslProxyPort": Services.prefs.getIntPref('network.proxy.ssl_port'),
-                "ftpProxy": Services.prefs.getCharPref('network.proxy.ftp'),
-                "ftpProxyPort": Services.prefs.getIntPref('network.proxy.ftp_port'),
-                }
-            """)
-
-        self.assertEqual(result["proxyType"], 1)
-        self.assertEqual(result["httpProxy"], url, 'httpProxy was not set')
-        self.assertEqual(result["httpProxyPort"], port, 'httpProxyPort was not set')
-        self.assertEqual(result["sslProxy"], url, 'sslProxy url was not set')
-        self.assertEqual(result["sslProxyPort"], port, 'sslProxyPort was not set')
-        self.assertEqual(result["ftpProxy"], url, 'ftpProxy was not set')
-        self.assertEqual(result["ftpProxyPort"], port, 'ftpProxyPort was not set')
+        super(TestProxyCapabilities, self).setUp()
 
-    def test_we_can_set_a_manual_proxy_with_a_socks_proxy_with_socks_version(self):
-        port = 4444
-        url = "http://marionette.test"
-        capabilities = {"requiredCapabilities":
-                            {
-                                "proxy":{
-                                    "proxyType": "manual",
-                                    "socksProxy": url,
-                                    "socksProxyPort": port,
-                                    "socksVersion": 4,
-                                    "socksUsername": "cake",
-                                    "socksPassword": "made with cake"
-                                }
-                            }
-                        }
-        self.marionette.start_session(capabilities)
-        result = None
-        with self.marionette.using_context('chrome'):
-            result = self.marionette.execute_script("""return {
-                "proxyType" : Services.prefs.getIntPref('network.proxy.type'),
-                "socksProxy" : Services.prefs.getCharPref('network.proxy.socks'),
-                "socksProxyPort": Services.prefs.getIntPref('network.proxy.socks_port'),
-                "socksVersion": Services.prefs.getIntPref('network.proxy.socks_version'),
-                }
-            """)
-        self.assertEqual(result["socksProxy"], url, 'socksProxy was not set')
-        self.assertEqual(result["socksProxyPort"], port, 'socksProxyPort was not set')
-        self.assertEqual(result["socksVersion"], 4, 'socksVersion was not set to 4')
-
-    def test_we_can_set_a_manual_proxy_with_a_socks_proxy_with_no_socks_version(self):
-        port = 4444
-        url = "http://marionette.test"
-        capabilities = {"requiredCapabilities":
-                                    {
-                                    "proxy":{
-                                        "proxyType": "manual",
-                                        "socksProxy": url,
-                                        "socksProxyPort": port,
-                                        "socksUsername": "cake",
-                                        "socksPassword": "made with cake"
-                                     }
-                                    }
-        }
-        self.marionette.start_session(capabilities)
-        result = None
-        with self.marionette.using_context('chrome'):
-            result = self.marionette.execute_script("""return {
-                "proxyType" : Services.prefs.getIntPref('network.proxy.type'),
-                "socksProxy" : Services.prefs.getCharPref('network.proxy.socks'),
-                "socksProxyPort": Services.prefs.getIntPref('network.proxy.socks_port'),
-                "socksVersion": Services.prefs.getIntPref('network.proxy.socks_version'),
-
-                }
-            """)
-        self.assertEqual(result["socksProxy"], url, 'socksProxy was not set')
-        self.assertEqual(result["socksProxyPort"], port, 'socksProxyPort was not set')
-        self.assertEqual(result["socksVersion"], 5, 'socksVersion was not set to 5')
-
-    def test_when_not_all_manual_proxy_details_are_in_capabilities(self):
-        port = 4444
-        url = "http://marionette.test"
-        capabilities = {"requiredCapabilities":
-                                    {
-                                    "proxy":{
-                                        "proxyType": "manual",
-                                        "ftpProxy": url,
-                                        "ftpProxyPort": port,
-                                     }
-                                    }
-        }
-        self.marionette.start_session(capabilities)
-        result = None
-        with self.marionette.using_context('chrome'):
-            result = self.marionette.execute_script("""return {
-                "proxyType" : Services.prefs.getIntPref('network.proxy.type'),
-                "httpProxy" : Services.prefs.getCharPref('network.proxy.http'),
-                "httpProxyPort": Services.prefs.getIntPref('network.proxy.http_port'),
-                "sslProxy": Services.prefs.getCharPref('network.proxy.ssl'),
-                "sslProxyPort": Services.prefs.getIntPref('network.proxy.ssl_port'),
-                "ftpProxy": Services.prefs.getCharPref('network.proxy.ftp'),
-                "ftpProxyPort": Services.prefs.getIntPref('network.proxy.ftp_port'),
-                }
-            """)
-
-        self.assertEqual(result["proxyType"], 1)
-        self.assertNotEqual(result["httpProxy"], url,
-                            'httpProxy was set. {}'.format(result["httpProxy"]))
-        self.assertNotEqual(result["httpProxyPort"], port, 'httpProxyPort was set')
-        self.assertNotEqual(result["sslProxy"], url, 'sslProxy url was set')
-        self.assertNotEqual(result["sslProxyPort"], port, 'sslProxyPort was set')
-        self.assertEqual(result["ftpProxy"], url, 'ftpProxy was set')
-        self.assertEqual(result["ftpProxyPort"], port, 'ftpProxyPort was set')
-
-
-
-    def test_proxy_is_a_string_should_throw_invalid_argument(self):
-        capabilities = {"requiredCapabilities":
-                                    {
-                                    "proxy":"I really should be a dictionary"
-                                    }
-                            }
-        try:
-            self.marionette.start_session(capabilities)
-            self.fail("We should have started a session because proxy should be a dict")
-        except InvalidArgumentException as e:
-            assert e.message == "Value of 'proxy' should be an object"
-
-    def test_proxy_is_passed_in_with_no_proxy_doesnt_set_it(self):
-        capabilities = {"requiredCapabilities":
-            {
-                "proxy": {"proxyType": "NOPROXY"},
-            }
-        }
-        self.marionette.start_session(capabilities)
-        result = None
-        with self.marionette.using_context('chrome'):
-            result = self.marionette.execute_script("""return {
-                "proxyType": Services.prefs.getIntPref('network.proxy.type'),
-                };
-            """)
-
-        self.assertEqual(result["proxyType"], 0)
+        self.marionette.delete_session()
 
     def tearDown(self):
         if not self.marionette.session:
             self.marionette.start_session()
-        else:
-            self.marionette.restart(clean=True)
+
+        with self.marionette.using_context("chrome"):
+            self.marionette.execute_script("""
+                Cu.import("resource://gre/modules/Preferences.jsm");
+                Preferences.resetBranch("network.proxy");
+            """)
+
+        super(TestProxyCapabilities, self).tearDown()
+
+    def test_proxy_object_none_by_default(self):
+        self.marionette.start_session()
+        self.assertNotIn("proxy", self.marionette.session_capabilities)
+
+    def test_proxy_object_in_returned_capabilities(self):
+        capabilities = {"proxy": {"proxyType": "system"}}
+
+        self.marionette.start_session(capabilities)
+        self.assertEqual(self.marionette.session_capabilities["proxy"],
+                         capabilities["proxy"])
+
+    def test_proxy_type_autodetect(self):
+        capabilities = {"proxy": {"proxyType": "autodetect"}}
+
+        self.marionette.start_session(capabilities)
+        self.assertEqual(self.marionette.session_capabilities["proxy"],
+                         capabilities["proxy"])
+
+    def test_proxy_type_direct(self):
+        capabilities = {"proxy": {"proxyType": "direct"}}
+
+        self.marionette.start_session(capabilities)
+        self.assertEqual(self.marionette.session_capabilities["proxy"],
+                         capabilities["proxy"])
+
+    def test_proxy_type_manual_without_port(self):
+        proxy_hostname = "marionette.test"
+        capabilities = {"proxy": {
+            "proxyType": "manual",
+            "ftpProxy": "{}:21".format(proxy_hostname),
+            "httpProxy": "{}:80".format(proxy_hostname),
+            "sslProxy": "{}:443".format(proxy_hostname),
+    def test_proxy_type_pac(self):
+        pac_url = "http://marionette.test"
+        capabilities = {"proxy": {"proxyType": "pac", "proxyAutoconfigUrl": pac_url}}
+
+        self.marionette.start_session(capabilities)
+        self.assertEqual(self.marionette.session_capabilities["proxy"],
+                         capabilities["proxy"])
+
+    def test_proxy_type_system(self):
+        capabilities = {"proxy": {"proxyType": "system"}}
+
+        self.marionette.start_session(capabilities)
+        self.assertEqual(self.marionette.session_capabilities["proxy"],
+                         capabilities["proxy"])
+
+    def test_invalid_proxy_object(self):
+        capabilities = {"proxy": "I really should be a dictionary"}
+
+        with self.assertRaises(errors.SessionNotCreatedException):
+            self.marionette.start_session(capabilities)
+
+    def test_missing_proxy_type(self):
+        with self.assertRaises(errors.SessionNotCreatedException):
+            self.marionette.start_session({"proxy": {"proxyAutoconfigUrl": "foobar"}})
+
+    def test_invalid_proxy_type(self):
+        capabilities = {"proxy": {"proxyType": "NOPROXY"}}
+
+        with self.assertRaises(errors.SessionNotCreatedException):
+            self.marionette.start_session(capabilities)
+
+    def test_invalid_autoconfig_url_for_pac(self):
+        with self.assertRaises(errors.SessionNotCreatedException):
+            self.marionette.start_session({"proxy": {"proxyType": "pac"}})
+
+        with self.assertRaises(errors.SessionNotCreatedException):
+            self.marionette.start_session({"proxy": {"proxyType": "pac",
+                                                     "proxyAutoconfigUrl": None}})
--- a/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
@@ -1,15 +1,16 @@
 [test_marionette.py]
 [test_cli_arguments.py]
 skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921 and bug 1322993
 [test_geckoinstance.py]
 [test_data_driven.py]
 [test_session.py]
 [test_capabilities.py]
+[test_proxy.py]
 [test_accessibility.py]
 [test_expectedfail.py]
 expected = fail
 [test_click.py]
 [test_click_chrome.py]
 skip-if = appname == 'fennec'
 [test_checkbox.py]
 [test_checkbox_chrome.py]
--- a/testing/marionette/session.js
+++ b/testing/marionette/session.js
@@ -1,16 +1,18 @@
 /* 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";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.importGlobalProperties(["URL"]);
+
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.import("chrome://marionette/content/assert.js");
 const {
   error,
   InvalidArgumentError,
@@ -103,22 +105,22 @@ session.PageLoadStrategy = {
   Normal: "normal",
 };
 
 /** Proxy configuration object representation. */
 session.Proxy = class {
   /** @class */
   constructor() {
     this.proxyType = null;
+    this.ftpProxy = null;
+    this.ftpProxyPort = null;
     this.httpProxy = null;
     this.httpProxyPort = null;
     this.sslProxy = null;
     this.sslProxyPort = null;
-    this.ftpProxy = null;
-    this.ftpProxyPort = null;
     this.socksProxy = null;
     this.socksProxyPort = null;
     this.socksVersion = null;
     this.proxyAutoconfigUrl = null;
   }
 
   /**
    * Sets Firefox proxy settings.
@@ -135,31 +137,43 @@ session.Proxy = class {
         return true;
 
       case "direct":
         Preferences.set("network.proxy.type", 0);
         return true;
 
       case "manual":
         Preferences.set("network.proxy.type", 1);
-        if (this.httpProxy && this.httpProxyPort) {
+
+        if (this.ftpProxy) {
+          Preferences.set("network.proxy.ftp", this.ftpProxy);
+          if (Number.isInteger(this.ftpProxyPort)) {
+            Preferences.set("network.proxy.ftp_port", this.ftpProxyPort);
+          }
+        }
+
+        if (this.httpProxy) {
           Preferences.set("network.proxy.http", this.httpProxy);
-          Preferences.set("network.proxy.http_port", this.httpProxyPort);
+          if (Number.isInteger(this.httpProxyPort)) {
+            Preferences.set("network.proxy.http_port", this.httpProxyPort);
+          }
         }
-        if (this.sslProxy && this.sslProxyPort) {
+
+        if (this.sslProxy) {
           Preferences.set("network.proxy.ssl", this.sslProxy);
-          Preferences.set("network.proxy.ssl_port", this.sslProxyPort);
+          if (Number.isInteger(this.sslProxyPort)) {
+            Preferences.set("network.proxy.ssl_port", this.sslProxyPort);
+          }
         }
-        if (this.ftpProxy && this.ftpProxyPort) {
-          Preferences.set("network.proxy.ftp", this.ftpProxy);
-          Preferences.set("network.proxy.ftp_port", this.ftpProxyPort);
-        }
+
         if (this.socksProxy) {
           Preferences.set("network.proxy.socks", this.socksProxy);
-          Preferences.set("network.proxy.socks_port", this.socksProxyPort);
+          if (Number.isInteger(this.socksProxyPort)) {
+            Preferences.set("network.proxy.socks_port", this.socksProxyPort);
+          }
           if (this.socksVersion) {
             Preferences.set("network.proxy.socks_version", this.socksVersion);
           }
         }
         return true;
 
       case "pac":
         Preferences.set("network.proxy.type", 2);
@@ -171,86 +185,144 @@ session.Proxy = class {
         Preferences.set("network.proxy.type", 5);
         return true;
 
       default:
         return false;
     }
   }
 
-  toString() { return "[object session.Proxy]"; }
-
-  /**
-   * @return {Object.<string, (number|string)>}
-   *     JSON serialisation of proxy object.
-   */
-  toJSON() {
-    return marshal({
-      proxyType: this.proxyType,
-      httpProxy: this.httpProxy,
-      httpProxyPort: this.httpProxyPort,
-      sslProxy: this.sslProxy,
-      sslProxyPort: this.sslProxyPort,
-      ftpProxy: this.ftpProxy,
-      ftpProxyPort: this.ftpProxyPort,
-      socksProxy: this.socksProxy,
-      socksProxyPort: this.socksProxyPort,
-      socksProxyVersion: this.socksProxyVersion,
-      proxyAutoconfigUrl: this.proxyAutoconfigUrl,
-    });
-  }
-
   /**
    * @param {Object.<string, ?>} json
    *     JSON Object to unmarshal.
+   *
+   * @throws {InvalidArgumentError}
+   *     When proxy configuration is invalid.
    */
   static fromJSON(json) {
+    // Parse hostname and optional port from host
+    function fromHost(scheme, host) {
+      assert.string(host);
+
+      if (host.includes("://")) {
+        throw new InvalidArgumentError(`${host} contains a scheme`);
+      }
+
+      let url;
+      try {
+        // To parse the host a scheme has to be added temporarily.
+        // If the returned value for the port is an empty string it
+        // could mean no port or the default port for this scheme was
+        // specified. In such a case parse again with a different
+        // scheme to ensure we filter out the default port.
+        url = new URL("http://" + host);
+        if (url.port == "") {
+          url = new URL("https://" + host);
+        }
+      } catch (e) {
+        throw new InvalidArgumentError(e.message);
+      }
+
+      // 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 != "/" ||
+          url.search != "" ||
+          url.hash != "") {
+        throw new InvalidArgumentError(
+            `${host} was not of the form host[:port]`);
+      }
+
+      return [url.hostname, port];
+    }
+
     let p = new session.Proxy();
     if (typeof json == "undefined" || json === null) {
       return p;
     }
 
     assert.object(json);
 
     assert.in("proxyType", json);
     p.proxyType = assert.string(json.proxyType);
 
     switch (p.proxyType) {
-      case "manual":
-        if (typeof json.httpProxy != "undefined") {
-          p.httpProxy = assert.string(json.httpProxy);
-          p.httpProxyPort = assert.positiveInteger(json.httpProxyPort);
-        }
-
-        if (typeof json.sslProxy != "undefined") {
-          p.sslProxy = assert.string(json.sslProxy);
-          p.sslProxyPort = assert.positiveInteger(json.sslProxyPort);
-        }
-
-        if (typeof json.ftpProxy != "undefined") {
-          p.ftpProxy = assert.string(json.ftpProxy);
-          p.ftpProxyPort = assert.positiveInteger(json.ftpProxyPort);
-        }
-
-        if (typeof json.socksProxy != "undefined") {
-          p.socksProxy = assert.string(json.socksProxy);
-          p.socksProxyPort = assert.positiveInteger(json.socksProxyPort);
-          p.socksProxyVersion = assert.positiveInteger(
-              json.socksProxyVersion);
-        }
-
+      case "autodetect":
+      case "direct":
+      case "system":
         break;
 
       case "pac":
         p.proxyAutoconfigUrl = assert.string(json.proxyAutoconfigUrl);
         break;
+
+      case "manual":
+        if (typeof json.ftpProxy != "undefined") {
+          [p.ftpProxy, p.ftpProxyPort] = fromHost("ftp", json.ftpProxy);
+        }
+        if (typeof json.httpProxy != "undefined") {
+          [p.httpProxy, p.httpProxyPort] = fromHost("http", json.httpProxy);
+        }
+        if (typeof json.sslProxy != "undefined") {
+          [p.sslProxy, p.sslProxyPort] = fromHost("https", json.sslProxy);
+        }
+        if (typeof json.socksProxy != "undefined") {
+          [p.socksProxy, p.socksProxyPort] = fromHost("socks", json.socksProxy);
+          p.socksProxyVersion = assert.positiveInteger(
+              json.socksProxyVersion);
+        }
+        break;
+
+      default:
+        throw new InvalidArgumentError(
+            `Invalid type of proxy: ${p.proxyType}`);
     }
 
     return p;
   }
+
+  /**
+   * @return {Object.<string, (number|string)>}
+   *     JSON serialisation of proxy object.
+   */
+  toJSON() {
+    function toHost(hostname, port) {
+      if (!hostname) {
+        return null;
+      }
+
+      if (port != null) {
+        return `${hostname}:${port}`;
+      }
+
+      return hostname;
+    }
+
+    return marshal({
+      proxyType: this.proxyType,
+      ftpProxy: toHost(this.ftpProxy, this.ftpProxyPort),
+      httpProxy: toHost(this.httpProxy, this.httpProxyPort),
+      sslProxy: toHost(this.sslProxy, this.sslProxyPort),
+      socksProxy: toHost(this.socksProxy, this.socksProxyPort),
+      socksProxyVersion: this.socksProxyVersion,
+      proxyAutoconfigUrl: this.proxyAutoconfigUrl,
+    });
+  }
+
+  toString() { return "[object session.Proxy]"; }
 };
 
 /** WebDriver session capabilities representation. */
 session.Capabilities = class extends Map {
   /** @class */
   constructor() {
     super([
       // webdriver
--- a/testing/marionette/test_session.js
+++ b/testing/marionette/test_session.js
@@ -93,23 +93,19 @@ add_test(function test_PageLoadStrategy(
   run_next_test();
 });
 
 add_test(function test_Proxy_ctor() {
   let p = new session.Proxy();
   let props = [
     "proxyType",
     "httpProxy",
-    "httpProxyPort",
     "sslProxy",
-    "sslProxyPort",
     "ftpProxy",
-    "ftpProxyPort",
     "socksProxy",
-    "socksProxyPort",
     "socksVersion",
     "proxyAutoconfigUrl",
   ];
   for (let prop of props) {
     ok(prop in p, `${prop} in ${JSON.stringify(props)}`);
     equal(p[prop], null);
   }
 
@@ -127,107 +123,159 @@ add_test(function test_Proxy_init() {
   p.proxyType = "pac";
   p.proxyAutoconfigUrl = "http://localhost:1234";
   ok(p.init());
 
   equal(Preferences.get("network.proxy.type"), 2);
   equal(Preferences.get("network.proxy.autoconfig_url"),
       "http://localhost:1234");
 
+  // direct
+  p = new session.Proxy();
+  p.proxyType = "direct";
+  ok(p.init());
+  equal(Preferences.get("network.proxy.type"), 0);
+
   // autodetect
   p = new session.Proxy();
   p.proxyType = "autodetect";
   ok(p.init());
   equal(Preferences.get("network.proxy.type"), 4);
 
   // system
   p = new session.Proxy();
   p.proxyType = "system";
   ok(p.init());
   equal(Preferences.get("network.proxy.type"), 5);
 
-  // direct
-  p = new session.Proxy();
-  p.proxyType = "direct";
-  ok(p.init());
-  equal(Preferences.get("network.proxy.type"), 0);
+  // manual
+  for (let proxy of ["ftp", "http", "ssl", "socks"]) {
+    p = new session.Proxy();
+    p.proxyType = "manual";
+    p[`${proxy}Proxy`] = "foo";
+    p[`${proxy}ProxyPort`] = 42;
+    if (proxy === "socks") {
+      p[`${proxy}Version`] = 4;
+    }
+
+    ok(p.init());
+    equal(Preferences.get("network.proxy.type"), 1);
+    equal(Preferences.get(`network.proxy.${proxy}`), "foo");
+    equal(Preferences.get(`network.proxy.${proxy}_port`), 42);
+    if (proxy === "socks") {
+      equal(Preferences.get(`network.proxy.${proxy}_version`), 4);
+    }
+  }
 
   run_next_test();
 });
 
 add_test(function test_Proxy_toString() {
   equal(new session.Proxy().toString(), "[object session.Proxy]");
 
   run_next_test();
 });
 
 add_test(function test_Proxy_toJSON() {
   let p = new session.Proxy();
   deepEqual(p.toJSON(), {});
 
+  // manual
   p = new session.Proxy();
   p.proxyType = "manual";
   deepEqual(p.toJSON(), {proxyType: "manual"});
 
+  for (let proxy of ["ftpProxy", "httpProxy", "sslProxy", "socksProxy"]) {
+    let expected = {proxyType: "manual"}
+
+    let manual = new session.Proxy();
+    manual.proxyType = "manual";
+
+    // without port
+    manual[proxy] = "foo";
+    expected[proxy] = "foo"
+    deepEqual(manual.toJSON(), expected);
+
+    // with port
+    manual[proxy] = "foo";
+    manual[`${proxy}Port`] = 0;
+    expected[proxy] = "foo:0";
+    deepEqual(manual.toJSON(), expected);
+
+    manual[`${proxy}Port`] = 42;
+    expected[proxy] = "foo:42"
+    deepEqual(manual.toJSON(), expected);
+  }
+
   run_next_test();
 });
 
 add_test(function test_Proxy_fromJSON() {
   deepEqual({}, session.Proxy.fromJSON(undefined).toJSON());
   deepEqual({}, session.Proxy.fromJSON(null).toJSON());
 
   for (let typ of [true, 42, "foo", []]) {
     Assert.throws(() => session.Proxy.fromJSON(typ), InvalidArgumentError);
   }
 
-  // must contain proxyType
+  // must contain a valid proxyType
   Assert.throws(() => session.Proxy.fromJSON({}), InvalidArgumentError);
-  deepEqual({proxyType: "foo"},
-      session.Proxy.fromJSON({proxyType: "foo"}).toJSON());
+  Assert.throws(() => session.Proxy.fromJSON({proxyType: "foo"}),
+      InvalidArgumentError);
 
   // manual
   session.Proxy.fromJSON({proxyType: "manual"});
 
   for (let proxy of ["httpProxy", "sslProxy", "ftpProxy", "socksProxy"]) {
     let manual = {proxyType: "manual"};
 
-    for (let typ of [true, 42, [], {}, null]) {
-      manual[proxy] = typ;
-      Assert.throws(() => session.Proxy.fromJSON(manual),
-          InvalidArgumentError);
-    }
-
-    manual[proxy] = "foo";
-    Assert.throws(() => session.Proxy.fromJSON(manual),
-        InvalidArgumentError);
-
-    for (let typ of ["bar", true, [], {}, null, undefined]) {
-      manual[proxy + "Port"] = typ;
+    // invalid hosts
+    for (let host of [true, 42, [], {}, null, "http://foo",
+        "foo:-1", "foo:65536", "foo/test", "foo#42", "foo?foo=bar",
+        "2001:db8::1"]) {
+      manual[proxy] = host;
       Assert.throws(() => session.Proxy.fromJSON(manual),
           InvalidArgumentError);
     }
 
-    manual[proxy] = "foo";
-    manual[proxy + "Port"] = 1234;
+    let expected = {"proxyType": "manual"};
+    if (proxy == "socksProxy") {
+      manual.socksProxyVersion = 5;
+      expected.socksProxyVersion = 5;
+    }
+
+    // valid proxy hosts with port
+    for (let host of ["foo:1", "foo:80", "foo:443", "foo:65535",
+        "127.0.0.1:42", "[2001:db8::1]:42"]) {
+      manual[proxy] = host;
+      expected[proxy] = host;
+
+      deepEqual(expected, session.Proxy.fromJSON(manual).toJSON());
+    }
 
-    let expected = {
-      "proxyType": "manual",
-      [proxy]: "foo",
-      [proxy + "Port"]: 1234,
-    };
+    // Without a port the default port of the scheme is used
+    for (let host of ["foo", "foo:"]) {
+      manual[proxy] = host;
 
-    if (proxy == "socksProxy") {
-      manual.socksProxyVersion = 42;
-      expected.socksProxyVersion = 42;
+      // For socks no default port is available
+      if (proxy === "socksProxy") {
+        expected[proxy] = `foo`;
+      } else {
+        let default_ports = {"ftpProxy": 21, "httpProxy": 80,
+           "sslProxy": 443};
+
+        expected[proxy] = `foo:${default_ports[proxy]}`;
+      }
+      deepEqual(expected, session.Proxy.fromJSON(manual).toJSON());
     }
-    deepEqual(expected, session.Proxy.fromJSON(manual).toJSON());
   }
 
+  // missing required socks version
   Assert.throws(() => session.Proxy.fromJSON(
-      {proxyType: "manual", socksProxy: "foo", socksProxyPort: 1234}),
+      {proxyType: "manual", socksProxy: "foo:1234"}),
       InvalidArgumentError);
 
   run_next_test();
 });
 
 add_test(function test_Capabilities_ctor() {
   let caps = new session.Capabilities();
   ok(caps.has("browserName"));