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

--HG--
extra : rebase_source : 71c19fd8b432c16a3f13f7d0bd0424064f3e5661
This commit is contained in:
Kris Maglione 2017-09-27 18:15:12 -07:00
Родитель e3089ef89e
Коммит 89ae1721a0
10 изменённых файлов: 204 добавлений и 96 удалений

Просмотреть файл

@ -2077,6 +2077,7 @@ GK_ATOM(tabs, "tabs")
GK_ATOM(webRequestBlocking, "webRequestBlocking")
GK_ATOM(http, "http")
GK_ATOM(https, "https")
GK_ATOM(proxy, "proxy")
//---------------------------------------------------------------------------
// Special atoms

Просмотреть файл

@ -113,15 +113,24 @@ interface ChannelWrapper : EventTarget {
* 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,
optional MozRequestMatchOptions options);
/**
@ -372,3 +381,27 @@ dictionary MozFrameAncestorInfo {
required ByteString url;
required unsigned long long frameId;
};
/**
* 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;
};
dictionary MozRequestMatchOptions {
/**
* True if we're matching for the proxy portion of a proxied request.
*/
boolean isProxy = false;
};

Просмотреть файл

@ -158,14 +158,20 @@ URLInfo::Path() const
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;
}

Просмотреть файл

@ -130,7 +130,7 @@ private:
// 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)
@ -139,6 +139,14 @@ public:
mHost.SetIsVoid(true);
}
URLInfo(nsIURI* aURI, bool aNoRef)
: URLInfo(aURI)
{
if (aNoRef) {
mURINoRef = mURI;
}
}
URLInfo(const URLInfo& aOther)
: URLInfo(aOther.mURI.get())
{}
@ -150,6 +158,7 @@ public:
const nsString& Path() const;
const nsString& FilePath() const;
const nsString& Spec() const;
const nsCString& CSpec() const;
bool InheritsPrincipal() const;
@ -162,9 +171,10 @@ private:
mutable RefPtr<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;
};

Просмотреть файл

@ -59,7 +59,7 @@ public:
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);
}

Просмотреть файл

@ -17,32 +17,6 @@ function WebRequestEventManager(context, eventName) {
let name = `webRequest.${eventName}`;
let register = (fire, filter, info) => {
let listener = data => {
// data.isProxy is only set from the WebRequest AuthRequestor handler,
// in which case we know that this is onAuthRequired. If this is proxy
// authorization, we allow without any additional matching/filtering.
let isProxyAuth = data.isProxy && context.extension.hasPermission("proxy");
// Prevent listening in on requests originating from system principal to
// prevent tinkering with OCSP, app and addon updates, etc. However,
// proxy addons need to be able to provide auth for any request so we
// allow those through. The exception is for proxy extensions handling
// proxy authentication.
if (data.isSystemPrincipal && !isProxyAuth) {
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 && !isProxyAuth && !hosts.matches(data.documentURI)) {
return;
}
let browserData = {tabId: -1, windowId: -1};
if (data.browser) {
browserData = tabTracker.getBrowserData(data.browser);
@ -97,6 +71,7 @@ function WebRequestEventManager(context, eventName) {
let listenerDetails = {
addonId: context.extension.id,
extension: context.extension.policy,
blockingAllowed,
tabParent: context.xulBrowser.frameLoader.tabParent,
};

Просмотреть файл

@ -82,14 +82,15 @@ ChannelWrapper::SetChannel(nsIChannel* aChannel)
{
detail::ChannelHolder::SetChannel(aChannel);
ClearCachedAttributes();
ChannelWrapperBinding::ClearCachedFinalURIValue(this);
ChannelWrapperBinding::ClearCachedFinalURLValue(this);
mFinalURLInfo.reset();
ChannelWrapperBinding::ClearCachedProxyInfoValue(this);
}
void
ChannelWrapper::ClearCachedAttributes()
{
ChannelWrapperBinding::ClearCachedFinalURIValue(this);
ChannelWrapperBinding::ClearCachedFinalURLValue(this);
ChannelWrapperBinding::ClearCachedProxyInfoValue(this);
ChannelWrapperBinding::ClearCachedRemoteAddressValue(this);
ChannelWrapperBinding::ClearCachedStatusCodeValue(this);
ChannelWrapperBinding::ClearCachedStatusLineValue(this);
@ -370,7 +371,7 @@ ChannelWrapper::IsSystemLoad() const
bool
ChannelWrapper::GetCanModify(ErrorResult& aRv) const
{
nsCOMPtr<nsIURI> uri = GetFinalURI(aRv);
nsCOMPtr<nsIURI> uri = FinalURI();
nsAutoCString spec;
if (uri) {
uri->GetSpec(spec);
@ -440,6 +441,93 @@ ChannelWrapper::GetDocumentURL(nsCString& aRetVal) const
}
}
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.reset();
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 dom::MozRequestMatchOptions& aOptions) const
{
if (!HaveChannel()) {
return false;
}
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)) {
return false;
}
bool isProxy = aOptions.mIsProxy && aExtension->HasPermission(nsGkAtoms::proxy);
if (!isProxy && IsSystemLoad()) {
return false;
}
if (auto origin = DocumentURLInfo()) {
nsAutoCString baseURL;
aExtension->GetBaseURL(baseURL);
if (!isProxy &&
!StringBeginsWith(origin->CSpec(), baseURL) &&
!aExtension->CanAccessURI(*origin)) {
return false;
}
}
}
return true;
}
int64_t
NormalizeWindowID(nsILoadInfo* aLoadInfo, uint64_t windowID)
{
@ -654,25 +742,20 @@ ChannelWrapper::GetStatusLine(nsCString& aRetVal) const
*****************************************************************************/
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);
if (HaveChannel()) {
aRetVal = FinalURLInfo().Spec();
}
}

Просмотреть файл

@ -10,6 +10,9 @@
#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"
@ -145,9 +148,14 @@ public:
IMPL_EVENT_HANDLER(stop);
already_AddRefed<nsIURI> GetFinalURI(ErrorResult& aRv) const;
already_AddRefed<nsIURI> FinalURI() const;
void GetFinalURL(nsCString& aRetVal, ErrorResult& aRv) const;
void GetFinalURL(nsString& aRetVal) const;
bool Matches(const dom::MozRequestFilter& aFilter,
const WebExtensionPolicy* aExtension,
const dom::MozRequestMatchOptions& aOptions) const;
already_AddRefed<nsILoadInfo> GetLoadInfo() const
@ -229,6 +237,11 @@ private:
void FireEvent(const nsAString& aType);
const URLInfo& FinalURLInfo() const;
const URLInfo* DocumentURLInfo() const;
uint64_t WindowId(nsILoadInfo* aLoadInfo) const;
nsresult GetFrameAncestors(nsILoadInfo* aLoadInfo, nsTArray<dom::MozFrameAncestorInfo>& aFrameAncestors) const;

Просмотреть файл

@ -240,7 +240,6 @@ var ContentPolicyManager = {
let response = null;
let listenerKind = "onStop";
let data = Object.assign({requestId, browser, serialize: serializeRequestData}, msg.data);
data.URI = data.url;
delete data.ids;
try {
@ -263,15 +262,31 @@ var ContentPolicyManager = {
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);
}
},
@ -661,24 +676,10 @@ HttpObserverManager = {
}
},
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,
@ -686,13 +687,12 @@ HttpObserverManager = {
originUrl: channel.originURL || undefined,
documentUrl: channel.documentURL || undefined,
originURI: channel.originURI,
documentURI: channel.documentURI,
isSystemPrincipal: channel.isSystemLoad,
windowId: channel.windowId,
parentWindowId: channel.parentWindowId,
frameAncestors: channel.frameAncestors || undefined,
ip: channel.remoteAddress,
proxyInfo: channel.proxyInfo,
@ -700,18 +700,6 @@ HttpObserverManager = {
serialize: serializeRequestData,
};
try {
let {frameAncestors} = channel;
if (frameAncestors !== null) {
data.frameAncestors = frameAncestors;
}
} catch (e) {}
// 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);
},
@ -774,10 +762,9 @@ HttpObserverManager = {
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, extraData)) {
continue;
}
@ -902,13 +889,13 @@ HttpObserverManager = {
}
},
shouldHookListener(listener, channel) {
shouldHookListener(listener, channel, extraData) {
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, extraData)) {
return true;
}
}
@ -920,7 +907,8 @@ HttpObserverManager = {
this.runChannelListener(channel, "headersReceived");
}
if (!channel.hasAuthRequestor && this.shouldHookListener(this.listeners.authRequired, channel)) {
if (!channel.hasAuthRequestor &&
this.shouldHookListener(this.listeners.authRequired, channel, {isProxy: true})) {
channel.channel.notificationCallbacks = new AuthRequestor(channel.channel, this);
channel.hasAuthRequestor = true;
}

Просмотреть файл

@ -3,7 +3,6 @@
var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components;
var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {});
var {MatchPattern} = Cu.import("resource://gre/modules/MatchPattern.jsm", {});
const BASE = "http://example.com/browser/toolkit/modules/tests/browser";
const URL = BASE + "/file_WebRequest_page2.html";
@ -65,7 +64,7 @@ add_task(async function setup() {
});
add_task(async function filter_urls() {
let filter = {urls: new MatchPattern("*://*/*_style_*")};
let filter = {urls: new MatchPatternSet(["*://*/*_style_*"])};
WebRequest.onBeforeRequest.addListener(onBeforeRequest, filter, ["blocking"]);
WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, filter, ["blocking"]);