Bug 1255894: Part 6 - Implement StreamFilter DOM bindings. r?baku,mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 27 Aug 2017 19:51:36 -0700
changeset 653834 f0f2d0009df89fb61bcaf48204f57c549a8ad7bf
parent 653833 431559c1a595ccc78884c1ab989fe3963744194f
child 653835 8c6617c8b30fd9b4ebe8e4a57e14818dfe1d0027
push id76425
push usermaglione.k@gmail.com
push dateMon, 28 Aug 2017 04:09:59 +0000
reviewersbaku, mixedpuppy
bugs1255894
milestone57.0a1
Bug 1255894: Part 6 - Implement StreamFilter DOM bindings. r?baku,mixedpuppy MozReview-Commit-ID: 6EaVrIep1gC
dom/bindings/Bindings.conf
dom/webidl/StreamFilter.webidl
dom/webidl/StreamFilterDataEvent.webidl
dom/webidl/moz.build
toolkit/components/extensions/webrequest/StreamFilter.cpp
toolkit/components/extensions/webrequest/StreamFilter.h
toolkit/components/extensions/webrequest/StreamFilterChild.cpp
toolkit/components/extensions/webrequest/StreamFilterChild.h
toolkit/components/extensions/webrequest/StreamFilterEvents.cpp
toolkit/components/extensions/webrequest/StreamFilterEvents.h
toolkit/components/extensions/webrequest/moz.build
--- 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',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/webrequest/StreamFilter.cpp
@@ -0,0 +1,290 @@
+/* -*- 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);
+
+  mozilla::HoldJSObjects(this);
+
+  ConnectToPBackground();
+};
+
+StreamFilter::~StreamFilter()
+{
+  mozilla::DropJSObjects(this);
+
+  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);
+  }
+
+  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, this, 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_INHERITED(StreamFilter)
+  NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(StreamFilter)
+  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,96 @@
+/* -*-  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();
+
+  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
--- a/toolkit/components/extensions/webrequest/StreamFilterChild.cpp
+++ b/toolkit/components/extensions/webrequest/StreamFilterChild.cpp
@@ -1,22 +1,24 @@
 /* -*- 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()
@@ -219,22 +221,32 @@ StreamFilterChild::SetNextState()
     break;
 
   case State::Disconnecting:
     mNextState = State::Disconnected;
     SendDisconnect();
     break;
 
   case State::FinishedTransferringData:
+    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()
 {
@@ -245,16 +257,22 @@ StreamFilterChild::MaybeStopRequest()
   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
  *****************************************************************************/
 
@@ -262,16 +280,20 @@ 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);
 
@@ -333,27 +355,84 @@ StreamFilterChild::Write(Data&& aData, E
   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;
 
+  mStreamFilter->FireEvent(NS_LITERAL_STRING("start"));
   return IPC_OK();
 }
 
 IPCResult
 StreamFilterChild::RecvStopRequest(const nsresult& aStatus)
 {
   mReceivedOnStop = true;
   MaybeStopRequest();
@@ -363,16 +442,17 @@ StreamFilterChild::RecvStopRequest(const
 /*****************************************************************************
  * Incoming request data handling
  *****************************************************************************/
 
 void
 StreamFilterChild::EmitData(const Data& aData)
 {
   MOZ_ASSERT(CanFlushData());
+  mStreamFilter->FireDataEvent(aData);
 
   MaybeStopRequest();
 }
 
 void
 StreamFilterChild::FlushBufferedData()
 {
   while (!mBufferedData.isEmpty() && CanFlushData()) {
--- a/toolkit/components/extensions/webrequest/StreamFilterChild.h
+++ b/toolkit/components/extensions/webrequest/StreamFilterChild.h
@@ -4,30 +4,36 @@
  * 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)
   {}
 
@@ -79,30 +85,38 @@ public:
 
   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);
@@ -117,14 +131,16 @@ private:
   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_INHERITED(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,79 @@
+/* -*-  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 "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
--- a/toolkit/components/extensions/webrequest/moz.build
+++ b/toolkit/components/extensions/webrequest/moz.build
@@ -8,17 +8,19 @@ 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',
 ]
 
@@ -26,18 +28,20 @@ EXPORTS += [
     'nsWebRequestListener.h',
 ]
 
 EXPORTS.mozilla += [
     'WebRequestService.h',
 ]
 
 EXPORTS.mozilla.extensions += [
+    'StreamFilter.h',
     'StreamFilterBase.h',
     'StreamFilterChild.h',
+    'StreamFilterEvents.h',
     'StreamFilterParent.h',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 
 with Files("**"):