Bug 1255894: Part 7 - Expose response stream filtering via the webRequest API. r=mixedpuppy
authorKris Maglione <maglione.k@gmail.com>
Sat, 02 Sep 2017 13:37:31 -0700
changeset 428218 3599cdb1a849999f4d669885ddd7dbab21909cf5
parent 428217 26ce00544f9862adf2e07ce2b001280e59015969
child 428219 cc92f00288cd3e433f47ab7d5d724afbc46d0450
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1255894
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1255894: Part 7 - Expose response stream filtering via the webRequest API. r=mixedpuppy MozReview-Commit-ID: AErBFGJyFg5
toolkit/components/extensions/ext-c-toolkit.js
toolkit/components/extensions/ext-c-webRequest.js
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/jar.mn
toolkit/components/extensions/schemas/web_request.json
toolkit/modules/addons/WebRequest.jsm
--- a/toolkit/components/extensions/ext-c-toolkit.js
+++ b/toolkit/components/extensions/ext-c-toolkit.js
@@ -72,16 +72,23 @@ extensions.registerModules({
   },
   test: {
     url: "chrome://extensions/content/ext-c-test.js",
     scopes: ["addon_child", "content_child", "devtools_child", "proxy_script"],
     paths: [
       ["test"],
     ],
   },
+  webRequest: {
+    url: "chrome://extensions/content/ext-c-webRequest.js",
+    scopes: ["addon_child"],
+    paths: [
+      ["webRequest"],
+    ],
+  },
 });
 
 if (AppConstants.MOZ_BUILD_APP === "browser") {
   extensions.registerModules({
     identity: {
       url: "chrome://extensions/content/ext-c-identity.js",
       scopes: ["addon_child"],
       paths: [
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-webRequest.js
@@ -0,0 +1,18 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+this.webRequest = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      webRequest: {
+        filterResponseData(requestId) {
+          requestId = parseInt(requestId, 10);
+
+          return context.cloneScope.StreamFilter.create(
+            requestId, context.extension.id);
+        },
+      },
+    };
+  }
+};
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -95,29 +95,39 @@ function WebRequestEventManager(context,
     }
     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" && !context.extension.hasPermission("webRequestBlocking")) {
+        if (desc == "blocking" && !blockingAllowed) {
           Cu.reportError("Using webRequest.addListener with the blocking option " +
                          "requires the 'webRequestBlocking' permission.");
         } else {
           info2.push(desc);
         }
       }
     }
 
-    WebRequest[eventName].addListener(listener, filter2, info2);
+    let listenerDetails = {
+      addonId: context.extension.id,
+      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);
 }
 
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -36,8 +36,9 @@ toolkit.jar:
     content/extensions/ext-c-extension.js
 #ifndef ANDROID
     content/extensions/ext-c-identity.js
 #endif
     content/extensions/ext-c-runtime.js
     content/extensions/ext-c-storage.js
     content/extensions/ext-c-test.js
     content/extensions/ext-c-toolkit.js
+    content/extensions/ext-c-webRequest.js
--- a/toolkit/components/extensions/schemas/web_request.json
+++ b/toolkit/components/extensions/schemas/web_request.json
@@ -198,16 +198,33 @@
         "parameters": [
           {
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": []
           }
         ]
+      },
+      {
+        "name": "filterResponseData",
+        "permissions": ["webRequestBlocking"],
+        "type": "function",
+        "description": "...",
+        "parameters": [
+          {
+            "name": "requestId",
+            "type": "string"
+          }
+        ],
+        "returns": {
+          "type": "object",
+          "additionalProperties": {"type": "any"},
+          "isInstanceOf": "StreamFilter"
+        }
       }
     ],
     "events": [
       {
         "name": "onBeforeRequest",
         "type": "function",
         "description": "Fired when a request is about to occur.",
         "parameters": [
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -24,16 +24,20 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
                                   "resource://gre/modules/ExtensionUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
                                   "resource://gre/modules/WebRequestCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload",
                                   "resource://gre/modules/WebRequestUpload.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "webReqService",
+                                   "@mozilla.org/addons/webrequest-service;1",
+                                   "mozIWebRequestService");
+
 XPCOMUtils.defineLazyGetter(this, "ExtensionError", () => ExtensionUtils.ExtensionError);
 
 let WebRequestListener = Components.Constructor("@mozilla.org/webextensions/webRequestListener;1",
                                                 "nsIWebRequestListener", "init");
 
 function attachToChannel(channel, key, data) {
   if (channel instanceof Ci.nsIWritablePropertyBag2) {
     let wrapper = {wrappedJSObject: data};
@@ -47,17 +51,18 @@ function extractFromChannel(channel, key
     let data = channel.get(key);
     return data && data.wrappedJSObject;
   }
   return null;
 }
 
 function getData(channel) {
   const key = "mozilla.webRequest.data";
-  return extractFromChannel(channel, key) || attachToChannel(channel, key, {});
+  return (extractFromChannel(channel, key) ||
+          attachToChannel(channel, key, {registeredFilters: new Map()}));
 }
 
 function getFinalChannelURI(channel) {
   let {loadInfo} = channel;
   // resultPrincipalURI may be null, but originalURI never will be.
   return (loadInfo && loadInfo.resultPrincipalURI) || channel.originalURI;
 }
 
@@ -84,26 +89,26 @@ function parseFilter(filter) {
   if (!filter) {
     filter = {};
   }
 
   // FIXME: Support windowId filtering.
   return {urls: filter.urls || null, types: filter.types || null};
 }
 
-function parseExtra(extra, allowed = []) {
+function parseExtra(extra, allowed = [], optionsObj = {}) {
   if (extra) {
     for (let ex of extra) {
       if (allowed.indexOf(ex) == -1) {
         throw new ExtensionError(`Invalid option ${ex}`);
       }
     }
   }
 
-  let result = {};
+  let result = Object.assign({}, optionsObj);
   for (let al of allowed) {
     if (extra && extra.indexOf(al) != -1) {
       result[al] = true;
     }
   }
   return result;
 }
 
@@ -572,19 +577,25 @@ HttpObserverManager = {
     }
     if (needModify && !this.modifyInitialized) {
       this.modifyInitialized = true;
       Services.obs.addObserver(this, "http-on-before-connect");
     } else if (!needModify && this.modifyInitialized) {
       this.modifyInitialized = false;
       Services.obs.removeObserver(this, "http-on-before-connect");
     }
+
+    let haveBlocking = Object.values(this.listeners)
+                             .some(listeners => Array.from(listeners.values())
+                                                     .some(listener => listener.blockingAllowed));
+
     this.needTracing = this.listeners.onStart.size ||
                        this.listeners.onError.size ||
-                       this.listeners.onStop.size;
+                       this.listeners.onStop.size ||
+                       haveBlocking;
 
     let needExamine = this.needTracing ||
                       this.listeners.headersReceived.size ||
                       this.listeners.authRequired.size;
 
     if (needExamine && !this.examineInitialized) {
       this.examineInitialized = true;
       Services.obs.addObserver(this, "http-on-examine-response");
@@ -592,17 +603,17 @@ HttpObserverManager = {
       Services.obs.addObserver(this, "http-on-examine-merged-response");
     } else if (!needExamine && this.examineInitialized) {
       this.examineInitialized = false;
       Services.obs.removeObserver(this, "http-on-examine-response");
       Services.obs.removeObserver(this, "http-on-examine-cached-response");
       Services.obs.removeObserver(this, "http-on-examine-merged-response");
     }
 
-    let needRedirect = this.listeners.onRedirect.size;
+    let needRedirect = this.listeners.onRedirect.size || haveBlocking;
     if (needRedirect && !this.redirectInitialized) {
       this.redirectInitialized = true;
       ChannelEventSink.register();
     } else if (!needRedirect && this.redirectInitialized) {
       this.redirectInitialized = false;
       ChannelEventSink.unregister();
     }
 
@@ -881,55 +892,87 @@ HttpObserverManager = {
         // about:newtab and other non-host URIs will throw.  Those wont be in
         // the host permitted list, so we pass on the error.
       }
     }
 
     return true;
   },
 
+  registerChannel(channel, opts) {
+    if (!opts.blockingAllowed || !opts.addonId) {
+      return;
+    }
+
+    let data = getData(channel);
+    if (data.registeredFilters.has(opts.addonId)) {
+      return;
+    }
+
+    let filter = webReqService.registerTraceableChannel(
+      parseInt(data.requestId, 10),
+      channel,
+      opts.addonId,
+      opts.tabParent);
+
+    data.registeredFilters.set(opts.addonId, filter);
+  },
+
+  destroyFilters(channel) {
+    let filters = getData(channel).registeredFilters;
+    for (let [key, filter] of filters.entries()) {
+      filter.destruct();
+      filters.delete(key);
+    }
+  },
+
   runChannelListener(channel, loadContext = null, kind, extraData = null) {
     let handlerResults = [];
     let requestHeaders;
     let responseHeaders;
 
     try {
       if (this.activityInitialized) {
         let channelData = getData(channel);
         if (kind === "onError") {
+          this.destroyFilters(channel);
           if (channelData.errorNotified) {
             return;
           }
           channelData.errorNotified = true;
         } else if (this.errorCheck(channel, loadContext, channelData)) {
           return;
         }
       }
 
       let {loadInfo} = channel;
       let policyType = (loadInfo ? loadInfo.externalContentPolicyType
                                  : Ci.nsIContentPolicy.TYPE_OTHER);
 
-      let includeStatus = (["headersReceived", "authRequired", "onRedirect", "onStart", "onStop"].includes(kind) &&
-                           channel instanceof Ci.nsIHttpChannel);
+      let includeStatus = ["headersReceived", "authRequired", "onRedirect", "onStart", "onStop"].includes(kind);
+      let registerFilter = ["opening", "modify", "afterModify", "headersReceived", "authRequired", "onRedirect"].includes(kind);
 
       let canModify = this.canModify(channel);
       let commonData = null;
       let uri = getFinalChannelURI(channel);
       let requestBody;
       for (let [callback, opts] of this.listeners[kind].entries()) {
         if (!this.shouldRunListener(policyType, uri, opts.filter)) {
           continue;
         }
 
         if (!commonData) {
           commonData = this.getRequestData(channel, loadContext, policyType, extraData);
         }
         let data = Object.assign({}, commonData);
 
+        if (registerFilter) {
+          this.registerChannel(channel, opts);
+        }
+
         if (opts.requestHeaders) {
           requestHeaders = requestHeaders || new RequestHeaderChanger(channel);
           data.requestHeaders = requestHeaders.toArray();
         }
 
         if (opts.responseHeaders) {
           responseHeaders = responseHeaders || new ResponseHeaderChanger(channel);
           data.responseHeaders = responseHeaders.toArray();
@@ -1091,66 +1134,70 @@ HttpObserverManager = {
       channel.notificationCallbacks = new AuthRequestor(channel, this);
       channelData.hasAuthRequestor = true;
     }
   },
 
   onChannelReplaced(oldChannel, newChannel) {
     // We want originalURI, this will provide a moz-ext rather than jar or file
     // uri on redirects.
+    this.destroyFilters(oldChannel);
     this.runChannelListener(oldChannel, this.getLoadContext(oldChannel),
                             "onRedirect", {redirectUrl: newChannel.originalURI.spec});
   },
 
   onStartRequest(channel, loadContext) {
+    this.destroyFilters(channel);
     this.runChannelListener(channel, loadContext, "onStart");
   },
 
   onStopRequest(channel, loadContext) {
     this.runChannelListener(channel, loadContext, "onStop");
   },
 };
 
 var onBeforeRequest = {
   allowedOptions: ["blocking", "requestBody"],
 
-  addListener(callback, filter = null, opt_extraInfoSpec = null) {
-    let opts = parseExtra(opt_extraInfoSpec, this.allowedOptions);
+  addListener(callback, filter = null, options = null, optionsObject = null) {
+    let opts = parseExtra(options, this.allowedOptions);
     opts.filter = parseFilter(filter);
     ContentPolicyManager.addListener(callback, opts);
+
+    opts = Object.assign({}, opts, optionsObject);
     HttpObserverManager.addListener("opening", callback, opts);
   },
 
   removeListener(callback) {
     HttpObserverManager.removeListener("opening", callback);
     ContentPolicyManager.removeListener(callback);
   },
 };
 
 function HttpEvent(internalEvent, options) {
   this.internalEvent = internalEvent;
   this.options = options;
 }
 
 HttpEvent.prototype = {
-  addListener(callback, filter = null, opt_extraInfoSpec = null) {
-    let opts = parseExtra(opt_extraInfoSpec, this.options);
+  addListener(callback, filter = null, options = null, optionsObject = null) {
+    let opts = parseExtra(options, this.options, optionsObject);
     opts.filter = parseFilter(filter);
     HttpObserverManager.addListener(this.internalEvent, callback, opts);
   },
 
   removeListener(callback) {
     HttpObserverManager.removeListener(this.internalEvent, callback);
   },
 };
 
 var onBeforeSendHeaders = new HttpEvent("modify", ["requestHeaders", "blocking"]);
 var onSendHeaders = new HttpEvent("afterModify", ["requestHeaders"]);
 var onHeadersReceived = new HttpEvent("headersReceived", ["blocking", "responseHeaders"]);
-var onAuthRequired = new HttpEvent("authRequired", ["blocking", "responseHeaders"]); // TODO asyncBlocking
+var onAuthRequired = new HttpEvent("authRequired", ["blocking", "responseHeaders"]);
 var onBeforeRedirect = new HttpEvent("onRedirect", ["responseHeaders"]);
 var onResponseStarted = new HttpEvent("onStart", ["responseHeaders"]);
 var onCompleted = new HttpEvent("onStop", ["responseHeaders"]);
 var onErrorOccurred = new HttpEvent("onError");
 
 var WebRequest = {
   // http-on-modify observer for HTTP(S), content policy for the other protocols (notably, data:)
   onBeforeRequest: onBeforeRequest,