Bug 1255894: Part 6 - Implement StreamFilter DOM bindings. r=baku,mixedpuppy
authorKris Maglione <maglione.k@gmail.com>
Sun, 27 Aug 2017 19:51:36 -0700
changeset 428217 26ce00544f9862adf2e07ce2b001280e59015969
parent 428216 acc5f5f5946cec6afcd87ed4cebc73a0a8c4226e
child 428218 3599cdb1a849999f4d669885ddd7dbab21909cf5
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)
reviewersbaku, mixedpuppy
bugs1255894
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1255894: Part 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,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
--- 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,34 @@ StreamFilterChild::SetNextState()
     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()
 {
@@ -245,16 +259,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 +282,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 +357,86 @@ 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;
 
+  if (mStreamFilter) {
+    mStreamFilter->FireEvent(NS_LITERAL_STRING("start"));
+  }
   return IPC_OK();
 }
 
 IPCResult
 StreamFilterChild::RecvStopRequest(const nsresult& aStatus)
 {
   mReceivedOnStop = true;
   MaybeStopRequest();
@@ -363,16 +446,19 @@ StreamFilterChild::RecvStopRequest(const
 /*****************************************************************************
  * 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()) {
--- 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(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("**"):