Bug 1173811 - Part 1: Propagate the response URL to intercepted channels when necessary (non-e10s). r=mayhemer,bkelly

This commit is contained in:
Josh Matthews 2015-10-22 09:23:39 -04:00
parent 0e71734c14
commit d3726427db
21 changed files with 243 additions and 51 deletions

View File

@ -109,16 +109,19 @@ class FinishResponse final : public nsRunnable
RefPtr<InternalResponse> mInternalResponse;
ChannelInfo mWorkerChannelInfo;
const nsCString mScriptSpec;
const nsCString mResponseURLSpec;
public:
FinishResponse(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
InternalResponse* aInternalResponse,
const ChannelInfo& aWorkerChannelInfo,
const nsACString& aScriptSpec)
const nsACString& aScriptSpec,
const nsACString& aResponseURLSpec)
: mChannel(aChannel)
, mInternalResponse(aInternalResponse)
, mWorkerChannelInfo(aWorkerChannelInfo)
, mScriptSpec(aScriptSpec)
, mResponseURLSpec(aResponseURLSpec)
{
}
@ -154,7 +157,7 @@ public:
mChannel->SynthesizeHeader(entries[i].mName, entries[i].mValue);
}
rv = mChannel->FinishSynthesizedResponse();
rv = mChannel->FinishSynthesizedResponse(mResponseURLSpec);
NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to finish synthesized response");
return rv;
}
@ -232,15 +235,18 @@ struct RespondWithClosure
RefPtr<InternalResponse> mInternalResponse;
ChannelInfo mWorkerChannelInfo;
const nsCString mScriptSpec;
const nsCString mResponseURLSpec;
RespondWithClosure(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
InternalResponse* aInternalResponse,
const ChannelInfo& aWorkerChannelInfo,
const nsCString& aScriptSpec)
const nsCString& aScriptSpec,
const nsACString& aResponseURLSpec)
: mInterceptedChannel(aChannel)
, mInternalResponse(aInternalResponse)
, mWorkerChannelInfo(aWorkerChannelInfo)
, mScriptSpec(aScriptSpec)
, mResponseURLSpec(aResponseURLSpec)
{
}
};
@ -253,7 +259,8 @@ void RespondWithCopyComplete(void* aClosure, nsresult aStatus)
event = new FinishResponse(data->mInterceptedChannel,
data->mInternalResponse,
data->mWorkerChannelInfo,
data->mScriptSpec);
data->mScriptSpec,
data->mResponseURLSpec);
} else {
event = new CancelChannelRunnable(data->mInterceptedChannel,
NS_ERROR_INTERCEPTION_FAILED);
@ -356,9 +363,22 @@ RespondWithHandler::ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValu
return;
}
// When an opaque response is encountered, we need the original channel's principal
// to reflect the final URL. Non-opaque responses are either same-origin or CORS-enabled
// cross-origin responses, which are treated as same-origin by consumers.
nsCString responseURL;
if (response->Type() == ResponseType::Opaque) {
ir->GetUnfilteredUrl(responseURL);
if (NS_WARN_IF(responseURL.IsEmpty())) {
autoCancel.SetCancelStatus(NS_ERROR_INTERCEPTION_FAILED);
return;
}
}
nsAutoPtr<RespondWithClosure> closure(new RespondWithClosure(mInterceptedChannel, ir,
worker->GetChannelInfo(),
mScriptSpec));
mScriptSpec,
responseURL));
nsCOMPtr<nsIInputStream> body;
ir->GetUnfilteredBody(getter_AddRefs(body));
// Errors and redirects may not have a body.

View File

@ -78,12 +78,18 @@ InterceptedJARChannel::SynthesizeHeader(const nsACString& aName,
}
NS_IMETHODIMP
InterceptedJARChannel::FinishSynthesizedResponse()
InterceptedJARChannel::FinishSynthesizedResponse(const nsACString& aFinalURLSpec)
{
if (NS_WARN_IF(!mChannel)) {
return NS_ERROR_NOT_AVAILABLE;
}
if (!aFinalURLSpec.IsEmpty()) {
// We don't support rewriting responses for JAR channels where the principal
// needs to be modified.
return NS_ERROR_NOT_IMPLEMENTED;
}
mChannel->OverrideWithSynthesizedResponse(mSynthesizedInput, mContentType);
mResponseBody = nullptr;

View File

@ -27,7 +27,7 @@ class ChannelInfo;
* which do not implement nsIChannel.
*/
[scriptable, uuid(6be00c37-2e85-42ee-b53c-e6508ce4cef0)]
[scriptable, uuid(afe6aae6-a80d-405b-856e-df36c19bf3c8)]
interface nsIInterceptedChannel : nsISupports
{
/**
@ -51,9 +51,11 @@ interface nsIInterceptedChannel : nsISupports
/**
* Instruct a channel that has been intercepted that a response has been
* synthesized and can now be read. No further header modification is allowed
* after this point.
* after this point. The caller may optionally pass a spec for a URL that
* this response originates from; an empty string will cause the original
* intercepted request's URL to be used instead.
*/
void finishSynthesizedResponse();
void finishSynthesizedResponse(in ACString finalURLSpec);
/**
* Cancel the pending intercepted request.

View File

@ -267,6 +267,21 @@ HttpBaseChannel::GetLoadFlags(nsLoadFlags *aLoadFlags)
NS_IMETHODIMP
HttpBaseChannel::SetLoadFlags(nsLoadFlags aLoadFlags)
{
bool synthesized = false;
nsresult rv = GetResponseSynthesized(&synthesized);
NS_ENSURE_SUCCESS(rv, rv);
// If this channel is marked as awaiting a synthesized response,
// modifying certain load flags can interfere with the implementation
// of the network interception logic. This takes care of a couple
// known cases that attempt to mark channels as anonymous due
// to cross-origin redirects; since the response is entirely synthesized
// this is an unnecessary precaution.
// This should be removed when bug 1201683 is fixed.
if (synthesized && aLoadFlags != mLoadFlags) {
aLoadFlags &= ~LOAD_ANONYMOUS;
}
mLoadFlags = aLoadFlags;
mForceMainDocumentChannel = (aLoadFlags & LOAD_DOCUMENT_URI);
return NS_OK;

View File

@ -2444,6 +2444,12 @@ HttpChannelChild::ForceIntercepted()
return NS_OK;
}
NS_IMETHODIMP
HttpChannelChild::ForceIntercepted(uint64_t aInterceptionID)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
bool
HttpChannelChild::RecvIssueDeprecationWarning(const uint32_t& warning,
const bool& asError)

View File

@ -88,6 +88,7 @@ public:
NS_IMETHOD GetLocalPort(int32_t* port) override;
NS_IMETHOD GetRemoteAddress(nsACString& addr) override;
NS_IMETHOD GetRemotePort(int32_t* port) override;
NS_IMETHOD ForceIntercepted(uint64_t aInterceptionID) override;
// nsISupportsPriority
NS_IMETHOD SetPriority(int32_t value) override;
// nsIClassOfService

View File

@ -203,7 +203,9 @@ public:
NS_IMETHOD Run()
{
mChannel->FinishSynthesizedResponse();
// The URL passed as an argument here doesn't matter, since the child will
// receive a redirection notification as a result of this synthesized response.
mChannel->FinishSynthesizedResponse(EmptyCString());
return NS_OK;
}
};

View File

@ -179,7 +179,7 @@ InterceptedChannelChrome::SynthesizeHeader(const nsACString& aName, const nsACSt
}
NS_IMETHODIMP
InterceptedChannelChrome::FinishSynthesizedResponse()
InterceptedChannelChrome::FinishSynthesizedResponse(const nsACString& aFinalURLSpec)
{
if (!mChannel) {
return NS_ERROR_NOT_AVAILABLE;
@ -208,23 +208,38 @@ InterceptedChannelChrome::FinishSynthesizedResponse()
mSynthesizedResponseHead.ref(), securityInfo);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> uri;
mChannel->GetURI(getter_AddRefs(uri));
nsCOMPtr<nsIURI> originalURI;
mChannel->GetURI(getter_AddRefs(originalURI));
bool usingSSL = false;
uri->SchemeIs("https", &usingSSL);
// Then we open a real cache entry to read the synthesized response from.
rv = mChannel->OpenCacheEntry(usingSSL);
NS_ENSURE_SUCCESS(rv, rv);
mSynthesizedCacheEntry = nullptr;
if (!mChannel->AwaitingCacheCallbacks()) {
rv = mChannel->ContinueConnect();
nsCOMPtr<nsIURI> responseURI;
if (!aFinalURLSpec.IsEmpty()) {
nsresult rv = NS_NewURI(getter_AddRefs(responseURI), aFinalURLSpec);
NS_ENSURE_SUCCESS(rv, rv);
} else {
responseURI = originalURI;
}
bool equal = false;
originalURI->Equals(responseURI, &equal);
if (!equal) {
nsresult rv =
mChannel->StartRedirectChannelToURI(responseURI, nsIChannelEventSink::REDIRECT_INTERNAL);
NS_ENSURE_SUCCESS(rv, rv);
} else {
bool usingSSL = false;
responseURI->SchemeIs("https", &usingSSL);
// Then we open a real cache entry to read the synthesized response from.
rv = mChannel->OpenCacheEntry(usingSSL);
NS_ENSURE_SUCCESS(rv, rv);
mSynthesizedCacheEntry = nullptr;
if (!mChannel->AwaitingCacheCallbacks()) {
rv = mChannel->ContinueConnect();
NS_ENSURE_SUCCESS(rv, rv);
}
}
mChannel = nullptr;
return NS_OK;
}
@ -330,7 +345,7 @@ InterceptedChannelContent::SynthesizeHeader(const nsACString& aName, const nsACS
}
NS_IMETHODIMP
InterceptedChannelContent::FinishSynthesizedResponse()
InterceptedChannelContent::FinishSynthesizedResponse(const nsACString& aFinalURLSpec)
{
if (NS_WARN_IF(!mChannel)) {
return NS_ERROR_NOT_AVAILABLE;

View File

@ -72,7 +72,7 @@ public:
nsICacheEntry* aEntry);
NS_IMETHOD ResetInterception() override;
NS_IMETHOD FinishSynthesizedResponse() override;
NS_IMETHOD FinishSynthesizedResponse(const nsACString& aFinalURLSpec) override;
NS_IMETHOD GetChannel(nsIChannel** aChannel) override;
NS_IMETHOD SynthesizeStatus(uint16_t aStatus, const nsACString& aReason) override;
NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) override;
@ -100,7 +100,7 @@ public:
nsIStreamListener* aListener);
NS_IMETHOD ResetInterception() override;
NS_IMETHOD FinishSynthesizedResponse() override;
NS_IMETHOD FinishSynthesizedResponse(const nsACString& aFinalURLSpec) override;
NS_IMETHOD GetChannel(nsIChannel** aChannel) override;
NS_IMETHOD SynthesizeStatus(uint16_t aStatus, const nsACString& aReason) override;
NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) override;

View File

@ -2010,12 +2010,19 @@ nsHttpChannel::StartRedirectChannelToURI(nsIURI *upgradedURI, uint32_t flags)
if (!(flags & nsIChannelEventSink::REDIRECT_STS_UPGRADE)) {
// Ensure that internally-redirected channels cannot be intercepted, which would look
// like two separate requests to the nsINetworkInterceptController.
nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL;
rv = mRedirectChannel->GetLoadFlags(&loadFlags);
NS_ENSURE_SUCCESS(rv, rv);
loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
rv = mRedirectChannel->SetLoadFlags(loadFlags);
NS_ENSURE_SUCCESS(rv, rv);
if (mInterceptCache == INTERCEPTED) {
nsCOMPtr<nsIHttpChannelInternal> httpRedirect = do_QueryInterface(mRedirectChannel);
if (httpRedirect) {
httpRedirect->ForceIntercepted(mInterceptionID);
}
} else {
nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL;
rv = mRedirectChannel->GetLoadFlags(&loadFlags);
NS_ENSURE_SUCCESS(rv, rv);
loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
rv = mRedirectChannel->SetLoadFlags(loadFlags);
NS_ENSURE_SUCCESS(rv, rv);
}
}
PushRedirectAsyncFunc(
@ -2993,11 +3000,10 @@ nsHttpChannel::OpenCacheEntry(bool isHttps)
if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY)
cacheEntryOpenFlags |= nsICacheStorage::OPEN_BYPASS_IF_BUSY;
if (mPostID) {
extension.Append(nsPrintfCString("%d", mPostID));
}
if (PossiblyIntercepted()) {
extension.Append(nsPrintfCString("u%lld", mInterceptionID));
} else if (mPostID) {
extension.Append(nsPrintfCString("%d", mPostID));
}
// If this channel should be intercepted, we do not open a cache entry for this channel
@ -4984,7 +4990,7 @@ nsHttpChannel::AsyncOpen(nsIStreamListener *listener, nsISupports *context)
return rv;
}
if (ShouldIntercept()) {
if (mInterceptCache != INTERCEPTED && ShouldIntercept()) {
mInterceptCache = MAYBE_INTERCEPT;
SetCouldBeSynthesized();
}
@ -5344,6 +5350,21 @@ nsHttpChannel::SetupFallbackChannel(const char *aFallbackKey)
return NS_OK;
}
NS_IMETHODIMP
nsHttpChannel::ForceIntercepted(uint64_t aInterceptionID)
{
ENSURE_CALLED_BEFORE_ASYNC_OPEN();
if (NS_WARN_IF(mLoadFlags & LOAD_BYPASS_SERVICE_WORKER)) {
return NS_ERROR_NOT_AVAILABLE;
}
MarkIntercepted();
mResponseCouldBeSynthesized = true;
mInterceptionID = aInterceptionID;
return NS_OK;
}
//-----------------------------------------------------------------------------
// nsHttpChannel::nsISupportsPriority
//-----------------------------------------------------------------------------

View File

@ -134,6 +134,7 @@ public:
NS_IMETHOD AsyncOpen2(nsIStreamListener *aListener) override;
// nsIHttpChannelInternal
NS_IMETHOD SetupFallbackChannel(const char *aFallbackKey) override;
NS_IMETHOD ForceIntercepted(uint64_t aInterceptionID) override;
// nsISupportsPriority
NS_IMETHOD SetPriority(int32_t value) override;
// nsIClassOfService
@ -433,8 +434,9 @@ private:
MAYBE_INTERCEPT, // interception in progress, but can be cancelled
INTERCEPTED, // a synthesized response has been provided
} mInterceptCache;
// Unique ID of this channel for the interception purposes.
const uint64_t mInterceptionID;
// ID of this channel for the interception purposes. Unique unless this
// channel is replacing an intercepted one via an redirection.
uint64_t mInterceptionID;
bool PossiblyIntercepted() {
return mInterceptCache != DO_NOT_INTERCEPT;

View File

@ -39,7 +39,7 @@ interface nsIHttpUpgradeListener : nsISupports
* using any feature exposed by this interface, be aware that this interface
* will change and you will be broken. You have been warned.
*/
[scriptable, uuid(99767aaf-937d-4f2f-8990-bc79bd7c0ece)]
[scriptable, uuid(9eabaac6-cc7c-4ca1-9430-65f2daaa578f)]
interface nsIHttpChannelInternal : nsISupports
{
/**
@ -222,6 +222,13 @@ interface nsIHttpChannelInternal : nsISupports
readonly attribute PRTime lastModifiedTime;
/**
* Force a channel that has not been AsyncOpen'ed to skip any check for possible
* interception and proceed immediately to open a previously-synthesized cache
* entry using the provided ID.
*/
void forceIntercepted(in uint64_t aInterceptionID);
readonly attribute boolean responseSynthesized;
/**

View File

@ -63,7 +63,7 @@ function make_channel(url, body, cb) {
synthesized.data = body;
NetUtil.asyncCopy(synthesized, channel.responseBody, function() {
channel.finishSynthesizedResponse();
channel.finishSynthesizedResponse('');
});
}
if (cb) {
@ -150,7 +150,7 @@ add_test(function() {
synthesized.data = NON_REMOTE_BODY;
NetUtil.asyncCopy(synthesized, channel.responseBody, function() {
channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length);
channel.finishSynthesizedResponse();
channel.finishSynthesizedResponse('');
});
});
});
@ -178,7 +178,7 @@ add_test(function() {
// set the content-type to ensure that the stream converter doesn't hold up notifications
// and cause the test to fail
intercepted.synthesizeHeader("Content-Type", "text/plain");
intercepted.finishSynthesizedResponse();
intercepted.finishSynthesizedResponse('');
});
});
chan.asyncOpen(new ChannelListener(handle_synthesized_response, null,
@ -220,7 +220,7 @@ add_test(function() {
NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
let channel = intercepted.channel;
intercepted.finishSynthesizedResponse();
intercepted.finishSynthesizedResponse('');
channel.cancel(Cr.NS_BINDING_ABORTED);
});
});
@ -237,7 +237,7 @@ add_test(function() {
NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
intercepted.channel.cancel(Cr.NS_BINDING_ABORTED);
intercepted.finishSynthesizedResponse();
intercepted.finishSynthesizedResponse('');
});
});
chan.asyncOpen(new ChannelListener(run_next_test, null,

View File

@ -106,6 +106,12 @@
"url": "/_mozilla/service-workers/service-worker/extendable-event-waituntil.https.html"
}
],
"service-workers/service-worker/fetch-canvas-tainting-cache.https.html": [
{
"path": "service-workers/service-worker/fetch-canvas-tainting-cache.https.html",
"url": "/_mozilla/service-workers/service-worker/fetch-canvas-tainting-cache.https.html"
}
],
"service-workers/service-worker/fetch-canvas-tainting.https.html": [
{
"path": "service-workers/service-worker/fetch-canvas-tainting.https.html",

View File

@ -0,0 +1,5 @@
[fetch-request-css-base-url.https.html]
type: testharness
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1201160
[CSS's base URL must be the request URL even when fetched from other URL.]
expected: FAIL

View File

@ -0,0 +1,3 @@
[worker-interception-iframe.https.html]
type: testharness
disabled: not a test

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<title>Service Worker: canvas tainting of the fetched image using cached responses</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/get-host-info.sub.js"></script>
<script src="resources/test-helpers.sub.js?pipe=sub"></script>
<body>
<script>
async_test(function(t) {
var SCOPE = 'resources/fetch-canvas-tainting-iframe.html?cache';
var SCRIPT = 'resources/fetch-rewrite-worker.js';
var host_info = get_host_info();
login_https(t)
.then(function() {
return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
})
.then(function(registration) {
return wait_for_state(t, registration.installing, 'activated');
})
.then(function() { return with_iframe(SCOPE); })
.then(function(frame) {
return new Promise(function(resolve, reject) {
var channel = new MessageChannel();
channel.port1.onmessage = t.step_func(function(e) {
assert_equals(e.data.results, 'finish');
frame.remove();
service_worker_unregister_and_done(t, SCOPE);
});
frame.contentWindow.postMessage({},
host_info['HTTPS_ORIGIN'],
[channel.port2]);
});
})
.catch(unreached_rejection(t));
}, 'Verify canvas tainting of fetched image in a Service Worker');
</script>
</body>

View File

@ -65,7 +65,8 @@ function redirect_fetch_test(t, test) {
};
frame.contentWindow.postMessage({
url: url,
request_init: test.request_init
request_init: test.request_init,
redirect_dest: test.redirect_dest,
}, '*', [channel.port2]);
});

View File

@ -3,11 +3,26 @@
<script>
var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE';
var host_info = get_host_info();
var params = get_query_params(location.href);
var NOT_TAINTED = 'NOT_TAINTED';
var TAINTED = 'TAINTED';
var LOAD_ERROR = 'LOAD_ERROR';
function get_query_params(url) {
var search = (new URL(url)).search;
if (!search) {
return {};
}
var ret = {};
var params = search.substring(1).split('&');
params.forEach(function(param) {
var element = param.split('=');
ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
});
return ret;
}
function create_test_case_promise(url, cross_origin) {
return new Promise(function(resolve) {
var img = new Image();
@ -35,6 +50,10 @@ function create_test_case_promise(url, cross_origin) {
}
function create_test_promise(url, cross_origin, expected_result) {
if (params['cache']) {
url += "&cache";
}
return new Promise(function(resolve, reject) {
create_test_case_promise(url, cross_origin)
.then(function(result) {
@ -140,7 +159,7 @@ window.addEventListener('message', function(evt) {
remote_image_url +
'&mode=same-origin&url=' + encodeURIComponent(image_url),
'',
NOT_TAINTED),
TAINTED),
create_test_promise(
remote_image_url +
'&mode=same-origin&url=' + encodeURIComponent(image_url),
@ -191,9 +210,7 @@ window.addEventListener('message', function(evt) {
encodeURIComponent(remote_image_url +
'&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
'',
TAINTED), // We expect TAINTED since the default origin behavior here
// is taint, and it doesn't matter what kind of fetch the
// SW performs.
NOT_TAINTED),
create_test_promise(
image_url +
'&mode=cors&url=' +

View File

@ -3,6 +3,13 @@ window.addEventListener('message', function(evt) {
var port = evt.ports[0];
var data = evt.data;
fetch(new Request(data.url, data.request_init)).then(function(response) {
if (data.request_init.mode === 'no-cors' && data.redirect_dest != 'same-origin') {
if (response.type === 'opaque') {
return {result: 'success', detail: ''};
} else {
return {result: 'failure', detail: 'expected opaque response'};
}
}
return response.json();
}).then(function(body) {
port.postMessage({result: body.result, detail: body.detail});

View File

@ -80,7 +80,25 @@ self.addEventListener('fetch', function(event) {
expectedType
})));
}
resolve(response);
if (params['cache']) {
var cacheName = "cached-fetches-" + Date.now();
var cache;
var cachedResponse;
return self.caches.open(cacheName).then(function(opened) {
cache = opened;
return cache.put(request, response);
}).then(function() {
return cache.match(request);
}).then(function(cached) {
cachedResponse = cached;
return self.caches.delete(cacheName);
}).then(function() {
resolve(cachedResponse);
});
} else {
resolve(response);
}
}, reject)
}));
});