merge mozilla-inbound to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Mon, 04 Sep 2017 11:12:40 +0200
changeset 428225 632e42dca494ec3d90b70325d9c359f80cb3f38a
parent 428212 cef1935ebd9b328fc1ab5f5171cc7cdb6844a855 (current diff)
parent 428224 277554180e9faaab82cb27a891f5f8d01ede4d9d (diff)
child 428226 85519b4f1c7bcc193330856f25a995df32de9dec
child 428235 4b2edbc1ae7997a85bb619894a6c06c265c5ebb8
child 428325 0669574f5d86ad63b9545c7abdb020705a93d154
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone57.0a1
first release with
nightly linux32
632e42dca494 / 57.0a1 / 20170904100131 / files
nightly linux64
632e42dca494 / 57.0a1 / 20170904100131 / files
nightly mac
632e42dca494 / 57.0a1 / 20170904100131 / files
nightly win32
632e42dca494 / 57.0a1 / 20170904100131 / files
nightly win64
632e42dca494 / 57.0a1 / 20170904100131 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central. r=merge a=merge MozReview-Commit-ID: EzZGZI0GBca
--- 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,