Bug 1381290 - FindProxyForURL() should use object return type and deprecate string formati; r?mattw draft
authorEric Jung <eric.jung@getfoxyproxy.org>
Thu, 29 Jun 2017 17:26:56 -0700
changeset 614055 54896e50e774ed57690f4073accc441bad9ef5f2
parent 613987 1ad2fb1ac352471178139bcab0659d58b5d0f1e2
child 638766 78f37b02db18ec34bf0f5565ceef9f60babff3e0
push id69903
push userbmo:eric@ericjung.net
push dateSun, 23 Jul 2017 22:20:52 +0000
reviewersmattw
bugs1381290
milestone56.0a1
Bug 1381290 - FindProxyForURL() should use object return type and deprecate string formati; r?mattw MozReview-Commit-ID: DTwWCCvy4ks
toolkit/components/extensions/ProxyScriptContext.jsm
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/components/extensions/test/mochitest/test_ext_proxy.html
toolkit/components/extensions/test/mochitest/test_ext_proxy_legacy.html
toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
toolkit/components/extensions/test/xpcshell/test_proxy_scripts_legacy.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -20,16 +20,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "ProxyService",
                                    "@mozilla.org/network/protocol-proxy-service;1",
                                    "nsIProtocolProxyService");
 
 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
 
+// Should DNS be resolved on the SOCKS proxy server? (does not apply to HTTP and HTTP proxy servers)
+const TRANSPARENT_PROXY_RESOLVES_HOST = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+
 // The length of time (seconds) to wait for a proxy to resolve before ignoring it.
 const PROXY_TIMEOUT_SEC = 10;
 
 const {
   defineLazyGetter,
 } = ExtensionUtils;
 
 const {
@@ -126,60 +129,84 @@ class ProxyScriptContext extends BaseCon
         message: error.message,
         fileName: error.fileName,
         lineNumber: error.lineNumber,
         stack: error.stack,
       });
       return defaultProxyInfo;
     }
 
-    if (!ret || typeof ret !== "string") {
+    if (!ret || (typeof ret !== "string" && !Array.isArray(ret))) {
       this.extension.emit("proxy-error", {
-        message: "FindProxyForURL: Return type must be a string",
+        message: "FindProxyForURL: Return type must be a string or array of objects",
       });
       return defaultProxyInfo;
     }
 
+    let isLegacyFormat = typeof ret === "string";
+    let unparsedRules = isLegacyFormat ? ret.split(";") : ret;
     let rules = [];
-    for (let rule of ret.split(";")) {
+    for (let rule of unparsedRules) {
       try {
-        rule = this.parseLegacyRule(rule);
+        rule = isLegacyFormat ? this.parseLegacyRule(rule) : this.parseRule(rule);
+        rules.push(rule);
         if (rule.type === PROXY_TYPES.DIRECT) {
           break;
         }
-        rules.push(rule);
       } catch (e) {
         this.extension.emit("proxy-error", {
           message: "FindProxyForURL: " + (e.message || e),
         });
         break;
       }
     }
+    return this.createProxyInfo(rules, defaultProxyInfo);
+  }
 
-    let proxyInfo = this.createProxyInfo(rules);
+  parseRule(rule) {
+    let {type, host, port, username, password, proxyDNS, failoverTimeout} = rule;
+    type = this.validateType(type);
 
-    return proxyInfo || defaultProxyInfo;
+    if (type === "direct") {
+      return {type};
+    }
+    host = this.validateHost(host);
+    port = this.validatePort(port);
+    this.validateUsername(username);
+    this.validatePassword(password);
+    this.validateProxyDNS(proxyDNS, type);
+    this.validateFailoverTimeout(failoverTimeout);
+    return {type, host, port, username, password, proxyDNS, failoverTimeout};
   }
 
   /**
    * Creates a new proxy info object using the return value of FindProxyForURL.
    *
-   * @param {Array<string>} rules The list of proxy rules returned by FindProxyForURL.
-   *    (e.g. ["PROXY 1.2.3.4:8080", "SOCKS 1.1.1.1:9090", "DIRECT"])
+   * @param {Array<Object>} rules The list of proxy rules returned by FindProxyForURL.
+   * @param {Object} defaultProxyInfo The proxy (or list of proxies) that
+   *     would be used by default for the given URI. This may be null.
    * @returns {nsIProxyInfo} The proxy info to apply for the given URI.
    */
-  createProxyInfo(rules) {
-    let proxyInfo = null;
+  createProxyInfo(rules, defaultProxyInfo) {
+    let proxyInfo = defaultProxyInfo;
     while (rules.length) {
-      let {type, host, port} = rules.pop();
-      proxyInfo = ProxyService.newProxyInfo(
-        type, host, port, 0, PROXY_TIMEOUT_SEC, proxyInfo);
+      let {type, host, port, username, password, proxyDNS, failoverTimeout} = rules.pop();
       if (type === "DIRECT") {
         return proxyInfo;
       }
+      // TODO: After bug1360404 is fixed, use ProxyService.newProxyInfoWithAuth() all the time--not just for SOCKS
+      if (type == PROXY_TYPES.SOCKS || type == PROXY_TYPES.SOCKS4) {
+        proxyInfo = ProxyService.newProxyInfoWithAuth(
+          type, host, port, username, password, proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0,
+          failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC, proxyInfo);
+      } else {
+        proxyInfo = ProxyService.newProxyInfo(
+          type, host, port, proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0,
+          failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC, proxyInfo);
+      }
     }
     return proxyInfo;
   }
 
   parseLegacyRule(rule) {
     rule = rule.trim();
 
     if (!rule) {
@@ -190,49 +217,99 @@ class ProxyScriptContext extends BaseCon
     if (!parts[0]) {
       throw new Error(`Too few arguments passed for proxy rule: "${rule}"`);
     }
 
     if (parts.length > 2) {
       throw new Error(`Too many arguments passed for proxy rule: "${rule}"`);
     }
 
-    let type = parts[0].toLowerCase();
+    let type = this.validateType(parts[0]);
     switch (type) {
-      case PROXY_TYPES.PROXY:
       case PROXY_TYPES.HTTP:
       case PROXY_TYPES.HTTPS:
       case PROXY_TYPES.SOCKS:
       case PROXY_TYPES.SOCKS4:
-        if (!parts[1]) {
-          throw new Error(`Missing argument for proxy type: "${parts[0]}"`);
+        if (parts.length != 2) {
+          throw new Error(`Incorrect number of arguments for proxy rule: "${rule}"`);
         }
-
-        if (parts.length != 2) {
-          throw new Error(`Too many arguments for proxy rule: "${rule}"`);
-        }
-
         let [host, port] = parts[1].split(":");
-        if (!host || !port) {
-          throw new Error(`Unable to parse host and port from proxy rule: "${rule}"`);
-        }
-
-        if (type == PROXY_TYPES.PROXY) {
-          // PROXY_TYPES.HTTP and PROXY_TYPES.PROXY are synonyms
-          type = PROXY_TYPES.HTTP;
-        }
-
+        host = this.validateHost(host);
+        port = this.validatePort(port);
         return {type, host, port};
       case PROXY_TYPES.DIRECT:
         if (parts.length != 1) {
-          throw new Error(`Invalid argument for proxy type: "${parts[0]}"`);
+          throw new Error(`"${parts[0]}" takes no arguments`);
         }
         return {type};
-      default:
-        throw new Error(`Unrecognized proxy type: "${parts[0]}"`);
+    }
+  }
+
+  validateType(type) {
+    if (typeof type != "string" || !PROXY_TYPES.hasOwnProperty(type.toUpperCase())) {
+      throw new Error(`Invalid proxy server type: "${type}"`);
+    }
+    type = type.toLowerCase();
+    if (type == PROXY_TYPES.PROXY) {
+      // PROXY_TYPES.HTTP and PROXY_TYPES.PROXY are synonyms
+      return PROXY_TYPES.HTTP;
+    }
+    return type;
+  }
+
+  validateHost(host) {
+    if (typeof host != "string" || host.split(/\s+/).length != 1) {
+      throw new Error(`Invalid proxy server host: "${host}"`);
+    }
+    host = host.trim();
+    if (!host.length) {
+      throw new Error("Proxy server host cannot be empty");
+    }
+    return host;
+  }
+
+  validatePort(port) {
+    port = Number.parseInt(port, 10);
+    if (!Number.isInteger(port)) {
+      throw new Error(`Invalid proxy server port: "${port}"`);
+    }
+
+    if (port < 1 || port > 0xffff) {
+      throw new Error(`Proxy server port ${port} outside range 1 to 65535`);
+    }
+    return port;
+  }
+
+  validateUsername(username) {
+    if (username !== undefined && typeof username != "string") {
+      throw new Error(`Invalid proxy server username: "${username}"`);
+    }
+  }
+
+  validatePassword(password) {
+    if (password !== undefined && typeof password != "string") {
+      throw new Error(`Invalid proxy server password: "${password}"`);
+    }
+  }
+
+  validateProxyDNS(proxyDNS, type) {
+    // Do we want the SOCKS layer to send the hostname and port to the proxy and let it do the DNS?
+    if (proxyDNS !== undefined) {
+      if (typeof proxyDNS != "boolean") {
+        throw new Error(`Invalid proxyDNS value: "${proxyDNS}"`);
+      }
+      if (proxyDNS && type != PROXY_TYPES.SOCKS && type != PROXY_TYPES.SOCKS4) {
+        throw new Error(`proxyDNS can only be true for SOCKS proxy servers`);
+      }
+    }
+  }
+
+  validateFailoverTimeout(failoverTimeout) {
+    if (failoverTimeout !== undefined && (!Number.isInteger(failoverTimeout) || failoverTimeout < 1)) {
+      throw new Error(`Invalid failover timeout: "${failoverTimeout}"`);
     }
   }
 
   /**
    * Unloads the proxy filter and shuts down the sandbox.
    */
   unload() {
     super.unload();
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -76,16 +76,17 @@ skip-if = os == 'android' # bug 1369440
 [test_ext_exclude_include_globs.html]
 [test_ext_external_messaging.html]
 [test_ext_generate.html]
 [test_ext_geolocation.html]
 skip-if = os == 'android' # Android support Bug 1336194
 [test_ext_notifications.html]
 [test_ext_permission_xhr.html]
 [test_ext_proxy.html]
+[test_ext_proxy_legacy.html]
 skip-if = os == 'android' && debug # Bug 1357635
 [test_ext_runtime_connect.html]
 [test_ext_runtime_connect_twoway.html]
 [test_ext_runtime_connect2.html]
 [test_ext_runtime_disconnect.html]
 [test_ext_runtime_id.html]
 [test_ext_sandbox_var.html]
 [test_ext_sendmessage_doublereply.html]
--- a/toolkit/components/extensions/test/mochitest/test_ext_proxy.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_proxy.html
@@ -79,38 +79,40 @@ add_task(async function test_invalid_Fin
       message: "The proxy script must define FindProxyForURL as a function",
     });
 });
 
 add_task(async function test_invalid_FindProxyForURL_return_types() {
   await testProxyScript(
     () => {
       function FindProxyForURL() {
-        return 5;
+        return {type: "socks", host: "foo.bar", port: 1080, username: "mungosantamaria", password: "pass123"};
       }
     }, {
-      message: "FindProxyForURL: Return type must be a string",
+      message: "FindProxyForURL: Return type must be a string or array of objects",
     });
 
   await testProxyScript(
     () => {
       function FindProxyForURL() {
-        return "INVALID";
+        return [{type: "pptp", host: "foo.bar", port: 1080, username: "mungosantamaria", password: "pass123", proxyDNS: true, failoverTimeout: 3},
+          {type: "http", host: "192.168.1.1", port: 3128, username: "mungosantamaria", password: "word321"}];
       }
     }, {
-      message: "FindProxyForURL: Unrecognized proxy type: \"INVALID\"",
+      message: "FindProxyForURL: Invalid proxy server type: \"pptp\"",
     });
 
   await testProxyScript(
     () => {
       function FindProxyForURL() {
-        return "SOCKS";
+        return [{type: "pptp", host: "foo.bar", port: 65536, username: "mungosantamaria", password: "pass123", proxyDNS: true, failoverTimeout: 3},
+          {type: "http", host: "192.168.1.1", port: 3128, username: "mungosantamaria", password: "word321"}];
       }
     }, {
-      message: "FindProxyForURL: Missing argument for proxy type: \"SOCKS\"",
+      message: "FindProxyForURL: Proxy server port 65536 outside range 1 to 65535",
     });
 
   await testProxyScript(
     () => {
       function FindProxyForURL() {
         return "PROXY 1.2.3.4:8080 EXTRA";
       }
     }, {
@@ -118,53 +120,53 @@ add_task(async function test_invalid_Fin
     });
 
   await testProxyScript(
     () => {
       function FindProxyForURL() {
         return "PROXY :";
       }
     }, {
-      message: "FindProxyForURL: Unable to parse host and port from proxy rule: \"PROXY :\"",
+      message: "FindProxyForURL: Proxy server host cannot be empty",
     });
 
   await testProxyScript(
     () => {
       function FindProxyForURL() {
         return "PROXY :8080";
       }
     }, {
-      message: "FindProxyForURL: Unable to parse host and port from proxy rule: \"PROXY :8080\"",
+      message: "FindProxyForURL: Proxy server host cannot be empty",
     });
 
   await testProxyScript(
     () => {
       function FindProxyForURL() {
         return "PROXY ::";
       }
     }, {
-      message: "FindProxyForURL: Unable to parse host and port from proxy rule: \"PROXY ::\"",
+      message: "FindProxyForURL: Proxy server host cannot be empty",
     });
 
   await testProxyScript(
     () => {
       function FindProxyForURL() {
         return "PROXY 1.2.3.4:";
       }
     }, {
-      message: "FindProxyForURL: Unable to parse host and port from proxy rule: \"PROXY 1.2.3.4:\"",
+      message: "FindProxyForURL: Invalid proxy server port: \"NaN\"",
     });
 
   await testProxyScript(
     () => {
       function FindProxyForURL() {
         return "DIRECT 1.2.3.4:8080";
       }
     }, {
-      message: "FindProxyForURL: Invalid argument for proxy type: \"DIRECT\"",
+      message: "FindProxyForURL: \"DIRECT\" takes no arguments",
     });
 });
 
 add_task(async function test_runtime_error_in_FindProxyForURL() {
   await testProxyScript(
     () => {
       function FindProxyForURL() {
         return not_defined; // eslint-disable-line no-undef
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_proxy_legacy.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Tests for the proxy API</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+/* eslint no-unused-vars: ["error", {"args": "none", "varsIgnorePattern": "^(FindProxyForURL)$"}] */
+
+"use strict";
+
+async function testProxyScript(script, expected) {
+  let extension = ExtensionTestUtils.loadExtension({
+    background() {
+      let errorReceived = false;
+      browser.proxy.onProxyError.addListener(error => {
+        if (!errorReceived) {
+          errorReceived = true;
+          browser.test.sendMessage("proxy-error-received", error);
+        }
+      });
+
+      browser.proxy.registerProxyScript("proxy_script.js");
+    },
+    manifest: {
+      "permissions": ["proxy"],
+    },
+    files: {
+      "proxy_script.js": String(script).replace(/^.*?\{([^]*)\}$/, "$1"),
+    },
+  });
+
+  await extension.startup();
+
+  let win = window.open("http://example.com/");
+  let error = await extension.awaitMessage("proxy-error-received");
+  is(error.message, expected.message, "Correct error message received");
+
+  if (expected.errorInfo) {
+    ok(error.fileName.includes("proxy_script.js"), "Error should include file name");
+    is(error.lineNumber, 3, "Error should include line number");
+    ok(error.stack.includes("proxy_script.js:3:9"), "Error should include stack trace");
+  }
+
+  win.close();
+  await extension.unload();
+}
+
+add_task(async function test_invalid_FindProxyForURL_type() {
+  await testProxyScript(
+    () => { }, {
+      message: "The proxy script must define FindProxyForURL as a function",
+    });
+
+  await testProxyScript(
+    () => {
+      var FindProxyForURL = 5; // eslint-disable-line mozilla/var-only-at-top-level
+    }, {
+      message: "The proxy script must define FindProxyForURL as a function",
+    });
+});
+
+add_task(async function test_invalid_FindProxyForURL_return_types() {
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return 5;
+      }
+    }, {
+      message: "FindProxyForURL: Return type must be a string or array of objects",
+    });
+
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return "INVALID";
+      }
+    }, {
+      message: "FindProxyForURL: Invalid proxy server type: \"INVALID\"",
+    });
+
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return "SOCKS";
+      }
+    }, {
+      message: "FindProxyForURL: Incorrect number of arguments for proxy rule: \"SOCKS\"",
+    });
+
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return "PROXY 1.2.3.4:8080 EXTRA";
+      }
+    }, {
+      message: "FindProxyForURL: Too many arguments passed for proxy rule: \"PROXY 1.2.3.4:8080 EXTRA\"",
+    });
+
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return "PROXY :";
+      }
+    }, {
+      message: "FindProxyForURL: Proxy server host cannot be empty",
+    });
+
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return "PROXY :8080";
+      }
+    }, {
+      message: "FindProxyForURL: Proxy server host cannot be empty",
+    });
+
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return "PROXY ::";
+      }
+    }, {
+      message: "FindProxyForURL: Proxy server host cannot be empty",
+    });
+
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return "PROXY 1.2.3.4:";
+      }
+    }, {
+      message: "FindProxyForURL: Invalid proxy server port: \"NaN\"",
+    });
+
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return "DIRECT 1.2.3.4:8080";
+      }
+    }, {
+      message: "FindProxyForURL: \"DIRECT\" takes no arguments",
+    });
+});
+
+add_task(async function test_runtime_error_in_FindProxyForURL() {
+  await testProxyScript(
+    () => {
+      function FindProxyForURL() {
+        return not_defined; // eslint-disable-line no-undef
+      }
+    }, {
+      message: "not_defined is not defined",
+      errorInfo: true,
+    });
+});
+
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
@@ -4,16 +4,18 @@
 
 Cu.import("resource://gre/modules/Extension.jsm");
 Cu.import("resource://gre/modules/ProxyScriptContext.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gProxyService",
                                    "@mozilla.org/network/protocol-proxy-service;1",
                                    "nsIProtocolProxyService");
 
+const TRANSPARENT_PROXY_RESOLVES_HOST = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+
 async function testProxyScript(options, expected = {}) {
   let scriptData = String(options.scriptData).replace(/^.*?\{([^]*)\}$/, "$1");
   let extensionData = {
     background() {
       browser.test.onMessage.addListener((message, data) => {
         if (message === "runtime-message") {
           browser.runtime.onMessage.addListener((msg, sender, respond) => {
             if (msg === "finish-from-pac-script") {
@@ -70,286 +72,137 @@ async function testProxyScript(options, 
         resolve(pi);
       },
     });
   });
 
   if (!proxyInfo) {
     equal(proxyInfo, expected.proxyInfo, "Expected proxyInfo to be null");
   } else {
-    let expectedProxyInfo = expected.proxyInfo;
+    expected = expected.proxyInfo;
     for (let proxy = proxyInfo; proxy; proxy = proxy.failoverProxy) {
-      equal(proxy.host, expectedProxyInfo.host, `Expected proxy host to be ${expectedProxyInfo.host}`);
-      equal(proxy.port, expectedProxyInfo.port, `Expected proxy port to be ${expectedProxyInfo.port}`);
-      equal(proxy.type, expectedProxyInfo.type, `Expected proxy type to be ${expectedProxyInfo.type}`);
-      expectedProxyInfo = expectedProxyInfo.failoverProxy;
+      if (expected) {
+        let {type, host, port, username, password, proxyDNS, failoverTimeout} = expected;
+        equal(proxy.host, host, `Expected proxy host to be ${host}`);
+        equal(proxy.port, port, `Expected proxy port to be ${port}`);
+        equal(proxy.type, type, `Expected proxy type to be ${type}`);
+        equal(proxy.username, username || "", `Expected proxy username to be ${username}`);
+        equal(proxy.password, password || "", `Expected proxy password to be ${password}`);
+        equal(proxy.flags, proxyDNS == undefined ? 0 : proxyDNS, `Expected proxyDNS to be ${proxyDNS}`);
+        // Default timeout is 10
+        equal(proxy.failoverTimeout, failoverTimeout || 10, `Expected failoverTimeout to be ${failoverTimeout}`);
+        expected = expected.failoverProxy;
+      }
     }
   }
 
   await extension.unload();
   script.unload();
 }
 
-add_task(async function testUndefinedFindProxyForURL() {
-  await testProxyScript({
-    scriptData() { },
-  }, {
-    proxyInfo: null,
-  });
-});
-
-add_task(async function testWrongTypeForFindProxyForURL() {
-  await testProxyScript({
-    scriptData() {
-      let FindProxyForURL = "foo";
-    },
-  }, {
-    proxyInfo: null,
-  });
-});
-
-add_task(async function testInvalidReturnTypeForFindProxyForURL() {
+add_task(async function testEmptyObject() {
   await testProxyScript({
     scriptData() {
       function FindProxyForURL(url, host) {
-        return -1;
-      }
-    },
-  }, {
-    proxyInfo: null,
-  });
-});
-
-add_task(async function testSimpleProxyScript() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        if (host === "www.mozilla.org") {
-          return "DIRECT";
-        }
-      }
-    },
-  }, {
-    proxyInfo: null,
-  });
-});
-
-add_task(async function testRuntimeErrorInProxyScript() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        return RUNTIME_ERROR; // eslint-disable-line no-undef
-      }
-    },
-  }, {
-    proxyInfo: null,
-  });
-});
-
-add_task(async function testProxyScriptWithUnexpectedReturnType() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        return "UNEXPECTED 1.2.3.4:8080";
+        return {};
       }
     },
   }, {
     proxyInfo: null,
   });
 });
 
-add_task(async function testSocksReturnType() {
+add_task(async function testHTTPType() {
   await testProxyScript({
     scriptData() {
       function FindProxyForURL(url, host) {
-        return "SOCKS foo.bar:1080";
+        return [{type: "http", host: "foo.bar", port: 3128}];
       }
     },
   }, {
     proxyInfo: {
       host: "foo.bar",
-      port: "1080",
-      type: "socks",
-      failoverProxy: null,
-    },
-  });
-});
-
-add_task(async function testSocks4ReturnType() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        return "SOCKS4 1.2.3.4:1080";
-      }
-    },
-  }, {
-    proxyInfo: {
-      host: "1.2.3.4",
-      port: "1080",
-      type: "socks4",
-      failoverProxy: null,
-    },
-  });
-});
-
-add_task(async function testSocksReturnTypeWithHostCheck() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        if (host === "www.mozilla.org") {
-          return "SOCKS 4.4.4.4:9002";
-        }
-      }
-    },
-  }, {
-    proxyInfo: {
-      host: "4.4.4.4",
-      port: "9002",
-      type: "socks",
-      failoverProxy: null,
+      port: "3128",
+      type: "http",
     },
   });
 });
 
-add_task(async function testProxyReturnType() {
+add_task(async function testHTTPSType() {
   await testProxyScript({
     scriptData() {
       function FindProxyForURL(url, host) {
-        return "PROXY 1.2.3.4:8080";
+        return [{type: "https", host: "foo.com", port: 3129}];
       }
     },
   }, {
     proxyInfo: {
-      host: "1.2.3.4",
-      port: "8080",
-      type: "http",
-      failoverProxy: null,
+      host: "foo.com",
+      port: "3129",
+      type: "https",
     },
   });
 });
 
-add_task(async function testUnusualWhitespaceForFindProxyForURL() {
+add_task(async function testAllOptionalProperties() {
   await testProxyScript({
     scriptData() {
       function FindProxyForURL(url, host) {
-        return "   PROXY    1.2.3.4:8080      ";
+        return [{type: "socks", host: "foo.bar", port: 1080, username: "mungo", password: "santamaria123", proxyDNS: true, failoverTimeout: 5}];
       }
     },
   }, {
     proxyInfo: {
-      host: "1.2.3.4",
-      port: "8080",
-      type: "http",
+      type: "socks",
+      host: "foo.bar",
+      port: 1080,
+      username: "mungo",
+      password: "santamaria123",
+      failoverTimeout: 5,
       failoverProxy: null,
-    },
-  });
-});
-
-add_task(async function testInvalidProxyScriptIgnoresFailover() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        return "PROXY 1.2.3.4:8080; UNEXPECTED; SOCKS 1.2.3.4:8080";
-      }
-    },
-  }, {
-    proxyInfo: {
-      host: "1.2.3.4",
-      port: "8080",
-      type: "http",
-      failoverProxy: null,
+      proxyDNS: TRANSPARENT_PROXY_RESOLVES_HOST,
     },
   });
 });
 
-add_task(async function testProxyScriptWithValidFailovers() {
+add_task(async function testAllOptionalPropertiesWithMultipleFailover() {
   await testProxyScript({
     scriptData() {
       function FindProxyForURL(url, host) {
-        return "PROXY 1.2.3.4:8080; SOCKS 4.4.4.4:9000; DIRECT";
-      }
-    },
-  }, {
-    proxyInfo: {
-      host: "1.2.3.4",
-      port: "8080",
-      type: "http",
-      failoverProxy: {
-        host: "4.4.4.4",
-        port: "9000",
-        type: "socks",
-        failoverProxy: null,
-      },
-    },
-  });
-});
-
-add_task(async function testProxyScriptWithAnInvalidFailover() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        return "PROXY 1.2.3.4:8080; INVALID 1.2.3.4:9090; SOCKS 4.4.4.4:9000; DIRECT";
+        return [{type: "socks", host: "foo.bar", port: 1080, username: "johnsmith", password: "pass123", proxyDNS: true, failoverTimeout: 3},
+          {type: "http", host: "192.168.1.1", port: 3128}, {type: "https", host: "192.168.1.2", port: 1121, failoverTimeout: 1},
+          {type: "socks", host: "192.168.1.3", port: 1999, proxyDNS: true, username: "mungosantamaria", password: "foobar"}];
       }
     },
   }, {
     proxyInfo: {
-      host: "1.2.3.4",
-      port: "8080",
-      type: "http",
-      failoverProxy: null,
+      type: "socks",
+      host: "foo.bar",
+      port: 1080,
+      proxyDNS: true,
+      username: "johnsmith",
+      password: "pass123",
+      failoverTimeout: 3,
+      failoverProxy: {
+        host: "192.168.1.1",
+        port: 3128,
+        type: "http",
+        failoverProxy: {
+          host: "192.168.1.2",
+          port: 1121,
+          type: "https",
+          failoverTimeout: 1,
+          failoverProxy: {
+            host: "192.168.1.3",
+            port: 1999,
+            type: "socks",
+            proxyDNS: TRANSPARENT_PROXY_RESOLVES_HOST,
+            username: "mungosantamaria",
+            password: "foobar",
+            failoverProxy: {
+              type: "dect",
+            },
+          },
+        },
+      },
     },
   });
 });
-
-add_task(async function testProxyScriptWithEmptyFailovers() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        return ";;;;;PROXY 1.2.3.4:8080";
-      }
-    },
-  }, {
-    proxyInfo: null,
-  });
-});
-
-add_task(async function testProxyScriptWithInvalidReturn() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        return "SOCKS :8080;";
-      }
-    },
-  }, {
-    proxyInfo: null,
-  });
-});
-
-add_task(async function testProxyScriptWithRuntimeUpdate() {
-  await testProxyScript({
-    scriptData() {
-      let settings = {};
-      function FindProxyForURL(url, host) {
-        if (settings.host === "www.mozilla.org") {
-          return "PROXY 1.2.3.4:8080;";
-        }
-        return "DIRECT";
-      }
-      browser.runtime.onMessage.addListener((msg, sender, respond) => {
-        if (msg.host) {
-          settings.host = msg.host;
-          browser.runtime.sendMessage("finish-from-pac-script");
-          return Promise.resolve(msg);
-        }
-      });
-    },
-    runtimeMessage: {
-      host: "www.mozilla.org",
-    },
-  }, {
-    proxyInfo: {
-      host: "1.2.3.4",
-      port: "8080",
-      type: "http",
-      failoverProxy: null,
-    },
-  });
-});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_scripts_legacy.js
@@ -0,0 +1,359 @@
+"use strict";
+
+/* eslint no-unused-vars: ["error", {"args": "none", "varsIgnorePattern": "^(FindProxyForURL)$"}] */
+
+Cu.import("resource://gre/modules/Extension.jsm");
+Cu.import("resource://gre/modules/ProxyScriptContext.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gProxyService",
+                                   "@mozilla.org/network/protocol-proxy-service;1",
+                                   "nsIProtocolProxyService");
+
+async function testProxyScript(options, expected = {}) {
+  let scriptData = String(options.scriptData).replace(/^.*?\{([^]*)\}$/, "$1");
+  let extensionData = {
+    background() {
+      browser.test.onMessage.addListener((message, data) => {
+        if (message === "runtime-message") {
+          browser.runtime.onMessage.addListener((msg, sender, respond) => {
+            if (msg === "finish-from-pac-script") {
+              browser.test.notifyPass("proxy");
+              return Promise.resolve(msg);
+            }
+          });
+          browser.runtime.sendMessage(data, {toProxyScript: true}).then(response => {
+            browser.test.sendMessage("runtime-message-sent");
+          });
+        } else if (message === "finish-from-xpcshell-test") {
+          browser.test.notifyPass("proxy");
+        }
+      });
+    },
+    files: {
+      "proxy.js": scriptData,
+    },
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  let extension_internal = extension.extension;
+
+  await extension.startup();
+
+  let script = new ProxyScriptContext(extension_internal, extension_internal.getURL("proxy.js"));
+
+  try {
+    await script.load();
+  } catch (error) {
+    equal(error, expected.error, "Expected error received");
+    script.unload();
+    await extension.unload();
+    return;
+  }
+
+  if (options.runtimeMessage) {
+    extension.sendMessage("runtime-message", options.runtimeMessage);
+    await extension.awaitMessage("runtime-message-sent");
+  } else {
+    extension.sendMessage("finish-from-xpcshell-test");
+  }
+
+  await extension.awaitFinish("proxy");
+
+  let proxyInfo = await new Promise((resolve, reject) => {
+    let channel = NetUtil.newChannel({
+      uri: "http://www.mozilla.org/",
+      loadUsingSystemPrincipal: true,
+    });
+
+    gProxyService.asyncResolve(channel, 0, {
+      onProxyAvailable(req, uri, pi, status) {
+        resolve(pi);
+      },
+    });
+  });
+
+  if (!proxyInfo) {
+    equal(proxyInfo, expected.proxyInfo, "Expected proxyInfo to be null");
+  } else {
+    let expectedProxyInfo = expected.proxyInfo;
+    for (let proxy = proxyInfo; proxy; proxy = proxy.failoverProxy) {
+      equal(proxy.host, expectedProxyInfo.host, `Expected proxy host to be ${expectedProxyInfo.host}`);
+      equal(proxy.port, expectedProxyInfo.port, `Expected proxy port to be ${expectedProxyInfo.port}`);
+      equal(proxy.type, expectedProxyInfo.type, `Expected proxy type to be ${expectedProxyInfo.type}`);
+      expectedProxyInfo = expectedProxyInfo.failoverProxy;
+    }
+  }
+
+  await extension.unload();
+  script.unload();
+}
+
+add_task(async function testUndefinedFindProxyForURL() {
+  await testProxyScript({
+    scriptData() { },
+  }, {
+    proxyInfo: null,
+  });
+});
+
+add_task(async function testWrongTypeForFindProxyForURL() {
+  await testProxyScript({
+    scriptData() {
+      let FindProxyForURL = "foo";
+    },
+  }, {
+    proxyInfo: null,
+  });
+});
+
+add_task(async function testInvalidReturnTypeForFindProxyForURL() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return -1;
+      }
+    },
+  }, {
+    proxyInfo: null,
+  });
+});
+
+add_task(async function testSimpleProxyScript() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        if (host === "www.mozilla.org") {
+          return "DIRECT";
+        }
+      }
+    },
+  }, {
+    proxyInfo: null,
+  });
+});
+
+add_task(async function testRuntimeErrorInProxyScript() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return RUNTIME_ERROR; // eslint-disable-line no-undef
+      }
+    },
+  }, {
+    proxyInfo: null,
+  });
+});
+
+add_task(async function testProxyScriptWithUnexpectedReturnType() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "UNEXPECTED 1.2.3.4:8080";
+      }
+    },
+  }, {
+    proxyInfo: null,
+  });
+});
+
+add_task(async function testSocksReturnType() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "SOCKS foo.bar:1080";
+      }
+    },
+  }, {
+    proxyInfo: {
+      host: "foo.bar",
+      port: "1080",
+      type: "socks",
+      failoverProxy: null,
+    },
+  });
+});
+
+add_task(async function testSocks4ReturnType() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "SOCKS4 1.2.3.4:1080";
+      }
+    },
+  }, {
+    proxyInfo: {
+      host: "1.2.3.4",
+      port: "1080",
+      type: "socks4",
+      failoverProxy: null,
+    },
+  });
+});
+
+add_task(async function testSocksReturnTypeWithHostCheck() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        if (host === "www.mozilla.org") {
+          return "SOCKS 4.4.4.4:9002";
+        }
+      }
+    },
+  }, {
+    proxyInfo: {
+      host: "4.4.4.4",
+      port: "9002",
+      type: "socks",
+      failoverProxy: null,
+    },
+  });
+});
+
+add_task(async function testProxyReturnType() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "PROXY 1.2.3.4:8080";
+      }
+    },
+  }, {
+    proxyInfo: {
+      host: "1.2.3.4",
+      port: "8080",
+      type: "http",
+      failoverProxy: null,
+    },
+  });
+});
+
+add_task(async function testUnusualWhitespaceForFindProxyForURL() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "   PROXY    1.2.3.4:8080      ";
+      }
+    },
+  }, {
+    proxyInfo: {
+      host: "1.2.3.4",
+      port: "8080",
+      type: "http",
+      failoverProxy: null,
+    },
+  });
+});
+
+add_task(async function testInvalidProxyScriptIgnoresFailover() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "PROXY 1.2.3.4:8080; UNEXPECTED; SOCKS 1.2.3.4:8080";
+      }
+    },
+  }, {
+    proxyInfo: {
+      host: "1.2.3.4",
+      port: "8080",
+      type: "http",
+      failoverProxy: null,
+    },
+  });
+});
+
+add_task(async function testProxyScriptWithValidFailovers() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "PROXY 1.2.3.4:8080; SOCKS 4.4.4.4:9000; DIRECT";
+      }
+    },
+  }, {
+    proxyInfo: {
+      host: "1.2.3.4",
+      port: "8080",
+      type: "http",
+      failoverProxy: {
+        host: "4.4.4.4",
+        port: "9000",
+        type: "socks",
+        failoverProxy: {
+          type: "direct",
+          host: null,
+          port: -1,
+        },
+      },
+    },
+  });
+});
+
+add_task(async function testProxyScriptWithAnInvalidFailover() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "PROXY 1.2.3.4:8080; INVALID 1.2.3.4:9090; SOCKS 4.4.4.4:9000; DIRECT";
+      }
+    },
+  }, {
+    proxyInfo: {
+      host: "1.2.3.4",
+      port: "8080",
+      type: "http",
+      failoverProxy: null,
+    },
+  });
+});
+
+add_task(async function testProxyScriptWithEmptyFailovers() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return ";;;;;PROXY 1.2.3.4:8080";
+      }
+    },
+  }, {
+    proxyInfo: null,
+  });
+});
+
+add_task(async function testProxyScriptWithInvalidReturn() {
+  await testProxyScript({
+    scriptData() {
+      function FindProxyForURL(url, host) {
+        return "SOCKS :8080;";
+      }
+    },
+  }, {
+    proxyInfo: null,
+  });
+});
+
+add_task(async function testProxyScriptWithRuntimeUpdate() {
+  await testProxyScript({
+    scriptData() {
+      let settings = {};
+      function FindProxyForURL(url, host) {
+        if (settings.host === "www.mozilla.org") {
+          return "PROXY 1.2.3.4:8080;";
+        }
+        return "DIRECT";
+      }
+      browser.runtime.onMessage.addListener((msg, sender, respond) => {
+        if (msg.host) {
+          settings.host = msg.host;
+          browser.runtime.sendMessage("finish-from-pac-script");
+          return Promise.resolve(msg);
+        }
+      });
+    },
+    runtimeMessage: {
+      host: "www.mozilla.org",
+    },
+  }, {
+    proxyInfo: {
+      host: "1.2.3.4",
+      port: "8080",
+      type: "http",
+      failoverProxy: null,
+    },
+  });
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -91,9 +91,10 @@ skip-if = os == "android"
 [test_ext_unknown_permissions.js]
 [test_ext_legacy_extension_context.js]
 [test_ext_legacy_extension_embedding.js]
 [test_locale_converter.js]
 [test_locale_data.js]
 [test_native_messaging.js]
 skip-if = os == "android"
 [test_proxy_scripts.js]
+[test_proxy_scripts_legacy.js]
 [include:xpcshell-content.ini]