Bug 1402944: Part 5 - Move request filtering and permission matching into ChannelWrapper. r?mixedpuppy,ehsan draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 22 Sep 2017 23:09:52 -0700
changeset 670804 34bf44fca36716da45103fce33ce26757201d80d
parent 670803 14027f62bd457ac283cb7ecefc349169ee1c9686
child 670805 46a93dbaeee0f24c946abc85388cfc64f93a482b
push id81714
push usermaglione.k@gmail.com
push dateTue, 26 Sep 2017 21:30:45 +0000
reviewersmixedpuppy, ehsan
bugs1402944
milestone58.0a1
Bug 1402944: Part 5 - Move request filtering and permission matching into ChannelWrapper. r?mixedpuppy,ehsan This allows us to reuse the same URLInfo objects for each permission or extension that we match, and also avoids a lot of XPConnect overhead we wind up incurring when we access URI objects from the JS side. MozReview-Commit-ID: GqgVRjQ3wYQ
dom/webidl/ChannelWrapper.webidl
toolkit/components/extensions/MatchPattern.cpp
toolkit/components/extensions/MatchPattern.h
toolkit/components/extensions/WebExtensionPolicy.h
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/webrequest/ChannelWrapper.cpp
toolkit/components/extensions/webrequest/ChannelWrapper.h
toolkit/modules/addons/WebRequest.jsm
--- a/dom/webidl/ChannelWrapper.webidl
+++ b/dom/webidl/ChannelWrapper.webidl
@@ -108,25 +108,33 @@ interface ChannelWrapper : EventTarget {
   [Pure, SetterThrows]
   attribute boolean suspended;
 
 
   /**
    * The final URI of the channel (as returned by NS_GetFinalChannelURI) after
    * any redirects have been processed.
    */
-  [Cached, GetterThrows, Pure]
+  [Cached, Pure]
   readonly attribute URI finalURI;
 
   /**
    * The string version of finalURI (but cheaper to access than
    * finalURI.spec).
    */
-  [Cached, GetterThrows, Pure]
-  readonly attribute ByteString finalURL;
+  [Cached, Pure]
+  readonly attribute DOMString finalURL;
+
+
+  /**
+   * Returns true if the request matches the given request filter, and the
+   * given extension has permission to access it.
+   */
+  boolean matches(optional MozRequestFilter filter,
+                  optional WebExtensionPolicy? extension = null);
 
 
   /**
    * The current HTTP status code of the request. This will be 0 if a response
    * has not yet been received, or if the request is not an HTTP request.
    */
   [Cached, Pure]
   readonly attribute unsigned long statusCode;
@@ -340,8 +348,25 @@ dictionary MozProxyInfo {
   ByteString? username = null;
 
   /**
    * The timeout, in seconds, before the network stack will failover to the
    * next candidate proxy server if it has not received a response.
    */
   unsigned long failoverTimeout;
 };
+
+/**
+ * An object used for filtering requests.
+ */
+dictionary MozRequestFilter {
+  /**
+   * If present, the request only matches if its `type` attribute matches one
+   * of the given types.
+   */
+  sequence<MozContentPolicyType>? types = null;
+
+  /**
+   * If present, the request only matches if its finalURI matches the given
+   * match pattern set.
+   */
+  MatchPatternSet? urls = null;
+};
--- a/toolkit/components/extensions/MatchPattern.cpp
+++ b/toolkit/components/extensions/MatchPattern.cpp
@@ -153,24 +153,30 @@ URLInfo::Path() const
     nsCString path;
     if (NS_SUCCEEDED(URINoRef()->GetPathQueryRef(path))) {
       AppendUTF8toUTF16(path, mPath);
     }
   }
   return mPath;
 }
 
+const nsCString&
+URLInfo::CSpec() const
+{
+  if (mCSpec.IsEmpty()) {
+    Unused << URINoRef()->GetSpec(mCSpec);
+  }
+  return mCSpec;
+}
+
 const nsString&
 URLInfo::Spec() const
 {
   if (mSpec.IsEmpty()) {
-    nsCString spec;
-    if (NS_SUCCEEDED(URINoRef()->GetSpec(spec))) {
-      AppendUTF8toUTF16(spec, mSpec);
-    }
+    AppendUTF8toUTF16(CSpec(), mSpec);
   }
   return mSpec;
 }
 
 nsIURI*
 URLInfo::URINoRef() const
 {
   if (!mURINoRef) {
--- a/toolkit/components/extensions/MatchPattern.h
+++ b/toolkit/components/extensions/MatchPattern.h
@@ -125,51 +125,61 @@ private:
 
   void SortAndUniquify();
 };
 
 
 // A helper class to lazily retrieve, transcode, and atomize certain URI
 // properties the first time they're used, and cache the results, so that they
 // can be used across multiple match operations.
-class MOZ_STACK_CLASS URLInfo final
+class URLInfo final
 {
 public:
   MOZ_IMPLICIT URLInfo(nsIURI* aURI)
     : mURI(aURI)
   {
     mHost.SetIsVoid(true);
   }
 
+  URLInfo(nsIURI* aURI, bool aNoRef)
+    : URLInfo(aURI)
+  {
+    if (aNoRef) {
+      mURINoRef = mURI;
+    }
+  }
+
   URLInfo(const URLInfo& aOther)
     : URLInfo(aOther.mURI.get())
   {}
 
   nsIURI* URI() const { return mURI; }
 
   nsIAtom* Scheme() const;
   const nsCString& Host() const;
   const nsString& Path() const;
   const nsString& FilePath() const;
   const nsString& Spec() const;
+  const nsCString& CSpec() const;
 
   bool InheritsPrincipal() const;
 
 private:
   nsIURI* URINoRef() const;
 
   nsCOMPtr<nsIURI> mURI;
   mutable nsCOMPtr<nsIURI> mURINoRef;
 
   mutable nsCOMPtr<nsIAtom> mScheme;
   mutable nsCString mHost;
 
-  mutable nsAutoString mPath;
-  mutable nsAutoString mFilePath;
-  mutable nsAutoString mSpec;
+  mutable nsString mPath;
+  mutable nsString mFilePath;
+  mutable nsString mSpec;
+  mutable nsCString mCSpec;
 
   mutable Maybe<bool> mInheritsPrincipal;
 };
 
 
 // Similar to URLInfo, but for cookies.
 class MOZ_STACK_CLASS CookieInfo final
 {
--- a/toolkit/components/extensions/WebExtensionPolicy.h
+++ b/toolkit/components/extensions/WebExtensionPolicy.h
@@ -54,17 +54,17 @@ public:
   {
     MOZ_ALWAYS_SUCCEEDS(mBaseURI->GetSpec(aBaseURL));
   }
 
   void GetURL(const nsAString& aPath, nsAString& aURL, ErrorResult& aRv) const;
 
   Result<nsString, nsresult> GetURL(const nsAString& aPath) const;
 
-  bool CanAccessURI(nsIURI* aURI, bool aExplicit = false) const
+  bool CanAccessURI(const URLInfo& aURI, bool aExplicit = false) const
   {
     return mHostPermissions && mHostPermissions->Matches(aURI, aExplicit);
   }
 
   bool IsPathWebAccessible(const nsAString& aPath) const
   {
     return mWebAccessiblePaths.Matches(aPath);
   }
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -12,34 +12,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 // EventManager-like class specifically for WebRequest. Inherits from
 // EventManager. Takes care of converting |details| parameter
 // when invoking listeners.
 function WebRequestEventManager(context, eventName) {
   let name = `webRequest.${eventName}`;
   let register = (fire, filter, info) => {
     let listener = data => {
-      // Prevent listening in on requests originating from system principal to
-      // prevent tinkering with OCSP, app and addon updates, etc.
-      if (data.isSystemPrincipal) {
-        return;
-      }
-
-      // Check hosts permissions for both the resource being requested,
-      const hosts = context.extension.whiteListedHosts;
-      if (!hosts.matches(data.URI)) {
-        return;
-      }
-      // and the origin that is loading the resource.
-      const origin = data.documentUrl;
-      const own = origin && origin.startsWith(context.extension.getURL());
-      if (origin && !own && !hosts.matches(data.documentURI)) {
-        return;
-      }
-
       let browserData = {tabId: -1, windowId: -1};
       if (data.browser) {
         browserData = tabTracker.getBrowserData(data.browser);
       }
       if (filter.tabId != null && browserData.tabId != filter.tabId) {
         return;
       }
       if (filter.windowId != null && browserData.windowId != filter.windowId) {
@@ -84,16 +66,17 @@ function WebRequestEventManager(context,
         } else {
           info2.push(desc);
         }
       }
     }
 
     let listenerDetails = {
       addonId: context.extension.id,
+      extension: context.extension.policy,
       blockingAllowed,
       tabParent: context.xulBrowser.frameLoader.tabParent,
     };
 
     WebRequest[eventName].addListener(
       listener, filter2, info2,
       listenerDetails);
     return () => {
--- a/toolkit/components/extensions/webrequest/ChannelWrapper.cpp
+++ b/toolkit/components/extensions/webrequest/ChannelWrapper.cpp
@@ -91,16 +91,17 @@ ChannelWrapper::ClearCachedAttributes()
   ChannelWrapperBinding::ClearCachedFinalURLValue(this);
   ChannelWrapperBinding::ClearCachedProxyInfoValue(this);
   ChannelWrapperBinding::ClearCachedRemoteAddressValue(this);
   ChannelWrapperBinding::ClearCachedStatusCodeValue(this);
   ChannelWrapperBinding::ClearCachedStatusLineValue(this);
   if (!mFiredErrorEvent) {
     ChannelWrapperBinding::ClearCachedErrorStringValue(this);
   }
+  mFinalURLInfo.reset();
 }
 
 /*****************************************************************************
  * ...
  *****************************************************************************/
 
 void
 ChannelWrapper::Cancel(uint32_t aResult, ErrorResult& aRv)
@@ -365,17 +366,17 @@ ChannelWrapper::IsSystemLoad() const
     }
   }
   return false;
 }
 
 bool
 ChannelWrapper::GetCanModify(ErrorResult& aRv) const
 {
-  nsCOMPtr<nsIURI> uri = GetFinalURI(aRv);
+  nsCOMPtr<nsIURI> uri = FinalURI();
   nsAutoCString spec;
   if (uri) {
     uri->GetSpec(spec);
   }
   if (!uri || AddonManagerWebAPI::IsValidSite(uri)) {
     return false;
   }
 
@@ -435,16 +436,91 @@ ChannelWrapper::GetOriginURL(nsCString& 
 void
 ChannelWrapper::GetDocumentURL(nsCString& aRetVal) const
 {
   if (nsCOMPtr<nsIURI> uri = GetDocumentURI()) {
     Unused << uri->GetSpec(aRetVal);
   }
 }
 
+
+const URLInfo&
+ChannelWrapper::FinalURLInfo() const
+{
+  if (mFinalURLInfo.isNothing()) {
+    ErrorResult rv;
+    nsCOMPtr<nsIURI> uri = FinalURI();
+    MOZ_ASSERT(uri);
+    mFinalURLInfo.emplace(uri.get(), true);
+
+    // If this is a WebSocket request, mangle the URL so that the scheme is
+    // ws: or wss:, as appropriate.
+    auto& url = mFinalURLInfo.ref();
+    if (Type() == MozContentPolicyType::Websocket &&
+        (url.Scheme() == nsGkAtoms::http ||
+         url.Scheme() == nsGkAtoms::https)) {
+      nsAutoCString spec(url.CSpec());
+      spec.Replace(0, 4, NS_LITERAL_CSTRING("ws"));
+
+      Unused << NS_NewURI(getter_AddRefs(uri), spec);
+      MOZ_RELEASE_ASSERT(uri);
+      mFinalURLInfo.emplace(uri.get(), true);
+    }
+  }
+  return mFinalURLInfo.ref();
+}
+
+const URLInfo*
+ChannelWrapper::DocumentURLInfo() const
+{
+  if (mDocumentURLInfo.isNothing()) {
+    nsCOMPtr<nsIURI> uri = GetDocumentURI();
+    if (!uri) {
+      return nullptr;
+    }
+    mDocumentURLInfo.emplace(uri.get(), true);
+  }
+  return &mDocumentURLInfo.ref();
+}
+
+
+bool
+ChannelWrapper::Matches(const dom::MozRequestFilter& aFilter,
+                        const WebExtensionPolicy* aExtension) const
+{
+  if (!aFilter.mTypes.IsNull() && !aFilter.mTypes.Value().Contains(Type())) {
+    return false;
+  }
+
+  auto& urlInfo = FinalURLInfo();
+  if (aFilter.mUrls && !aFilter.mUrls->Matches(urlInfo)) {
+    return false;
+  }
+
+  if (aExtension) {
+    if (!aExtension->CanAccessURI(urlInfo) || IsSystemLoad()) {
+      return false;
+    }
+
+    if (auto origin = DocumentURLInfo()) {
+      nsAutoCString baseURL;
+      aExtension->GetBaseURL(baseURL);
+
+      if (!StringBeginsWith(origin->CSpec(), baseURL) &&
+          !aExtension->CanAccessURI(*origin)) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
+
+
 int64_t
 NormalizeWindowID(nsILoadInfo* aLoadInfo, uint64_t windowID)
 {
   if (windowID == aLoadInfo->GetTopOuterWindowID()) {
     return 0;
   }
   return windowID;
 }
@@ -595,36 +671,29 @@ ChannelWrapper::GetStatusLine(nsCString&
   }
 }
 
 /*****************************************************************************
  * ...
  *****************************************************************************/
 
 already_AddRefed<nsIURI>
-ChannelWrapper::GetFinalURI(ErrorResult& aRv) const
+ChannelWrapper::FinalURI() const
 {
-  nsresult rv = NS_ERROR_UNEXPECTED;
   nsCOMPtr<nsIURI> uri;
   if (nsCOMPtr<nsIChannel> chan = MaybeChannel()) {
-    rv = NS_GetFinalChannelURI(chan, getter_AddRefs(uri));
-  }
-  if (NS_FAILED(rv)) {
-    aRv.Throw(rv);
+    NS_GetFinalChannelURI(chan, getter_AddRefs(uri));
   }
   return uri.forget();;
 }
 
 void
-ChannelWrapper::GetFinalURL(nsCString& aRetVal, ErrorResult& aRv) const
+ChannelWrapper::GetFinalURL(nsString& aRetVal) const
 {
-  nsCOMPtr<nsIURI> uri = GetFinalURI(aRv);
-  if (uri) {
-    Unused << uri->GetSpec(aRetVal);
-  }
+  aRetVal = FinalURLInfo().Spec();
 }
 
 /*****************************************************************************
  * ...
  *****************************************************************************/
 
 nsresult
 FillProxyInfo(MozProxyInfo &aDict, nsIProxyInfo* aProxyInfo)
--- a/toolkit/components/extensions/webrequest/ChannelWrapper.h
+++ b/toolkit/components/extensions/webrequest/ChannelWrapper.h
@@ -5,16 +5,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_extensions_ChannelWrapper_h
 #define mozilla_extensions_ChannelWrapper_h
 
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/ChannelWrapperBinding.h"
 
+#include "mozilla/extensions/MatchPattern.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 
 #include "mozilla/DOMEventTargetHelper.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIChannel.h"
 #include "nsIHttpChannel.h"
@@ -140,19 +143,23 @@ public:
 
   void ErrorCheck();
 
   IMPL_EVENT_HANDLER(error);
   IMPL_EVENT_HANDLER(start);
   IMPL_EVENT_HANDLER(stop);
 
 
-  already_AddRefed<nsIURI> GetFinalURI(ErrorResult& aRv) const;
+  already_AddRefed<nsIURI> FinalURI() const;
+
+  void GetFinalURL(nsString& aRetVal) const;
 
-  void GetFinalURL(nsCString& aRetVal, ErrorResult& aRv) const;
+
+  bool Matches(const dom::MozRequestFilter& aFilter,
+               const WebExtensionPolicy* aExtension) const;
 
 
   already_AddRefed<nsILoadInfo> GetLoadInfo() const
   {
     nsCOMPtr<nsIChannel> chan = MaybeChannel();
     if (chan) {
       return chan->GetLoadInfo();
     }
@@ -223,16 +230,21 @@ private:
       aRv.Throw(NS_ERROR_UNEXPECTED);
       return false;
     }
     return true;
   }
 
   void FireEvent(const nsAString& aType);
 
+
+  const URLInfo& FinalURLInfo() const;
+  const URLInfo* DocumentURLInfo() const;
+
+
   uint64_t WindowId(nsILoadInfo* aLoadInfo) const;
 
   static uint64_t GetNextId()
   {
     static uint64_t sNextId = 1;
     return ++sNextId;
   }
 
@@ -241,16 +253,19 @@ private:
   mutable Maybe<URLInfo> mFinalURLInfo;
   mutable Maybe<URLInfo> mDocumentURLInfo;
 
   UniquePtr<WebRequestChannelEntry> mChannelEntry;
 
   // The overridden Content-Type header value.
   nsCString mContentTypeHdr = VoidCString();
 
+  mutable Maybe<URLInfo> mFinalURLInfo;
+  mutable Maybe<URLInfo> mDocumentURLInfo;
+
   const uint64_t mId = GetNextId();
   nsCOMPtr<nsISupports> mParent;
 
   bool mAddedStreamListener = false;
   bool mFiredErrorEvent = false;
   bool mSuspended = false;
 
 
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -235,17 +235,16 @@ var ContentPolicyManager = {
       if (!callback) {
         // It's possible that this listener has been removed and the
         // child hasn't learned yet.
         continue;
       }
       let response = null;
       let listenerKind = "onStop";
       let data = Object.assign({requestId, browser, serialize: serializeRequestData}, msg.data);
-      data.URI = data.url;
 
       delete data.ids;
       try {
         response = callback(data);
         if (response) {
           if (response.cancel) {
             listenerKind = "onError";
             data.error = "NS_ERROR_ABORT";
@@ -258,25 +257,41 @@ var ContentPolicyManager = {
       } finally {
         runLater(() => this.runChannelListener(listenerKind, data));
       }
     }
 
     return {};
   },
 
+  shouldRunListener(policyType, url, opts) {
+    let {filter} = opts;
+
+    if (filter.types && !filter.types.includes(policyType)) {
+      return false;
+    }
+
+    if (filter.urls && !filter.urls.matches(url)) {
+      return false;
+    }
+
+    let {extension} = opts;
+    if (extension && !extension.allowedOrigins.matches(url)) {
+      return false;
+    }
+
+    return true;
+  },
+
   runChannelListener(kind, data) {
     let listeners = HttpObserverManager.listeners[kind];
-    let uri = Services.io.newURI(data.url);
-    let policyType = data.type;
     for (let [callback, opts] of listeners.entries()) {
-      if (!HttpObserverManager.shouldRunListener(policyType, uri, opts.filter)) {
-        continue;
+      if (this.shouldRunListener(data.type, data.url, opts)) {
+        callback(data);
       }
-      callback(data);
     }
   },
 
   addListener(callback, opts) {
     // Clone opts, since we're going to modify them for IPC.
     opts = Object.assign({}, opts);
     let id = this.nextId++;
     opts.id = id;
@@ -656,60 +671,38 @@ HttpObserverManager = {
         }
       });
     } else if (lastActivity !== this.GOOD_LAST_ACTIVITY &&
                lastActivity !== nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) {
       channel.lastActivity = activitySubtype;
     }
   },
 
-  shouldRunListener(policyType, uri, filter) {
-    // force the protocol to be ws again.
-    if (policyType == "websocket" && ["http", "https"].includes(uri.scheme)) {
-      uri = Services.io.newURI(`ws${uri.spec.substring(4)}`);
-    }
-
-    if (filter.types && !filter.types.includes(policyType)) {
-      return false;
-    }
-
-    return !filter.urls || filter.urls.matches(uri);
-  },
-
   getRequestData(channel, extraData) {
     let data = {
       requestId: String(channel.id),
       url: channel.finalURL,
-      URI: channel.finalURI,
       method: channel.method,
       browser: channel.browserElement,
       type: channel.type,
       fromCache: channel.fromCache,
 
       originUrl: channel.originURL || undefined,
       documentUrl: channel.documentURL || undefined,
-      originURI: channel.originURI,
-      documentURI: channel.documentURI,
-      isSystemPrincipal: channel.isSystemLoad,
 
       windowId: channel.windowId,
       parentWindowId: channel.parentWindowId,
 
       ip: channel.remoteAddress,
 
       proxyInfo: channel.proxyInfo,
 
       serialize: serializeRequestData,
     };
 
-    // force the protocol to be ws again.
-    if (data.type == "websocket" && data.url.startsWith("http")) {
-      data.url = `ws${data.url.substring(4)}`;
-    }
-
     return Object.assign(data, extraData);
   },
 
   registerChannel(channel, opts) {
     if (!opts.blockingAllowed || !opts.addonId) {
       return;
     }
 
@@ -762,20 +755,19 @@ HttpObserverManager = {
       if (kind !== "onError" && channel.errorString) {
         return;
       }
 
       let includeStatus = ["headersReceived", "authRequired", "onRedirect", "onStart", "onStop"].includes(kind);
       let registerFilter = ["opening", "modify", "afterModify", "headersReceived", "authRequired", "onRedirect"].includes(kind);
 
       let commonData = null;
-      let uri = channel.finalURI;
       let requestBody;
       for (let [callback, opts] of this.listeners[kind].entries()) {
-        if (!this.shouldRunListener(channel.type, uri, opts.filter)) {
+        if (!channel.matches(opts.filter, opts.extension)) {
           continue;
         }
 
         if (!commonData) {
           commonData = this.getRequestData(channel, extraData);
           if (includeStatus) {
             commonData.statusCode = channel.statusCode;
             commonData.statusLine = channel.statusLine;
@@ -887,17 +879,17 @@ HttpObserverManager = {
   },
 
   shouldHookListener(listener, channel) {
     if (listener.size == 0) {
       return false;
     }
 
     for (let opts of listener.values()) {
-      if (this.shouldRunListener(channel.type, channel.finalURI, opts.filter)) {
+      if (channel.matches(opts.filter, opts.extension)) {
         return true;
       }
     }
     return false;
   },
 
   examine(channel, topic, data) {
     if (this.listeners.headersReceived.size) {