merge mozilla-central to autoland. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Mon, 04 Sep 2017 11:13:51 +0200
changeset 378766 4b2edbc1ae7997a85bb619894a6c06c265c5ebb8
parent 378752 cc1dd06b768883dd5d66b1486558a98f69c4fdff (current diff)
parent 378765 632e42dca494ec3d90b70325d9c359f80cb3f38a (diff)
child 378767 b0599327dfdd1a49fe863c5473b7fb15590da3a3
push id50353
push userarchaeopteryx@coole-files.de
push dateMon, 04 Sep 2017 09:14:07 +0000
treeherderautoland@4b2edbc1ae79 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
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
merge mozilla-central to autoland. r=merge a=merge
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -796,16 +796,25 @@ DOMInterfaces = {
     'headerFile': 'mozilla/dom/workers/bindings/SharedWorker.h',
 },
 
 'SharedWorkerGlobalScope': {
     'headerFile': 'mozilla/dom/WorkerScope.h',
     'implicitJSContext': [ 'close' ],
 },
 
+'StreamFilter': {
+    'nativeType': 'mozilla::extensions::StreamFilter',
+},
+
+'StreamFilterDataEvent': {
+    'nativeType': 'mozilla::extensions::StreamFilterDataEvent',
+    'headerFile': 'mozilla/extensions/StreamFilterEvents.h',
+},
+
 'StructuredCloneHolder': {
     'nativeType': 'mozilla::dom::StructuredCloneBlob',
     'wrapperCache': False,
 },
 
 'StyleSheet': {
     'nativeType': 'mozilla::StyleSheet',
     'headerFile': 'mozilla/StyleSheetInlines.h',
new file mode 100644
--- /dev/null
+++ b/dom/webidl/StreamFilter.webidl
@@ -0,0 +1,144 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+/**
+ * This is a Mozilla-specific WebExtension API, which is not available to web
+ * content. It allows monitoring and filtering of HTTP response stream data.
+ *
+ * This API should currently be considered experimental, and is not defined by
+ * any standard.
+ */
+
+enum StreamFilterStatus {
+  /**
+   * The StreamFilter is not fully initialized. No methods may be called until
+   * a "start" event has been received.
+   */
+  "uninitialized",
+  /**
+   * The underlying channel is currently transferring data, which will be
+   * dispatched via "data" events.
+   */
+  "transferringdata",
+  /**
+   * The underlying channel has finished transferring data. Data may still be
+   * written via write() calls at this point.
+   */
+  "finishedtransferringdata",
+  /**
+   * Data transfer is currently suspended. It may be resumed by a call to
+   * resume(). Data may still be written via write() calls in this state.
+   */
+  "suspended",
+  /**
+   * The channel has been closed by a call to close(). No further data wlil be
+   * delivered via "data" events, and no further data may be written via
+   * write() calls.
+   */
+  "closed",
+  /**
+   * The channel has been disconnected by a call to disconnect(). All further
+   * data will be delivered directly, without passing through the filter. No
+   * further events will be dispatched, and no further data may be written by
+   * write() calls.
+   */
+  "disconnected",
+  /**
+   * An error has occurred and the channel is disconnected. The `error`
+   * property contains the details of the error.
+   */
+  "failed",
+};
+
+/**
+ * An interface which allows an extension to intercept, and optionally modify,
+ * response data from an HTTP request.
+ */
+[Exposed=(Window,System),
+ Func="mozilla::extensions::StreamFilter::IsAllowedInContext"]
+interface StreamFilter : EventTarget {
+  /**
+   * Creates a stream filter for the given add-on and the given extension ID.
+   */
+  [ChromeOnly]
+  static StreamFilter create(unsigned long long requestId, DOMString addonId);
+
+  /**
+   * Suspends processing of the request. After this is called, no further data
+   * will be delivered until the request is resumed.
+   */
+  [Throws]
+  void suspend();
+
+  /**
+   * Resumes delivery of data for a suspended request.
+   */
+  [Throws]
+  void resume();
+
+  /**
+   * Closes the request. After this is called, no more data may be written to
+   * the stream, and no further data will be delivered.
+   *
+   * This *must* be called after the consumer is finished writing data, unless
+   * disconnect() has already been called.
+   */
+  [Throws]
+  void close();
+
+  /**
+   * Disconnects the stream filter from the request. After this is called, no
+   * further data will be delivered to the filter, and any unprocessed data
+   * will be written directly to the output stream.
+   */
+  [Throws]
+  void disconnect();
+
+  /**
+   * Writes a chunk of data to the output stream. This may not be called
+   * before the "start" event has been received.
+   */
+  [Throws]
+  void write((ArrayBuffer or Uint8Array) data);
+
+  /**
+   * Returns the current status of the stream.
+   */
+  [Pure]
+  readonly attribute StreamFilterStatus status;
+
+  /**
+   * After an "error" event has been dispatched, this contains a message
+   * describing the error.
+   */
+  [Pure]
+  readonly attribute DOMString error;
+
+  /**
+   * Dispatched with a StreamFilterDataEvent whenever incoming data is
+   * available on the stream. This data will not be delivered to the output
+   * stream unless it is explicitly written via a write() call.
+   */
+  attribute EventHandler ondata;
+
+  /**
+   * Dispatched when the stream is opened, and is about to begin delivering
+   * data.
+   */
+  attribute EventHandler onstart;
+
+  /**
+   * Dispatched when the stream has closed, and has no more data to deliver.
+   * The output stream remains open and writable until close() is called.
+   */
+  attribute EventHandler onstop;
+
+  /**
+   * Dispatched when an error has occurred. No further data may be read or
+   * written after this point.
+   */
+  attribute EventHandler onerror;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/StreamFilterDataEvent.webidl
@@ -0,0 +1,28 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * This is a Mozilla-specific WebExtension API, which is not available to web
+ * content. It allows monitoring and filtering of HTTP response stream data.
+ *
+ * This API should currently be considered experimental, and is not defined by
+ * any standard.
+ */
+
+[Constructor(DOMString type, optional StreamFilterDataEventInit eventInitDict),
+ Func="mozilla::extensions::StreamFilter::IsAllowedInContext",
+ Exposed=(Window,System)]
+interface StreamFilterDataEvent : Event {
+  /**
+   * Contains a chunk of data read from the input stream.
+   */
+  [Pure]
+  readonly attribute ArrayBuffer data;
+};
+
+dictionary StreamFilterDataEventInit : EventInit {
+  required ArrayBuffer data;
+};
+
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -293,16 +293,19 @@ with Files("SocketCommon.webidl"):
     BUG_COMPONENT = ("Core", "DOM: Device Interfaces")
 
 with Files("SourceBuffer*"):
     BUG_COMPONENT = ("Core", "Audio/Video")
 
 with Files("StereoPannerNode.webidl"):
     BUG_COMPONENT = ("Core", "Web Audio")
 
+with Files("StreamFilter*"):
+    BUG_COMPONENT = ("Toolkit", "WebExtensions: Request Handling")
+
 with Files("Style*"):
     BUG_COMPONENT = ("Core", "DOM: CSS Object Model")
 
 with Files("SubtleCrypto.webidl"):
     BUG_COMPONENT = ("Core", "DOM: Security")
 
 with Files("TCP*"):
     BUG_COMPONENT = ("Core", "DOM: Device Interfaces")
@@ -790,16 +793,18 @@ WEBIDL_FILES = [
     'SocketCommon.webidl',
     'SourceBuffer.webidl',
     'SourceBufferList.webidl',
     'StereoPannerNode.webidl',
     'Storage.webidl',
     'StorageEvent.webidl',
     'StorageManager.webidl',
     'StorageType.webidl',
+    'StreamFilter.webidl',
+    'StreamFilterDataEvent.webidl',
     'StructuredCloneHolder.webidl',
     'StyleSheet.webidl',
     'StyleSheetList.webidl',
     'SubtleCrypto.webidl',
     'SVGAElement.webidl',
     'SVGAngle.webidl',
     'SVGAnimatedAngle.webidl',
     'SVGAnimatedBoolean.webidl',
--- a/ipc/glue/BackgroundChildImpl.cpp
+++ b/ipc/glue/BackgroundChildImpl.cpp
@@ -27,16 +27,17 @@
 #include "mozilla/dom/quota/PQuotaChild.h"
 #include "mozilla/dom/StorageIPC.h"
 #include "mozilla/dom/GamepadEventChannelChild.h"
 #include "mozilla/dom/GamepadTestChannelChild.h"
 #include "mozilla/dom/LocalStorage.h"
 #include "mozilla/dom/MessagePortChild.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/dom/TabGroup.h"
+#include "mozilla/extensions/StreamFilterChild.h"
 #include "mozilla/ipc/IPCStreamAlloc.h"
 #include "mozilla/ipc/PBackgroundTestChild.h"
 #include "mozilla/ipc/PChildToParentStreamChild.h"
 #include "mozilla/ipc/PParentToChildStreamChild.h"
 #include "mozilla/layout/VsyncChild.h"
 #include "mozilla/net/HttpBackgroundChannelChild.h"
 #include "mozilla/net/PUDPSocketChild.h"
 #include "mozilla/dom/network/UDPSocketChild.h"
@@ -422,16 +423,36 @@ BackgroundChildImpl::AllocPCacheStreamCo
 bool
 BackgroundChildImpl::DeallocPCacheStreamControlChild(PCacheStreamControlChild* aActor)
 {
   dom::cache::DeallocPCacheStreamControlChild(aActor);
   return true;
 }
 
 // -----------------------------------------------------------------------------
+// StreamFilter API
+// -----------------------------------------------------------------------------
+
+extensions::PStreamFilterChild*
+BackgroundChildImpl::AllocPStreamFilterChild(const uint64_t& aChannelId, const nsString& aAddonId)
+{
+  RefPtr<extensions::StreamFilterChild> agent = new extensions::StreamFilterChild();
+  return agent.forget().take();
+}
+
+bool
+BackgroundChildImpl::DeallocPStreamFilterChild(PStreamFilterChild* aActor)
+{
+  RefPtr<extensions::StreamFilterChild> child =
+    dont_AddRef(static_cast<extensions::StreamFilterChild*>(aActor));
+  MOZ_ASSERT(child);
+  return true;
+}
+
+// -----------------------------------------------------------------------------
 // MessageChannel/MessagePort API
 // -----------------------------------------------------------------------------
 
 dom::PMessagePortChild*
 BackgroundChildImpl::AllocPMessagePortChild(const nsID& aUUID,
                                             const nsID& aDestinationUUID,
                                             const uint32_t& aSequenceID)
 {
--- a/ipc/glue/BackgroundChildImpl.h
+++ b/ipc/glue/BackgroundChildImpl.h
@@ -148,16 +148,23 @@ protected:
 
   virtual PMessagePortChild*
   AllocPMessagePortChild(const nsID& aUUID, const nsID& aDestinationUUID,
                          const uint32_t& aSequenceID) override;
 
   virtual bool
   DeallocPMessagePortChild(PMessagePortChild* aActor) override;
 
+  virtual PStreamFilterChild*
+  AllocPStreamFilterChild(const uint64_t& aChannelId,
+                          const nsString& aAddonId) override;
+
+  virtual bool
+  DeallocPStreamFilterChild(PStreamFilterChild* aActor) override;
+
   virtual PChildToParentStreamChild*
   AllocPChildToParentStreamChild() override;
 
   virtual bool
   DeallocPChildToParentStreamChild(PChildToParentStreamChild* aActor) override;
 
   virtual PParentToChildStreamChild*
   AllocPParentToChildStreamChild() override;
--- a/ipc/glue/BackgroundParentImpl.cpp
+++ b/ipc/glue/BackgroundParentImpl.cpp
@@ -25,16 +25,17 @@
 #include "mozilla/dom/ServiceWorkerRegistrar.h"
 #include "mozilla/dom/asmjscache/AsmJSCache.h"
 #include "mozilla/dom/cache/ActorUtils.h"
 #include "mozilla/dom/indexedDB/ActorsParent.h"
 #include "mozilla/dom/ipc/IPCBlobInputStreamParent.h"
 #include "mozilla/dom/ipc/PendingIPCBlobParent.h"
 #include "mozilla/dom/quota/ActorsParent.h"
 #include "mozilla/dom/StorageIPC.h"
+#include "mozilla/extensions/StreamFilterParent.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/ipc/IPCStreamAlloc.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/ipc/PBackgroundTestParent.h"
 #include "mozilla/ipc/PChildToParentStreamParent.h"
 #include "mozilla/ipc/PParentToChildStreamParent.h"
 #include "mozilla/layout/VsyncParent.h"
@@ -63,16 +64,17 @@ using mozilla::dom::cache::PCacheParent;
 using mozilla::dom::cache::PCacheStorageParent;
 using mozilla::dom::cache::PCacheStreamControlParent;
 using mozilla::dom::FileSystemBase;
 using mozilla::dom::FileSystemRequestParent;
 using mozilla::dom::MessagePortParent;
 using mozilla::dom::PMessagePortParent;
 using mozilla::dom::UDPSocketParent;
 using mozilla::dom::WebAuthnTransactionParent;
+using mozilla::extensions::StreamFilterParent;
 
 namespace {
 
 void
 AssertIsOnMainThread()
 {
   MOZ_ASSERT(NS_IsMainThread());
 }
@@ -710,16 +712,54 @@ BackgroundParentImpl::AllocPCacheStreamC
 
 bool
 BackgroundParentImpl::DeallocPCacheStreamControlParent(PCacheStreamControlParent* aActor)
 {
   dom::cache::DeallocPCacheStreamControlParent(aActor);
   return true;
 }
 
+PStreamFilterParent*
+BackgroundParentImpl::AllocPStreamFilterParent(const uint64_t& aChannelId, const nsString& aAddonId)
+{
+  AssertIsInMainProcess();
+  AssertIsOnBackgroundThread();
+
+  return StreamFilterParent::Create(aChannelId, aAddonId).take();
+}
+
+mozilla::ipc::IPCResult
+BackgroundParentImpl::RecvPStreamFilterConstructor(PStreamFilterParent* aActor,
+                                                   const uint64_t& aChannelId,
+                                                   const nsString& aAddonId)
+{
+  AssertIsInMainProcess();
+  AssertIsOnBackgroundThread();
+
+  StreamFilterParent* filter = static_cast<StreamFilterParent*>(aActor);
+
+  RefPtr<ContentParent> parent = BackgroundParent::GetContentParent(this);
+
+  filter->Init(parent.forget());
+
+  return IPC_OK();
+}
+
+bool
+BackgroundParentImpl::DeallocPStreamFilterParent(PStreamFilterParent* aActor)
+{
+  AssertIsInMainProcess();
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+
+  RefPtr<StreamFilterParent> filter = dont_AddRef(
+      static_cast<StreamFilterParent*>(aActor));
+  return true;
+}
+
 PMessagePortParent*
 BackgroundParentImpl::AllocPMessagePortParent(const nsID& aUUID,
                                               const nsID& aDestinationUUID,
                                               const uint32_t& aSequenceID)
 {
   AssertIsInMainProcess();
   AssertIsOnBackgroundThread();
 
--- a/ipc/glue/BackgroundParentImpl.h
+++ b/ipc/glue/BackgroundParentImpl.h
@@ -13,16 +13,18 @@
 namespace mozilla {
 
 namespace layout {
 class VsyncParent;
 } // namespace layout
 
 namespace ipc {
 
+using mozilla::extensions::PStreamFilterParent;
+
 // Instances of this class should never be created directly. This class is meant
 // to be inherited in BackgroundImpl.
 class BackgroundParentImpl : public PBackgroundParent
 {
 protected:
   BackgroundParentImpl();
   virtual ~BackgroundParentImpl();
 
@@ -197,16 +199,28 @@ protected:
   virtual bool
   DeallocPMessagePortParent(PMessagePortParent* aActor) override;
 
   virtual mozilla::ipc::IPCResult
   RecvMessagePortForceClose(const nsID& aUUID,
                             const nsID& aDestinationUUID,
                             const uint32_t& aSequenceID) override;
 
+  virtual PStreamFilterParent*
+  AllocPStreamFilterParent(const uint64_t& aChannelId,
+                           const nsString& aAddonId) override;
+
+  virtual mozilla::ipc::IPCResult
+  RecvPStreamFilterConstructor(PStreamFilterParent* aActor,
+                               const uint64_t& aChannelId,
+                               const nsString& aAddonId) override;
+
+  virtual bool
+  DeallocPStreamFilterParent(PStreamFilterParent* aActor) override;
+
   virtual PAsmJSCacheEntryParent*
   AllocPAsmJSCacheEntryParent(const dom::asmjscache::OpenMode& aOpenMode,
                               const dom::asmjscache::WriteParams& aWriteParams,
                               const PrincipalInfo& aPrincipalInfo) override;
 
   virtual bool
   DeallocPAsmJSCacheEntryParent(PAsmJSCacheEntryParent* aActor) override;
 
--- a/ipc/glue/PBackground.ipdl
+++ b/ipc/glue/PBackground.ipdl
@@ -14,16 +14,17 @@ include protocol PCacheStreamControl;
 include protocol PFileDescriptorSet;
 include protocol PFileSystemRequest;
 include protocol PGamepadEventChannel;
 include protocol PGamepadTestChannel;
 include protocol PHttpBackgroundChannel;
 include protocol PIPCBlobInputStream;
 include protocol PPendingIPCBlob;
 include protocol PMessagePort;
+include protocol PStreamFilter;
 include protocol PCameras;
 include protocol PQuota;
 include protocol PChildToParentStream;
 include protocol PParentToChildStream;
 include protocol PServiceWorkerManager;
 include protocol PWebAuthnTransaction;
 include protocol PUDPSocket;
 include protocol PVsync;
@@ -63,16 +64,17 @@ sync protocol PBackground
   manages PFileDescriptorSet;
   manages PFileSystemRequest;
   manages PGamepadEventChannel;
   manages PGamepadTestChannel;
   manages PHttpBackgroundChannel;
   manages PIPCBlobInputStream;
   manages PPendingIPCBlob;
   manages PMessagePort;
+  manages PStreamFilter;
   manages PCameras;
   manages PQuota;
   manages PChildToParentStream;
   manages PParentToChildStream;
   manages PServiceWorkerManager;
   manages PWebAuthnTransaction;
   manages PUDPSocket;
   manages PVsync;
@@ -107,16 +109,18 @@ parent:
   async PServiceWorkerManager();
 
   async ShutdownServiceWorkerRegistrar();
 
   async PCacheStorage(Namespace aNamespace, PrincipalInfo aPrincipalInfo);
 
   async PMessagePort(nsID uuid, nsID destinationUuid, uint32_t sequenceId);
 
+  async PStreamFilter(uint64_t channelId, nsString addonId);
+
   async PChildToParentStream();
 
   async MessagePortForceClose(nsID uuid, nsID destinationUuid, uint32_t sequenceId);
 
   async PAsmJSCacheEntry(OpenMode openMode,
                          WriteParams write,
                          PrincipalInfo principalInfo);
 
--- a/ipc/ipdl/ipdl/lower.py
+++ b/ipc/ipdl/ipdl/lower.py
@@ -333,25 +333,29 @@ def _promise(resolvetype, rejecttype, ta
     inner = Type('Private') if resolver else None
     return Type('MozPromise', T=[resolvetype, rejecttype, tail], inner=inner)
 
 def _makePromise(returns, side, resolver=False):
     if len(returns) > 1:
         resolvetype = _tuple([d.bareType(side) for d in returns])
     else:
         resolvetype = returns[0].bareType(side)
+
+    needmove = not all(d.isCopyable() for d in returns)
+
     return _promise(resolvetype,
                     _PromiseRejectReason.Type(),
-                    ExprLiteral.FALSE, resolver=resolver)
+                    ExprLiteral.TRUE if needmove else ExprLiteral.FALSE,
+                    resolver=resolver)
 
 def _makeResolver(returns, side):
     if len(returns) > 1:
-        resolvetype = _tuple([d.bareType(side) for d in returns])
+        resolvetype = _tuple([d.moveType(side) for d in returns])
     else:
-        resolvetype = returns[0].bareType(side)
+        resolvetype = returns[0].moveType(side)
     return TypeFunction([Decl(resolvetype, '')])
 
 def _cxxArrayType(basetype, const=0, ref=0):
     return Type('nsTArray', T=basetype, const=const, ref=ref, hasimplicitcopyctor=False)
 
 def _cxxManagedContainerType(basetype, const=0, ref=0):
     return Type('ManagedContainer', T=basetype,
                 const=const, ref=ref, hasimplicitcopyctor=False)
@@ -581,19 +585,24 @@ def _cxxConstRefType(ipdltype, side):
         return t
     if ipdltype.isIPDL() and ipdltype.isShmem():
         t.ref = 1
         return t
     t.const = 1
     t.ref = 1
     return t
 
+def _cxxTypeNeedsMove(ipdltype):
+    return ipdltype.isIPDL() and (ipdltype.isArray() or
+                                  ipdltype.isShmem() or
+                                  ipdltype.isEndpoint())
+
 def _cxxMoveRefType(ipdltype, side):
     t = _cxxBareType(ipdltype, side)
-    if ipdltype.isIPDL() and (ipdltype.isArray() or ipdltype.isShmem() or ipdltype.isEndpoint()):
+    if _cxxTypeNeedsMove(ipdltype):
         t.ref = 2
         return t
     return _cxxConstRefType(ipdltype, side)
 
 def _cxxPtrToType(ipdltype, side):
     t = _cxxBareType(ipdltype, side)
     if ipdltype.isIPDL() and ipdltype.isActor():
         t.ptr = 0
@@ -626,16 +635,19 @@ def _deallocMethod(ptype, side):
 class _HybridDecl:
     """A hybrid decl stores both an IPDL type and all the C++ type
 info needed by later passes, along with a basic name for the decl."""
     def __init__(self, ipdltype, name):
         self.ipdltype = ipdltype
         self.name = name
         self.idnum = 0
 
+    def isCopyable(self):
+        return not _cxxTypeNeedsMove(self.ipdltype)
+
     def var(self):
         return ExprVar(self.name)
 
     def bareType(self, side):
         """Return this decl's unqualified C++ type."""
         return _cxxBareType(self.ipdltype, side)
 
     def refType(self, side):
@@ -4079,19 +4091,19 @@ class _GenerateProtocolActorCode(ipdl.as
         getpromise = [ Whitespace.NL,
                        StmtDecl(Decl(_refptr(promise), 'promise'),
                                 init=ExprCall(ExprSelect(ExprCall(ExprSelect(self.protocol.callGetChannel(), '->', 'PopPromise'),
                                                                   args=[ self.msgvar ]),
                                                          '.', Type('downcast', T=promise)))),
                        ifnotpromise ]
         if len(md.returns) > 1:
             resolvearg = ExprCall(ExprVar('MakeTuple'),
-                                  args=[p.var() for p in md.returns])
+                                  args=[ExprMove(p.var()) for p in md.returns])
         else:
-            resolvearg = md.returns[0].var()
+            resolvearg = ExprMove(md.returns[0].var())
 
         resolvepromise = [ StmtExpr(ExprCall(ExprSelect(ExprVar('promise'), '->', 'Resolve'),
                                              args=[ resolvearg,
                                                     ExprVar('__func__')])) ]
         rejectpromise = [ StmtExpr(ExprCall(ExprSelect(ExprVar('promise'), '->', 'Reject'),
                                             args=[ reason, ExprVar('__func__') ])) ]
         ifresolve = StmtIf(resolve)
         ifresolve.addifstmts(desstmts)
@@ -4282,23 +4294,23 @@ class _GenerateProtocolActorCode(ipdl.as
                    + [ self.logMessage(md, self.replyvar, 'Sending reply '),
                        StmtDecl(Decl(Type.BOOL, sendok.name),
                                 init=ExprCall(
                                     ExprSelect(self.protocol.callGetChannel(),
                                                '->', 'Send'),
                                     args=[ self.replyvar ])),
                        failifsendok ])
         if len(md.returns) > 1:
-            resolvedecl = Decl(_tuple([p.bareType(self.side) for p in md.returns],
+            resolvedecl = Decl(_tuple([p.moveType(self.side) for p in md.returns],
                                       const=1, ref=1),
                                'aParam')
             destructexpr = ExprCall(ExprVar('Tie'),
                                     args=[ p.var() for p in md.returns ])
         else:
-            resolvedecl = Decl(md.returns[0].bareType(self.side), 'aParam')
+            resolvedecl = Decl(md.returns[0].moveType(self.side), 'aParam')
             destructexpr = md.returns[0].var()
         selfvar = ExprVar('self__')
         ifactorisdead = StmtIf(ExprNot(selfvar))
         ifactorisdead.addifstmts([_printWarningMessage("Not resolving promise because actor is dead."),
                                   StmtReturn()])
         ifactorisdestroyed = StmtIf(ExprBinary(self.protocol.stateVar(), '==',
                                                self.protocol.deadState()))
         ifactorisdestroyed.addifstmts([_printWarningMessage("Not resolving promise because actor is destroyed."),
@@ -4307,17 +4319,17 @@ class _GenerateProtocolActorCode(ipdl.as
                                 ifactorisdestroyed ]
         resolverfn = ExprLambda([ExprVar.THIS, selfvar, routingId, seqno],
                                  [resolvedecl])
         resolverfn.addstmts(returnifactorisdead
                             + [ StmtDecl(Decl(Type.BOOL, resolve.name),
                                          init=ExprLiteral.TRUE) ]
                             + [ StmtDecl(Decl(p.bareType(self.side), p.var().name))
                                for p in md.returns ]
-                            + [ StmtExpr(ExprAssn(destructexpr, ExprVar('aParam'))),
+                            + [ StmtExpr(ExprAssn(destructexpr, ExprMove(ExprVar('aParam')))),
                                 StmtDecl(Decl(Type('IPC::Message', ptr=1), self.replyvar.name),
                                          init=ExprCall(ExprVar(md.pqReplyCtorFunc()),
                                                        args=[ routingId ])) ]
                             + [ self.checkedWrite(None, resolve, self.replyvar,
                                                   sentinelKey=resolve.name) ]
                             + [ self.checkedWrite(r.ipdltype, r.var(), self.replyvar,
                                                   sentinelKey=r.name)
                                  for r in md.returns ])
--- a/toolkit/components/build/nsToolkitCompsCID.h
+++ b/toolkit/components/build/nsToolkitCompsCID.h
@@ -87,16 +87,19 @@
 #endif
 
 #define NS_ADDONCONTENTPOLICY_CONTRACTID \
   "@mozilla.org/addons/content-policy;1"
 
 #define NS_ADDONPATHSERVICE_CONTRACTID \
     "@mozilla.org/addon-path-service;1"
 
+#define NS_WEBREQUESTSERVICE_CONTRACTID \
+  "@mozilla.org/addons/webrequest-service;1"
+
 /////////////////////////////////////////////////////////////////////////////
 
 #define ALERT_NOTIFICATION_CID \
 { 0x9a7b7a41, 0x0b47, 0x47f7, { 0xb6, 0x1b, 0x15, 0xa2, 0x10, 0xd6, 0xf0, 0x20 } }
 
 // {A0CCAAF8-09DA-44D8-B250-9AC3E93C8117}
 #define NS_ALERTSSERVICE_CID \
 { 0xa0ccaaf8, 0x9da, 0x44d8, { 0xb2, 0x50, 0x9a, 0xc3, 0xe9, 0x3c, 0x81, 0x17 } }
@@ -187,8 +190,12 @@
 #define NS_ADDON_PATH_SERVICE_CID \
 { 0xa39f39d0, 0xdfb6, 0x11e3, { 0x8b, 0x68, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 } }
 
 #define NS_ADDON_POLICY_SERVICE_CID \
 { 0x562de129, 0x8338, 0x482c, { 0xbb, 0x96, 0xa1, 0xff, 0x09, 0xee, 0x53, 0xcc } }
 
 #define NS_ADDON_POLICY_SERVICE_CONTRACTID \
   "@mozilla.org/addons/policy-service;1"
+
+// {5dd0c968-d74d-42c3-b930-36145f885c3b}
+#define NS_WEBREQUEST_SERVICE_CID \
+{ 0x5dd0c968, 0xd74d, 0x42c3, { 0xb9, 0x30, 0x36, 0x14, 0x5f, 0x88, 0x5c, 0x3b } }
--- a/toolkit/components/build/nsToolkitCompsModule.cpp
+++ b/toolkit/components/build/nsToolkitCompsModule.cpp
@@ -33,16 +33,17 @@
 
 #include "nsBrowserStatusFilter.h"
 #include "mozilla/FinalizationWitnessService.h"
 #include "mozilla/NativeOSFileInternals.h"
 #include "mozilla/AddonContentPolicy.h"
 #include "mozilla/AddonManagerStartup.h"
 #include "mozilla/AddonPathService.h"
 #include "mozilla/ExtensionPolicyService.h"
+#include "mozilla/WebRequestService.h"
 
 #if defined(XP_WIN)
 #include "NativeFileWatcherWin.h"
 #else
 #include "NativeFileWatcherNotSupported.h"
 #endif // (XP_WIN)
 
 #include "nsWebRequestListener.h"
@@ -123,16 +124,17 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsUpdateP
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(FinalizationWitnessService, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(NativeOSFileInternalsService)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NativeFileWatcherService, Init)
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(AddonContentPolicy)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonPathService, AddonPathService::GetInstance)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonManagerStartup, AddonManagerStartup::GetInstance)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(ExtensionPolicyService, ExtensionPolicyService::GetInstance)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(WebRequestService, WebRequestService::GetInstance)
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsWebRequestListener)
 
 NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
 #if defined(MOZ_HAS_PERFSTATS)
 NS_DEFINE_NAMED_CID(NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID);
 #endif // defined (MOZ_HAS_PERFSTATS)
 
@@ -159,16 +161,17 @@ NS_DEFINE_NAMED_CID(NS_BROWSERSTATUSFILT
 NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID);
 #endif
 NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID);
 NS_DEFINE_NAMED_CID(NATIVE_OSFILE_INTERNALS_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_ADDONCONTENTPOLICY_CID);
 NS_DEFINE_NAMED_CID(NS_ADDON_PATH_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_ADDON_MANAGER_STARTUP_CID);
 NS_DEFINE_NAMED_CID(NS_ADDON_POLICY_SERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_WEBREQUEST_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NATIVE_FILEWATCHER_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_WEBREQUESTLISTENER_CID);
 
 static const Module::CIDEntry kToolkitCIDs[] = {
   { &kNS_TOOLKIT_APPSTARTUP_CID, false, nullptr, nsAppStartupConstructor },
 #if defined(MOZ_HAS_TERMINATOR)
   { &kNS_TOOLKIT_TERMINATOR_CID, false, nullptr, nsTerminatorConstructor },
 #endif
@@ -195,16 +198,17 @@ static const Module::CIDEntry kToolkitCI
   { &kNS_UPDATEPROCESSOR_CID, false, nullptr, nsUpdateProcessorConstructor },
 #endif
   { &kFINALIZATIONWITNESSSERVICE_CID, false, nullptr, FinalizationWitnessServiceConstructor },
   { &kNATIVE_OSFILE_INTERNALS_SERVICE_CID, false, nullptr, NativeOSFileInternalsServiceConstructor },
   { &kNS_ADDONCONTENTPOLICY_CID, false, nullptr, AddonContentPolicyConstructor },
   { &kNS_ADDON_PATH_SERVICE_CID, false, nullptr, AddonPathServiceConstructor },
   { &kNS_ADDON_MANAGER_STARTUP_CID, false, nullptr, AddonManagerStartupConstructor },
   { &kNS_ADDON_POLICY_SERVICE_CID, false, nullptr, ExtensionPolicyServiceConstructor },
+  { &kNS_WEBREQUEST_SERVICE_CID, false, nullptr, WebRequestServiceConstructor },
   { &kNATIVE_FILEWATCHER_SERVICE_CID, false, nullptr, NativeFileWatcherServiceConstructor },
   { &kNS_WEBREQUESTLISTENER_CID, false, nullptr, nsWebRequestListenerConstructor },
   { nullptr }
 };
 
 static const Module::ContractIDEntry kToolkitContracts[] = {
   { NS_APPSTARTUP_CONTRACTID, &kNS_TOOLKIT_APPSTARTUP_CID },
 #if defined(MOZ_HAS_TERMINATOR)
@@ -234,16 +238,17 @@ static const Module::ContractIDEntry kTo
   { NS_UPDATEPROCESSOR_CONTRACTID, &kNS_UPDATEPROCESSOR_CID },
 #endif
   { FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID },
   { NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID, &kNATIVE_OSFILE_INTERNALS_SERVICE_CID },
   { NS_ADDONCONTENTPOLICY_CONTRACTID, &kNS_ADDONCONTENTPOLICY_CID },
   { NS_ADDONPATHSERVICE_CONTRACTID, &kNS_ADDON_PATH_SERVICE_CID },
   { NS_ADDONMANAGERSTARTUP_CONTRACTID, &kNS_ADDON_MANAGER_STARTUP_CID },
   { NS_ADDON_POLICY_SERVICE_CONTRACTID, &kNS_ADDON_POLICY_SERVICE_CID },
+  { NS_WEBREQUESTSERVICE_CONTRACTID, &kNS_WEBREQUEST_SERVICE_CID },
   { NATIVE_FILEWATCHER_SERVICE_CONTRACTID, &kNATIVE_FILEWATCHER_SERVICE_CID },
   { NS_WEBREQUESTLISTENER_CONTRACTID, &kNS_WEBREQUESTLISTENER_CID },
   { nullptr }
 };
 
 static const mozilla::Module::CategoryEntry kToolkitCategories[] = {
   { "content-policy", NS_ADDONCONTENTPOLICY_CONTRACTID, NS_ADDONCONTENTPOLICY_CONTRACTID },
   { nullptr }
--- a/toolkit/components/extensions/ExtensionTestCommon.jsm
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -308,37 +308,47 @@ this.ExtensionTestCommon = class Extensi
     }
 
     zipW.close();
 
     return file;
   }
 
   /**
-   * Properly serialize a script into eval-able code string.
+   * Properly serialize a function into eval-able code string.
    *
-   * @param {string|function|Array} script
+   * @param {function} script
    * @returns {string}
    */
-  static serializeScript(script) {
-    if (Array.isArray(script)) {
-      return script.map(this.serializeScript).join(";");
-    }
-    if (typeof script !== "function") {
-      return script;
-    }
+  static serializeFunction(script) {
     // Serialization of object methods doesn't include `function` anymore.
     const method = /^(async )?(\w+)\(/;
 
     let code = script.toString();
     let match = code.match(method);
     if (match && match[2] !== "function") {
       code = code.replace(method, "$1function $2(");
     }
-    return `(${code})();`;
+    return code;
+  }
+
+  /**
+   * Properly serialize a script into eval-able code string.
+   *
+   * @param {string|function|Array} script
+   * @returns {string}
+   */
+  static serializeScript(script) {
+    if (Array.isArray(script)) {
+      return Array.from(script, this.serializeScript, this).join(";");
+    }
+    if (typeof script !== "function") {
+      return script;
+    }
+    return `(${this.serializeFunction(script)})();`;
   }
 
   /**
    * Generates a new extension using |Extension.generateXPI|, and initializes a
    * new |Extension| instance which will execute it.
    *
    * @param {object} data
    * @returns {Extension}
--- 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,25 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+var {
+  ExtensionError,
+} = ExtensionCommon;
+
+this.webRequest = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      webRequest: {
+        filterResponseData(requestId) {
+          if (AppConstants.RELEASE_OR_BETA) {
+            throw new ExtensionError("filterResponseData() unsupported in release builds");
+          }
+          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/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -1,14 +1,15 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'WebExtensions: General')
 
 EXTRA_JS_MODULES += [
     'Extension.jsm',
     'ExtensionChild.jsm',
     'ExtensionChildDevToolsUtils.jsm',
     'ExtensionCommon.jsm',
--- 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": [
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9eb8d73d501725e50814b0385eac18f9466f95d4
GIT binary patch
literal 392
zc$@)<0eAi%iwFplK+{+N18i?{Wo<5KbZu+^MN&IcqcIHZ^DCCrJX(s72Ds{G?g}Wb
z6t<FR#xHD%fq#E;zAxa$k}d6O*WO({{ds)(_xBkm&1t`ZuiXrhs2m@*T(*}(rnrQj
zWL82rQ6H=i+t-(0H}~7`OBGGL-`y?y(tY>SzI&_jYrpwjJ7-86mJw^J9YYl4$k$%d
z_nT15;GnG5K~b8FAms>7a;X>`y%j7ra*a^V&0(Yji4v_SOkKvC6M=OhVPrZM0wsAj
zx?ONy6<j1c3$o1M!&A!FcsCg+b!fK;{^TA+Nu?H8N|Zxg;AfExnRI7dX<iI=hidl0
zD$Y`zPglaHiA$yo>E<&n`lX<A>wMx8xw{#D@KKXB=VYJBg@ean1WD=QnoBbr>?out
zb1F3Io>*VGo<9ROWt;xGB{c-%;Kjk3MAF&jdRng%xqLf2-E;b2snN6n^>C0Ngvf*Y
myv;SufoS?A?#@`fwtRbrSl@kl?DY?vzu12z{$dYv0ssKRFTaui
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/lorem.html.gz^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/html; charset=utf-8
+Content-Encoding: gzip
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -44,17 +44,20 @@ support-files =
   file_simple_xhr_frame.html
   file_simple_xhr_frame2.html
   redirect_auto.sjs
   redirection.sjs
   file_privilege_escalation.html
   file_ext_test_api_injection.js
   file_permission_xhr.html
   file_teardown_test.js
+  lorem.html.gz
+  lorem.html.gz^headers^
   return_headers.sjs
+  slow_response.sjs
   webrequest_worker.js
   !/toolkit/components/passwordmgr/test/authenticate.sjs
   !/dom/tests/mochitest/geolocation/network_geolocation.sjs
 
 [test_ext_clipboard.html]
 # skip-if = # disabled test case with_permission_allow_copy, see inline comment.
 [test_ext_inIncognitoContext_window.html]
 skip-if = os == 'android' # Android does not support multiple windows.
@@ -122,16 +125,18 @@ skip-if = os == 'android'
 [test_ext_web_accessible_resources.html]
 [test_ext_webrequest_auth.html]
 skip-if = os == 'android'
 [test_ext_webrequest_background_events.html]
 [test_ext_webrequest_hsts.html]
 [test_ext_webrequest_basic.html]
 [test_ext_webrequest_filter.html]
 [test_ext_webrequest_frameId.html]
+[test_ext_webrequest_responseBody.html]
+skip-if = release_or_beta || os == 'android'
 [test_ext_webrequest_suspend.html]
 [test_ext_webrequest_upload.html]
 skip-if = os == 'android' # Currently fails in emulator tests
 [test_ext_webrequest_permission.html]
 [test_ext_webrequest_websocket.html]
 [test_ext_webnavigation.html]
 [test_ext_webnavigation_filters.html]
 [test_ext_window_postMessage.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/slow_response.sjs
@@ -0,0 +1,58 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */
+"use strict";
+
+/* eslint-disable no-unused-vars */
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+const DELAY = AppConstants.DEBUG ? 2000 : 200;
+
+let nsTimer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
+
+let timer;
+function delay() {
+  return new Promise(resolve => {
+    timer = nsTimer(resolve, DELAY, Ci.nsITimer.TYPE_ONE_SHOT);
+  });
+}
+
+const PARTS = [
+  `<!DOCTYPE html>
+    <html lang="en">
+    <head>
+      <meta charset="UTF-8">
+      <title></title>
+    </head>
+    <body>`,
+  "Lorem ipsum dolor sit amet, <br>",
+  "consectetur adipiscing elit, <br>",
+  "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br>",
+  "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <br>",
+  "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br>",
+  "Excepteur sint occaecat cupidatat non proident, <br>",
+  "sunt in culpa qui officia deserunt mollit anim id est laborum.<br>",
+  `
+    </body>
+    </html>`,
+];
+
+async function handleRequest(request, response) {
+  response.processAsync();
+
+  response.setHeader("Content-Type", "text/html", false);
+  response.setHeader("Cache-Control", "no-cache", false);
+
+  await delay();
+
+  for (let part of PARTS) {
+    response.write(`${part}\n`);
+    await delay();
+  }
+
+  response.finish();
+}
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_responseBody.html
@@ -0,0 +1,461 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>WebRequest response body filter test</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+"use strict";
+
+const SEQUENTIAL = false;
+
+const PARTS = [
+  `<!DOCTYPE html>
+    <html lang="en">
+    <head>
+      <meta charset="UTF-8">
+      <title></title>
+    </head>
+    <body>`,
+  "Lorem ipsum dolor sit amet, <br>",
+  "consectetur adipiscing elit, <br>",
+  "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br>",
+  "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <br>",
+  "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br>",
+  "Excepteur sint occaecat cupidatat non proident, <br>",
+  "sunt in culpa qui officia deserunt mollit anim id est laborum.<br>",
+  `
+    </body>
+    </html>`,
+].map(part => `${part}\n`);
+
+const TIMEOUT = AppConstants.DEBUG ? 2000 : 200;
+const TASKS = [
+  {
+    url: "slow_response.sjs",
+    task(filter, resolve, num) {
+      let decoder = new TextDecoder("utf-8");
+
+      browser.test.assertEq("uninitialized", filter.status,
+                            `(${num}): Got expected initial status`);
+
+      filter.onstart = event => {
+        browser.test.assertEq("transferringdata", filter.status,
+                              `(${num}): Got expected onStart status`);
+      };
+
+      filter.onstop = event => {
+        browser.test.fail(`(${num}): Got unexpected onStop event while disconnected`);
+      };
+
+      let n = 0;
+      filter.ondata = async event => {
+        let str = decoder.decode(event.data, {stream: true});
+
+        if (n < 3) {
+          browser.test.assertEq(JSON.stringify(PARTS[n]),
+                                JSON.stringify(str),
+                                `(${num}): Got expected part`);
+        }
+        n++;
+
+        filter.write(event.data);
+
+        if (n == 3) {
+          filter.suspend();
+
+          browser.test.assertEq("suspended", filter.status,
+                                `(${num}): Got expected suspended status`);
+
+          let fail = event => {
+            browser.test.fail(`(${num}): Got unexpected data event while suspended`);
+          };
+          filter.addEventListener("data", fail);
+
+          await new Promise(resolve => setTimeout(resolve, TIMEOUT * 3));
+
+          browser.test.assertEq("suspended", filter.status,
+                                `(${num}): Got expected suspended status`);
+
+          filter.removeEventListener("data", fail);
+          filter.resume();
+          browser.test.assertEq("transferringdata", filter.status,
+                                `(${num}): Got expected resumed status`);
+        } else if (n > 4) {
+          filter.disconnect();
+
+          filter.addEventListener("data", event => {
+            browser.test.fail(`(${num}): Got unexpected data event while disconnected`);
+          });
+
+          browser.test.assertEq("disconnected", filter.status,
+                                `(${num}): Got expected disconnected status`);
+
+          resolve();
+        }
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.join(""), "Got expected final HTML");
+    },
+  },
+  {
+    url: "slow_response.sjs",
+    task(filter, resolve, num) {
+      let decoder = new TextDecoder("utf-8");
+
+      filter.onstop = event => {
+        browser.test.fail(`(${num}): Got unexpected onStop event while disconnected`);
+      };
+
+      let n = 0;
+      filter.ondata = async event => {
+        let str = decoder.decode(event.data, {stream: true});
+
+        if (n < 3) {
+          browser.test.assertEq(JSON.stringify(PARTS[n]),
+                                JSON.stringify(str),
+                                `(${num}): Got expected part`);
+        }
+        n++;
+
+        filter.write(event.data);
+
+        if (n == 3) {
+          filter.suspend();
+
+          await new Promise(resolve => setTimeout(resolve, TIMEOUT * 3));
+
+          filter.disconnect();
+
+          resolve();
+        }
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.join(""), "Got expected final HTML");
+    },
+  },
+  {
+    url: "slow_response.sjs",
+    task(filter, resolve, num) {
+      let encoder = new TextEncoder("utf-8");
+
+      filter.onstop = event => {
+        browser.test.fail(`(${num}): Got unexpected onStop event while disconnected`);
+      };
+
+      let n = 0;
+      filter.ondata = async event => {
+        n++;
+
+        filter.write(event.data);
+
+        function checkState(state) {
+          browser.test.assertEq(state, filter.status, `(${num}): Got expected status`);
+        }
+        if (n == 3) {
+          filter.resume();
+          checkState("transferringdata");
+          filter.suspend();
+          checkState("suspended");
+          filter.suspend();
+          checkState("suspended");
+          filter.resume();
+          checkState("transferringdata");
+          filter.suspend();
+          checkState("suspended");
+
+          await new Promise(resolve => setTimeout(resolve, TIMEOUT * 3));
+
+          checkState("suspended");
+          filter.disconnect();
+          checkState("disconnected");
+
+          for (let method of ["suspend", "resume", "close"]) {
+            browser.test.assertThrows(
+              () => {
+                filter[method]();
+              },
+              /.*/,
+              `(${num}): ${method}() should throw while disconnected`);
+          }
+
+          browser.test.assertThrows(
+            () => {
+              filter.write(encoder.encode("Foo bar"));
+            },
+            /.*/,
+            `(${num}): write() should throw while disconnected`);
+
+          filter.disconnect();
+
+          resolve();
+        }
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.join(""), "Got expected final HTML");
+    },
+  },
+  {
+    url: "slow_response.sjs",
+    task(filter, resolve, num) {
+      let encoder = new TextEncoder("utf-8");
+      let decoder = new TextDecoder("utf-8");
+
+      filter.onstop = event => {
+        browser.test.fail(`(${num}): Got unexpected onStop event while closed`);
+      };
+
+      browser.test.assertThrows(
+        () => {
+          filter.write(encoder.encode("Foo bar"));
+        },
+        /.*/,
+        `(${num}): write() should throw prior to connection`);
+
+      let n = 0;
+      filter.ondata = async event => {
+        n++;
+
+        filter.write(event.data);
+
+        browser.test.log(`(${num}): Got part ${n}: ${JSON.stringify(decoder.decode(event.data))}`);
+
+        function checkState(state) {
+          browser.test.assertEq(state, filter.status, `(${num}): Got expected status`);
+        }
+        if (n == 3) {
+          filter.close();
+
+          checkState("closed");
+
+          for (let method of ["suspend", "resume", "disconnect"]) {
+            browser.test.assertThrows(
+              () => {
+                filter[method]();
+              },
+              /.*/,
+              `(${num}): ${method}() should throw while closed`);
+          }
+
+          browser.test.assertThrows(
+            () => {
+              filter.write(encoder.encode("Foo bar"));
+            },
+            /.*/,
+            `(${num}): write() should throw while closed`);
+
+          filter.close();
+
+          resolve();
+        }
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.slice(0, 3).join(""), "Got expected final HTML");
+    },
+  },
+/*
+  {
+    url: "lorem.html.gz",
+    task(filter, resolve, num) {
+      let response = "";
+      let decoder = new TextDecoder("utf-8");
+
+      filter.onstart = event => {
+        browser.test.log(`(${num}): Request start`);
+      };
+
+      filter.onstop = event => {
+        browser.test.assertEq("finishedtransferringdata", filter.status,
+                              `(${num}): Got expected onStop status`);
+
+        filter.close();
+        browser.test.assertEq("closed", filter.status,
+                              `Got expected closed status`);
+
+
+        browser.test.assertEq(JSON.stringify(PARTS.join("")),
+                              JSON.stringify(response),
+                              `(${num}): Got expected response`);
+
+        resolve();
+      };
+
+      filter.ondata = event => {
+        let str = decoder.decode(event.data, {stream: true});
+        response += str;
+
+        filter.write(event.data);
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.join(""), "Got expected final HTML");
+    },
+  },
+*/
+];
+
+function serializeTest(test, num) {
+  /* globals ExtensionTestCommon */
+
+  let url = `${test.url}?test_num=${num}`;
+  let task = ExtensionTestCommon.serializeFunction(test.task);
+
+  return `{url: ${JSON.stringify(url)}, task: ${task}}`;
+}
+
+add_task(async function() {
+  function background(TASKS) {
+    async function runTest(test, num, details) {
+      browser.test.log(`Running test #${num}: ${details.url}`);
+
+      let filter = browser.webRequest.filterResponseData(details.requestId);
+
+      try {
+        await new Promise(resolve => {
+          test.task(filter, resolve, num, details);
+        });
+      } catch (e) {
+        browser.test.fail(`Task #${num} threw an unexpected exception: ${e} :: ${e.stack}`);
+      }
+
+      browser.test.log(`Finished test #${num}: ${details.url}`);
+      browser.test.sendMessage(`finished-${num}`);
+    }
+
+    browser.webRequest.onBeforeRequest.addListener(
+      details => {
+        for (let [num, test] of TASKS.entries()) {
+          if (details.url.endsWith(test.url)) {
+            runTest(test, num, details);
+            break;
+          }
+        }
+      }, {
+        urls: ["http://mochi.test/*?test_num=*"],
+      },
+      ["blocking"]);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `
+      const PARTS = ${JSON.stringify(PARTS)};
+      const TIMEOUT = ${TIMEOUT};
+
+      (${background})([${TASKS.map(serializeTest)}])
+    `,
+
+    manifest: {
+      permissions: [
+        "webRequest",
+        "webRequestBlocking",
+        "http://mochi.test/",
+      ],
+    },
+  });
+
+  await extension.startup();
+
+  async function runTest(test, num) {
+    let url = `${test.url}?test_num=${num}`;
+
+    let resp = await fetch(url);
+    let body = await resp.text();
+
+    await extension.awaitMessage(`finished-${num}`);
+
+    info(`Verifying test #${num}: ${url}`);
+    await test.verify(body);
+  }
+
+  if (SEQUENTIAL) {
+    for (let [num, test] of TASKS.entries()) {
+      await runTest(test, num);
+    }
+  } else {
+    await Promise.all(TASKS.map(runTest));
+  }
+
+  await extension.unload();
+});
+
+add_task(async function test_permissions() {
+  let extension = ExtensionTestUtils.loadExtension({
+    background() {
+      browser.test.assertEq(
+          undefined, browser.webRequest.filterResponseData,
+          "filterResponseData is undefined without blocking permissions");
+    },
+
+    manifest: {
+      permissions: [
+        "webRequest",
+        "http://mochi.test/",
+      ],
+    },
+  });
+
+  await extension.startup();
+  await extension.unload();
+});
+
+add_task(async function test_invalidId() {
+  let extension = ExtensionTestUtils.loadExtension({
+    async background() {
+      let filter = browser.webRequest.filterResponseData("34159628");
+
+      await new Promise(resolve => { filter.onerror = resolve; });
+
+      browser.test.assertEq("Invalid request ID",
+                            filter.error,
+                            "Got expected error");
+
+      browser.test.notifyPass("invalid-request-id");
+    },
+
+    manifest: {
+      permissions: [
+        "webRequest",
+        "webRequestBlocking",
+        "http://mochi.test/",
+      ],
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("invalid-request-id");
+  await extension.unload();
+});
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/PStreamFilter.ipdl
@@ -0,0 +1,41 @@
+/* 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/. */
+
+include protocol PBackground;
+
+namespace mozilla {
+namespace extensions {
+
+protocol PStreamFilter
+{
+  manager PBackground;
+
+parent:
+  async Write(uint8_t[] data);
+
+  async FlushedData();
+
+  async Suspend();
+  async Resume();
+  async Close();
+  async Disconnect();
+
+child:
+  async Initialized(bool aSuccess);
+  async Resumed();
+  async Suspended();
+  async Closed();
+
+  async FlushData();
+
+  async StartRequest();
+  async Data(uint8_t[] data);
+  async StopRequest(nsresult aStatus);
+
+  async __delete__();
+};
+
+} // namespace extensions
+} // namespace mozilla
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilter.cpp
@@ -0,0 +1,294 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#include "StreamFilter.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/SystemGroup.h"
+#include "mozilla/extensions/StreamFilterChild.h"
+#include "mozilla/extensions/StreamFilterEvents.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsLiteralString.h"
+#include "nsThreadUtils.h"
+#include "nsTArray.h"
+
+using namespace JS;
+using namespace mozilla::dom;
+
+using mozilla::ipc::BackgroundChild;
+using mozilla::ipc::PBackgroundChild;
+
+namespace mozilla {
+namespace extensions {
+
+/*****************************************************************************
+ * Initialization
+ *****************************************************************************/
+
+StreamFilter::StreamFilter(nsIGlobalObject* aParent,
+                           uint64_t aRequestId,
+                           const nsAString& aAddonId)
+  : mParent(aParent)
+  , mChannelId(aRequestId)
+  , mAddonId(NS_Atomize(aAddonId))
+{
+  MOZ_ASSERT(aParent);
+
+  ConnectToPBackground();
+};
+
+StreamFilter::~StreamFilter()
+{
+  ForgetActor();
+}
+
+void
+StreamFilter::ForgetActor()
+{
+  if (mActor) {
+    mActor->Cleanup();
+    mActor->SetStreamFilter(nullptr);
+  }
+}
+
+
+/* static */ already_AddRefed<StreamFilter>
+StreamFilter::Create(GlobalObject& aGlobal, uint64_t aRequestId, const nsAString& aAddonId)
+{
+  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+  MOZ_ASSERT(global);
+
+  RefPtr<StreamFilter> filter = new StreamFilter(global, aRequestId, aAddonId);
+  return filter.forget();
+}
+
+/*****************************************************************************
+ * Actor allocation
+ *****************************************************************************/
+
+void
+StreamFilter::ConnectToPBackground()
+{
+  PBackgroundChild* background = BackgroundChild::GetForCurrentThread();
+  if (background) {
+    ActorCreated(background);
+  } else {
+    bool ok = BackgroundChild::GetOrCreateForCurrentThread(this);
+    MOZ_RELEASE_ASSERT(ok);
+  }
+}
+
+void
+StreamFilter::ActorFailed()
+{
+  MOZ_CRASH("Failed to create a PBackgroundChild actor");
+}
+
+void
+StreamFilter::ActorCreated(PBackgroundChild* aBackground)
+{
+  MOZ_ASSERT(aBackground);
+  MOZ_ASSERT(!mActor);
+
+  nsAutoString addonId;
+  mAddonId->ToString(addonId);
+
+  PStreamFilterChild* actor = aBackground->SendPStreamFilterConstructor(mChannelId, addonId);
+  MOZ_ASSERT(actor);
+
+  mActor = static_cast<StreamFilterChild*>(actor);
+  mActor->SetStreamFilter(this);
+}
+
+/*****************************************************************************
+ * Binding methods
+ *****************************************************************************/
+
+template <typename T>
+static inline bool
+ReadTypedArrayData(nsTArray<uint8_t>& aData, const T& aArray, ErrorResult& aRv)
+{
+  aArray.ComputeLengthAndData();
+  if (!aData.SetLength(aArray.Length(), fallible)) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+    return false;
+  }
+  memcpy(aData.Elements(), aArray.Data(), aArray.Length());
+  return true;
+}
+
+void
+StreamFilter::Write(const ArrayBufferOrUint8Array& aData, ErrorResult& aRv)
+{
+  if (!mActor) {
+    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
+    return;
+  }
+
+  nsTArray<uint8_t> data;
+
+  bool ok;
+  if (aData.IsArrayBuffer()) {
+    ok = ReadTypedArrayData(data, aData.GetAsArrayBuffer(), aRv);
+  } else if (aData.IsUint8Array()) {
+    ok = ReadTypedArrayData(data, aData.GetAsUint8Array(), aRv);
+  } else {
+    MOZ_ASSERT_UNREACHABLE("Argument should be ArrayBuffer or Uint8Array");
+    return;
+  }
+
+  if (ok) {
+    mActor->Write(Move(data), aRv);
+  }
+}
+
+StreamFilterStatus
+StreamFilter::Status() const
+{
+  if (!mActor) {
+    return StreamFilterStatus::Uninitialized;
+  }
+  return mActor->Status();
+}
+
+void
+StreamFilter::Suspend(ErrorResult& aRv)
+{
+  if (mActor) {
+    mActor->Suspend(aRv);
+  } else {
+    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
+  }
+}
+
+void
+StreamFilter::Resume(ErrorResult& aRv)
+{
+  if (mActor) {
+    mActor->Resume(aRv);
+  } else {
+    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
+  }
+}
+
+void
+StreamFilter::Disconnect(ErrorResult& aRv)
+{
+  if (mActor) {
+    mActor->Disconnect(aRv);
+  } else {
+    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
+  }
+}
+
+void
+StreamFilter::Close(ErrorResult& aRv)
+{
+  if (mActor) {
+    mActor->Close(aRv);
+  } else {
+    aRv.Throw(NS_ERROR_NOT_INITIALIZED);
+  }
+}
+
+/*****************************************************************************
+ * Event emitters
+ *****************************************************************************/
+
+void
+StreamFilter::FireEvent(const nsAString& aType)
+{
+  EventInit init;
+  init.mBubbles = false;
+  init.mCancelable = false;
+
+  RefPtr<Event> event = Event::Constructor(this, aType, init);
+  event->SetTrusted(true);
+
+  bool defaultPrevented;
+  DispatchEvent(event, &defaultPrevented);
+}
+
+void
+StreamFilter::FireDataEvent(const nsTArray<uint8_t>& aData)
+{
+  AutoEntryScript aes(mParent, "StreamFilter data event");
+  JSContext* cx = aes.cx();
+
+  RootedDictionary<StreamFilterDataEventInit> init(cx);
+  init.mBubbles = false;
+  init.mCancelable = false;
+
+  auto buffer = ArrayBuffer::Create(cx, aData.Length(), aData.Elements());
+  if (!buffer) {
+    // TODO: There is no way to recover from this. This chunk of data is lost.
+    FireErrorEvent(NS_LITERAL_STRING("Out of memory"));
+    return;
+  }
+
+  init.mData.Init(buffer);
+
+  RefPtr<StreamFilterDataEvent> event =
+    StreamFilterDataEvent::Constructor(this, NS_LITERAL_STRING("data"), init);
+  event->SetTrusted(true);
+
+  bool defaultPrevented;
+  DispatchEvent(event, &defaultPrevented);
+}
+
+void
+StreamFilter::FireErrorEvent(const nsAString& aError)
+{
+  MOZ_ASSERT(mError.IsEmpty());
+
+  mError = aError;
+  FireEvent(NS_LITERAL_STRING("error"));
+}
+
+/*****************************************************************************
+ * Glue
+ *****************************************************************************/
+
+/* static */ bool
+StreamFilter::IsAllowedInContext(JSContext* aCx, JSObject* /* unused */)
+{
+  return nsContentUtils::CallerHasPermission(aCx, NS_LITERAL_STRING("webRequestBlocking"));
+}
+
+JSObject*
+StreamFilter::WrapObject(JSContext* aCx, HandleObject aGivenProto)
+{
+  return StreamFilterBinding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(StreamFilter)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StreamFilter)
+  NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(StreamFilter, DOMEventTargetHelper)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(StreamFilter, DOMEventTargetHelper)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(StreamFilter, DOMEventTargetHelper)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_ADDREF_INHERITED(StreamFilter, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(StreamFilter, DOMEventTargetHelper)
+
+} // namespace extensions
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilter.h
@@ -0,0 +1,98 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#ifndef mozilla_extensions_StreamFilter_h
+#define mozilla_extensions_StreamFilter_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/StreamFilterBinding.h"
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIAtom.h"
+#include "nsIIPCBackgroundChildCreateCallback.h"
+
+namespace mozilla {
+namespace extensions {
+
+class StreamFilterChild;
+
+using namespace mozilla::dom;
+
+class StreamFilter : public DOMEventTargetHelper
+                   , public nsIIPCBackgroundChildCreateCallback
+{
+  friend class StreamFilterChild;
+
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(StreamFilter, DOMEventTargetHelper)
+
+  static already_AddRefed<StreamFilter>
+  Create(GlobalObject& global,
+         uint64_t aRequestId,
+         const nsAString& aAddonId);
+
+  explicit StreamFilter(nsIGlobalObject* aParent,
+                        uint64_t aRequestId,
+                        const nsAString& aAddonId);
+
+  IMPL_EVENT_HANDLER(start);
+  IMPL_EVENT_HANDLER(stop);
+  IMPL_EVENT_HANDLER(data);
+  IMPL_EVENT_HANDLER(error);
+
+  void Write(const ArrayBufferOrUint8Array& aData,
+             ErrorResult& aRv);
+
+  void GetError(nsAString& aError)
+  {
+    aError = mError;
+  }
+
+  StreamFilterStatus Status() const;
+  void Suspend(ErrorResult& aRv);
+  void Resume(ErrorResult& aRv);
+  void Disconnect(ErrorResult& aRv);
+  void Close(ErrorResult& aRv);
+
+  nsISupports* GetParentObject() const { return mParent; }
+
+  virtual JSObject* WrapObject(JSContext* aCx,
+                               JS::Handle<JSObject*> aGivenProto) override;
+
+  static bool
+  IsAllowedInContext(JSContext* aCx, JSObject* aObj);
+
+protected:
+  virtual ~StreamFilter();
+
+  void FireEvent(const nsAString& aType);
+
+  void FireDataEvent(const nsTArray<uint8_t>& aData);
+
+  void FireErrorEvent(const nsAString& aError);
+
+private:
+  void
+  ConnectToPBackground();
+
+  void ForgetActor();
+
+  nsCOMPtr<nsIGlobalObject> mParent;
+  RefPtr<StreamFilterChild> mActor;
+
+  nsString mError;
+
+  const uint64_t mChannelId;
+  const nsCOMPtr<nsIAtom> mAddonId;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_StreamFilter_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilterBase.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#ifndef mozilla_extensions_StreamFilterBase_h
+#define mozilla_extensions_StreamFilterBase_h
+
+#include "mozilla/LinkedList.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace extensions {
+
+class StreamFilterBase
+{
+public:
+  typedef nsTArray<uint8_t> Data;
+
+protected:
+  class BufferedData : public LinkedListElement<BufferedData> {
+  public:
+    explicit BufferedData(Data&& aData) : mData(Move(aData)) {}
+
+    Data mData;
+  };
+
+  LinkedList<BufferedData> mBufferedData;
+
+  inline void
+  BufferData(Data&& aData) {
+    mBufferedData.insertBack(new BufferedData(Move(aData)));
+  };
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_StreamFilterBase_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilterChild.cpp
@@ -0,0 +1,518 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#include "StreamFilterChild.h"
+#include "StreamFilter.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+namespace extensions {
+
+using mozilla::dom::StreamFilterStatus;
+using mozilla::ipc::IPCResult;
+
+/*****************************************************************************
+ * Initialization and cleanup
+ *****************************************************************************/
+
+void
+StreamFilterChild::Cleanup()
+{
+  switch (mState) {
+  case State::Closing:
+  case State::Closed:
+  case State::Error:
+  case State::Disconnecting:
+  case State::Disconnected:
+    break;
+
+  default:
+    ErrorResult rv;
+    Disconnect(rv);
+    break;
+  }
+}
+
+/*****************************************************************************
+ * State change methods
+ *****************************************************************************/
+
+void
+StreamFilterChild::Suspend(ErrorResult& aRv)
+{
+  switch (mState) {
+  case State::TransferringData:
+    mState = State::Suspending;
+    mNextState = State::Suspended;
+
+    SendSuspend();
+    break;
+
+  case State::Suspending:
+    switch (mNextState) {
+    case State::Suspended:
+    case State::Resuming:
+      mNextState = State::Suspended;
+      break;
+
+    default:
+      aRv.Throw(NS_ERROR_FAILURE);
+      return;
+    }
+    break;
+
+  case State::Resuming:
+    switch (mNextState) {
+    case State::TransferringData:
+    case State::Suspending:
+      mNextState = State::Suspending;
+      break;
+
+    default:
+      aRv.Throw(NS_ERROR_FAILURE);
+      return;
+    }
+    break;
+
+  case State::Suspended:
+    break;
+
+  default:
+    aRv.Throw(NS_ERROR_FAILURE);
+    break;
+  }
+}
+
+void
+StreamFilterChild::Resume(ErrorResult& aRv)
+{
+  switch (mState) {
+  case State::Suspended:
+    mState = State::Resuming;
+    mNextState = State::TransferringData;
+
+    SendResume();
+    break;
+
+  case State::Suspending:
+    switch (mNextState) {
+    case State::Suspended:
+    case State::Resuming:
+      mNextState = State::Resuming;
+      break;
+
+    default:
+      aRv.Throw(NS_ERROR_FAILURE);
+      return;
+    }
+    break;
+
+  case State::Resuming:
+  case State::TransferringData:
+    break;
+
+  default:
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+
+  FlushBufferedData();
+}
+
+void
+StreamFilterChild::Disconnect(ErrorResult& aRv)
+{
+  switch (mState) {
+  case State::Suspended:
+  case State::TransferringData:
+  case State::FinishedTransferringData:
+    mState = State::Disconnecting;
+    mNextState = State::Disconnected;
+
+    SendDisconnect();
+    break;
+
+  case State::Suspending:
+  case State::Resuming:
+    switch (mNextState) {
+    case State::Suspended:
+    case State::Resuming:
+    case State::Disconnecting:
+      mNextState = State::Disconnecting;
+      break;
+
+    default:
+      aRv.Throw(NS_ERROR_FAILURE);
+      return;
+    }
+    break;
+
+  case State::Disconnecting:
+  case State::Disconnected:
+    break;
+
+  default:
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+}
+
+void
+StreamFilterChild::Close(ErrorResult& aRv)
+{
+  switch (mState) {
+  case State::Suspended:
+  case State::TransferringData:
+  case State::FinishedTransferringData:
+    mState = State::Closing;
+    mNextState = State::Closed;
+
+    SendClose();
+    break;
+
+  case State::Suspending:
+  case State::Resuming:
+    mNextState = State::Closing;
+    break;
+
+  case State::Closing:
+    MOZ_DIAGNOSTIC_ASSERT(mNextState == State::Closed);
+    break;
+
+  case State::Closed:
+    break;
+
+  default:
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+
+  mBufferedData.clear();
+}
+
+/*****************************************************************************
+ * Internal state management
+ *****************************************************************************/
+
+void
+StreamFilterChild::SetNextState()
+{
+  mState = mNextState;
+
+  switch (mNextState) {
+  case State::Suspending:
+    mNextState = State::Suspended;
+    SendSuspend();
+    break;
+
+  case State::Resuming:
+    mNextState = State::TransferringData;
+    SendResume();
+    break;
+
+  case State::Closing:
+    mNextState = State::Closed;
+    SendClose();
+    break;
+
+  case State::Disconnecting:
+    mNextState = State::Disconnected;
+    SendDisconnect();
+    break;
+
+  case State::FinishedTransferringData:
+    if (mStreamFilter) {
+      mStreamFilter->FireEvent(NS_LITERAL_STRING("stop"));
+      // We don't need access to the stream filter after this point, so break our
+      // reference cycle, so that it can be collected if we're the last reference.
+      mStreamFilter = nullptr;
+    }
+    break;
+
+  case State::TransferringData:
+    FlushBufferedData();
+    break;
+
+  case State::Closed:
+  case State::Disconnected:
+  case State::Error:
+    mStreamFilter = nullptr;
+    break;
+
+  default:
+    break;
+  }
+}
+
+void
+StreamFilterChild::MaybeStopRequest()
+{
+  if (!mReceivedOnStop || !mBufferedData.isEmpty()) {
+    return;
+  }
+
+  switch (mState) {
+  case State::Suspending:
+  case State::Resuming:
+    mNextState = State::FinishedTransferringData;
+    return;
+
+  default:
+    mState = State::FinishedTransferringData;
+    if (mStreamFilter) {
+      mStreamFilter->FireEvent(NS_LITERAL_STRING("stop"));
+      // We don't need access to the stream filter after this point, so break our
+      // reference cycle, so that it can be collected if we're the last reference.
+      mStreamFilter = nullptr;
+    }
+    break;
+  }
+}
+
+/*****************************************************************************
+ * State change acknowledgment callbacks
+ *****************************************************************************/
+
+IPCResult
+StreamFilterChild::RecvInitialized(const bool& aSuccess)
+{
+  MOZ_ASSERT(mState == State::Uninitialized);
+
+  if (aSuccess) {
+    mState = State::Initialized;
+  } else {
+    mState = State::Error;
+    if (mStreamFilter) {
+      mStreamFilter->FireErrorEvent(NS_LITERAL_STRING("Invalid request ID"));
+      mStreamFilter = nullptr;
+    }
+  }
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterChild::RecvClosed() {
+  MOZ_DIAGNOSTIC_ASSERT(mState == State::Closing);
+
+  SetNextState();
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterChild::RecvSuspended() {
+  MOZ_DIAGNOSTIC_ASSERT(mState == State::Suspending);
+
+  SetNextState();
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterChild::RecvResumed() {
+  MOZ_DIAGNOSTIC_ASSERT(mState == State::Resuming);
+
+  SetNextState();
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterChild::RecvFlushData() {
+  MOZ_DIAGNOSTIC_ASSERT(mState == State::Disconnecting);
+
+  SendFlushedData();
+  SetNextState();
+  return IPC_OK();
+}
+
+/*****************************************************************************
+ * Other binding methods
+ *****************************************************************************/
+
+void
+StreamFilterChild::Write(Data&& aData, ErrorResult& aRv)
+{
+  switch (mState) {
+  case State::Suspending:
+  case State::Resuming:
+    switch (mNextState) {
+    case State::Suspended:
+    case State::TransferringData:
+      break;
+
+    default:
+      aRv.Throw(NS_ERROR_FAILURE);
+      return;
+    }
+    break;
+
+  case State::Suspended:
+  case State::TransferringData:
+  case State::FinishedTransferringData:
+    break;
+
+  default:
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+
+  SendWrite(Move(aData));
+}
+
+StreamFilterStatus
+StreamFilterChild::Status() const
+{
+  switch (mState) {
+  case State::Uninitialized:
+  case State::Initialized:
+    return StreamFilterStatus::Uninitialized;
+
+  case State::TransferringData:
+    return StreamFilterStatus::Transferringdata;
+
+  case State::Suspended:
+    return StreamFilterStatus::Suspended;
+
+  case State::FinishedTransferringData:
+    return StreamFilterStatus::Finishedtransferringdata;
+
+  case State::Resuming:
+  case State::Suspending:
+    switch (mNextState) {
+    case State::TransferringData:
+    case State::Resuming:
+      return StreamFilterStatus::Transferringdata;
+
+    case State::Suspended:
+    case State::Suspending:
+      return StreamFilterStatus::Suspended;
+
+    case State::Closing:
+      return StreamFilterStatus::Closed;
+
+    case State::Disconnecting:
+      return StreamFilterStatus::Disconnected;
+
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unexpected next state");
+      return StreamFilterStatus::Suspended;
+    }
+    break;
+
+  case State::Closing:
+  case State::Closed:
+    return StreamFilterStatus::Closed;
+
+  case State::Disconnecting:
+  case State::Disconnected:
+    return StreamFilterStatus::Disconnected;
+
+  case State::Error:
+    return StreamFilterStatus::Failed;
+  };
+
+  MOZ_ASSERT_UNREACHABLE("Not reached");
+  return StreamFilterStatus::Failed;
+}
+
+/*****************************************************************************
+ * Request state notifications
+ *****************************************************************************/
+
+IPCResult
+StreamFilterChild::RecvStartRequest()
+{
+  MOZ_ASSERT(mState == State::Initialized);
+
+  mState = State::TransferringData;
+
+  if (mStreamFilter) {
+    mStreamFilter->FireEvent(NS_LITERAL_STRING("start"));
+  }
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterChild::RecvStopRequest(const nsresult& aStatus)
+{
+  mReceivedOnStop = true;
+  MaybeStopRequest();
+  return IPC_OK();
+}
+
+/*****************************************************************************
+ * Incoming request data handling
+ *****************************************************************************/
+
+void
+StreamFilterChild::EmitData(const Data& aData)
+{
+  MOZ_ASSERT(CanFlushData());
+  if (mStreamFilter) {
+    mStreamFilter->FireDataEvent(aData);
+  }
+
+  MaybeStopRequest();
+}
+
+void
+StreamFilterChild::FlushBufferedData()
+{
+  while (!mBufferedData.isEmpty() && CanFlushData()) {
+    UniquePtr<BufferedData> data(mBufferedData.popFirst());
+
+    EmitData(data->mData);
+  }
+}
+
+IPCResult
+StreamFilterChild::RecvData(Data&& aData)
+{
+  MOZ_ASSERT(!mReceivedOnStop);
+
+  switch (mState) {
+  case State::TransferringData:
+  case State::Resuming:
+    EmitData(aData);
+    break;
+
+  case State::FinishedTransferringData:
+    MOZ_ASSERT_UNREACHABLE("Received data in unexpected state");
+    EmitData(aData);
+    break;
+
+  case State::Suspending:
+  case State::Suspended:
+    BufferData(Move(aData));
+    break;
+
+  case State::Disconnecting:
+    SendWrite(Move(aData));
+    break;
+
+  case State::Closing:
+    break;
+
+  default:
+    MOZ_ASSERT_UNREACHABLE("Received data in unexpected state");
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  return IPC_OK();
+}
+
+/*****************************************************************************
+ * Glue
+ *****************************************************************************/
+
+void
+StreamFilterChild::ActorDestroy(ActorDestroyReason aWhy)
+{
+  mStreamFilter = nullptr;
+}
+
+} // namespace extensions
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilterChild.h
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#ifndef mozilla_extensions_StreamFilterChild_h
+#define mozilla_extensions_StreamFilterChild_h
+
+#include "StreamFilterBase.h"
+#include "mozilla/extensions/PStreamFilterChild.h"
+#include "mozilla/extensions/StreamFilter.h"
+
+#include "mozilla/ErrorResult.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/dom/StreamFilterBinding.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+namespace extensions {
+
+using mozilla::dom::StreamFilterStatus;
+using mozilla::ipc::IPCResult;
+
+class StreamFilter;
+
+class StreamFilterChild final : public PStreamFilterChild
+                              , public StreamFilterBase
+{
+  friend class StreamFilter;
+
+public:
+  NS_INLINE_DECL_REFCOUNTING(StreamFilterChild)
+
+  StreamFilterChild()
+    : mState(State::Uninitialized)
+    , mReceivedOnStop(false)
+  {}
+
+  enum class State {
+    // Uninitialized, waiting for constructor response from parent.
+    Uninitialized,
+    // Initialized, but channel has not begun transferring data.
+    Initialized,
+    // The stream's OnStartRequest event has been dispatched, and the channel is
+    // transferring data.
+    TransferringData,
+    // The channel's OnStopRequest event has been dispatched, and the channel is
+    // no longer transferring data. Data may still be written to the output
+    // stream listener.
+    FinishedTransferringData,
+    // The channel is being suspended, and we're waiting for confirmation of
+    // suspension from the parent.
+    Suspending,
+    // The channel has been suspended in the parent. Data may still be written
+    // to the output stream listener in this state.
+    Suspended,
+    // The channel is suspended. Resume has been called, and we are waiting for
+    // confirmation of resumption from the parent.
+    Resuming,
+    // The close() method has been called, and no further output may be written.
+    // We are waiting for confirmation from the parent.
+    Closing,
+    // The close() method has been called, and we have been disconnected from
+    // our parent.
+    Closed,
+    // The channel is being disconnected from the parent, and all further events
+    // and data will pass unfiltered. Data received by the child in this state
+    // will be automatically written ot the output stream listener. No data may
+    // be explicitly written.
+    Disconnecting,
+    // The channel has been disconnected from the parent, and all further data
+    // and events will be transparently passed to the output stream listener
+    // without passing through the child.
+    Disconnected,
+    // An error has occurred and the child is disconnected from the parent.
+    Error,
+  };
+
+  void Suspend(ErrorResult& aRv);
+  void Resume(ErrorResult& aRv);
+  void Disconnect(ErrorResult& aRv);
+  void Close(ErrorResult& aRv);
+  void Cleanup();
+
+  void Write(Data&& aData, ErrorResult& aRv);
+
+  State GetState() const
+  {
+    return mState;
+  }
+
+  StreamFilterStatus Status() const;
+
+protected:
+  virtual IPCResult RecvInitialized(const bool& aSuccess) override;
+
+  virtual IPCResult RecvStartRequest() override;
+  virtual IPCResult RecvData(Data&& data) override;
+  virtual IPCResult RecvStopRequest(const nsresult& aStatus) override;
+
+  virtual IPCResult RecvClosed() override;
+  virtual IPCResult RecvSuspended() override;
+  virtual IPCResult RecvResumed() override;
+  virtual IPCResult RecvFlushData() override;
+
+  virtual IPCResult Recv__delete__() override { return IPC_OK(); }
+
+  void
+  SetStreamFilter(StreamFilter* aStreamFilter)
+  {
+    mStreamFilter = aStreamFilter;
+  }
+
+private:
+  ~StreamFilterChild() {}
+
+  void SetNextState();
+
+  void MaybeStopRequest();
+
+  void EmitData(const Data& aData);
+
+  bool
+  CanFlushData()
+  {
+    return (mState == State::TransferringData ||
+            mState == State::Resuming);
+  }
+
+  void FlushBufferedData();
+
+  virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+
+  State mState;
+  State mNextState;
+  bool mReceivedOnStop;
+
+  RefPtr<StreamFilter> mStreamFilter;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_StreamFilterChild_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilterEvents.cpp
@@ -0,0 +1,56 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#include "mozilla/extensions/StreamFilterEvents.h"
+
+namespace mozilla {
+namespace extensions {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(StreamFilterDataEvent)
+
+NS_IMPL_ADDREF_INHERITED(StreamFilterDataEvent, Event)
+NS_IMPL_RELEASE_INHERITED(StreamFilterDataEvent, Event)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(StreamFilterDataEvent, Event)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(StreamFilterDataEvent, Event)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mData)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(StreamFilterDataEvent, Event)
+  tmp->mData = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StreamFilterDataEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+
+/* static */ already_AddRefed<StreamFilterDataEvent>
+StreamFilterDataEvent::Constructor(EventTarget* aEventTarget,
+                                   const nsAString& aType,
+                                   const StreamFilterDataEventInit& aParam)
+{
+  RefPtr<StreamFilterDataEvent> event = new StreamFilterDataEvent(aEventTarget);
+
+  bool trusted = event->Init(aEventTarget);
+  event->InitEvent(aType, aParam.mBubbles, aParam.mCancelable);
+  event->SetTrusted(trusted);
+  event->SetComposed(aParam.mComposed);
+
+  event->SetData(aParam.mData);
+
+  return event.forget();
+}
+
+JSObject*
+StreamFilterDataEvent::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return StreamFilterDataEventBinding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace extensions
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilterEvents.h
@@ -0,0 +1,80 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#ifndef mozilla_extensions_StreamFilterEvents_h
+#define mozilla_extensions_StreamFilterEvents_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/StreamFilterDataEventBinding.h"
+#include "mozilla/extensions/StreamFilter.h"
+
+#include "jsapi.h"
+
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/dom/Event.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+
+namespace mozilla {
+namespace extensions {
+
+using namespace JS;
+using namespace mozilla::dom;
+
+class StreamFilterDataEvent : public Event
+{
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(StreamFilterDataEvent, Event)
+
+  explicit StreamFilterDataEvent(EventTarget* aEventTarget)
+    : Event(aEventTarget, nullptr, nullptr)
+  {
+    mozilla::HoldJSObjects(this);
+  }
+
+  static already_AddRefed<StreamFilterDataEvent>
+  Constructor(EventTarget* aEventTarget,
+              const nsAString& aType,
+              const StreamFilterDataEventInit& aParam);
+
+  static already_AddRefed<StreamFilterDataEvent>
+  Constructor(GlobalObject& aGlobal,
+              const nsAString& aType,
+              const StreamFilterDataEventInit& aParam,
+              ErrorResult& aRv)
+  {
+    nsCOMPtr<EventTarget> target = do_QueryInterface(aGlobal.GetAsSupports());
+    return Constructor(target, aType, aParam);
+  }
+
+  void GetData(JSContext* aCx, JS::MutableHandleObject aResult)
+  {
+    aResult.set(mData);
+  }
+
+  virtual JSObject* WrapObjectInternal(JSContext* aCx,
+                                       JS::Handle<JSObject*> aGivenProto) override;
+
+protected:
+  virtual ~StreamFilterDataEvent()
+  {
+    mozilla::DropJSObjects(this);
+  }
+
+private:
+  JS::Heap<JSObject*> mData;
+
+  void
+  SetData(const ArrayBuffer& aData)
+  {
+    mData = aData.Obj();
+  }
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_StreamFilterEvents_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilterParent.cpp
@@ -0,0 +1,455 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#include "StreamFilterParent.h"
+
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/nsIContentParent.h"
+#include "nsIChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIInputStream.h"
+#include "nsITraceableChannel.h"
+#include "nsProxyRelease.h"
+#include "nsStringStream.h"
+
+namespace mozilla {
+namespace extensions {
+
+/*****************************************************************************
+ * Initialization
+ *****************************************************************************/
+
+StreamFilterParent::StreamFilterParent(uint64_t aChannelId, const nsAString& aAddonId)
+  : mChannelId(aChannelId)
+  , mAddonId(NS_Atomize(aAddonId))
+  , mPBackgroundThread(NS_GetCurrentThread())
+  , mIOThread(do_GetMainThread())
+  , mBufferMutex("StreamFilter buffer mutex")
+  , mReceivedStop(false)
+  , mSentStop(false)
+  , mContext(nullptr)
+  , mOffset(0)
+  , mState(State::Uninitialized)
+{
+}
+
+StreamFilterParent::~StreamFilterParent()
+{
+  NS_ReleaseOnMainThreadSystemGroup("StreamFilterParent::mOrigListener",
+                                    mOrigListener.forget());
+  NS_ReleaseOnMainThreadSystemGroup("StreamFilterParent::mContext",
+                                    mContext.forget());
+}
+
+void
+StreamFilterParent::Init(already_AddRefed<nsIContentParent> aContentParent)
+{
+  AssertIsPBackgroundThread();
+
+  SystemGroup::Dispatch(
+    TaskCategory::Network,
+    NewRunnableMethod<already_AddRefed<nsIContentParent>&&>(
+        "StreamFilterParent::DoInit",
+        this, &StreamFilterParent::DoInit, Move(aContentParent)));
+}
+
+void
+StreamFilterParent::DoInit(already_AddRefed<nsIContentParent>&& aContentParent)
+{
+  AssertIsMainThread();
+
+  nsCOMPtr<nsIContentParent> contentParent = aContentParent;
+
+  bool success = false;
+  auto guard = MakeScopeExit([&] {
+    RefPtr<StreamFilterParent> self(this);
+
+    RunOnPBackgroundThread(FUNC, [=] {
+      if (self->IPCActive()) {
+        self->mState = State::Initialized;
+        self->CheckResult(self->SendInitialized(success));
+      }
+    });
+  });
+
+  auto& webreq = WebRequestService::GetSingleton();
+
+  mChannel = webreq.GetTraceableChannel(mChannelId, mAddonId, contentParent);
+  if (NS_WARN_IF(!mChannel)) {
+    return;
+  }
+
+  nsCOMPtr<nsITraceableChannel> traceable = do_QueryInterface(mChannel);
+  MOZ_RELEASE_ASSERT(traceable);
+
+  nsresult rv = traceable->SetNewListener(this, getter_AddRefs(mOrigListener));
+  success = NS_SUCCEEDED(rv);
+}
+
+/*****************************************************************************
+ * nsIThreadRetargetableStreamListener
+ *****************************************************************************/
+
+NS_IMETHODIMP
+StreamFilterParent::CheckListenerChain()
+{
+  AssertIsMainThread();
+
+  nsCOMPtr<nsIThreadRetargetableStreamListener> trsl =
+    do_QueryInterface(mOrigListener);
+  if (trsl) {
+    return trsl->CheckListenerChain();
+  }
+  return NS_ERROR_FAILURE;
+}
+
+/*****************************************************************************
+ * Error handling
+ *****************************************************************************/
+
+void
+StreamFilterParent::Broken()
+{
+  AssertIsPBackgroundThread();
+
+  mState = State::Disconnecting;
+
+  RefPtr<StreamFilterParent> self(this);
+  RunOnIOThread(FUNC, [=] {
+    self->FlushBufferedData();
+
+    RunOnPBackgroundThread(FUNC, [=] {
+      if (self->IPCActive()) {
+        self->mState = State::Disconnected;
+      }
+    });
+  });
+}
+
+/*****************************************************************************
+ * State change requests
+ *****************************************************************************/
+
+IPCResult
+StreamFilterParent::RecvClose()
+{
+  AssertIsPBackgroundThread();
+
+  mState = State::Closed;
+
+  if (!mSentStop) {
+    RefPtr<StreamFilterParent> self(this);
+    RunOnMainThread(FUNC, [=] {
+      nsresult rv = self->EmitStopRequest(NS_OK);
+      Unused << NS_WARN_IF(NS_FAILED(rv));
+    });
+  }
+
+  Unused << SendClosed();
+  Unused << Send__delete__(this);
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterParent::RecvSuspend()
+{
+  AssertIsPBackgroundThread();
+
+  if (mState == State::TransferringData) {
+    RefPtr<StreamFilterParent> self(this);
+    RunOnMainThread(FUNC, [=] {
+      self->mChannel->Suspend();
+
+      RunOnPBackgroundThread(FUNC, [=] {
+        if (self->IPCActive()) {
+          self->mState = State::Suspended;
+          self->CheckResult(self->SendSuspended());
+        }
+      });
+    });
+  }
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterParent::RecvResume()
+{
+  AssertIsPBackgroundThread();
+
+  if (mState == State::Suspended) {
+    // Change state before resuming so incoming data is handled correctly
+    // immediately after resuming.
+    mState = State::TransferringData;
+
+    RefPtr<StreamFilterParent> self(this);
+    RunOnMainThread(FUNC, [=] {
+      self->mChannel->Resume();
+
+      RunOnPBackgroundThread(FUNC, [=] {
+        if (self->IPCActive()) {
+          self->CheckResult(self->SendResumed());
+        }
+      });
+    });
+  }
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterParent::RecvDisconnect()
+{
+  AssertIsPBackgroundThread();
+
+  if (mState == State::Suspended) {
+  RefPtr<StreamFilterParent> self(this);
+    RunOnMainThread(FUNC, [=] {
+      self->mChannel->Resume();
+    });
+  } else if (mState != State::TransferringData) {
+    return IPC_OK();
+  }
+
+  mState = State::Disconnecting;
+  CheckResult(SendFlushData());
+  return IPC_OK();
+}
+
+IPCResult
+StreamFilterParent::RecvFlushedData()
+{
+  AssertIsPBackgroundThread();
+
+  MOZ_ASSERT(mState == State::Disconnecting);
+
+  Unused << Send__delete__(this);
+
+  RefPtr<StreamFilterParent> self(this);
+  RunOnIOThread(FUNC, [=] {
+    self->FlushBufferedData();
+
+    RunOnPBackgroundThread(FUNC, [=] {
+      self->mState = State::Disconnected;
+    });
+  });
+  return IPC_OK();
+}
+
+/*****************************************************************************
+ * Data output
+ *****************************************************************************/
+
+IPCResult
+StreamFilterParent::RecvWrite(Data&& aData)
+{
+  AssertIsPBackgroundThread();
+
+  mIOThread->Dispatch(
+    NewRunnableMethod<Data&&>("StreamFilterParent::WriteMove",
+                              this,
+                              &StreamFilterParent::WriteMove,
+                              Move(aData)),
+    NS_DISPATCH_NORMAL);
+  return IPC_OK();
+}
+
+void
+StreamFilterParent::WriteMove(Data&& aData)
+{
+  nsresult rv = Write(aData);
+  Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+nsresult
+StreamFilterParent::Write(Data& aData)
+{
+  AssertIsIOThread();
+
+  nsCOMPtr<nsIInputStream> stream;
+  nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream),
+                                      reinterpret_cast<char*>(aData.Elements()),
+                                      aData.Length());
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mOrigListener->OnDataAvailable(mChannel, mContext, stream,
+                                      mOffset, aData.Length());
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mOffset += aData.Length();
+  return NS_OK;
+}
+
+/*****************************************************************************
+ * nsIStreamListener
+ *****************************************************************************/
+
+NS_IMETHODIMP
+StreamFilterParent::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext)
+{
+  AssertIsMainThread();
+
+  mContext = aContext;
+
+  if (mState != State::Disconnected) {
+    RefPtr<StreamFilterParent> self(this);
+    RunOnPBackgroundThread(FUNC, [=] {
+      if (self->IPCActive()) {
+        self->mState = State::TransferringData;
+        self->CheckResult(self->SendStartRequest());
+      }
+    });
+  }
+
+  return mOrigListener->OnStartRequest(aRequest, aContext);
+}
+
+NS_IMETHODIMP
+StreamFilterParent::OnStopRequest(nsIRequest* aRequest,
+                                  nsISupports* aContext,
+                                  nsresult aStatusCode)
+{
+  AssertIsMainThread();
+
+  mReceivedStop = true;
+  if (mState == State::Disconnected) {
+    return EmitStopRequest(aStatusCode);
+  }
+
+  RefPtr<StreamFilterParent> self(this);
+  RunOnPBackgroundThread(FUNC, [=] {
+    if (self->IPCActive()) {
+      self->CheckResult(self->SendStopRequest(aStatusCode));
+    }
+  });
+  return NS_OK;
+}
+
+nsresult
+StreamFilterParent::EmitStopRequest(nsresult aStatusCode)
+{
+  AssertIsMainThread();
+  MOZ_ASSERT(!mSentStop);
+
+  mSentStop = true;
+  return mOrigListener->OnStopRequest(mChannel, mContext, aStatusCode);
+}
+
+/*****************************************************************************
+ * Incoming data handling
+ *****************************************************************************/
+
+void
+StreamFilterParent::DoSendData(Data&& aData)
+{
+  AssertIsPBackgroundThread();
+
+  if (mState == State::TransferringData) {
+    CheckResult(SendData(aData));
+  }
+}
+
+NS_IMETHODIMP
+StreamFilterParent::OnDataAvailable(nsIRequest* aRequest,
+                                    nsISupports* aContext,
+                                    nsIInputStream* aInputStream,
+                                    uint64_t aOffset,
+                                    uint32_t aCount)
+{
+  // Note: No AssertIsIOThread here. Whatever thread we're on now is, by
+  // definition, the IO thread.
+  mIOThread = NS_GetCurrentThread();
+
+  if (mState == State::Disconnected) {
+    // If we're offloading data in a thread pool, it's possible that we'll
+    // have buffered some additional data while waiting for the buffer to
+    // flush. So, if there's any buffered data left, flush that before we
+    // flush this incoming data.
+    //
+    // Note: When in the eDisconnected state, the buffer list is guaranteed
+    // never to be accessed by another thread during an OnDataAvailable call.
+    if (!mBufferedData.isEmpty()) {
+      FlushBufferedData();
+    }
+
+    mOffset += aCount;
+    return mOrigListener->OnDataAvailable(aRequest, aContext, aInputStream,
+                                          mOffset - aCount, aCount);
+  }
+
+  Data data;
+  data.SetLength(aCount);
+
+  uint32_t count;
+  nsresult rv = aInputStream->Read(reinterpret_cast<char*>(data.Elements()),
+                                   aCount, &count);
+  NS_ENSURE_SUCCESS(rv, rv);
+  NS_ENSURE_TRUE(count == aCount, NS_ERROR_UNEXPECTED);
+
+  if (mState == State::Disconnecting) {
+    MutexAutoLock al(mBufferMutex);
+    BufferData(Move(data));
+  } else if (mState == State::Closed) {
+    return NS_ERROR_FAILURE;
+  } else {
+    mPBackgroundThread->Dispatch(
+      NewRunnableMethod<Data&&>("StreamFilterParent::DoSendData",
+                                this,
+                                &StreamFilterParent::DoSendData,
+                                Move(data)),
+      NS_DISPATCH_NORMAL);
+  }
+  return NS_OK;
+}
+
+nsresult
+StreamFilterParent::FlushBufferedData()
+{
+  AssertIsIOThread();
+
+  // When offloading data to a thread pool, OnDataAvailable isn't guaranteed
+  // to always run in the same thread, so it's possible for this function to
+  // run in parallel with OnDataAvailable.
+  MutexAutoLock al(mBufferMutex);
+
+  while (!mBufferedData.isEmpty()) {
+    UniquePtr<BufferedData> data(mBufferedData.popFirst());
+
+    nsresult rv = Write(data->mData);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  if (mReceivedStop && !mSentStop) {
+    RefPtr<StreamFilterParent> self(this);
+    RunOnMainThread(FUNC, [=] {
+      if (!mSentStop) {
+        nsresult rv = self->EmitStopRequest(NS_OK);
+        Unused << NS_WARN_IF(NS_FAILED(rv));
+      }
+    });
+  }
+
+  return NS_OK;
+}
+
+/*****************************************************************************
+ * Glue
+ *****************************************************************************/
+
+void
+StreamFilterParent::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsPBackgroundThread();
+
+  if (mState != State::Disconnected && mState != State::Closed) {
+    Broken();
+  }
+}
+
+NS_IMPL_ISUPPORTS(StreamFilterParent, nsIStreamListener, nsIRequestObserver, nsIThreadRetargetableStreamListener)
+
+} // namespace extensions
+} // namespace mozilla
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilterParent.h
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#ifndef mozilla_extensions_StreamFilterParent_h
+#define mozilla_extensions_StreamFilterParent_h
+
+#include "StreamFilterBase.h"
+#include "mozilla/extensions/PStreamFilterParent.h"
+
+#include "mozilla/LinkedList.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/SystemGroup.h"
+#include "mozilla/WebRequestService.h"
+#include "nsIStreamListener.h"
+#include "nsIThread.h"
+#include "nsIThreadRetargetableStreamListener.h"
+#include "nsThreadUtils.h"
+
+#if defined(_MSC_VER)
+#  define FUNC __FUNCSIG__
+#else
+#  define FUNC __PRETTY_FUNCTION__
+#endif
+
+namespace mozilla {
+namespace dom {
+  class nsIContentParent;
+}
+
+namespace extensions {
+
+using namespace mozilla::dom;
+using mozilla::ipc::IPCResult;
+
+class StreamFilterParent final
+  : public PStreamFilterParent
+  , public nsIStreamListener
+  , public nsIThreadRetargetableStreamListener
+  , public StreamFilterBase
+{
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSISTREAMLISTENER
+  NS_DECL_NSIREQUESTOBSERVER
+  NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
+
+  explicit StreamFilterParent(uint64_t aChannelId, const nsAString& aAddonId);
+
+  enum class State
+  {
+    // The parent has been created, but not yet constructed by the child.
+    Uninitialized,
+    // The parent has been successfully constructed.
+    Initialized,
+    // The OnRequestStarted event has been received, and data is being
+    // transferred to the child.
+    TransferringData,
+    // The channel is suspended.
+    Suspended,
+    // The channel has been closed by the child, and will send or receive data.
+    Closed,
+    // The channel is being disconnected from the child, so that all further
+    // data and events pass unfiltered to the output listener. Any data
+    // currnetly in transit to, or buffered by, the child will be written to the
+    // output listener before we enter the Disconnected atate.
+    Disconnecting,
+    // The channel has been disconnected from the child, and all further data
+    // and events will be passed directly to the output listener.
+    Disconnected,
+  };
+
+  static already_AddRefed<StreamFilterParent>
+  Create(uint64_t aChannelId, const nsAString& aAddonId)
+  {
+    RefPtr<StreamFilterParent> filter = new StreamFilterParent(aChannelId, aAddonId);
+    return filter.forget();
+  }
+
+  void Init(already_AddRefed<nsIContentParent> aContentParent);
+
+protected:
+  virtual ~StreamFilterParent();
+
+  virtual IPCResult RecvWrite(Data&& aData) override;
+  virtual IPCResult RecvFlushedData() override;
+  virtual IPCResult RecvSuspend() override;
+  virtual IPCResult RecvResume() override;
+  virtual IPCResult RecvClose() override;
+  virtual IPCResult RecvDisconnect() override;
+
+private:
+  bool IPCActive()
+  {
+    return (mState != State::Closed &&
+            mState != State::Disconnecting &&
+            mState != State::Disconnected);
+  }
+
+  void DoInit(already_AddRefed<nsIContentParent>&& aContentParent);
+
+  nsresult FlushBufferedData();
+
+  nsresult Write(Data& aData);
+
+  void WriteMove(Data&& aData);
+
+  void DoSendData(Data&& aData);
+
+  nsresult EmitStopRequest(nsresult aStatusCode);
+
+  virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+  void Broken();
+
+  void
+  CheckResult(bool aResult)
+  {
+    if (NS_WARN_IF(!aResult)) {
+      Broken();
+    }
+  }
+
+  void
+  AssertIsPBackgroundThread()
+  {
+    MOZ_ASSERT(NS_GetCurrentThread() == mPBackgroundThread);
+  }
+
+  void
+  AssertIsIOThread()
+  {
+    MOZ_ASSERT(NS_GetCurrentThread() == mIOThread);
+  }
+
+  void
+  AssertIsMainThread()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+
+  template<typename Function>
+  void
+  RunOnMainThread(const char* aName, Function&& aFunc)
+  {
+    SystemGroup::Dispatch(TaskCategory::Network,
+                          Move(NS_NewRunnableFunction(aName, aFunc)));
+  }
+
+  template<typename Function>
+  void
+  RunOnPBackgroundThread(const char* aName, Function&& aFunc)
+  {
+    mPBackgroundThread->Dispatch(Move(NS_NewRunnableFunction(aName, aFunc)),
+                                 NS_DISPATCH_NORMAL);
+  }
+
+  template<typename Function>
+  void
+  RunOnIOThread(const char* aName, Function&& aFunc)
+  {
+    mIOThread->Dispatch(Move(NS_NewRunnableFunction(aName, aFunc)),
+                        NS_DISPATCH_NORMAL);
+  }
+
+  const uint64_t mChannelId;
+  const nsCOMPtr<nsIAtom> mAddonId;
+
+  nsCOMPtr<nsIChannel> mChannel;
+  nsCOMPtr<nsIStreamListener> mOrigListener;
+
+  nsCOMPtr<nsIThread> mPBackgroundThread;
+  nsCOMPtr<nsIThread> mIOThread;
+
+  Mutex mBufferMutex;
+
+  bool mReceivedStop;
+  bool mSentStop;
+
+  nsCOMPtr<nsISupports> mContext;
+  uint64_t mOffset;
+
+  volatile State mState;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_StreamFilterParent_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/WebRequestService.cpp
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#include "WebRequestService.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "nsCOMPtr.h"
+#include "nsIChannel.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsISupports.h"
+#include "nsITabParent.h"
+#include "nsITraceableChannel.h"
+
+#include "mozilla/dom/TabParent.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+WebRequestService::WebRequestService()
+  : mDataLock("WebRequest service data lock")
+{
+}
+
+WebRequestService::~WebRequestService()
+{
+}
+
+NS_IMPL_ISUPPORTS(WebRequestService, mozIWebRequestService)
+
+/* static */ WebRequestService&
+WebRequestService::GetSingleton()
+{
+  static RefPtr<WebRequestService> instance;
+  if (!instance) {
+    instance = new WebRequestService();
+    ClearOnShutdown(&instance);
+  }
+  return *instance;
+}
+
+
+NS_IMETHODIMP
+WebRequestService::RegisterTraceableChannel(uint64_t aChannelId,
+                                            nsIChannel* aChannel,
+                                            const nsAString& aAddonId,
+                                            nsITabParent* aTabParent,
+                                            nsIJSRAIIHelper** aHelper)
+{
+  nsCOMPtr<nsITraceableChannel> traceableChannel = do_QueryInterface(aChannel);
+  NS_ENSURE_TRUE(traceableChannel, NS_ERROR_INVALID_ARG);
+
+  nsCOMPtr<nsIAtom> addonId = NS_Atomize(aAddonId);
+  ChannelParent* entry = new ChannelParent(aChannelId, aChannel,
+                                           addonId, aTabParent);
+
+  RefPtr<Destructor> destructor = new Destructor(entry);
+  destructor.forget(aHelper);
+
+  return NS_OK;
+}
+
+already_AddRefed<nsIChannel>
+WebRequestService::GetTraceableChannel(uint64_t aChannelId,
+                                       nsIAtom* aAddonId,
+                                       nsIContentParent* aContentParent)
+{
+  MutexAutoLock al(mDataLock);
+
+  auto entry = mChannelEntries.Get(aChannelId);
+  if (!entry) {
+    return nullptr;
+  }
+
+  for (auto channelEntry : entry->mTabParents) {
+    nsIContentParent* contentParent = nullptr;
+    if (channelEntry->mTabParent) {
+      contentParent = static_cast<nsIContentParent*>(
+        channelEntry->mTabParent->Manager());
+    }
+
+    if (channelEntry->mAddonId == aAddonId && contentParent == aContentParent) {
+      nsCOMPtr<nsIChannel> channel = do_QueryReferent(entry->mChannel);
+      return channel.forget();
+    }
+  }
+
+  return nullptr;
+}
+
+
+WebRequestService::ChannelParent::ChannelParent(uint64_t aChannelId, nsIChannel* aChannel,
+                                                nsIAtom* aAddonId, nsITabParent* aTabParent)
+  : mTabParent(static_cast<TabParent*>(aTabParent))
+  , mAddonId(aAddonId)
+  , mChannelId(aChannelId)
+{
+  auto service = &GetSingleton();
+  MutexAutoLock al(service->mDataLock);
+
+  auto entry = service->mChannelEntries.LookupOrAdd(mChannelId);
+
+  entry->mChannel = do_GetWeakReference(aChannel);
+  entry->mTabParents.insertBack(this);
+}
+
+WebRequestService::ChannelParent::~ChannelParent()
+{
+  MOZ_ASSERT(mDetached);
+}
+
+void
+WebRequestService::ChannelParent::Detach()
+{
+  if (mDetached) {
+    return;
+  }
+  auto service = &GetSingleton();
+  MutexAutoLock al(service->mDataLock);
+
+  auto& map = service->mChannelEntries;
+  auto entry = map.Get(mChannelId);
+  MOZ_ASSERT(entry);
+
+  removeFrom(entry->mTabParents);
+  if (entry->mTabParents.isEmpty()) {
+    map.Remove(mChannelId);
+  }
+  mDetached = true;
+}
+
+WebRequestService::ChannelEntry::~ChannelEntry()
+{
+  while (ChannelParent* parent = mTabParents.getFirst()) {
+    parent->Detach();
+  }
+}
+
+WebRequestService::Destructor::~Destructor()
+{
+  if (NS_WARN_IF(!mDestructCalled)) {
+    Destruct();
+  }
+}
+
+NS_IMETHODIMP
+WebRequestService::Destructor::Destruct()
+{
+  if (NS_WARN_IF(mDestructCalled)) {
+    return NS_ERROR_FAILURE;
+  }
+  mDestructCalled = true;
+
+  mChannelParent->Detach();
+  delete mChannelParent;
+
+  return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(WebRequestService::Destructor, nsIJSRAIIHelper)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/WebRequestService.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+#ifndef mozilla_WebRequestService_h
+#define mozilla_WebRequestService_h
+
+#include "mozIWebRequestService.h"
+
+#include "mozilla/LinkedList.h"
+#include "mozilla/Mutex.h"
+#include "nsCOMPtr.h"
+#include "nsHashKeys.h"
+#include "nsClassHashtable.h"
+#include "nsIAtom.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsWeakPtr.h"
+
+using namespace mozilla;
+
+namespace mozilla {
+namespace dom {
+  class TabParent;
+  class nsIContentParent;
+}
+}
+
+class WebRequestService : public mozIWebRequestService
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_MOZIWEBREQUESTSERVICE
+
+  explicit WebRequestService();
+
+  static already_AddRefed<WebRequestService> GetInstance()
+  {
+    return do_AddRef(&GetSingleton());
+  }
+
+  static WebRequestService& GetSingleton();
+
+  already_AddRefed<nsIChannel>
+  GetTraceableChannel(uint64_t aChannelId, nsIAtom* aAddonId,
+                      dom::nsIContentParent* aContentParent);
+
+protected:
+  virtual ~WebRequestService();
+
+private:
+  class ChannelParent : public LinkedListElement<ChannelParent>
+  {
+  public:
+    explicit ChannelParent(uint64_t aChannelId, nsIChannel* aChannel, nsIAtom* aAddonId, nsITabParent* aTabParent);
+    ~ChannelParent();
+
+    void Detach();
+
+    const RefPtr<dom::TabParent> mTabParent;
+    const nsCOMPtr<nsIAtom> mAddonId;
+
+  private:
+    const uint64_t mChannelId;
+    bool mDetached = false;
+  };
+
+  class Destructor : public nsIJSRAIIHelper
+  {
+  public:
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSIJSRAIIHELPER
+
+    explicit Destructor(ChannelParent* aChannelParent)
+      : mChannelParent(aChannelParent)
+      , mDestructCalled(false)
+    {}
+
+  protected:
+    virtual ~Destructor();
+
+  private:
+    ChannelParent* mChannelParent;
+    bool mDestructCalled;
+  };
+
+  class ChannelEntry
+  {
+  public:
+    ~ChannelEntry();
+    // Note: We can't keep a strong pointer to the channel here, since channels
+    // are not cycle collected, and a reference to this object will be stored on
+    // the channel in order to keep the entry alive.
+    nsWeakPtr mChannel;
+    LinkedList<ChannelParent> mTabParents;
+  };
+
+  nsClassHashtable<nsUint64HashKey, ChannelEntry> mChannelEntries;
+  Mutex mDataLock;
+};
+
+#endif // mozilla_WebRequestService_h
--- a/toolkit/components/extensions/webrequest/moz.build
+++ b/toolkit/components/extensions/webrequest/moz.build
@@ -1,21 +1,48 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 XPIDL_SOURCES += [
+    'mozIWebRequestService.idl',
     'nsIWebRequestListener.idl',
 ]
 
 XPIDL_MODULE = 'webextensions'
 
+UNIFIED_SOURCES += [
+    'nsWebRequestListener.cpp',
+    'StreamFilter.cpp',
+    'StreamFilterChild.cpp',
+    'StreamFilterEvents.cpp',
+    'StreamFilterParent.cpp',
+    'WebRequestService.cpp',
+]
+
+IPDL_SOURCES += [
+    'PStreamFilter.ipdl',
+]
+
 EXPORTS += [
     'nsWebRequestListener.h',
 ]
 
-UNIFIED_SOURCES += [
-    'nsWebRequestListener.cpp',
+EXPORTS.mozilla += [
+    'WebRequestService.h',
 ]
 
-FINAL_LIBRARY = 'xul'
\ No newline at end of file
+EXPORTS.mozilla.extensions += [
+    'StreamFilter.h',
+    'StreamFilterBase.h',
+    'StreamFilterChild.h',
+    'StreamFilterEvents.h',
+    'StreamFilterParent.h',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+with Files("**"):
+    BUG_COMPONENT = ("Toolkit", "WebExtensions: Request Handling")
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/mozIWebRequestService.idl
@@ -0,0 +1,18 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIChannel;
+interface nsIJSRAIIHelper;
+interface nsITabParent;
+
+[scriptable, builtinclass, uuid(1b1118ed-f208-4cfc-b841-5b31a78c2b7a)]
+interface mozIWebRequestService : nsISupports
+{
+  nsIJSRAIIHelper registerTraceableChannel(in uint64_t channelId,
+                                           in nsIChannel channel,
+                                           in AString addonId,
+                                           [optional] in nsITabParent tabParent);
+};
--- 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,