Bug 1381290 support proxyInfo object return from FindProxyForURL, r=kmag draft
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 29 Jun 2017 17:26:56 -0700
changeset 662530 b3e449d17d4346dbaa6d3bd5ce689c1ae8170ee0
parent 662474 02c78441c03d5d004e57596a130856861a30188a
child 662637 8c614638823a9f8dc4b3b0e8b9c52eadc918136b
child 662650 720d17019c339891f6807c3b79ef44669ccd39f7
push id79113
push usermixedpuppy@gmail.com
push dateMon, 11 Sep 2017 19:54:44 +0000
reviewerskmag
bugs1381290
milestone57.0a1
Bug 1381290 support proxyInfo object return from FindProxyForURL, r=kmag MozReview-Commit-ID: 4A2lxXPz9lF
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/xpcshell/test_proxy_scripts.js
toolkit/components/extensions/test/xpcshell/test_proxy_scripts_results.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -20,39 +20,176 @@ 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";
 
+// DNS is resolved on the SOCKS proxy server.
+const {TRANSPARENT_PROXY_RESOLVES_HOST} = Ci.nsIProxyInfo;
+
 // The length of time (seconds) to wait for a proxy to resolve before ignoring it.
 const PROXY_TIMEOUT_SEC = 10;
 
 const {
+  ExtensionError,
   defineLazyGetter,
 } = ExtensionUtils;
 
 const {
   BaseContext,
   CanOfAPIs,
   LocalAPIImplementation,
   SchemaAPIManager,
 } = ExtensionCommon;
 
 const PROXY_TYPES = Object.freeze({
   DIRECT: "direct",
   HTTPS: "https",
-  PROXY: "proxy",
-  HTTP: "http", // Synonym for PROXY_TYPES.PROXY
-  SOCKS: "socks",  // SOCKS5
+  PROXY: "http", // Synonym for PROXY_TYPES.HTTP
+  HTTP: "http",
+  SOCKS: "socks", // SOCKS5
   SOCKS4: "socks4",
 });
 
+const ProxyInfoData = {
+  validate(proxyData) {
+    if (proxyData.type && proxyData.type.toLowerCase() === "direct") {
+      return {type: proxyData.type};
+    }
+    for (let prop of ["type", "host", "port", "username", "password", "proxyDNS", "failoverTimeout"]) {
+      this[prop](proxyData);
+    }
+    return proxyData;
+  },
+
+  type(proxyData) {
+    let {type} = proxyData;
+    if (typeof type !== "string" || !PROXY_TYPES.hasOwnProperty(type.toUpperCase())) {
+      throw new ExtensionError(`FindProxyForURL: Invalid proxy server type: "${type}"`);
+    }
+    proxyData.type = PROXY_TYPES[type.toUpperCase()];
+  },
+
+  host(proxyData) {
+    let {host} = proxyData;
+    if (typeof host !== "string" || host.includes(" ")) {
+      throw new ExtensionError(`FindProxyForURL: Invalid proxy server host: "${host}"`);
+    }
+    if (!host.length) {
+      throw new ExtensionError("FindProxyForURL: Proxy server host cannot be empty");
+    }
+    proxyData.host = host;
+  },
+
+  port(proxyData) {
+    let port = Number.parseInt(proxyData.port, 10);
+    if (!Number.isInteger(port)) {
+      throw new ExtensionError(`FindProxyForURL: Invalid proxy server port: "${port}"`);
+    }
+
+    if (port < 1 || port > 0xffff) {
+      throw new ExtensionError(`FindProxyForURL: Proxy server port ${port} outside range 1 to 65535`);
+    }
+    proxyData.port = port;
+  },
+
+  username(proxyData) {
+    let {username} = proxyData;
+    if (username !== undefined && typeof username !== "string") {
+      throw new ExtensionError(`FindProxyForURL: Invalid proxy server username: "${username}"`);
+    }
+  },
+
+  password(proxyData) {
+    let {password} = proxyData;
+    if (password !== undefined && typeof password !== "string") {
+      throw new ExtensionError(`FindProxyForURL: Invalid proxy server password: "${password}"`);
+    }
+  },
+
+  proxyDNS(proxyData) {
+    let {proxyDNS, type} = proxyData;
+    if (proxyDNS !== undefined) {
+      if (typeof proxyDNS !== "boolean") {
+        throw new ExtensionError(`FindProxyForURL: Invalid proxyDNS value: "${proxyDNS}"`);
+      }
+      if (proxyDNS && type !== PROXY_TYPES.SOCKS && type !== PROXY_TYPES.SOCKS4) {
+        throw new ExtensionError(`FindProxyForURL: proxyDNS can only be true for SOCKS proxy servers`);
+      }
+    }
+  },
+
+  failoverTimeout(proxyData) {
+    let {failoverTimeout} = proxyData;
+    if (failoverTimeout !== undefined && (!Number.isInteger(failoverTimeout) || failoverTimeout < 1)) {
+      throw new ExtensionError(`FindProxyForURL: Invalid failover timeout: "${failoverTimeout}"`);
+    }
+  },
+
+  createProxyInfoFromData(proxyDataList, defaultProxyInfo) {
+    let {type, host, port, username, password, proxyDNS, failoverTimeout} =
+        ProxyInfoData.validate(proxyDataList.shift());
+    if (type === PROXY_TYPES.DIRECT) {
+      return defaultProxyInfo;
+    }
+    let failoverProxy = proxyDataList.length > 0 ? this.createProxyInfoFromData(proxyDataList, defaultProxyInfo) : defaultProxyInfo;
+    // When Bug 1360404 is fixed use ProxyService.newProxyInfoWithAuth() for all types.
+    if (type === PROXY_TYPES.SOCKS || type === PROXY_TYPES.SOCKS4) {
+      return ProxyService.newProxyInfoWithAuth(
+              type, host, port, username, password, proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0,
+              failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC, failoverProxy);
+    }
+    return ProxyService.newProxyInfo(
+            type, host, port, proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0,
+            failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC, failoverProxy);
+  },
+
+  /**
+   * Creates a new proxy info data object using the return value of FindProxyForURL.
+   *
+   * @param {Array<string>} rule A single proxy rule returned by FindProxyForURL.
+   *    (e.g. "PROXY 1.2.3.4:8080", "SOCKS 1.1.1.1:9090" or "DIRECT")
+   * @returns {nsIProxyInfo} The proxy info to apply for the given URI.
+   */
+  parseProxyInfoDataFromPAC(rule) {
+    if (!rule) {
+      throw new ExtensionError("FindProxyForURL: Missing Proxy Rule");
+    }
+
+    let parts = rule.toLowerCase().split(/\s+/);
+    if (!parts[0] || parts.length > 2) {
+      throw new ExtensionError(`FindProxyForURL: Invalid arguments passed for proxy rule: "${rule}"`);
+    }
+    let type = parts[0];
+    let [host, port] = parts.length > 1 ? parts[1].split(":") : [];
+
+    switch (PROXY_TYPES[type.toUpperCase()]) {
+      case PROXY_TYPES.HTTP:
+      case PROXY_TYPES.HTTPS:
+      case PROXY_TYPES.SOCKS:
+      case PROXY_TYPES.SOCKS4:
+        if (!host || !port) {
+          throw new ExtensionError(`FindProxyForURL: Invalid host or port from proxy rule: "${rule}"`);
+        }
+        return {type, host, port};
+      case PROXY_TYPES.DIRECT:
+        if (host || port) {
+          throw new ExtensionError(`FindProxyForURL: Invalid argument for proxy type: "${type}"`);
+        }
+        return {type};
+      default:
+        throw new ExtensionError(`FindProxyForURL: Unrecognized proxy type: "${type}"`);
+    }
+  },
+
+};
+
 class ProxyScriptContext extends BaseContext {
   constructor(extension, url, contextInfo = {}) {
     super("proxy_script", extension);
     this.contextInfo = contextInfo;
     this.extension = extension;
     this.messageManager = Services.cpmm;
     this.sandbox = Cu.Sandbox(this.extension.principal, {
       sandboxName: `proxyscript:${extension.id}:${url}`,
@@ -99,142 +236,76 @@ class ProxyScriptContext extends BaseCon
   get principal() {
     return this.extension.principal;
   }
 
   get cloneScope() {
     return this.sandbox;
   }
 
+  proxyInfoFromProxyData(proxyData, defaultProxyInfo) {
+    switch (typeof proxyData) {
+      case "string":
+        let proxyRules = [];
+        try {
+          for (let result of proxyData.split(";")) {
+            proxyRules.push(ProxyInfoData.parseProxyInfoDataFromPAC(result.trim()));
+          }
+        } catch (e) {
+          // If we have valid proxies already, lets use them and just emit
+          // errors for the failovers.
+          if (proxyRules.length === 0) {
+            throw e;
+          }
+          let error = this.normalizeError(e);
+          this.extension.emit("proxy-error", {
+            message: error.message,
+            fileName: error.fileName,
+            lineNumber: error.lineNumber,
+            stack: error.stack,
+          });
+        }
+        proxyData = proxyRules;
+        // fall through
+      case "object":
+        if (Array.isArray(proxyData) && proxyData.length > 0) {
+          return ProxyInfoData.createProxyInfoFromData(proxyData, defaultProxyInfo);
+        }
+        // Not an array, fall through to error.
+      default:
+        throw new ExtensionError("FindProxyForURL: Return type must be a string or array of objects");
+    }
+  }
+
   /**
    * This method (which is required by the nsIProtocolProxyService interface)
    * is called to apply proxy filter rules for the given URI and proxy object
    * (or list of proxy objects).
    *
    * @param {Object} service A reference to the Protocol Proxy Service.
    * @param {Object} uri The URI for which these proxy settings apply.
    * @param {Object} defaultProxyInfo The proxy (or list of proxies) that
    *     would be used by default for the given URI. This may be null.
    * @returns {Object} The proxy info to apply for the given URI.
    */
   applyFilter(service, uri, defaultProxyInfo) {
-    let ret;
     try {
       // Bug 1337001 - provide path and query components to non-https URLs.
-      ret = this.FindProxyForURL(uri.prePath, uri.host, this.contextInfo);
+      let ret = this.FindProxyForURL(uri.prePath, uri.host, this.contextInfo);
+      return this.proxyInfoFromProxyData(ret, defaultProxyInfo);
     } catch (e) {
       let error = this.normalizeError(e);
       this.extension.emit("proxy-error", {
         message: error.message,
         fileName: error.fileName,
         lineNumber: error.lineNumber,
         stack: error.stack,
       });
-      return defaultProxyInfo;
     }
-
-    if (!ret || typeof ret !== "string") {
-      this.extension.emit("proxy-error", {
-        message: "FindProxyForURL: Return type must be a string",
-      });
-      return defaultProxyInfo;
-    }
-
-    let rules = ret.split(";");
-    let proxyInfo = this.createProxyInfo(rules);
-
-    return proxyInfo || defaultProxyInfo;
-  }
-
-  /**
-   * 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"])
-   * @returns {nsIProxyInfo} The proxy info to apply for the given URI.
-   */
-  createProxyInfo(rules) {
-    if (!rules.length) {
-      return null;
-    }
-
-    let rule = rules[0].trim();
-
-    if (!rule) {
-      this.extension.emit("proxy-error", {
-        message: "FindProxyForURL: Missing Proxy Rule",
-      });
-      return null;
-    }
-
-    let parts = rule.split(/\s+/);
-    if (!parts[0]) {
-      this.extension.emit("proxy-error", {
-        message: `FindProxyForURL: Too many arguments passed for proxy rule: "${rule}"`,
-      });
-      return null;
-    }
-
-    if (parts.length > 2) {
-      this.extension.emit("proxy-error", {
-        message: `FindProxyForURL: Too many arguments passed for proxy rule: "${rule}"`,
-      });
-      return null;
-    }
-
-    switch (parts[0].toLowerCase()) {
-      case PROXY_TYPES.PROXY:
-      case PROXY_TYPES.HTTP:
-      case PROXY_TYPES.HTTPS:
-      case PROXY_TYPES.SOCKS:
-      case PROXY_TYPES.SOCKS4:
-        if (!parts[1]) {
-          this.extension.emit("proxy-error", {
-            message: `FindProxyForURL: Missing argument for proxy type: "${parts[0]}"`,
-          });
-          return null;
-        }
-
-        if (parts.length != 2) {
-          this.extension.emit("proxy-error", {
-            message: `FindProxyForURL: Too many arguments for proxy rule: "${rule}"`,
-          });
-          return null;
-        }
-
-        let [host, port] = parts[1].split(":");
-        if (!host || !port) {
-          this.extension.emit("proxy-error", {
-            message: `FindProxyForURL: Unable to parse host and port from proxy rule: "${rule}"`,
-          });
-          return null;
-        }
-
-        let type = parts[0];
-        if (parts[0].toLowerCase() == PROXY_TYPES.PROXY) {
-          // PROXY_TYPES.HTTP and PROXY_TYPES.PROXY are synonyms
-          type = PROXY_TYPES.HTTP;
-        }
-
-        let failoverProxy = this.createProxyInfo(rules.slice(1));
-        return ProxyService.newProxyInfo(type, host, port, 0,
-          PROXY_TIMEOUT_SEC, failoverProxy);
-      case PROXY_TYPES.DIRECT:
-        if (parts.length != 1) {
-          this.extension.emit("proxy-error", {
-            message: `FindProxyForURL: Invalid argument for proxy type: "${parts[0]}"`,
-          });
-        }
-        return null;
-      default:
-        this.extension.emit("proxy-error", {
-          message: `FindProxyForURL: Unrecognized proxy type: "${parts[0]}"`,
-        });
-        return null;
-    }
+    return defaultProxyInfo;
   }
 
   /**
    * Unloads the proxy filter and shuts down the sandbox.
    */
   unload() {
     super.unload();
     ProxyService.unregisterFilter(this);
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -85,17 +85,16 @@ 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_new_tab_processType.html]
 [test_ext_notifications.html]
 [test_ext_permission_xhr.html]
-[test_ext_proxy.html]
 skip-if = os == 'android' && debug # Bug 1357635
 [test_ext_redirect_jar.html]
 [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]
deleted file mode 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_proxy.html
+++ /dev/null
@@ -1,181 +0,0 @@
-<!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.register("proxy_script.js").then(() => {
-        browser.test.sendMessage("ready");
-      });
-
-      browser.test.onMessage.addListener(msg => {
-        if (msg === "unregister-proxy-script") {
-          browser.proxy.unregister().then(() => {
-            browser.test.notifyPass("proxy");
-          });
-        }
-      });
-    },
-    manifest: {
-      "permissions": ["proxy"],
-    },
-    files: {
-      "proxy_script.js": String(script).replace(/^.*?\{([^]*)\}$/, "$1"),
-    },
-  });
-
-  await extension.startup();
-  await extension.awaitMessage("ready");
-
-  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");
-  }
-
-  extension.sendMessage("unregister-proxy-script");
-  await extension.awaitFinish("proxy");
-
-  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",
-    });
-
-  await testProxyScript(
-    () => {
-      function FindProxyForURL() {
-        return "INVALID";
-      }
-    }, {
-      message: "FindProxyForURL: Unrecognized proxy type: \"INVALID\"",
-    });
-
-  await testProxyScript(
-    () => {
-      function FindProxyForURL() {
-        return "SOCKS";
-      }
-    }, {
-      message: "FindProxyForURL: Missing argument for proxy type: \"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: Unable to parse host and port from proxy rule: \"PROXY :\"",
-    });
-
-  await testProxyScript(
-    () => {
-      function FindProxyForURL() {
-        return "PROXY :8080";
-      }
-    }, {
-      message: "FindProxyForURL: Unable to parse host and port from proxy rule: \"PROXY :8080\"",
-    });
-
-  await testProxyScript(
-    () => {
-      function FindProxyForURL() {
-        return "PROXY ::";
-      }
-    }, {
-      message: "FindProxyForURL: Unable to parse host and port from proxy rule: \"PROXY ::\"",
-    });
-
-  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:\"",
-    });
-
-  await testProxyScript(
-    () => {
-      function FindProxyForURL() {
-        return "DIRECT 1.2.3.4:8080";
-      }
-    }, {
-      message: "FindProxyForURL: Invalid argument for proxy type: \"DIRECT\"",
-    });
-});
-
-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,352 +4,167 @@
 
 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) => {
+function getProxyInfo() {
+  return 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);
       },
     });
   });
+}
+async function testProxyScript(script, expected = {}) {
+  let scriptData = String(script).replace(/^.*?\{([^]*)\}$/, "$1");
+  let extensionData = {
+    manifest: {
+      "permissions": ["proxy"],
+    },
+    background() {
+      // Some tests generate multiple errors, we'll just rely on the first.
+      let seenError = false;
+      browser.proxy.onProxyError.addListener(error => {
+        if (!seenError) {
+          browser.test.sendMessage("proxy-error-received", error);
+          seenError = true;
+        }
+      });
 
-  if (!proxyInfo) {
-    equal(proxyInfo, expected.proxyInfo, "Expected proxyInfo to be null");
+      browser.test.onMessage.addListener(msg => {
+        if (msg === "unregister-proxy-script") {
+          browser.proxy.unregister().then(() => {
+            browser.test.notifyPass("proxy");
+          });
+        }
+      });
+
+      browser.proxy.register("proxy.js").then(() => {
+        browser.test.sendMessage("ready");
+      });
+    },
+    files: {
+      "proxy.js": scriptData,
+    },
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  await extension.awaitMessage("ready");
+
+  let errorWait = extension.awaitMessage("proxy-error-received");
+
+  let proxyInfo = await getProxyInfo();
+
+  let error = await errorWait;
+  equal(error.message, expected.message, "Correct error message received");
+  if (!expected.proxyInfo) {
+    equal(proxyInfo, null, "no proxyInfo received");
   } 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;
-    }
+    let {host, port, type} = expected.proxyInfo;
+    equal(proxyInfo.host, host, `Expected proxy host to be ${host}`);
+    equal(proxyInfo.port, port, `Expected proxy port to be ${port}`);
+    equal(proxyInfo.type, type, `Expected proxy type to be ${type}`);
   }
-
+  if (expected.errorInfo) {
+    ok(error.fileName.includes("proxy.js"), "Error should include file name");
+    equal(error.lineNumber, 3, "Error should include line number");
+    ok(error.stack.includes("proxy.js:3:7"), "Error should include stack trace");
+  }
+  extension.sendMessage("unregister-proxy-script");
+  await extension.awaitFinish("proxy");
   await extension.unload();
-  script.unload();
 }
 
-add_task(async function testUndefinedFindProxyForURL() {
-  await testProxyScript({
-    scriptData() { },
-  }, {
-    proxyInfo: null,
+add_task(async function test_invalid_FindProxyForURL_function() {
+  await testProxyScript(() => { }, {
+    message: "The proxy script must define FindProxyForURL as a function",
   });
-});
-
-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";
-        }
-      }
-    },
+  await testProxyScript(() => {
+    var FindProxyForURL = 5; // eslint-disable-line mozilla/var-only-at-top-level
   }, {
-    proxyInfo: null,
+    message: "The proxy script must define FindProxyForURL as a function",
   });
-});
 
-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";
-      }
-    },
+  await testProxyScript(() => {
+    function FindProxyForURL() {
+      return not_defined; // eslint-disable-line no-undef
+    }
   }, {
-    proxyInfo: null,
+    message: "not_defined is not defined",
+    errorInfo: true,
   });
-});
 
-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";
-      }
-    },
+  // The following tests will produce multiple errors.
+  await testProxyScript(() => {
+    function FindProxyForURL() {
+      return ";;;;;PROXY 1.2.3.4:8080";
+    }
   }, {
-    proxyInfo: {
-      host: "1.2.3.4",
-      port: "1080",
-      type: "socks4",
-      failoverProxy: null,
-    },
+    message: "FindProxyForURL: Missing Proxy Rule",
   });
-});
 
-add_task(async function testSocksReturnTypeWithHostCheck() {
-  await testProxyScript({
-    scriptData() {
-      function FindProxyForURL(url, host) {
-        if (host === "www.mozilla.org") {
-          return "SOCKS 4.4.4.4:9002";
-        }
-      }
-    },
+  // We take any valid proxy up to the error.
+  await testProxyScript(() => {
+    function FindProxyForURL() {
+      return "PROXY 1.2.3.4:8080; UNEXPECTED; SOCKS 1.2.3.4:8080";
+    }
   }, {
-    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";
-      }
-    },
-  }, {
+    message: "FindProxyForURL: Unrecognized proxy type: \"unexpected\"",
     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: null,
-      },
+async function getExtension(proxyResult) {
+  let extensionData = {
+    manifest: {
+      "permissions": ["proxy"],
     },
-  });
-});
-
-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);
-        }
+    background() {
+      browser.proxy.register("proxy.js").then(() => {
+        browser.test.sendMessage("ready");
       });
     },
-    runtimeMessage: {
-      host: "www.mozilla.org",
+    files: {
+      "proxy.js": `
+        function FindProxyForURL(url, host) {
+          return ${proxyResult};
+        }`,
     },
-  }, {
-    proxyInfo: {
-      host: "1.2.3.4",
-      port: "8080",
-      type: "http",
-      failoverProxy: null,
-    },
-  });
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  await extension.awaitMessage("ready");
+  return extension;
+}
+
+add_task(async function test_passthrough() {
+  let ext1 = await getExtension(null);
+  let ext2 = await getExtension("\"PROXY 1.2.3.4:8888\"");
+
+  let proxyInfo = await getProxyInfo();
+
+  equal(proxyInfo.host, "1.2.3.4", `second extension won`);
+  equal(proxyInfo.port, "8888", `second extension won`);
+  equal(proxyInfo.type, "http", `second extension won`);
+
+  await ext2.unload();
+
+  proxyInfo = await getProxyInfo();
+  equal(proxyInfo, null, `expected no proxy`);
+  await ext1.unload();
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_scripts_results.js
@@ -0,0 +1,338 @@
+"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");
+
+const TRANSPARENT_PROXY_RESOLVES_HOST = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+
+let extension;
+add_task(async function setup() {
+  let extensionData = {
+    manifest: {
+      "permissions": ["proxy"],
+    },
+    background() {
+      browser.proxy.onProxyError.addListener(error => {
+        browser.test.sendMessage("proxy-error-received", error);
+      });
+      browser.test.onMessage.addListener((message, data) => {
+        if (message === "set-proxy") {
+          browser.runtime.sendMessage(data, {toProxyScript: true}).then(response => {
+            browser.test.sendMessage("proxy-set", response);
+          });
+        }
+      });
+      browser.proxy.register("proxy.js").then(() => {
+        browser.test.sendMessage("ready");
+      });
+    },
+    files: {
+      "proxy.js": `"use strict";
+        let settings = {proxy: null};
+        function FindProxyForURL(url, host) {
+          return settings.proxy;
+        }
+        browser.runtime.onMessage.addListener((msg, sender, respond) => {
+          if (msg.proxy) {
+            settings.proxy = msg.proxy;
+            return Promise.resolve(settings.proxy);
+          }
+        });
+      `,
+    },
+  };
+  extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  await extension.awaitMessage("ready");
+});
+
+async function testProxyScript(test) {
+  let {proxy, expected} = test;
+  extension.sendMessage("set-proxy", {proxy});
+  let proxyInfoSent = await extension.awaitMessage("proxy-set");
+  deepEqual(proxyInfoSent, proxy, "got back proxy data from proxy script");
+
+  let errorMsg;
+  if (expected.error) {
+    errorMsg = extension.awaitMessage("proxy-error-received");
+  }
+  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 && pi.QueryInterface(Ci.nsIProxyInfo));
+      },
+    });
+  });
+
+  let expectedProxyInfo = expected.proxyInfo;
+  if (expected.error) {
+    equal(proxyInfo, null, "Expected proxyInfo to be null");
+    equal((await errorMsg).message, expected.error, "error received");
+  } else if (proxy == null) {
+    equal(proxyInfo, expectedProxyInfo, "proxy is direct");
+  } else {
+    for (let proxyUsed = proxyInfo; proxyUsed; proxyUsed = proxyUsed.failoverProxy) {
+      let {type, host, port, username, password, proxyDNS, failoverTimeout} = expectedProxyInfo;
+      equal(proxyUsed.host, host, `Expected proxy host to be ${host}`);
+      equal(proxyUsed.port, port, `Expected proxy port to be ${port}`);
+      equal(proxyUsed.type, type, `Expected proxy type to be ${type}`);
+      // May be null or undefined depending on use of newProxyInfoWithAuth or newProxyInfo
+      equal(proxyUsed.username || "", username || "", `Expected proxy username to be ${username}`);
+      equal(proxyUsed.password || "", password || "", `Expected proxy password to be ${password}`);
+      equal(proxyUsed.flags, proxyDNS == undefined ? 0 : proxyDNS, `Expected proxyDNS to be ${proxyDNS}`);
+      // Default timeout is 10
+      equal(proxyUsed.failoverTimeout, failoverTimeout || 10, `Expected failoverTimeout to be ${failoverTimeout}`);
+      expectedProxyInfo = expectedProxyInfo.failoverProxy;
+    }
+  }
+}
+
+add_task(async function test_pac_results() {
+  let tests = [
+    {
+      proxy: undefined,
+      expected: {
+        error: "FindProxyForURL: Return type must be a string or array of objects",
+      },
+    },
+    {
+      proxy: 5,
+      expected: {
+        error: "FindProxyForURL: Return type must be a string or array of objects",
+      },
+    },
+    {
+      proxy: "INVALID",
+      expected: {
+        error: "FindProxyForURL: Unrecognized proxy type: \"invalid\"",
+      },
+    },
+    {
+      proxy: "SOCKS",
+      expected: {
+        error: "FindProxyForURL: Invalid host or port from proxy rule: \"SOCKS\"",
+      },
+    },
+    {
+      proxy: "PROXY 1.2.3.4:8080 EXTRA",
+      expected: {
+        error: "FindProxyForURL: Invalid arguments passed for proxy rule: \"PROXY 1.2.3.4:8080 EXTRA\"",
+      },
+    },
+    {
+      proxy: "PROXY :",
+      expected: {
+        error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY :\"",
+      },
+    },
+    {
+      proxy: "PROXY :8080",
+      expected: {
+        error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY :8080\"",
+      },
+    },
+    {
+      proxy: "PROXY ::",
+      expected: {
+        error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY ::\"",
+      },
+    },
+    {
+      proxy: "PROXY 1.2.3.4:",
+      expected: {
+        error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY 1.2.3.4:\"",
+      },
+    },
+    {
+      proxy: "DIRECT 1.2.3.4:8080",
+      expected: {
+        error: "FindProxyForURL: Invalid argument for proxy type: \"direct\"",
+      },
+    },
+    {
+      proxy: ["SOCKS foo.bar:1080", {type: "http", host: "foo.bar", port: 3128}],
+      expected: {
+        error: "FindProxyForURL: Invalid proxy server type: \"undefined\"",
+      },
+    },
+    {
+      proxy: {type: "socks", host: "foo.bar", port: 1080, username: "mungosantamaria", password: "pass123"},
+      expected: {
+        error: "FindProxyForURL: Return type must be a string or array of objects",
+      },
+    },
+    {
+      proxy: [{type: "pptp", host: "foo.bar", port: 1080, username: "mungosantamaria", password: "pass123", proxyDNS: true, failoverTimeout: 3},
+              {type: "http", host: "192.168.1.1", port: 1128, username: "mungosantamaria", password: "word321"}],
+      expected: {
+        error: "FindProxyForURL: Invalid proxy server type: \"pptp\"",
+      },
+    },
+    {
+      proxy: [{type: "http", 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"}],
+      expected: {
+        error: "FindProxyForURL: Proxy server port 65536 outside range 1 to 65535",
+      },
+    },
+    {
+      proxy: [{type: "direct"}],
+      expected: {
+        proxyInfo: null,
+      },
+    },
+    {
+      proxy: "PROXY 1.2.3.4:8080",
+      expected: {
+        proxyInfo: {
+          host: "1.2.3.4",
+          port: "8080",
+          type: "http",
+          failoverProxy: null,
+        },
+      },
+    },
+    {
+      proxy: "   PROXY    2.3.4.5:8080      ",
+      expected: {
+        proxyInfo: {
+          host: "2.3.4.5",
+          port: "8080",
+          type: "http",
+          failoverProxy: null,
+        },
+      },
+    },
+    {
+      proxy: "PROXY 1.2.3.4:8080; SOCKS 4.4.4.4:9000; DIRECT",
+      expected: {
+        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,
+            },
+          },
+        },
+      },
+    },
+    {
+      proxy: [{type: "http", host: "foo.bar", port: 3128}],
+      expected: {
+        proxyInfo: {
+          host: "foo.bar",
+          port: "3128",
+          type: "http",
+        },
+      },
+    },
+    {
+      proxy: "SOCKS foo.bar:1080",
+      expected: {
+        proxyInfo: {
+          host: "foo.bar",
+          port: "1080",
+          type: "socks",
+        },
+      },
+    },
+    {
+      proxy: "SOCKS4 foo.bar:1080",
+      expected: {
+        proxyInfo: {
+          host: "foo.bar",
+          port: "1080",
+          type: "socks4",
+        },
+      },
+    },
+    {
+      proxy: [{type: "https", host: "foo.bar", port: 3128}],
+      expected: {
+        proxyInfo: {
+          host: "foo.bar",
+          port: "3128",
+          type: "https",
+        },
+      },
+    },
+    {
+      proxy: [{type: "socks", host: "foo.bar", port: 1080, username: "mungo", password: "santamaria123", proxyDNS: true, failoverTimeout: 5}],
+      expected: {
+        proxyInfo: {
+          type: "socks",
+          host: "foo.bar",
+          port: 1080,
+          username: "mungo",
+          password: "santamaria123",
+          failoverTimeout: 5,
+          failoverProxy: null,
+          proxyDNS: TRANSPARENT_PROXY_RESOLVES_HOST,
+        },
+      },
+    },
+    {
+      proxy: [{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"}],
+      expected: {
+        proxyInfo: {
+          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: "direct",
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+  ];
+  for (let test of tests) {
+    await testProxyScript(test);
+  }
+});
+
+add_task(async function shutdown() {
+  await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -68,8 +68,9 @@ skip-if = os == "android"
 [test_ext_storage_telemetry.js]
 skip-if = os == "android" # checking for telemetry needs to be updated: 1384923
 [test_ext_topSites.js]
 skip-if = os == "android"
 [test_native_messaging.js]
 skip-if = os == "android"
 [test_proxy_scripts.js]
 skip-if = os == "linux" # bug 1393940
+[test_proxy_scripts_results.js]