Bug 1409878 implement async proxy api draft
authorShane Caraveo <scaraveo@mozilla.com>
Fri, 23 Feb 2018 18:06:31 -0600
changeset 759258 d59058c599d4c5a213638f673d20da9bd6955594
parent 758396 ea3da643422c58d65335f1778dd6c89c09911585
push id100324
push usermixedpuppy@gmail.com
push dateSat, 24 Feb 2018 00:07:02 +0000
bugs1409878
milestone60.0a1
Bug 1409878 implement async proxy api MozReview-Commit-ID: 50xsccRy19A
toolkit/components/extensions/ProxyScriptContext.jsm
toolkit/components/extensions/WebRequestEventManager.jsm
toolkit/components/extensions/ext-proxy.js
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/moz.build
toolkit/components/extensions/schemas/proxy.json
toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js
toolkit/components/extensions/test/xpcshell/test_proxy_listener.js
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
toolkit/modules/addons/WebRequest.jsm
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -1,23 +1,32 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 /* 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";
 
-this.EXPORTED_SYMBOLS = ["ProxyScriptContext"];
+this.EXPORTED_SYMBOLS = ["ProxyScriptContext", "ProxyChannelFilter"];
 
 /* exported ProxyScriptContext */
+/* globals ChannelWrapper, tabTracker */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
+ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
+
+const {
+  apiManager,
+} = ExtensionParent;
+
+// global is not available in some xpcshell tests (without browser window)
+const tabTracker = apiManager.global && apiManager.global.tabTracker;
 
 ChromeUtils.defineModuleGetter(this, "ExtensionChild",
                                "resource://gre/modules/ExtensionChild.jsm");
 ChromeUtils.defineModuleGetter(this, "Schemas",
                                "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "ProxyService",
                                    "@mozilla.org/network/protocol-proxy-service;1",
                                    "nsIProtocolProxyService");
@@ -60,74 +69,74 @@ const ProxyInfoData = {
       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}"`);
+      throw new ExtensionError(`ProxyInfoData: 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}"`);
+      throw new ExtensionError(`ProxyInfoData: Invalid proxy server host: "${host}"`);
     }
     if (!host.length) {
-      throw new ExtensionError("FindProxyForURL: Proxy server host cannot be empty");
+      throw new ExtensionError("ProxyInfoData: 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}"`);
+      throw new ExtensionError(`ProxyInfoData: Invalid proxy server port: "${port}"`);
     }
 
     if (port < 1 || port > 0xffff) {
-      throw new ExtensionError(`FindProxyForURL: Proxy server port ${port} outside range 1 to 65535`);
+      throw new ExtensionError(`ProxyInfoData: 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}"`);
+      throw new ExtensionError(`ProxyInfoData: 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}"`);
+      throw new ExtensionError(`ProxyInfoData: 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}"`);
+        throw new ExtensionError(`ProxyInfoData: 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`);
+        throw new ExtensionError(`ProxyInfoData: 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}"`);
+      throw new ExtensionError(`ProxyInfoData: Invalid failover timeout: "${failoverTimeout}"`);
     }
   },
 
   createProxyInfoFromData(proxyDataList, defaultProxyInfo, proxyDataListIndex = 0) {
     if (proxyDataListIndex >= proxyDataList.length) {
       return defaultProxyInfo;
     }
     let {type, host, port, username, password, proxyDNS, failoverTimeout} =
@@ -155,47 +164,196 @@ const ProxyInfoData = {
    * 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");
+      throw new ExtensionError("ProxyInfoData: 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}"`);
+      throw new ExtensionError(`ProxyInfoData: 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}"`);
+          throw new ExtensionError(`ProxyInfoData: 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}"`);
+          throw new ExtensionError(`ProxyInfoData: Invalid argument for proxy type: "${type}"`);
         }
         return {type};
       default:
-        throw new ExtensionError(`FindProxyForURL: Unrecognized proxy type: "${type}"`);
+        throw new ExtensionError(`ProxyInfoData: Unrecognized proxy type: "${type}"`);
     }
   },
 
+  proxyInfoFromProxyData(context, 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 = context.normalizeError(e);
+          context.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("ProxyInfoData: proxyData must be a string or array of objects");
+    }
+  },
 };
 
+function normalizeFilter(filter) {
+  if (!filter) {
+    filter = {};
+  }
+
+  return {urls: filter.urls || null, types: filter.types || null};
+}
+
+class ProxyChannelFilter {
+  constructor(context, listener, filter, extraInfoSpec) {
+    this.context = context;
+    this.filter = normalizeFilter(filter);
+    this.listener = listener;
+    this.extraInfoSpec = extraInfoSpec || [];
+
+    ProxyService.registerChannelFilter(
+      this /* nsIProtocolProxyChannelFilter aFilter */,
+      0 /* unsigned long aPosition */
+    );
+  }
+
+  // Copy from WebRequest.jsm with small changes.
+  getRequestData(channel, extraData) {
+    let data = {
+      requestId: String(channel.id),
+      url: channel.finalURL,
+      method: channel.method,
+      type: channel.type,
+      fromCache: !!channel.fromCache,
+
+      originUrl: channel.originURL || undefined,
+      documentUrl: channel.documentURL || undefined,
+
+      frameId: channel.windowId,
+      parentFrameId: channel.parentWindowId,
+
+      frameAncestors: channel.frameAncestors || undefined,
+
+      timeStamp: Date.now(),
+
+      ...extraData,
+    };
+    if (this.extraInfoSpec.includes("requestHeaders")) {
+      data.requestHeaders = channel.getRequestHeaders();
+    }
+    return data;
+  }
+
+  /**
+   * 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 {nsIProtocolProxyService} service A reference to the Protocol Proxy Service.
+   * @param {nsIChannel} channel The channel for which these proxy settings apply.
+   * @param {nsIProxyInfo} defaultProxyInfo The proxy (or list of proxies) that
+   *     would be used by default for the given URI. This may be null.
+   * @param {nsIProtocolProxyChannelFilter} proxyFilter
+   */
+  async applyFilter(service, channel, defaultProxyInfo, proxyFilter) {
+    let proxyInfo = null;
+    try {
+      let wrapper = ChannelWrapper.get(channel);
+
+      let browserData = {tabId: -1, windowId: -1};
+      if (wrapper.browserElement && tabTracker) {
+        browserData = tabTracker.getBrowserData(wrapper.browserElement);
+      }
+      let filter = {...this.filter};
+      if (filter.tabId != null && browserData.tabId != filter.tabId) {
+        // Fall through to finally block.
+        return;
+      }
+      if (filter.windowId != null && browserData.windowId != filter.windowId) {
+        // Fall through to finally block.
+        return;
+      }
+
+      if (wrapper.matches(filter, this.context.extension.policy, {isProxy: true})) {
+        let data = this.getRequestData(wrapper, {tabId: browserData.tabId});
+
+        let ret = await this.listener(data);
+        if (ret === null) {
+          // Fall through to finally block.
+          return;
+        }
+        // We only accept proxyInfo objects, not the PAC strings. ProxyInfoData will
+        // accept either, so we want to enforce the limit here.
+        if (typeof ret !== "object") {
+          throw new ExtensionError("ProxyInfoData: proxyData must be an array of objects");
+        }
+        // We allow the call to return either a single proxyInfo or an array of proxyInfo.
+        if (!Array.isArray(ret)) {
+          ret = [ret];
+        }
+        proxyInfo = ProxyInfoData.createProxyInfoFromData(ret, defaultProxyInfo);
+      }
+    } catch (e) {
+      let error = this.context.normalizeError(e);
+      this.context.extension.emit("proxy-error", {
+        message: error.message,
+        fileName: error.fileName,
+        lineNumber: error.lineNumber,
+        stack: error.stack,
+      });
+    } finally {
+      // We must call onProxyFilterResult.
+      proxyFilter.onProxyFilterResult(proxyInfo || defaultProxyInfo);
+    }
+  }
+
+  destroy() {
+    ProxyService.unregisterFilter(this);
+  }
+}
+
 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: `Extension Proxy Script (${extension.policy.debugName}): ${url}`,
@@ -242,67 +400,33 @@ 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.
    * @param {Object} callback nsIProxyProtocolFilterResult to call onProxyFilterResult
          on with the proxy info to apply for the given URI.
    */
   applyFilter(service, uri, defaultProxyInfo, callback) {
     try {
       // TODO Bug 1337001 - provide path and query components to non-https URLs.
       let ret = this.FindProxyForURL(uri.prePath, uri.host, this.contextInfo);
-      ret = this.proxyInfoFromProxyData(ret, defaultProxyInfo);
+      ret = ProxyInfoData.proxyInfoFromProxyData(this, ret, defaultProxyInfo);
       callback.onProxyFilterResult(ret);
     } catch (e) {
       let error = this.normalizeError(e);
       this.extension.emit("proxy-error", {
         message: error.message,
         fileName: error.fileName,
         lineNumber: error.lineNumber,
         stack: error.stack,
copy from toolkit/components/extensions/ext-webRequest.js
copy to toolkit/components/extensions/WebRequestEventManager.jsm
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/WebRequestEventManager.jsm
@@ -2,28 +2,45 @@
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-toolkit.js */
 
 // This file expectes tabTracker to be defined in the global scope (e.g.
 // by ext-utils.js).
 /* global tabTracker */
 
+this.EXPORTED_SYMBOLS = ["WebRequestEventManager"];
+
 ChromeUtils.defineModuleGetter(this, "WebRequest",
                                "resource://gre/modules/WebRequest.jsm");
 
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
+
+const {
+  EventManager,
+} = ExtensionCommon;
+
+const {
+  apiManager,
+} = ExtensionParent;
+
+// global is not avialable in some xpcshell tests (without browser window)
+const tabTracker = apiManager.global && apiManager.global.tabTracker;
+
 // EventManager-like class specifically for WebRequest. Inherits from
 // EventManager. Takes care of converting |details| parameter
 // when invoking listeners.
-function WebRequestEventManager(context, eventName) {
-  let name = `webRequest.${eventName}`;
+function WebRequestEventManager(context, eventName, options = {}) {
+  let apiName = options.apiName || "webRequest";
+  let name = `${apiName}.${eventName}`;
   let register = (fire, filter, info) => {
     let listener = data => {
       let browserData = {tabId: -1, windowId: -1};
-      if (data.browser) {
+      if (data.browser && tabTracker) {
         browserData = tabTracker.getBrowserData(data.browser);
       }
       if (filter.tabId != null && browserData.tabId != filter.tabId) {
         return;
       }
       if (filter.windowId != null && browserData.windowId != filter.windowId) {
         return;
       }
@@ -50,62 +67,45 @@ function WebRequestEventManager(context,
     }
     if (filter.tabId) {
       filter2.tabId = filter.tabId;
     }
     if (filter.windowId) {
       filter2.windowId = filter.windowId;
     }
 
-    let blockingAllowed = context.extension.hasPermission("webRequestBlocking");
+    let blockingAllowed = context.extension.hasPermission("webRequestBlocking") || apiName == "proxy";
 
     let info2 = [];
     if (info) {
       for (let desc of info) {
         if (desc == "blocking" && !blockingAllowed) {
           Cu.reportError("Using webRequest.addListener with the blocking option " +
                          "requires the 'webRequestBlocking' permission.");
         } else {
           info2.push(desc);
         }
       }
     }
+    if (options.proxyAuthOnly) {
+      info2.push("blocking");
+    }
 
     let listenerDetails = {
       addonId: context.extension.id,
       extension: context.extension.policy,
       blockingAllowed,
       tabParent: context.xulBrowser.frameLoader.tabParent,
+      ...options,
     };
 
     WebRequest[eventName].addListener(
       listener, filter2, info2,
       listenerDetails);
     return () => {
       WebRequest[eventName].removeListener(listener);
     };
   };
 
   return EventManager.call(this, context, name, register);
 }
 
 WebRequestEventManager.prototype = Object.create(EventManager.prototype);
-
-this.webRequest = class extends ExtensionAPI {
-  getAPI(context) {
-    return {
-      webRequest: {
-        onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(),
-        onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
-        onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
-        onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(),
-        onAuthRequired: new WebRequestEventManager(context, "onAuthRequired").api(),
-        onBeforeRedirect: new WebRequestEventManager(context, "onBeforeRedirect").api(),
-        onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
-        onErrorOccurred: new WebRequestEventManager(context, "onErrorOccurred").api(),
-        onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
-        handlerBehaviorChanged: function() {
-          // TODO: Flush all caches.
-        },
-      },
-    };
-  }
-};
--- a/toolkit/components/extensions/ext-proxy.js
+++ b/toolkit/components/extensions/ext-proxy.js
@@ -7,33 +7,84 @@
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-toolkit.js */
 
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "ProxyScriptContext",
                                "resource://gre/modules/ProxyScriptContext.jsm");
+ChromeUtils.defineModuleGetter(this, "ProxyChannelFilter",
+                               "resource://gre/modules/ProxyScriptContext.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "ProxyService",
+                                   "@mozilla.org/network/protocol-proxy-service;1",
+                                   "nsIProtocolProxyService");
+ChromeUtils.import("resource://gre/modules/WebRequestEventManager.jsm");
+
 
 // WeakMap[Extension -> ProxyScriptContext]
-let proxyScriptContextMap = new WeakMap();
+const proxyScriptContextMap = new WeakMap();
+
+// EventManager-like class specifically for Proxy filters. Inherits from
+// EventManager. Takes care of converting |details| parameter
+// when invoking listeners.
+function ProxyFilterEventManager(context, eventName) {
+  let name = `proxy.${eventName}`;
+  let register = (fire, filterProps, extraInfoSpec = []) => {
+    let listener = (data) => {
+      return fire.sync(data);
+    };
+
+    let filter = {...filterProps};
+    if (filter.urls) {
+      let perms = new MatchPatternSet([...context.extension.whiteListedHosts.patterns,
+                                       ...context.extension.optionalOrigins.patterns]);
+
+      filter.urls = new MatchPatternSet(filter.urls);
+
+      if (!perms.overlapsAll(filter.urls)) {
+        Cu.reportError("The proxy.addListener filter doesn't overlap with host permissions.");
+      }
+    }
+
+    let proxyFilter = new ProxyChannelFilter(context, listener, filter, extraInfoSpec);
+    return () => {
+      proxyFilter.destroy();
+    };
+  };
+
+  return EventManager.call(this, context, name, register);
+}
+
+ProxyFilterEventManager.prototype = Object.create(EventManager.prototype);
 
 this.proxy = class extends ExtensionAPI {
   onShutdown() {
     let {extension} = this;
 
     let proxyScriptContext = proxyScriptContextMap.get(extension);
     if (proxyScriptContext) {
       proxyScriptContext.unload();
       proxyScriptContextMap.delete(extension);
     }
   }
 
   getAPI(context) {
     let {extension} = context;
+
+    let onError = new EventManager(context, "proxy.onError", fire => {
+      let listener = (name, error) => {
+        fire.async(error);
+      };
+      extension.on("proxy-error", listener);
+      return () => {
+        extension.off("proxy-error", listener);
+      };
+    }).api();
+
     return {
       proxy: {
         register(url) {
           this.unregister();
 
           let proxyScriptContext = new ProxyScriptContext(extension, url);
           if (proxyScriptContext.load()) {
             proxyScriptContextMap.set(extension, proxyScriptContext);
@@ -47,21 +98,20 @@ this.proxy = class extends ExtensionAPI 
             proxyScriptContextMap.delete(extension);
           }
         },
 
         registerProxyScript(url) {
           this.register(url);
         },
 
-        onProxyError: new EventManager(context, "proxy.onProxyError", fire => {
-          let listener = (name, error) => {
-            fire.async(error);
-          };
-          extension.on("proxy-error", listener);
-          return () => {
-            extension.off("proxy-error", listener);
-          };
-        }).api(),
+        onRequest: new ProxyFilterEventManager(context, "onRequest").api(),
+
+        onAuthRequired: new WebRequestEventManager(context, "onAuthRequired", {apiName: "proxy", proxyAuthOnly: true}).api(),
+
+        onError,
+
+        // TODO deprecate
+        onProxyError: onError,
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -4,95 +4,17 @@
 /* import-globals-from ext-toolkit.js */
 
 // This file expectes tabTracker to be defined in the global scope (e.g.
 // by ext-utils.js).
 /* global tabTracker */
 
 ChromeUtils.defineModuleGetter(this, "WebRequest",
                                "resource://gre/modules/WebRequest.jsm");
-
-// EventManager-like class specifically for WebRequest. Inherits from
-// EventManager. Takes care of converting |details| parameter
-// when invoking listeners.
-function WebRequestEventManager(context, eventName) {
-  let name = `webRequest.${eventName}`;
-  let register = (fire, filter, info) => {
-    let listener = data => {
-      let browserData = {tabId: -1, windowId: -1};
-      if (data.browser) {
-        browserData = tabTracker.getBrowserData(data.browser);
-      }
-      if (filter.tabId != null && browserData.tabId != filter.tabId) {
-        return;
-      }
-      if (filter.windowId != null && browserData.windowId != filter.windowId) {
-        return;
-      }
-
-      let event = data.serialize(eventName);
-      event.tabId = browserData.tabId;
-
-      return fire.sync(event);
-    };
-
-    let filter2 = {};
-    if (filter.urls) {
-      let perms = new MatchPatternSet([...context.extension.whiteListedHosts.patterns,
-                                       ...context.extension.optionalOrigins.patterns]);
-
-      filter2.urls = new MatchPatternSet(filter.urls);
-
-      if (!perms.overlapsAll(filter2.urls)) {
-        Cu.reportError("The webRequest.addListener filter doesn't overlap with host permissions.");
-      }
-    }
-    if (filter.types) {
-      filter2.types = filter.types;
-    }
-    if (filter.tabId) {
-      filter2.tabId = filter.tabId;
-    }
-    if (filter.windowId) {
-      filter2.windowId = filter.windowId;
-    }
-
-    let blockingAllowed = context.extension.hasPermission("webRequestBlocking");
-
-    let info2 = [];
-    if (info) {
-      for (let desc of info) {
-        if (desc == "blocking" && !blockingAllowed) {
-          Cu.reportError("Using webRequest.addListener with the blocking option " +
-                         "requires the 'webRequestBlocking' permission.");
-        } else {
-          info2.push(desc);
-        }
-      }
-    }
-
-    let listenerDetails = {
-      addonId: context.extension.id,
-      extension: context.extension.policy,
-      blockingAllowed,
-      tabParent: context.xulBrowser.frameLoader.tabParent,
-    };
-
-    WebRequest[eventName].addListener(
-      listener, filter2, info2,
-      listenerDetails);
-    return () => {
-      WebRequest[eventName].removeListener(listener);
-    };
-  };
-
-  return EventManager.call(this, context, name, register);
-}
-
-WebRequestEventManager.prototype = Object.create(EventManager.prototype);
+ChromeUtils.import("resource://gre/modules/WebRequestEventManager.jsm");
 
 this.webRequest = class extends ExtensionAPI {
   getAPI(context) {
     return {
       webRequest: {
         onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(),
         onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
         onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -24,16 +24,17 @@ EXTRA_JS_MODULES += [
     'ExtensionUtils.jsm',
     'FindContent.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
     'NativeManifests.jsm',
     'NativeMessaging.jsm',
     'ProxyScriptContext.jsm',
     'Schemas.jsm',
+    'WebRequestEventManager.jsm',
 ]
 
 EXTRA_COMPONENTS += [
     'extension-process-script.js',
     'extensions-toolkit.manifest',
 ]
 
 TESTING_JS_MODULES += [
--- a/toolkit/components/extensions/schemas/proxy.json
+++ b/toolkit/components/extensions/schemas/proxy.json
@@ -12,16 +12,34 @@
         }]
       }
     ]
   },
   {
     "namespace": "proxy",
     "description": "Use the browser.proxy API to register proxy scripts in Firefox. Proxy scripts in Firefox are proxy auto-config files with extra contextual information and support for additional return types.",
     "permissions": ["proxy"],
+    "types": [
+      {
+        "id": "AuthResponse",
+        "type": "object",
+        "description": "Return value for authentication handlers.",
+        "properties": {
+          "authCredentials": {
+            "type": "object",
+            "description": "If set, the request is made using the supplied credentials.",
+            "optional": true,
+            "properties": {
+              "username": {"type": "string"},
+              "password": {"type": "string"}
+            }
+          }
+        }
+      }
+    ],
     "functions": [
       {
         "name": "register",
         "type": "function",
         "description": "Registers the proxy script for the extension.",
         "async": true,
         "parameters": [
           {
@@ -50,19 +68,125 @@
             "type": "string",
             "format": "strictRelativeUrl"
           }
         ]
       }
     ],
     "events": [
       {
+        "name": "onRequest",
+        "type": "function",
+        "description": "Fired when proxy data is needed for a request.",
+        "parameters": [
+          {
+            "type": "object",
+            "name": "details",
+            "properties": {
+              "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+              "url": {"type": "string"},
+              "method": {"type": "string", "description": "Standard HTTP method."},
+              "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+              "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+              "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
+              "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
+              "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+              "type": {"$ref": "webRequest.ResourceType", "description": "How the requested resource will be used."},
+              "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+              "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
+              "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
+              "requestHeaders": {"$ref": "webRequest.HttpHeaders", "optional": true, "description": "The HTTP request headers that are going to be sent out with this request."}
+            }
+          }
+        ],
+        "extraParameters": [
+          {
+            "$ref": "webRequest.RequestFilter",
+            "name": "filter",
+            "description": "A set of filters that restricts the events that will be sent to this listener."
+          },
+          {
+            "type": "array",
+            "optional": true,
+            "name": "extraInfoSpec",
+            "description": "Array of extra information that should be passed to the listener function.",
+            "items": {
+              "type": "string",
+              "enum": ["requestHeaders"]
+            }
+          }
+        ]
+      },
+      {
+        "name": "onAuthRequired",
+        "type": "function",
+        "description": "Fired when an proxy authentication failure is received. This listener works the same as the WebRequest.onAuthRequired event with the exception that it only receives proxy authentication requests.",
+        "parameters": [
+          {
+            "type": "object",
+            "name": "details",
+            "properties": {
+              "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+              "url": {"type": "string"},
+              "method": {"type": "string", "description": "Standard HTTP method."},
+              "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+              "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+              "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
+              "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
+              "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+              "type": {"$ref": "webRequest.ResourceType", "description": "How the requested resource will be used."},
+              "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+              "scheme": {"type": "string", "description": "The authentication scheme, e.g. Basic or Digest."},
+              "realm": {"type": "string", "description": "The authentication realm provided by the server, if there is one.", "optional": true},
+              "challenger": {"type": "object", "description": "The server requesting authentication.", "properties": {"host": {"type": "string"}, "port": {"type": "integer"}}},
+              "isProxy": {"type": "boolean", "description": "True for Proxy-Authenticate, false for WWW-Authenticate."},
+              "responseHeaders": {"$ref": "webRequest.HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this response."},
+              "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."},
+              "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."}
+            }
+          }
+        ],
+        "extraParameters": [
+          {
+            "$ref": "webRequest.RequestFilter",
+            "name": "filter",
+            "description": "A set of filters that restricts the events that will be sent to this listener."
+          },
+          {
+            "type": "array",
+            "optional": true,
+            "name": "extraInfoSpec",
+            "description": "Array of extra information that should be passed to the listener function.",
+            "items": {
+              "type": "string",
+              "enum": ["responseHeaders"]
+            }
+          }
+        ],
+        "returns": {
+          "$ref": "AuthResponse",
+          "description": "Credentials to use for the authentication.",
+          "optional": true
+        }
+      },
+      {
+        "name": "onError",
+        "type": "function",
+        "description": "Notifies about proxy script errors.",
+        "parameters": [
+          {
+            "name": "error",
+            "type": "object"
+          }
+        ]
+      },
+      {
         "name": "onProxyError",
         "type": "function",
-        "description": "Notifies about proxy script errors.",
+        "description": "Please use $(ref:proxy.onError).",
         "parameters": [
           {
             "name": "error",
             "type": "object"
           }
         ]
       }
     ]
copy from toolkit/components/extensions/test/xpcshell/test_ext_proxy_auth.js
copy to toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js
--- a/toolkit/components/extensions/test/xpcshell/test_ext_proxy_auth.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js
@@ -21,96 +21,102 @@ proxy.registerPathHandler("/", (request,
 });
 
 function getExtension(background) {
   return ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: [
         "proxy",
         "webRequest",
-        "webRequestBlocking",
         "<all_urls>",
       ],
     },
     background: `(${background})(${proxy.identity.primaryPort})`,
-    files: {
-      "proxy.js": `
-        function FindProxyForURL(url, host) {
-          return "PROXY localhost:${proxy.identity.primaryPort}; DIRECT";
-        }`,
-    },
   });
 }
+
 add_task(async function test_webRequest_auth_proxy() {
   async function background(port) {
-    browser.webRequest.onBeforeRequest.addListener(details => {
-      browser.test.log(`details ${JSON.stringify(details)}\n`);
-      browser.test.assertEq("localhost", details.proxyInfo.host, "proxy host");
-      browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
-      browser.test.assertEq("http", details.proxyInfo.type, "proxy type");
-      browser.test.assertEq("", details.proxyInfo.username, "proxy username not set");
-    }, {urls: ["<all_urls>"]});
-    browser.webRequest.onAuthRequired.addListener(details => {
+    browser.webRequest.onBeforeSendHeaders.addListener(details => {
+      browser.test.log(`onBeforeSendHeaders ${JSON.stringify(details)}\n`);
+      browser.test.assertTrue(!!details.proxyInfo, "proxyInfo exists");
+      if (details.proxyInfo) {
+        browser.test.assertEq("localhost", details.proxyInfo.host, "proxy host");
+        browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
+        browser.test.assertEq("http", details.proxyInfo.type, "proxy type");
+        browser.test.assertEq("", details.proxyInfo.username, "proxy username not set");
+      }
+    }, {urls: ["<all_urls>"]}, ["requestHeaders"]);
+
+    browser.proxy.onAuthRequired.addListener(details => {
+      browser.test.log(`onAuthRequired ${JSON.stringify(details)}\n`);
       browser.test.assertTrue(details.isProxy, "proxied request");
-      browser.test.assertEq("localhost", details.proxyInfo.host, "proxy host");
-      browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
-      browser.test.assertEq("http", details.proxyInfo.type, "proxy type");
       browser.test.assertEq("localhost", details.challenger.host, "proxy host");
       browser.test.assertEq(port, details.challenger.port, "proxy port");
       return {authCredentials: {username: "puser", password: "ppass"}};
-    }, {urls: ["<all_urls>"]}, ["blocking"]);
+    }, {urls: ["<all_urls>"]}, ["responseHeaders"]);
+
     browser.webRequest.onCompleted.addListener(details => {
-      browser.test.log(`details ${JSON.stringify(details)}\n`);
-      browser.test.assertEq("localhost", details.proxyInfo.host, "proxy host");
-      browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
-      browser.test.assertEq("http", details.proxyInfo.type, "proxy type");
-      browser.test.assertEq("", details.proxyInfo.username, "proxy username not set by onAuthRequired");
-      browser.test.assertEq(undefined, details.proxyInfo.password, "no proxy password");
+      browser.test.log(`onCompleted ${JSON.stringify(details)}\n`);
+      browser.test.assertTrue(!!details.proxyInfo, "proxyInfo exists");
+      if (details.proxyInfo) {
+        browser.test.assertEq("localhost", details.proxyInfo.host, "proxy host");
+        browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
+        browser.test.assertEq("http", details.proxyInfo.type, "proxy type");
+        browser.test.assertEq("", details.proxyInfo.username, "proxy username not set by onAuthRequired");
+        browser.test.assertEq(undefined, details.proxyInfo.password, "no proxy password");
+      }
       browser.test.sendMessage("done");
     }, {urls: ["<all_urls>"]});
 
-    await browser.proxy.register("proxy.js");
-    browser.test.sendMessage("pac-ready");
+    // Handle the proxy request.
+    browser.proxy.onRequest.addListener(details => {
+      browser.test.log(`onRequest ${JSON.stringify(details)}`);
+      return [{host: "localhost", port, type: "http"}];
+    }, {urls: ["<all_urls>"]}, ["requestHeaders"]);
+    browser.test.sendMessage("ready");
   }
 
   let handlingExt = getExtension(background);
 
   await handlingExt.startup();
-  await handlingExt.awaitMessage("pac-ready");
+  await handlingExt.awaitMessage("ready");
 
   let contentPage = await ExtensionTestUtils.loadContentPage(`http://mozilla.org/`);
 
   await handlingExt.awaitMessage("done");
   await contentPage.close();
   await handlingExt.unload();
 });
 
 add_task(async function test_webRequest_auth_proxy_system() {
   async function background(port) {
     browser.webRequest.onBeforeRequest.addListener(details => {
       browser.test.fail("onBeforeRequest");
     }, {urls: ["<all_urls>"]});
-    browser.webRequest.onAuthRequired.addListener(details => {
+
+    browser.proxy.onAuthRequired.addListener(details => {
       browser.test.sendMessage("onAuthRequired");
-      // cancel is silently ignored, if it were not (e.g someone messes up in
-      // WebRequest.jsm and allows cancel) this test would fail.
       return {
-        cancel: true,
         authCredentials: {username: "puser", password: "ppass"},
       };
-    }, {urls: ["<all_urls>"]}, ["blocking"]);
+    }, {urls: ["<all_urls>"]});
 
-    await browser.proxy.register("proxy.js");
-    browser.test.sendMessage("pac-ready");
+    // Handle the proxy request.
+    browser.proxy.onRequest.addListener(details => {
+      browser.test.log(`onRequest ${JSON.stringify(details)}`);
+      return {host: "localhost", port, type: "http"};
+    }, {urls: ["<all_urls>"]});
+    browser.test.sendMessage("ready");
   }
 
   let handlingExt = getExtension(background);
 
   await handlingExt.startup();
-  await handlingExt.awaitMessage("pac-ready");
+  await handlingExt.awaitMessage("ready");
 
   function fetch(url) {
     return new Promise((resolve, reject) => {
       let xhr = new XMLHttpRequest();
       xhr.mozBackgroundRequest = true;
       xhr.open("GET", url);
       xhr.onload = () => { resolve(xhr.responseText); };
       xhr.onerror = () => { reject(xhr.status); };
copy from toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
copy to toolkit/components/extensions/test/xpcshell/test_proxy_listener.js
--- a/toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_listener.js
@@ -4,162 +4,181 @@
 
 ChromeUtils.import("resource://gre/modules/Extension.jsm");
 ChromeUtils.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;
+
 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");
+
+const testData = [
+  {
+    proxyInfo: "not_defined",
+    expected: {
+      error: {
+        message: "ProxyInfoData: proxyData must be an array of objects",
+        errorInfo: {
+          filename: "background.js",
+          line: 0,
+          stack: "background.js:0:0",
+        },
+      },
+    },
+  },
+  {
+    proxyInfo: [{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",
+              },
+            },
+          },
+        },
+      },
+    },
+  },
+];
+
+add_task(async function test_proxy_listener() {
   let extensionData = {
     manifest: {
-      "permissions": ["proxy"],
+      "permissions": ["proxy", "<all_urls>"],
     },
     background() {
       // Some tests generate multiple errors, we'll just rely on the first.
       let seenError = false;
-      browser.proxy.onProxyError.addListener(error => {
+      let proxyInfo;
+      browser.proxy.onError.addListener(error => {
         if (!seenError) {
           browser.test.sendMessage("proxy-error-received", error);
           seenError = true;
         }
       });
 
-      browser.test.onMessage.addListener(msg => {
-        if (msg === "unregister-proxy-script") {
-          browser.proxy.unregister().then(() => {
-            browser.test.notifyPass("proxy");
-          });
+      browser.proxy.onRequest.addListener(details => {
+        browser.test.log(`onRequest ${JSON.stringify(details)}`);
+        if (proxyInfo == "not_defined") {
+          return not_defined; // eslint-disable-line no-undef
+        }
+        return proxyInfo;
+      }, {urls: ["<all_urls>"]});
+
+      browser.test.onMessage.addListener((message, data) => {
+        if (message === "set-proxy") {
+          seenError = false;
+          proxyInfo = data.proxyInfo;
         }
       });
 
-      browser.proxy.register("proxy.js").then(() => {
-        browser.test.sendMessage("ready");
-      });
-    },
-    files: {
-      "proxy.js": scriptData,
+      browser.test.sendMessage("ready");
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   await extension.startup();
   await extension.awaitMessage("ready");
 
-  let errorWait = extension.awaitMessage("proxy-error-received");
-
-  let proxyInfo = await getProxyInfo();
+  for (let test of testData) {
+    extension.sendMessage("set-proxy", test);
+    let testError = test.expected.error;
+    let errorWait = testError && extension.awaitMessage("proxy-error-received");
 
-  let error = await errorWait;
-  equal(error.message, expected.message, "Correct error message received");
-  if (!expected.proxyInfo) {
-    equal(proxyInfo, null, "no proxyInfo received");
-  } else {
-    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();
-}
-
-add_task(async function test_invalid_FindProxyForURL_function() {
-  await testProxyScript(() => { }, {
-    message: "The proxy script must define FindProxyForURL as a function",
-  });
+    let proxyInfo = await getProxyInfo();
+    let expectedProxyInfo = test.expected.proxyInfo;
 
-  await testProxyScript(() => {
-    var FindProxyForURL = 5; // eslint-disable-line mozilla/var-only-at-top-level
-  }, {
-    message: "The proxy script must define FindProxyForURL as a function",
-  });
-
-  await testProxyScript(() => {
-    function FindProxyForURL() {
-      return not_defined; // eslint-disable-line no-undef
+    if (!proxyInfo) {
+      let error = await errorWait;
+      equal(error.message, testError.message, "Correct error message received");
+      equal(proxyInfo, null, "no proxyInfo received");
+    } 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;
+      }
     }
-  }, {
-    message: "not_defined is not defined",
-    errorInfo: true,
-  });
+  }
 
-  // The following tests will produce multiple errors.
-  await testProxyScript(() => {
-    function FindProxyForURL() {
-      return ";;;;;PROXY 1.2.3.4:8080";
-    }
-  }, {
-    message: "FindProxyForURL: Missing Proxy Rule",
-  });
-
-  // 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";
-    }
-  }, {
-    message: "FindProxyForURL: Unrecognized proxy type: \"unexpected\"",
-    proxyInfo: {
-      host: "1.2.3.4",
-      port: "8080",
-      type: "http",
-      failoverProxy: null,
-    },
-  });
+  await extension.unload();
 });
 
 async function getExtension(proxyResult) {
+  function background(proxyInfo) {
+    browser.test.log(`=== using proxyInfo ${JSON.stringify(proxyInfo)}`);
+    browser.proxy.onRequest.addListener(details => {
+      return proxyInfo;
+    }, {urls: ["<all_urls>"]});
+    browser.test.sendMessage("ready");
+  }
   let extensionData = {
     manifest: {
-      "permissions": ["proxy"],
-    },
-    background() {
-      browser.proxy.register("proxy.js").then(() => {
-        browser.test.sendMessage("ready");
-      });
+      "permissions": ["proxy", "<all_urls>"],
     },
-    files: {
-      "proxy.js": `
-        function FindProxyForURL(url, host) {
-          return ${proxyResult};
-        }`,
-    },
+    background: `(${background})(${JSON.stringify(proxyResult)})`,
   };
   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 ext2 = await getExtension({host: "1.2.3.4", port: 8888, type: "http"});
 
   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();
--- a/toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
@@ -105,26 +105,26 @@ add_task(async function test_invalid_Fin
   });
 
   // The following tests will produce multiple errors.
   await testProxyScript(() => {
     function FindProxyForURL() {
       return ";;;;;PROXY 1.2.3.4:8080";
     }
   }, {
-    message: "FindProxyForURL: Missing Proxy Rule",
+    message: "ProxyInfoData: Missing Proxy Rule",
   });
 
   // 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";
     }
   }, {
-    message: "FindProxyForURL: Unrecognized proxy type: \"unexpected\"",
+    message: "ProxyInfoData: Unrecognized proxy type: \"unexpected\"",
     proxyInfo: {
       host: "1.2.3.4",
       port: "8080",
       type: "http",
       failoverProxy: null,
     },
   });
 });
--- a/toolkit/components/extensions/test/xpcshell/test_proxy_scripts_results.js
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_scripts_results.js
@@ -100,97 +100,97 @@ async function testProxyResolution(test)
   }
 }
 
 add_task(async function test_pac_results() {
   let tests = [
     {
       proxy: undefined,
       expected: {
-        error: "FindProxyForURL: Return type must be a string or array of objects",
+        error: "ProxyInfoData: proxyData must be a string or array of objects",
       },
     },
     {
       proxy: 5,
       expected: {
-        error: "FindProxyForURL: Return type must be a string or array of objects",
+        error: "ProxyInfoData: proxyData must be a string or array of objects",
       },
     },
     {
       proxy: "INVALID",
       expected: {
-        error: "FindProxyForURL: Unrecognized proxy type: \"invalid\"",
+        error: "ProxyInfoData: Unrecognized proxy type: \"invalid\"",
       },
     },
     {
       proxy: "SOCKS",
       expected: {
-        error: "FindProxyForURL: Invalid host or port from proxy rule: \"SOCKS\"",
+        error: "ProxyInfoData: 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\"",
+        error: "ProxyInfoData: 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 :\"",
+        error: "ProxyInfoData: Invalid host or port from proxy rule: \"PROXY :\"",
       },
     },
     {
       proxy: "PROXY :8080",
       expected: {
-        error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY :8080\"",
+        error: "ProxyInfoData: Invalid host or port from proxy rule: \"PROXY :8080\"",
       },
     },
     {
       proxy: "PROXY ::",
       expected: {
-        error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY ::\"",
+        error: "ProxyInfoData: 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:\"",
+        error: "ProxyInfoData: 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\"",
+        error: "ProxyInfoData: 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\"",
+        error: "ProxyInfoData: 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",
+        error: "ProxyInfoData: proxyData 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\"",
+        error: "ProxyInfoData: 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",
+        error: "ProxyInfoData: Proxy server port 65536 outside range 1 to 65535",
       },
     },
     {
       proxy: [{type: "direct"}],
       expected: {
         proxyInfo: null,
       },
     },
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -44,16 +44,17 @@ skip-if = os == "android" # checking for
 skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
 [test_ext_management_uninstall_self.js]
 [test_ext_onmessage_removelistener.js]
 skip-if = true # This test no longer tests what it is meant to test.
 [test_ext_privacy.js]
 [test_ext_privacy_disable.js]
 [test_ext_privacy_update.js]
 [test_ext_proxy_auth.js]
+[test_ext_proxy_onauthrequired.js]
 [test_ext_proxy_socks.js]
 [test_ext_redirects.js]
 [test_ext_runtime_connect_no_receiver.js]
 [test_ext_runtime_getBrowserInfo.js]
 [test_ext_runtime_getPlatformInfo.js]
 [test_ext_runtime_onInstalled_and_onStartup.js]
 skip-if = true # bug 1315829
 [test_ext_runtime_sendMessage.js]
@@ -78,11 +79,12 @@ skip-if = os == "android" # checking for
 [test_ext_trustworthy_origin.js]
 [test_ext_topSites.js]
 skip-if = os == "android"
 [test_native_manifests.js]
 subprocess = true
 skip-if = os == "android"
 [test_ext_permissions.js]
 skip-if = os == "android" # Bug 1350559
+[test_proxy_listener.js]
 [test_proxy_scripts.js]
 [test_proxy_scripts_results.js]
 [test_ext_brokenlinks.js]
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -735,16 +735,20 @@ HttpObserverManager = {
 
       let registerFilter = this.FILTER_TYPES.has(kind);
       let commonData = null;
       let requestBody;
       this.listeners[kind].forEach((opts, callback) => {
         if (!channel.matches(opts.filter, opts.extension, extraData)) {
           return;
         }
+        // The proxy api authRequired handling only takes proxy auth requests.
+        if (opts.proxyAuthOnly && !extraData.isProxy) {
+          return;
+        }
 
         if (!commonData) {
           commonData = this.getRequestData(channel, extraData);
           if (this.STATUS_TYPES.has(kind)) {
             commonData.statusCode = channel.statusCode;
             commonData.statusLine = channel.statusLine;
           }
         }