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
--- 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) {