Bug 1119021 - Implement fetch() redirects correctly. r=bkelly

This commit is contained in:
Nikhil Marathe 2015-01-07 13:47:18 -08:00
parent 03579a2217
commit 86e81d17de
6 changed files with 507 additions and 18 deletions

View File

@ -65,6 +65,8 @@ http://127.0.0.1:8888 privileged
http://test:80 privileged
http://mochi.test:8888 privileged
http://test1.mochi.test:8888
http://sub1.test1.mochi.test:8888
http://sub2.xn--lt-uia.mochi.test:8888
http://test2.mochi.test:8888
http://example.org:80 privileged
http://test1.example.org:80 privileged

View File

@ -31,7 +31,9 @@
namespace mozilla {
namespace dom {
NS_IMPL_ISUPPORTS(FetchDriver, nsIStreamListener)
NS_IMPL_ISUPPORTS(FetchDriver,
nsIStreamListener, nsIChannelEventSink, nsIInterfaceRequestor,
nsIAsyncVerifyRedirectCallback)
FetchDriver::FetchDriver(InternalRequest* aRequest, nsIPrincipal* aPrincipal,
nsILoadGroup* aLoadGroup)
@ -89,7 +91,6 @@ FetchDriver::ContinueFetch(bool aCORSFlag)
nsAutoCString url;
mRequest->GetURL(url);
nsCOMPtr<nsIURI> requestURI;
// FIXME(nsm): Deal with relative URLs.
nsresult rv = NS_NewURI(getter_AddRefs(requestURI), url,
nullptr, nullptr);
if (NS_WARN_IF(NS_FAILED(rv))) {
@ -332,6 +333,11 @@ FetchDriver::HttpFetch(bool aCORSFlag, bool aCORSPreflightFlag, bool aAuthentica
return rv;
}
// Insert ourselves into the notification callbacks chain so we can handle
// cross-origin redirects.
chan->GetNotificationCallbacks(getter_AddRefs(mNotificationCallbacks));
chan->SetNotificationCallbacks(this);
// Step 3.1 "If the CORS preflight flag is set and one of these conditions is
// true..." is handled by the CORS proxy.
//
@ -688,5 +694,157 @@ FetchDriver::OnStopRequest(nsIRequest* aRequest,
return NS_OK;
}
// This is called when the channel is redirected.
NS_IMETHODIMP
FetchDriver::AsyncOnChannelRedirect(nsIChannel* aOldChannel,
nsIChannel* aNewChannel,
uint32_t aFlags,
nsIAsyncVerifyRedirectCallback *aCallback)
{
NS_PRECONDITION(aNewChannel, "Redirect without a channel?");
nsresult rv;
// Section 4.2, Step 4.6-4.7, enforcing a redirect count is done by Necko.
// The pref used is "network.http.redirection-limit" which is set to 20 by
// default.
//
// Step 4.8. We only unset this for spec compatibility. Any actions we take
// on mRequest here do not affect what the channel does.
mRequest->UnsetSameOriginDataURL();
//
// Requests that require preflight are not permitted to redirect.
// Fetch spec section 4.2 "HTTP Fetch", step 4.9 just uses the manual
// redirect flag to decide whether to execute step 4.10 or not. We do not
// represent it in our implementation.
// The only thing we do is to check if the request requires a preflight (part
// of step 4.9), in which case we abort. This part cannot be done by
// nsCORSListenerProxy since it does not have access to mRequest.
// which case. Step 4.10.3 is handled by OnRedirectVerifyCallback(), and all
// the other steps are handled by nsCORSListenerProxy.
if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags)) {
rv = DoesNotRequirePreflight(aNewChannel);
if (NS_FAILED(rv)) {
NS_WARNING("nsXMLHttpRequest::OnChannelRedirect: "
"DoesNotRequirePreflight returned failure");
return rv;
}
}
mRedirectCallback = aCallback;
mOldRedirectChannel = aOldChannel;
mNewRedirectChannel = aNewChannel;
nsCOMPtr<nsIChannelEventSink> outer =
do_GetInterface(mNotificationCallbacks);
if (outer) {
// The callee is supposed to call OnRedirectVerifyCallback() on success,
// and nobody has to call it on failure, so we can just return after this
// block.
rv = outer->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, this);
if (NS_FAILED(rv)) {
aOldChannel->Cancel(rv);
mRedirectCallback = nullptr;
mOldRedirectChannel = nullptr;
mNewRedirectChannel = nullptr;
}
return rv;
}
(void) OnRedirectVerifyCallback(NS_OK);
return NS_OK;
}
// Returns NS_OK if no preflight is required, error otherwise.
nsresult
FetchDriver::DoesNotRequirePreflight(nsIChannel* aChannel)
{
// If this is a same-origin request or the channel's URI inherits
// its principal, it's allowed.
if (nsContentUtils::CheckMayLoad(mPrincipal, aChannel, true)) {
return NS_OK;
}
// Check if we need to do a preflight request.
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
NS_ENSURE_TRUE(httpChannel, NS_ERROR_DOM_BAD_URI);
nsAutoCString method;
httpChannel->GetRequestMethod(method);
if (mRequest->Mode() == RequestMode::Cors_with_forced_preflight ||
!mRequest->Headers()->HasOnlySimpleHeaders() ||
(!method.LowerCaseEqualsLiteral("get") &&
!method.LowerCaseEqualsLiteral("post") &&
!method.LowerCaseEqualsLiteral("head"))) {
return NS_ERROR_DOM_BAD_URI;
}
return NS_OK;
}
NS_IMETHODIMP
FetchDriver::GetInterface(const nsIID& aIID, void **aResult)
{
if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
*aResult = static_cast<nsIChannelEventSink*>(this);
NS_ADDREF_THIS();
return NS_OK;
}
nsresult rv;
if (mNotificationCallbacks) {
rv = mNotificationCallbacks->GetInterface(aIID, aResult);
if (NS_SUCCEEDED(rv)) {
NS_ASSERTION(*aResult, "Lying nsIInterfaceRequestor implementation!");
return rv;
}
}
else if (aIID.Equals(NS_GET_IID(nsIStreamListener))) {
*aResult = static_cast<nsIStreamListener*>(this);
NS_ADDREF_THIS();
return NS_OK;
}
else if (aIID.Equals(NS_GET_IID(nsIRequestObserver))) {
*aResult = static_cast<nsIRequestObserver*>(this);
NS_ADDREF_THIS();
return NS_OK;
}
return QueryInterface(aIID, aResult);
}
NS_IMETHODIMP
FetchDriver::OnRedirectVerifyCallback(nsresult aResult)
{
// On a successful redirect we perform the following substeps of Section 4.2,
// step 4.10.
if (NS_SUCCEEDED(aResult)) {
// Step 4.10.3 "Set request's url to locationURL." so that when we set the
// Response's URL from the Request's URL in Section 4, step 6, we get the
// final value.
nsCOMPtr<nsIURI> newURI;
nsresult rv = NS_GetFinalChannelURI(mNewRedirectChannel, getter_AddRefs(newURI));
if (NS_WARN_IF(NS_FAILED(rv))) {
aResult = rv;
} else {
// We need to update our request's URL.
nsAutoCString newUrl;
newURI->GetSpec(newUrl);
mRequest->SetURL(newUrl);
}
}
if (NS_FAILED(aResult)) {
mOldRedirectChannel->Cancel(aResult);
}
mOldRedirectChannel = nullptr;
mNewRedirectChannel = nullptr;
mRedirectCallback->OnRedirectVerifyCallback(aResult);
mRedirectCallback = nullptr;
return NS_OK;
}
} // namespace dom
} // namespace mozilla

View File

@ -7,6 +7,9 @@
#define mozilla_dom_FetchDriver_h
#include "nsAutoPtr.h"
#include "nsIAsyncVerifyRedirectCallback.h"
#include "nsIChannelEventSink.h"
#include "nsIInterfaceRequestor.h"
#include "nsIStreamListener.h"
#include "nsRefPtr.h"
@ -35,12 +38,18 @@ protected:
{ };
};
class FetchDriver MOZ_FINAL : public nsIStreamListener
class FetchDriver MOZ_FINAL : public nsIStreamListener,
public nsIChannelEventSink,
public nsIInterfaceRequestor,
public nsIAsyncVerifyRedirectCallback
{
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIREQUESTOBSERVER
NS_DECL_NSISTREAMLISTENER
NS_DECL_NSICHANNELEVENTSINK
NS_DECL_NSIINTERFACEREQUESTOR
NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK
explicit FetchDriver(InternalRequest* aRequest, nsIPrincipal* aPrincipal,
nsILoadGroup* aLoadGroup);
@ -53,6 +62,10 @@ private:
nsRefPtr<InternalResponse> mResponse;
nsCOMPtr<nsIOutputStream> mPipeOutputStream;
nsRefPtr<FetchDriverObserver> mObserver;
nsCOMPtr<nsIInterfaceRequestor> mNotificationCallbacks;
nsCOMPtr<nsIAsyncVerifyRedirectCallback> mRedirectCallback;
nsCOMPtr<nsIChannel> mOldRedirectChannel;
nsCOMPtr<nsIChannel> mNewRedirectChannel;
uint32_t mFetchRecursionCount;
DebugOnly<bool> mResponseAvailableCalled;
@ -75,6 +88,7 @@ private:
void BeginResponse(InternalResponse* aResponse);
nsresult FailWithNetworkError();
nsresult SucceedWithResponse();
nsresult DoesNotRequirePreflight(nsIChannel* aChannel);
};
} // namespace dom

View File

@ -62,10 +62,8 @@ public:
, mCredentialsMode(RequestCredentials::Omit)
, mResponseTainting(RESPONSETAINT_BASIC)
, mCacheMode(RequestCache::Default)
, mRedirectCount(0)
, mAuthenticationFlag(false)
, mForceOriginHeader(false)
, mManualRedirect(false)
, mPreserveContentCodings(false)
// FIXME(nsm): This should be false by default, but will lead to the
// algorithm never loading data: URLs right now. See Bug 1018872 about
@ -92,10 +90,8 @@ public:
, mCredentialsMode(aOther.mCredentialsMode)
, mResponseTainting(aOther.mResponseTainting)
, mCacheMode(aOther.mCacheMode)
, mRedirectCount(aOther.mRedirectCount)
, mAuthenticationFlag(aOther.mAuthenticationFlag)
, mForceOriginHeader(aOther.mForceOriginHeader)
, mManualRedirect(aOther.mManualRedirect)
, mPreserveContentCodings(aOther.mPreserveContentCodings)
, mSameOriginDataURL(aOther.mSameOriginDataURL)
, mSandboxedStorageAreaURLs(aOther.mSandboxedStorageAreaURLs)
@ -132,6 +128,12 @@ public:
aURL.Assign(mURL);
}
void
SetURL(const nsACString& aURL)
{
mURL.Assign(aURL);
}
bool
ReferrerIsNone() const
{
@ -250,6 +252,12 @@ public:
return mSameOriginDataURL;
}
void
UnsetSameOriginDataURL()
{
mSameOriginDataURL = false;
}
void
SetBody(nsIInputStream* aStream)
{
@ -274,12 +282,6 @@ public:
private:
~InternalRequest();
void
SetURL(const nsACString& aURL)
{
mURL.Assign(aURL);
}
nsCString mMethod;
nsCString mURL;
nsRefPtr<InternalHeaders> mHeaders;
@ -300,11 +302,8 @@ private:
ResponseTainting mResponseTainting;
RequestCache mCacheMode;
uint32_t mRedirectCount;
bool mAuthenticationFlag;
bool mForceOriginHeader;
bool mManualRedirect;
bool mPreserveContentCodings;
bool mSameOriginDataURL;
bool mSandboxedStorageAreaURLs;

View File

@ -24,7 +24,11 @@ function testURL() {
ok(res.type !== "error", "Response should not be an error for " + entry[0]);
is(res.status, entry[2], "Status should match expected for " + entry[0]);
is(res.statusText, entry[3], "Status text should match expected for " + entry[0]);
ok(res.url.endsWith(path + entry[0]), "Response url should match request for simple fetch for " + entry[0]);
// This file redirects to pass2
if (entry[0] != "file_XHR_pass3.txt")
ok(res.url.endsWith(path + entry[0]), "Response url should match request for simple fetch for " + entry[0]);
else
ok(res.url.endsWith(path + "file_XHR_pass2.txt"), "Response url should match request for simple fetch for " + entry[0]);
is(res.headers.get('content-type'), entry[4], "Response should have content-type for " + entry[0]);
});
promises.push(p);
@ -56,7 +60,10 @@ function testRequestGET() {
ok(res.type !== "error", "Response should not be an error for " + entry[0]);
is(res.status, entry[2], "Status should match expected for " + entry[0]);
is(res.statusText, entry[3], "Status text should match expected for " + entry[0]);
ok(res.url.endsWith(path + entry[0]), "Response url should match request for simple fetch for " + entry[0]);
if (entry[0] != "file_XHR_pass3.txt")
ok(res.url.endsWith(path + entry[0]), "Response url should match request for simple fetch for " + entry[0]);
else
ok(res.url.endsWith(path + "file_XHR_pass2.txt"), "Response url should match request for simple fetch for " + entry[0]);
is(res.headers.get('content-type'), entry[4], "Response should have content-type for " + entry[0]);
});
promises.push(p);

View File

@ -873,6 +873,314 @@ function testCredentials() {
return finalPromise;
}
function testRedirects() {
var origin = "http://mochi.test:8888";
var tests = [
{ pass: 1,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
],
},
{ pass: 0,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://mochi.test:8888",
allowOrigin: origin
},
],
},
{ pass: 1,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://mochi.test:8888",
allowOrigin: "*"
},
],
},
{ pass: 0,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://mochi.test:8888",
},
],
},
{ pass: 1,
method: "GET",
hops: [{ server: "http://mochi.test:8888",
},
{ server: "http://mochi.test:8888",
},
{ server: "http://example.com",
allowOrigin: origin
},
],
},
{ pass: 0,
method: "GET",
hops: [{ server: "http://mochi.test:8888",
},
{ server: "http://mochi.test:8888",
},
{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://mochi.test:8888",
},
],
},
{ pass: 0,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://test2.mochi.test:8000",
allowOrigin: origin
},
{ server: "http://sub2.xn--lt-uia.mochi.test:8888",
allowOrigin: origin
},
{ server: "http://sub1.test1.mochi.test:8888",
allowOrigin: origin
},
],
},
{ pass: 0,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://test2.mochi.test:8000",
allowOrigin: origin
},
{ server: "http://sub2.xn--lt-uia.mochi.test:8888",
allowOrigin: "*"
},
{ server: "http://sub1.test1.mochi.test:8888",
allowOrigin: "*"
},
],
},
{ pass: 1,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://test2.mochi.test:8888",
allowOrigin: "*"
},
{ server: "http://sub2.xn--lt-uia.mochi.test:8888",
allowOrigin: "*"
},
{ server: "http://sub1.test1.mochi.test:8888",
allowOrigin: "*"
},
],
},
{ pass: 0,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://test2.mochi.test:8000",
allowOrigin: origin
},
{ server: "http://sub2.xn--lt-uia.mochi.test:8888",
allowOrigin: "x"
},
{ server: "http://sub1.test1.mochi.test:8888",
allowOrigin: origin
},
],
},
{ pass: 0,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://test2.mochi.test:8000",
allowOrigin: origin
},
{ server: "http://sub2.xn--lt-uia.mochi.test:8888",
allowOrigin: "*"
},
{ server: "http://sub1.test1.mochi.test:8888",
allowOrigin: origin
},
],
},
{ pass: 0,
method: "GET",
hops: [{ server: "http://example.com",
allowOrigin: origin
},
{ server: "http://test2.mochi.test:8000",
allowOrigin: origin
},
{ server: "http://sub2.xn--lt-uia.mochi.test:8888",
allowOrigin: "*"
},
{ server: "http://sub1.test1.mochi.test:8888",
},
],
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain" },
hops: [{ server: "http://mochi.test:8888",
},
{ server: "http://example.com",
allowOrigin: origin,
},
],
},
{ pass: 0,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain",
"my-header": "myValue",
},
hops: [{ server: "http://mochi.test:8888",
},
{ server: "http://example.com",
allowOrigin: origin,
allowHeaders: "my-header",
},
],
},
{ pass: 0,
method: "DELETE",
hops: [{ server: "http://mochi.test:8888",
},
{ server: "http://example.com",
allowOrigin: origin,
},
],
},
{ pass: 0,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain",
"my-header": "myValue",
},
hops: [{ server: "http://example.com",
allowOrigin: origin,
},
{ server: "http://sub1.test1.mochi.test:8888",
allowOrigin: origin,
},
],
},
{ pass: 0,
method: "DELETE",
hops: [{ server: "http://example.com",
allowOrigin: origin,
},
{ server: "http://sub1.test1.mochi.test:8888",
allowOrigin: origin,
},
],
},
{ pass: 0,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain",
"my-header": "myValue",
},
hops: [{ server: "http://example.com",
},
{ server: "http://sub1.test1.mochi.test:8888",
allowOrigin: origin,
allowHeaders: "my-header",
},
],
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain" },
hops: [{ server: "http://mochi.test:8888",
},
{ server: "http://example.com",
allowOrigin: origin,
},
],
},
{ pass: 0,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain",
"my-header": "myValue",
},
hops: [{ server: "http://example.com",
allowOrigin: origin,
allowHeaders: "my-header",
},
{ server: "http://mochi.test:8888",
allowOrigin: origin,
allowHeaders: "my-header",
},
],
},
];
var fetches = [];
for (test of tests) {
req = {
url: test.hops[0].server + corsServerPath + "hop=1&hops=" +
escape(test.hops.toSource()),
method: test.method,
headers: test.headers,
body: test.body,
};
if (test.pass) {
if (test.body)
req.url += "&body=" + escape(test.body);
}
var request = new Request(req.url, { method: req.method,
headers: req.headers,
body: req.body });
fetches.push((function(request, test) {
return fetch(request).then(function(res) {
if (test.pass) {
is(isNetworkError(res), false,
"shouldn't have failed in test for " + test.toSource());
is(res.status, 200, "wrong status in test for " + test.toSource());
is(res.statusText, "OK", "wrong status text for " + test.toSource());
is((new URL(res.url)).host, (new URL(test.hops[test.hops.length-1].server)).host, "Response URL should be redirected URL");
return res.text().then(function(v) {
is(v, "<res>hello pass</res>\n",
"wrong responseText in test for " + test.toSource());
});
}
else {
is(isNetworkError(res), true,
"should have failed in test for " + test.toSource());
is(res.status, 0, "wrong status in test for " + test.toSource());
is(res.statusText, "", "wrong status text for " + test.toSource());
return res.text().then(function(v) {
is(v, "",
"wrong responseText in test for " + test.toSource());
});
}
});
})(request, test));
}
return Promise.all(fetches);
}
function runTest() {
var done = function() {
if (typeof SimpleTest === "object") {
@ -889,6 +1197,7 @@ function runTest() {
.then(testModeNoCors)
.then(testModeCors)
.then(testCredentials)
.then(testRedirects)
// Put more promise based tests here.
.then(done)
.catch(function(e) {