Bug 1396856: Part 4 - Update WebRequest.jsm to use ChannelWrapper bindings. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 05 Sep 2017 22:46:32 -0700
changeset 659699 119959bddf5ac5ac66259f1ec6f52eb28a1cc142
parent 659698 9ab476a07f9d2029782f2425eae73be6cee83a86
child 730028 b743e831b4ebddd171680c440b930117776329fa
push id78171
push usermaglione.k@gmail.com
push dateWed, 06 Sep 2017 06:26:38 +0000
reviewersmixedpuppy
bugs1396856
milestone57.0a1
Bug 1396856: Part 4 - Update WebRequest.jsm to use ChannelWrapper bindings. r?mixedpuppy MozReview-Commit-ID: 7s7SOQ1XVaw
toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.html
toolkit/components/extensions/test/mochitest/file_WebRequest_permission_redirected.html
toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html
toolkit/components/extensions/test/mochitest/test_ext_webrequest_permission.html
toolkit/modules/addons/WebRequest.jsm
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.html
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 
 <html>
 <head>
 <meta charset="utf-8">
 </head>
 <body>
 
-<script src="http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.js"></script>
+<script src="https://example.org/tests/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.js"></script>
 <script>
 "use strict";
 
 window.parent.postMessage({
   page: "original",
   script: window.testScript,
 }, "*");
 </script>
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_redirected.html
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_redirected.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 
 <html>
 <head>
 <meta charset="utf-8">
 </head>
 <body>
 
-<script src="http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.js"></script>
+<script src="https://example.org/tests/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.js"></script>
 <script>
 "use strict";
 
 window.parent.postMessage({
   page: "redirected",
   script: window.testScript,
 }, "*");
 </script>
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html
@@ -84,22 +84,22 @@ let expected = {
 };
 
 function checkDetails(details) {
   let url = new URL(details.url);
   let filename = url.pathname.split("/").pop();
   let expect = expected[filename];
   is(expect.type, details.type, `${details.type} type matches`);
   if (expect.toplevel) {
-    is(0, details.frameId, "expect load at top level");
-    is(-1, details.parentFrameId, "expect top level frame to have no parent");
+    is(details.frameId, 0, "expect load at top level");
+    is(details.parentFrameId, -1, "expect top level frame to have no parent");
   } else if (details.type == "sub_frame") {
     ok(details.frameId > 0, "expect sub_frame to load into a new frame");
     if (expect.toplevelParent) {
-      is(0, details.parentFrameId, "expect sub_frame to have top level parent");
+      is(details.parentFrameId, 0, "expect sub_frame to have top level parent");
     } else {
       ok(details.parentFrameId > 0, "expect sub_frame to have parent");
     }
     expect.subframeId = details.frameId;
     expect.parentId = details.parentFrameId;
   } else if (expect.sandboxed) {
     is(details.documentUrl, undefined, "null principal documentUrl for sandboxed request");
   } else {
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_permission.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_permission.html
@@ -12,17 +12,17 @@
 <script type="text/javascript">
 "use strict";
 
 add_task(async function test_permissions() {
   function background() {
     browser.webRequest.onBeforeRequest.addListener(details => {
       if (details.url.includes("_original")) {
         let redirectUrl = details.url
-                                 .replace("mochi.test:8888", "example.com")
+                                 .replace("example.org", "example.com")
                                  .replace("_original", "_redirected");
         return {redirectUrl};
       }
       return {};
     }, {urls: ["<all_urls>"]}, ["blocking"]);
   }
 
   let extensionData = {
@@ -42,17 +42,17 @@ add_task(async function test_permissions
     let promise = new Promise(resolve => {
       let listener = event => {
         window.removeEventListener("message", listener);
         resolve(event.data);
       };
       window.addEventListener("message", listener);
     });
 
-    iframe.setAttribute("src", "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.html");
+    iframe.setAttribute("src", "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_WebRequest_permission_original.html");
     let result = await promise;
     document.body.removeChild(iframe);
     return result;
   }
 
   let results = await check();
   is(results.page, "redirected", "Regular webRequest redirect works on an unprivileged page");
   is(results.script, "redirected", "Regular webRequest redirect works from an unprivileged page");
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const EXPORTED_SYMBOLS = ["WebRequest"];
 
 /* exported WebRequest */
 
+/* globals ChannelWrapper */
+
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const {nsIHttpActivityObserver, nsISocketTransport} = Ci;
 
 Cu.import("resource://gre/modules/Services.jsm");
@@ -33,59 +35,16 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@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};
-    channel.setPropertyAsInterface(key, wrapper);
-  }
-  return data;
-}
-
-function extractFromChannel(channel, key) {
-  if (channel instanceof Ci.nsIPropertyBag2 && channel.hasKey(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, {registeredFilters: new Map()}));
-}
-
-function getFinalChannelURI(channel) {
-  let {loadInfo} = channel;
-  // resultPrincipalURI may be null, but originalURI never will be.
-  return (loadInfo && loadInfo.resultPrincipalURI) || channel.originalURI;
-}
-
-var RequestId = {
-  count: 1,
-  create(channel = null) {
-    let id = (this.count++).toString();
-    if (channel) {
-      getData(channel).requestId = id;
-    }
-    return id;
-  },
-
-  get(channel) {
-    return (channel && getData(channel).requestId) || this.create(channel);
-  },
-};
-
 function runLater(job) {
   Services.tm.dispatchToMainThread(job);
 }
 
 function parseFilter(filter) {
   if (!filter) {
     filter = {};
   }
@@ -107,45 +66,28 @@ function parseExtra(extra, allowed = [],
   for (let al of allowed) {
     if (extra && extra.indexOf(al) != -1) {
       result[al] = true;
     }
   }
   return result;
 }
 
-function mergeStatus(data, channel, event) {
-  try {
-    data.statusCode = channel.responseStatus;
-    let statusText = channel.responseStatusText;
-    let maj = {};
-    let min = {};
-    channel.QueryInterface(Ci.nsIHttpChannelInternal).getResponseVersion(maj, min);
-    data.statusLine = `HTTP/${maj.value}.${min.value} ${data.statusCode} ${statusText}`;
-  } catch (e) {
-    // NS_ERROR_NOT_AVAILABLE might be thrown if it's an internal redirect, happening before
-    // any actual HTTP traffic. Otherwise, let's report.
-    if (event !== "onRedirect" || e.result !== Cr.NS_ERROR_NOT_AVAILABLE) {
-      Cu.reportError(`webRequest Error: ${e} trying to merge status in ${event}@${channel.name}`);
-    }
-  }
-}
-
 function isThenable(value) {
   return value && typeof value === "object" && typeof value.then === "function";
 }
 
 class HeaderChanger {
   constructor(channel) {
     this.channel = channel;
 
     this.originalHeaders = new Map();
-    this.visitHeaders((name, value) => {
+    for (let [name, value] of this.iterHeaders()) {
       this.originalHeaders.set(name.toLowerCase(), {name, value});
-    });
+    }
   }
 
   toArray() {
     return Array.from(this.originalHeaders,
                       ([key, {name, value}]) => ({name, value}));
   }
 
   validateHeaders(headers) {
@@ -197,84 +139,76 @@ class HeaderChanger {
       }
     }
   }
 }
 
 class RequestHeaderChanger extends HeaderChanger {
   setHeader(name, value) {
     try {
-      this.channel.setRequestHeader(name, value, false);
+      this.channel.setRequestHeader(name, value);
     } catch (e) {
       Cu.reportError(new Error(`Error setting request header ${name}: ${e}`));
     }
   }
 
-  visitHeaders(visitor) {
-    if (this.channel instanceof Ci.nsIHttpChannel) {
-      this.channel.visitRequestHeaders(visitor);
-    }
+  iterHeaders() {
+    return this.channel.getRequestHeaders().entries();
   }
 }
 
 class ResponseHeaderChanger extends HeaderChanger {
   setHeader(name, value) {
     try {
       if (name.toLowerCase() === "content-type" && value) {
         // The Content-Type header value can't be modified, so we
         // set the channel's content type directly, instead, and
         // record that we made the change for the sake of
         // subsequent observers.
         this.channel.contentType = value;
-
-        getData(this.channel).contentType = value;
+        this.channel._contentType = value;
       } else {
-        this.channel.setResponseHeader(name, value, false);
+        this.channel.setResponseHeader(name, value);
       }
     } catch (e) {
       Cu.reportError(new Error(`Error setting response header ${name}: ${e}`));
     }
   }
 
-  visitHeaders(visitor) {
-    if (this.channel instanceof Ci.nsIHttpChannel) {
-      try {
-        this.channel.visitResponseHeaders((name, value) => {
-          if (name.toLowerCase() === "content-type") {
-            value = getData(this.channel).contentType || value;
-          }
-
-          visitor(name, value);
-        });
-      } catch (e) {
-        // Throws if response headers aren't available yet.
+  * iterHeaders() {
+    for (let [name, value] of this.channel.getResponseHeaders()) {
+      if (name.toLowerCase() === "content-type") {
+        value = this.channel._contentType || value;
       }
+      yield [name, value];
     }
   }
 }
 
 var HttpObserverManager;
 
+var nextFakeRequestId = 1;
+
 var ContentPolicyManager = {
   policyData: new Map(),
   policies: new Map(),
   idMap: new Map(),
   nextId: 0,
 
   init() {
     Services.ppmm.initialProcessData.webRequestContentPolicies = this.policyData;
 
     Services.ppmm.addMessageListener("WebRequest:ShouldLoad", this);
     Services.mm.addMessageListener("WebRequest:ShouldLoad", this);
   },
 
   receiveMessage(msg) {
     let browser = msg.target instanceof Ci.nsIDOMXULElement ? msg.target : null;
 
-    let requestId = RequestId.create();
+    let requestId = `fakeRequest-${++nextFakeRequestId}`;
     for (let id of msg.data.ids) {
       let callback = this.policies.get(id);
       if (!callback) {
         // It's possible that this listener has been removed and the
         // child hasn't learned yet.
         continue;
       }
       let response = null;
@@ -336,32 +270,31 @@ var ContentPolicyManager = {
 
     this.policyData.delete(id);
     this.idMap.delete(callback);
     this.policies.delete(id);
   },
 };
 ContentPolicyManager.init();
 
-function StartStopListener(manager, channel, loadContext) {
+function StartStopListener(manager, channel) {
   this.manager = manager;
-  this.loadContext = loadContext;
-  new WebRequestListener(this, channel);
+  new WebRequestListener(this, channel.channel);
 }
 
 StartStopListener.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
                                          Ci.nsIStreamListener]),
 
   onStartRequest: function(request, context) {
-    this.manager.onStartRequest(request, this.loadContext);
+    this.manager.onStartRequest(ChannelWrapper.get(request));
   },
 
   onStopRequest(request, context, statusCode) {
-    this.manager.onStopRequest(request, this.loadContext);
+    this.manager.onStopRequest(ChannelWrapper.get(request));
   },
 };
 
 var ChannelEventSink = {
   _classDescription: "WebRequest channel event sink",
   _classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"),
   _contractID: "@mozilla.org/webrequest/channel-event-sink;1",
 
@@ -467,80 +400,80 @@ class AuthRequestor {
         return callbacks.getInterface(Ci.nsIAuthPrompt2);
       } catch (e) {}
     }
     throw Cr.NS_ERROR_NO_INTERFACE;
   }
 
   // nsIAuthPrompt2 asyncPromptAuth
   asyncPromptAuth(channel, callback, context, level, authInfo) {
+    let wrapper = ChannelWrapper.get(channel);
+
     let uri = channel.URI;
     let proxyInfo;
     let isProxy = !!(authInfo.flags & authInfo.AUTH_PROXY);
     if (isProxy && channel instanceof Ci.nsIProxiedChannel) {
       proxyInfo = channel.proxyInfo;
     }
     let data = {
       scheme: authInfo.authenticationScheme,
       realm: authInfo.realm,
       isProxy,
       challenger: {
         host: proxyInfo ? proxyInfo.host : uri.host,
         port: proxyInfo ? proxyInfo.port : uri.port,
       },
     };
 
-    let channelData = getData(channel);
     // In the case that no listener provides credentials, we fallback to the
     // previously set callback class for authentication.
-    channelData.authPromptForward = () => {
+    wrapper.authPromptForward = () => {
       try {
         let prompt = this._getForwardPrompt(data);
         prompt.asyncPromptAuth(channel, callback, context, level, authInfo);
       } catch (e) {
         Cu.reportError(`webRequest asyncPromptAuth failure ${e}`);
         callback.onAuthCancelled(context, false);
       }
-      channelData.authPromptForward = null;
-      channelData.authPromptCallback = null;
+      wrapper.authPromptForward = null;
+      wrapper.authPromptCallback = null;
     };
-    channelData.authPromptCallback = (authCredentials) => {
+    wrapper.authPromptCallback = (authCredentials) => {
       // The API allows for canceling the request, providing credentials or
       // doing nothing, so we do not provide a way to call onAuthCanceled.
       // Canceling the request will result in canceling the authentication.
       if (authCredentials &&
           typeof authCredentials.username === "string" &&
           typeof authCredentials.password === "string") {
         authInfo.username = authCredentials.username;
         authInfo.password = authCredentials.password;
         try {
           callback.onAuthAvailable(context, authInfo);
         } catch (e) {
           Cu.reportError(`webRequest onAuthAvailable failure ${e}`);
         }
         // At least one addon has responded, so we wont forward to the regular
         // prompt handlers.
-        channelData.authPromptForward = null;
-        channelData.authPromptCallback = null;
+        wrapper.authPromptForward = null;
+        wrapper.authPromptCallback = null;
       }
     };
 
-    let loadContext = this.httpObserver.getLoadContext(channel);
-    this.httpObserver.runChannelListener(channel, loadContext, "authRequired", data);
+    this.httpObserver.runChannelListener(wrapper, "authRequired", data);
 
     return {
       QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
       cancel() {
         try {
           callback.onAuthCancelled(context, false);
         } catch (e) {
           Cu.reportError(`webRequest onAuthCancelled failure ${e}`);
         }
-        channelData.authPromptForward = null;
-        channelData.authPromptCallback = null;
+        wrapper.authPromptForward = null;
+        wrapper.authPromptCallback = null;
       },
     };
   }
 }
 
 HttpObserverManager = {
   openingInitialized: false,
   modifyInitialized: false,
@@ -632,44 +565,28 @@ HttpObserverManager = {
     this.addOrRemove();
   },
 
   removeListener(kind, callback) {
     this.listeners[kind].delete(callback);
     this.addOrRemove();
   },
 
-  getLoadContext(channel) {
-    try {
-      return channel.QueryInterface(Ci.nsIChannel)
-                    .notificationCallbacks
-                    .getInterface(Components.interfaces.nsILoadContext);
-    } catch (e) {
-      try {
-        return channel.loadGroup
-                      .notificationCallbacks
-                      .getInterface(Components.interfaces.nsILoadContext);
-      } catch (e) {
-        return null;
-      }
-    }
-  },
-
   observe(subject, topic, data) {
-    let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+    let channel = ChannelWrapper.get(subject);
     switch (topic) {
       case "http-on-modify-request":
-        this.runChannelListener(channel, this.getLoadContext(channel), "opening");
+        this.runChannelListener(channel, "opening");
         break;
       case "http-on-before-connect":
-        this.runChannelListener(channel, this.getLoadContext(channel), "modify");
+        this.runChannelListener(channel, "modify");
         break;
       case "http-on-examine-cached-response":
       case "http-on-examine-merged-response":
-        getData(channel).fromCache = true;
+        channel.fromCache = true;
         // falls through
       case "http-on-examine-response":
         this.examine(channel, topic, data);
         break;
     }
   },
 
   // We map activity values with tentative error names, e.g. "STATUS_RESOLVING" => "NS_ERROR_NET_ON_RESOLVING".
@@ -681,291 +598,175 @@ HttpObserverManager = {
         map.set(iface[c], c.replace(prefix, "NS_ERROR_NET_ON_"));
       }
     }
     delete this.activityErrorsMap;
     this.activityErrorsMap = map;
     return this.activityErrorsMap;
   },
   GOOD_LAST_ACTIVITY: nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_HEADER,
-  observeActivity(channel, activityType, activitySubtype /* , aTimestamp, aExtraSizeData, aExtraStringData */) {
-    let channelData = getData(channel);
+  observeActivity(nativeChannel, activityType, activitySubtype /* , aTimestamp, aExtraSizeData, aExtraStringData */) {
+    let channel = ChannelWrapper.get(nativeChannel);
 
     // StartStopListener has to be activated early in the request to catch
     // SSL connection issues which do not get reported via nsIHttpActivityObserver.
     if (activityType == nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION &&
         activitySubtype == nsIHttpActivityObserver.ACTIVITY_SUBTYPE_REQUEST_HEADER) {
-      this.attachStartStopListener(channel, channelData);
+      this.attachStartStopListener(channel);
     }
 
-    let lastActivity = channelData.lastActivity || 0;
+    let lastActivity = channel.lastActivity || 0;
     if (activitySubtype === nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE &&
         lastActivity && lastActivity !== this.GOOD_LAST_ACTIVITY) {
-      let loadContext = this.getLoadContext(channel);
-      if (!this.errorCheck(channel, loadContext, channelData)) {
-        this.runChannelListener(channel, loadContext, "onError",
+      if (!this.errorCheck(channel)) {
+        this.runChannelListener(channel, "onError",
           {error: this.activityErrorsMap.get(lastActivity) ||
                   `NS_ERROR_NET_UNKNOWN_${lastActivity}`});
       }
     } else if (lastActivity !== this.GOOD_LAST_ACTIVITY &&
                lastActivity !== nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) {
-      channelData.lastActivity = activitySubtype;
+      channel.lastActivity = activitySubtype;
     }
   },
 
   shouldRunListener(policyType, uri, filter) {
     // force the protocol to be ws again.
     if (policyType == "websocket" && ["http", "https"].includes(uri.scheme)) {
       uri = new Services.io.newURI(`ws${uri.spec.substring(4)}`);
     }
-    return WebRequestCommon.typeMatches(policyType, filter.types) &&
-           WebRequestCommon.urlMatches(uri, filter.urls);
+
+    if (filter.types && !filter.types.includes(policyType)) {
+      return false;
+    }
+
+    return WebRequestCommon.urlMatches(uri, filter.urls);
   },
 
   get resultsMap() {
     delete this.resultsMap;
     this.resultsMap = new Map(Object.keys(Cr).map(name => [Cr[name], name]));
     return this.resultsMap;
   },
-  maybeError(channel, extraData = null, channelData = null) {
+
+  maybeError({channel}, extraData = null) {
     if (!(extraData && extraData.error) && channel.securityInfo) {
       let securityInfo = channel.securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
       if (NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
         let nsresult = NSSErrorsService.getXPCOMFromNSSError(securityInfo.errorCode);
         extraData = {error: NSSErrorsService.getErrorMessage(nsresult)};
       }
     }
     if (!(extraData && extraData.error)) {
       if (!Components.isSuccessCode(channel.status)) {
         extraData = {error: this.resultsMap.get(channel.status) || "NS_ERROR_NET_UNKNOWN"};
       }
     }
     return extraData;
   },
-  errorCheck(channel, loadContext, channelData = null) {
-    let errorData = this.maybeError(channel, null, channelData);
+
+  errorCheck(channel) {
+    let errorData = this.maybeError(channel);
     if (errorData) {
-      this.runChannelListener(channel, loadContext, "onError", errorData);
+      this.runChannelListener(channel, "onError", errorData);
     }
     return errorData;
   },
 
-  /**
-   * Resumes the channel if it is currently suspended due to this
-   * listener.
-   *
-   * @param {nsIChannel} channel
-   *        The channel to possibly suspend.
-   */
-  maybeResume(channel) {
-    let data = getData(channel);
-    if (data.suspended) {
-      channel.resume();
-      data.suspended = false;
-    }
-  },
+  getRequestData(channel, extraData) {
+    let data = {
+      requestId: String(channel.id),
+      url: channel.finalURL,
+      method: channel.method,
+      browser: channel.browserElement,
+      type: channel.type,
+      fromCache: channel.fromCache,
 
-  /**
-   * Suspends the channel if it is not currently suspended due to this
-   * listener. Returns true if the channel was suspended as a result of
-   * this call.
-   *
-   * @param {nsIChannel} channel
-   *        The channel to possibly suspend.
-   * @returns {boolean}
-   *        True if this call resulted in the channel being suspended.
-   */
-  maybeSuspend(channel) {
-    let data = getData(channel);
-    if (!data.suspended) {
-      channel.suspend();
-      data.suspended = true;
-      return true;
-    }
-  },
+      originUrl: channel.originURL || undefined,
+      documentUrl: channel.documentURL || undefined,
+      isSystemPrincipal: channel.isSystemPrincipal,
 
-  getRequestData(channel, loadContext, policyType, extraData) {
-    let {loadInfo} = channel;
+      windowId: channel.windowId,
+      parentWindowId: channel.parentWindowId,
 
-    let URI = getFinalChannelURI(channel);
-    let data = {
-      requestId: RequestId.get(channel),
-      url: URI.spec,
-      method: channel.requestMethod,
-      browser: loadContext && loadContext.topFrameElement,
-      type: WebRequestCommon.typeForPolicyType(policyType),
-      fromCache: getData(channel).fromCache,
-      // Defaults for a top level request
-      windowId: 0,
-      parentWindowId: -1,
+      ip: channel.remoteAddress,
+
+      proxyInfo: channel.proxyInfo,
     };
 
     // force the protocol to be ws again.
     if (data.type == "websocket" && data.url.startsWith("http")) {
       data.url = `ws${data.url.substring(4)}`;
     }
 
-    if (loadInfo) {
-      let originPrincipal = loadInfo.triggeringPrincipal;
-      if (!originPrincipal.isNullPrincipal && originPrincipal.URI) {
-        data.originUrl = originPrincipal.URI.spec;
-      }
-      let docPrincipal = loadInfo.loadingPrincipal;
-      if (docPrincipal && !docPrincipal.isNullPrincipal && docPrincipal.URI) {
-        data.documentUrl = docPrincipal.URI.spec;
-      }
-
-      // If there is no loadingPrincipal, check that the request is not going to
-      // inherit a system principal.  triggeringPrincipal is the context that
-      // initiated the load, but is not necessarily the principal that the
-      // request results in, only rely on that if no other principal is available.
-      let {isSystemPrincipal} = Services.scriptSecurityManager;
-      let isTopLevel = !loadInfo.loadingPrincipal && !!data.browser;
-      data.isSystemPrincipal = !isTopLevel &&
-                               isSystemPrincipal(loadInfo.loadingPrincipal ||
-                                                 loadInfo.principalToInherit ||
-                                                 loadInfo.triggeringPrincipal);
-
-      // Handle window and parent id values for sub_frame requests or requests
-      // inside a sub_frame.
-      if (loadInfo.frameOuterWindowID != 0) {
-        // This is a sub_frame.  Only frames (ie. iframe; a request with a frameloader)
-        // have a non-zero frameOuterWindowID.  For a sub_frame, outerWindowID
-        // points at the frames parent.  The parent frame is the main_frame if
-        // outerWindowID == parentOuterWindowID, in which case set parentWindowId
-        // to zero.
-        Object.assign(data, {
-          windowId: loadInfo.frameOuterWindowID,
-          parentWindowId: loadInfo.outerWindowID == loadInfo.parentOuterWindowID ? 0 : loadInfo.outerWindowID,
-        });
-      } else if (loadInfo.outerWindowID != loadInfo.parentOuterWindowID) {
-        // This is a non-frame (e.g. script, image, etc) request within a
-        // sub_frame.  We have to check parentOuterWindowID against the browser
-        // to see if it is the main_frame in which case the parenteWindowId
-        // available to the caller must be set to zero.
-        let parentMainFrame = data.browser && data.browser.outerWindowID == loadInfo.parentOuterWindowID;
-        Object.assign(data, {
-          windowId: loadInfo.outerWindowID,
-          parentWindowId: parentMainFrame ? 0 : loadInfo.parentOuterWindowID,
-        });
-      }
-    }
-
-    if (channel instanceof Ci.nsIHttpChannelInternal) {
-      try {
-        data.ip = channel.remoteAddress;
-      } catch (e) {
-        // The remoteAddress getter throws if the address is unavailable,
-        // but ip is an optional property so just ignore the exception.
-      }
-    }
-
-    if (channel instanceof Ci.nsIProxiedChannel && channel.proxyInfo) {
-      let pi = channel.proxyInfo;
-      data.proxyInfo = {
-        host: pi.host,
-        port: pi.port,
-        type: pi.type,
-        username: pi.username,
-        proxyDNS: pi.flags == Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST,
-        failoverTimeout: pi.failoverTimeout,
-      };
-    }
-
     return Object.assign(data, extraData);
   },
 
-  canModify(channel) {
-    let {isHostPermitted} = AddonManagerPermissions;
-
-    // Bug 1334550 introduced the possibility of having a JAR uri here,
-    // use the result uri if possible in that case.
-    let URI = getFinalChannelURI(channel);
-    if (URI && isHostPermitted(URI.host)) {
-      return false;
-    }
-
-    let {loadInfo} = channel;
-    if (loadInfo && loadInfo.loadingPrincipal) {
-      let {loadingPrincipal} = loadInfo;
-      try {
-        return loadingPrincipal.URI && !isHostPermitted(loadingPrincipal.URI.host);
-      } catch (e) {
-        // 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)) {
+    if (!channel.registeredFilters) {
+      channel.registeredFilters = new Map();
+    } else if (channel.registeredFilters.has(opts.addonId)) {
       return;
     }
 
     let filter = webReqService.registerTraceableChannel(
-      parseInt(data.requestId, 10),
-      channel,
+      channel.id,
+      channel.channel,
       opts.addonId,
       opts.tabParent);
 
-    data.registeredFilters.set(opts.addonId, filter);
+    channel.registeredFilters.set(opts.addonId, filter);
   },
 
   destroyFilters(channel) {
-    let filters = getData(channel).registeredFilters;
+    let filters = channel.registeredFilters || new Map();
     for (let [key, filter] of filters.entries()) {
       filter.destruct();
       filters.delete(key);
     }
   },
 
-  runChannelListener(channel, loadContext = null, kind, extraData = null) {
+  runChannelListener(channel, 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) {
+          if (channel.errorNotified) {
             return;
           }
-          channelData.errorNotified = true;
-        } else if (this.errorCheck(channel, loadContext, channelData)) {
+          channel.errorNotified = true;
+        } else if (this.errorCheck(channel)) {
           return;
         }
       }
 
-      let {loadInfo} = channel;
-      let policyType = (loadInfo ? loadInfo.externalContentPolicyType
-                                 : Ci.nsIContentPolicy.TYPE_OTHER);
-
       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 uri = channel.finalURI;
       let requestBody;
       for (let [callback, opts] of this.listeners[kind].entries()) {
-        if (!this.shouldRunListener(policyType, uri, opts.filter)) {
+        if (!this.shouldRunListener(channel.type, uri, opts.filter)) {
           continue;
         }
 
         if (!commonData) {
-          commonData = this.getRequestData(channel, loadContext, policyType, extraData);
+          commonData = this.getRequestData(channel, extraData);
+          if (includeStatus) {
+            commonData.statusCode = channel.statusCode;
+            commonData.statusLine = channel.statusLine;
+          }
         }
         let data = Object.assign({}, commonData);
 
         if (registerFilter) {
           this.registerChannel(channel, opts);
         }
 
         if (opts.requestHeaders) {
@@ -978,46 +779,42 @@ HttpObserverManager = {
           data.responseHeaders = responseHeaders.toArray();
         }
 
         if (opts.requestBody) {
           requestBody = requestBody || WebRequestUpload.createRequestBody(channel);
           data.requestBody = requestBody;
         }
 
-        if (includeStatus) {
-          mergeStatus(data, channel, kind);
-        }
-
         try {
           let result = callback(data);
 
-          if (canModify && result && typeof result === "object" && opts.blocking) {
+          if (channel.canModify && result && typeof result === "object" && opts.blocking) {
             handlerResults.push({opts, result});
           }
         } catch (e) {
           Cu.reportError(e);
         }
       }
     } catch (e) {
       Cu.reportError(e);
     }
 
-    return this.applyChanges(kind, channel, loadContext, handlerResults,
-                             requestHeaders, responseHeaders);
+    return this.applyChanges(kind, channel, handlerResults, requestHeaders, responseHeaders);
   },
 
-  async applyChanges(kind, channel, loadContext, handlerResults, requestHeaders, responseHeaders) {
+  async applyChanges(kind, channel, handlerResults, requestHeaders, responseHeaders) {
     let asyncHandlers = handlerResults.filter(({result}) => isThenable(result));
     let isAsync = asyncHandlers.length > 0;
     let shouldResume = false;
 
     try {
       if (isAsync) {
-        shouldResume = this.maybeSuspend(channel);
+        shouldResume = !channel.suspended;
+        channel.suspended = true;
 
         for (let value of asyncHandlers) {
           try {
             value.result = await value.result;
           } catch (e) {
             Cu.reportError(e);
             value.result = {};
           }
@@ -1025,137 +822,127 @@ HttpObserverManager = {
       }
 
       for (let {opts, result} of handlerResults) {
         if (!result || typeof result !== "object") {
           continue;
         }
 
         if (result.cancel) {
-          this.maybeResume(channel);
+          channel.suspended = false;
           channel.cancel(Cr.NS_ERROR_ABORT);
 
-          this.errorCheck(channel, loadContext);
+          this.errorCheck(channel);
           return;
         }
 
         if (result.redirectUrl) {
           try {
-            this.maybeResume(channel);
-
+            channel.suspended = false;
             channel.redirectTo(Services.io.newURI(result.redirectUrl));
             return;
           } catch (e) {
             Cu.reportError(e);
           }
         }
 
         if (opts.requestHeaders && result.requestHeaders && requestHeaders) {
           requestHeaders.applyChanges(result.requestHeaders);
         }
 
         if (opts.responseHeaders && result.responseHeaders && responseHeaders) {
           responseHeaders.applyChanges(result.responseHeaders);
         }
 
         if (kind === "authRequired" && opts.blocking && result.authCredentials) {
-          let channelData = getData(channel);
-          if (channelData.authPromptCallback) {
-            channelData.authPromptCallback(result.authCredentials);
+          if (channel.authPromptCallback) {
+            channel.authPromptCallback(result.authCredentials);
           }
         }
       }
       // If a listener did not cancel the request or provide credentials, we
       // forward the auth request to the base handler.
       if (kind === "authRequired") {
-        let channelData = getData(channel);
-        if (channelData.authPromptForward) {
-          channelData.authPromptForward();
+        if (channel.authPromptForward) {
+          channel.authPromptForward();
         }
       }
 
       if (kind === "modify") {
-        await this.runChannelListener(channel, loadContext, "afterModify");
+        await this.runChannelListener(channel, "afterModify");
       }
     } catch (e) {
       Cu.reportError(e);
     }
 
     // Only resume the channel if it was suspended by this call.
     if (shouldResume) {
-      this.maybeResume(channel);
+      channel.suspended = false;
     }
   },
 
   shouldHookListener(listener, channel) {
     if (listener.size == 0) {
       return false;
     }
 
-    let {loadInfo} = channel;
-    let policyType = (loadInfo ? loadInfo.externalContentPolicyType
-                               : Ci.nsIContentPolicy.TYPE_OTHER);
-    let uri = channel.URI;
     for (let opts of listener.values()) {
-      if (this.shouldRunListener(policyType, uri, opts.filter)) {
+      if (this.shouldRunListener(channel.type, channel.finalURI, opts.filter)) {
         return true;
       }
     }
     return false;
   },
 
-  attachStartStopListener(channel, channelData) {
+  attachStartStopListener(channel) {
     // Check whether we've already added a listener to this channel,
     // so we don't wind up chaining multiple listeners.
-    if (!this.needTracing || channelData.hasListener || !(channel instanceof Ci.nsITraceableChannel)) {
+    if (!this.needTracing || channel.hasListener ||
+        !(channel.channel instanceof Ci.nsITraceableChannel)) {
       return;
     }
-    let responseStatus = 0;
-    try {
-      responseStatus = channel.QueryInterface(Ci.nsIHttpChannel).responseStatus;
-    } catch (e) {
-      /* NS_ERROR_NOT_AVAILABLE if checked prior to onStartRequest. */
-    }
+
     // skip redirections, https://bugzilla.mozilla.org/show_bug.cgi?id=728901#c8
-    if (responseStatus < 300 || responseStatus >= 400) {
-      let loadContext = this.getLoadContext(channel);
-      new StartStopListener(this, channel, loadContext);
-      channelData.hasListener = true;
+    let {statusCode} = channel;
+    if (statusCode < 300 || statusCode >= 400) {
+      new StartStopListener(this, channel);
+      channel.hasListener = true;
     }
   },
 
   examine(channel, topic, data) {
-    let channelData = getData(channel);
-    this.attachStartStopListener(channel, channelData);
+    this.attachStartStopListener(channel);
 
     if (this.listeners.headersReceived.size) {
-      this.runChannelListener(channel, this.getLoadContext(channel), "headersReceived");
+      this.runChannelListener(channel, "headersReceived");
     }
 
-    if (!channelData.hasAuthRequestor && this.shouldHookListener(this.listeners.authRequired, channel)) {
-      channel.notificationCallbacks = new AuthRequestor(channel, this);
-      channelData.hasAuthRequestor = true;
+    if (!channel.hasAuthRequestor && this.shouldHookListener(this.listeners.authRequired, channel)) {
+      channel.channel.notificationCallbacks = new AuthRequestor(channel.channel, this);
+      channel.hasAuthRequestor = true;
     }
   },
 
   onChannelReplaced(oldChannel, newChannel) {
+    let channel = ChannelWrapper.get(oldChannel);
+
     // 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});
+    this.destroyFilters(channel);
+    this.runChannelListener(channel, "onRedirect", {redirectUrl: newChannel.originalURI.spec});
+    channel.channel = newChannel;
   },
 
-  onStartRequest(channel, loadContext) {
+  onStartRequest(channel) {
     this.destroyFilters(channel);
-    this.runChannelListener(channel, loadContext, "onStart");
+    this.runChannelListener(channel, "onStart");
   },
 
-  onStopRequest(channel, loadContext) {
-    this.runChannelListener(channel, loadContext, "onStop");
+  onStopRequest(channel) {
+    this.runChannelListener(channel, "onStop");
   },
 };
 
 var onBeforeRequest = {
   allowedOptions: ["blocking", "requestBody"],
 
   addListener(callback, filter = null, options = null, optionsObject = null) {
     let opts = parseExtra(options, this.allowedOptions);