Bug 1119021 - CORS support. r=baku,bkelly

Use nsCrossSiteListenerProxy.h helpers to implement CORS support.
Several CORS fixes and lots of CORS tests.

Fixes:
Use empty string stream if response has no stream.
Parse Access-Control-Expose-Headers correctly.
Copy over remaining InternalRequest constructor attributes and set unsafe request flag.
Call FailWithNetworkError() in more cases.
Add non-simple Request headers to unsafeHeaders list for CORS check.
Do not AsyncOpen channel directly when CORS preflight is required.
Fix check for simple request method (was checking the opposite condition).
This commit is contained in:
Nikhil Marathe 2015-01-07 15:50:54 -08:00
parent c64bab9aa5
commit 1941238a33
10 changed files with 995 additions and 63 deletions

View File

@ -971,8 +971,10 @@ FetchBody<Derived>::BeginConsumeBodyMainThread()
nsCOMPtr<nsIInputStream> stream;
DerivedClass()->GetBody(getter_AddRefs(stream));
if (!stream) {
NS_WARNING("Could not get stream");
return;
rv = NS_NewCStringInputStream(getter_AddRefs(stream), EmptyCString());
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
}
nsCOMPtr<nsIInputStreamPump> pump;

View File

@ -14,6 +14,7 @@
#include "nsIUploadChannel2.h"
#include "nsContentPolicyUtils.h"
#include "nsCORSListenerProxy.h"
#include "nsDataHandler.h"
#include "nsHostObjectProtocolHandler.h"
#include "nsNetUtil.h"
@ -95,6 +96,9 @@ FetchDriver::ContinueFetch(bool aCORSFlag)
return FailWithNetworkError();
}
// Begin Step 4 of the Fetch algorithm
// https://fetch.spec.whatwg.org/#fetching
// FIXME(nsm): Bug 1039846: Add CSP checks
nsAutoCString scheme;
@ -125,7 +129,7 @@ FetchDriver::ContinueFetch(bool aCORSFlag)
bool corsPreflight = false;
if (mRequest->Mode() == RequestMode::Cors_with_forced_preflight ||
(mRequest->UnsafeRequest() && (mRequest->HasSimpleMethod() || !mRequest->Headers()->HasOnlySimpleHeaders()))) {
(mRequest->UnsafeRequest() && (!mRequest->HasSimpleMethod() || !mRequest->Headers()->HasOnlySimpleHeaders()))) {
corsPreflight = true;
}
@ -274,47 +278,15 @@ FetchDriver::BasicFetch()
return FailWithNetworkError();
}
// This function implements the "HTTP Fetch" algorithm from the Fetch spec.
// Functionality is often split between here, the CORS listener proxy and the
// Necko HTTP implementation.
nsresult
FetchDriver::HttpFetch(bool aCORSFlag, bool aPreflightCORSFlag, bool aAuthenticationFlag)
FetchDriver::HttpFetch(bool aCORSFlag, bool aCORSPreflightFlag, bool aAuthenticationFlag)
{
// Step 1. "Let response be null."
mResponse = nullptr;
// XXXnsm: The ServiceWorker interception should happen automatically.
return ContinueHttpFetchAfterServiceWorker();
}
nsresult
FetchDriver::ContinueHttpFetchAfterServiceWorker()
{
if (!mResponse) {
// FIXME(nsm): Set skip SW flag.
// FIXME(nsm): Deal with CORS flags cases which will also call
// ContinueHttpFetchAfterCORSPreflight().
return ContinueHttpFetchAfterCORSPreflight();
}
// Otherwise ServiceWorker replied with a response.
return ContinueHttpFetchAfterNetworkFetch();
}
nsresult
FetchDriver::ContinueHttpFetchAfterCORSPreflight()
{
// mResponse is currently the CORS response.
// We may have to pass it via argument.
if (mResponse && mResponse->IsError()) {
return FailWithNetworkError();
}
return HttpNetworkFetch();
}
nsresult
FetchDriver::HttpNetworkFetch()
{
// We don't create a HTTPRequest copy since Necko sets the information on the
// nsIHttpChannel instead.
nsresult rv;
nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv);
@ -336,6 +308,13 @@ FetchDriver::HttpNetworkFetch()
return rv;
}
// Step 2 deals with letting ServiceWorkers intercept requests. This is
// handled by Necko after the channel is opened.
// FIXME(nsm): Bug 1119026: The channel's skip service worker flag should be
// set based on the Request's flag.
// From here on we create a channel and set its properties with the
// information from the InternalRequest. This is an implementation detail.
MOZ_ASSERT(mLoadGroup);
nsCOMPtr<nsIChannel> chan;
rv = NS_NewChannel(getter_AddRefs(chan),
@ -353,8 +332,39 @@ FetchDriver::HttpNetworkFetch()
return rv;
}
// Step 3.1 "If the CORS preflight flag is set and one of these conditions is
// true..." is handled by the CORS proxy.
//
// Step 3.2 "Set request's skip service worker flag." This isn't required
// since Necko will fall back to the network if the ServiceWorker does not
// respond with a valid Response.
//
// NS_StartCORSPreflight() will automatically kick off the original request
// if it succeeds, so we need to have everything setup for the original
// request too.
// Step 3.3 "Let credentials flag be set if either request's credentials mode
// is include, or request's credentials mode is same-origin and the CORS flag
// is unset, and unset otherwise."
bool useCredentials = false;
if (mRequest->GetCredentialsMode() == RequestCredentials::Include ||
(mRequest->GetCredentialsMode() == RequestCredentials::Same_origin && !aCORSFlag)) {
useCredentials = true;
}
// FIXME(nsm): Bug 1120715.
// Step 3.4 "If request's cache mode is default and request's header list
// contains a header named `If-Modified-Since`, `If-None-Match`,
// `If-Unmodified-Since`, `If-Match`, or `If-Range`, set request's cache mode
// to no-store."
// Step 3.5 begins "HTTP network or cache fetch".
// HTTP network or cache fetch
// ---------------------------
// Step 1 "Let HTTPRequest..." The channel is the HTTPRequest.
nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan);
if (httpChan) {
// Copy the method.
nsAutoCString method;
mRequest->GetMethod(method);
rv = httpChan->SetRequestMethod(method);
@ -363,39 +373,53 @@ FetchDriver::HttpNetworkFetch()
return rv;
}
// Set the same headers.
nsAutoTArray<InternalHeaders::Entry, 5> headers;
mRequest->Headers()->GetEntries(headers);
for (uint32_t i = 0; i < headers.Length(); ++i) {
httpChan->SetRequestHeader(headers[i].mName, headers[i].mValue, false /* merge */);
}
// Step 2. Set the referrer. This is handled better in Bug 1112922.
MOZ_ASSERT(mRequest->ReferrerIsURL());
nsCString referrer = mRequest->ReferrerAsURL();
if (!referrer.IsEmpty()) {
nsCOMPtr<nsIURI> uri;
rv = NS_NewURI(getter_AddRefs(uri), referrer, nullptr, nullptr, ios);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
return FailWithNetworkError();
}
rv = httpChan->SetReferrer(uri);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
return FailWithNetworkError();
}
}
// Step 3 "If HTTPRequest's force Origin header flag is set..."
if (mRequest->ForceOriginHeader()) {
nsAutoString origin;
rv = nsContentUtils::GetUTFOrigin(mPrincipal, origin);
if (NS_WARN_IF(NS_FAILED(rv))) {
FailWithNetworkError();
return rv;
return FailWithNetworkError();
}
httpChan->SetRequestHeader(NS_LITERAL_CSTRING("origin"),
NS_ConvertUTF16toUTF8(origin),
false /* merge */);
}
// Bug 1120722 - Authorization will be handled later.
// Auth may require prompting, we don't support it yet.
// The next patch in this same bug prevents this from aborting the request.
// Credentials checks for CORS are handled by nsCORSListenerProxy,
if (useCredentials) {
return FailWithNetworkError();
}
}
// Step 5. Proxy authentication will be handled by Necko.
// FIXME(nsm): Bug 1120715.
// Step 7-10. "If request's cache mode is neither no-store nor reload..."
// Continue setting up 'HTTPRequest'. Content-Type and body data.
nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(chan);
if (uploadChan) {
nsAutoCString contentType;
@ -404,7 +428,7 @@ FetchDriver::HttpNetworkFetch()
// This is an error because the Request constructor explicitly extracts and
// sets a content-type per spec.
if (result.Failed()) {
return result.ErrorCode();
return FailWithNetworkError();
}
nsCOMPtr<nsIInputStream> bodyStream;
@ -414,11 +438,47 @@ FetchDriver::HttpNetworkFetch()
mRequest->GetMethod(method);
rv = uploadChan->ExplicitSetUploadStream(bodyStream, contentType, -1, method, false /* aStreamHasHeaders */);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
return FailWithNetworkError();
}
}
}
return chan->AsyncOpen(this, nullptr);
// Set up a CORS proxy that will handle the various requirements of the CORS
// protocol. It handles the preflight cache and CORS response headers.
// If the request is allowed, it will start our original request
// and our observer will be notified. On failure, our observer is notified
// directly.
nsRefPtr<nsCORSListenerProxy> corsListener =
new nsCORSListenerProxy(this, mPrincipal, useCredentials);
rv = corsListener->Init(chan, true /* allow data uri */);
if (NS_WARN_IF(NS_FAILED(rv))) {
return FailWithNetworkError();
}
// If preflight is required, start a "CORS preflight fetch"
// https://fetch.spec.whatwg.org/#cors-preflight-fetch-0. All the
// implementation is handled by NS_StartCORSPreflight, we just set up the
// unsafeHeaders so they can be verified against the response's
// "Access-Control-Allow-Headers" header.
if (aCORSPreflightFlag) {
nsCOMPtr<nsIChannel> preflightChannel;
nsAutoTArray<nsCString, 5> unsafeHeaders;
mRequest->Headers()->GetUnsafeHeaders(unsafeHeaders);
rv = NS_StartCORSPreflight(chan, corsListener, mPrincipal,
useCredentials,
unsafeHeaders,
getter_AddRefs(preflightChannel));
} else {
rv = chan->AsyncOpen(corsListener, nullptr);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return FailWithNetworkError();
}
// Step 4 onwards of "HTTP Fetch" is handled internally by Necko.
return NS_OK;
}
nsresult
@ -457,6 +517,7 @@ FetchDriver::BeginAndGetFilteredResponse(InternalResponse* aResponse)
}
MOZ_ASSERT(filteredResponse);
MOZ_ASSERT(mObserver);
mObserver->OnResponseAvailable(filteredResponse);
mResponseAvailableCalled = true;
return filteredResponse.forget();
@ -472,18 +533,25 @@ FetchDriver::BeginResponse(InternalResponse* aResponse)
nsresult
FetchDriver::SucceedWithResponse()
{
mObserver->OnResponseEnd();
workers::AssertIsOnMainThread();
if (mObserver) {
mObserver->OnResponseEnd();
mObserver = nullptr;
}
return NS_OK;
}
nsresult
FetchDriver::FailWithNetworkError()
{
MOZ_ASSERT(mObserver);
workers::AssertIsOnMainThread();
nsRefPtr<InternalResponse> error = InternalResponse::NetworkError();
mObserver->OnResponseAvailable(error);
mResponseAvailableCalled = true;
mObserver->OnResponseEnd();
if (mObserver) {
mObserver->OnResponseAvailable(error);
mResponseAvailableCalled = true;
mObserver->OnResponseEnd();
mObserver = nullptr;
}
return NS_OK;
}
@ -517,7 +585,9 @@ NS_IMETHODIMP
FetchDriver::OnStartRequest(nsIRequest* aRequest,
nsISupports* aContext)
{
workers::AssertIsOnMainThread();
MOZ_ASSERT(!mPipeOutputStream);
MOZ_ASSERT(mObserver);
nsresult rv;
aRequest->GetStatus(&rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
@ -607,8 +677,10 @@ FetchDriver::OnStopRequest(nsIRequest* aRequest,
nsISupports* aContext,
nsresult aStatusCode)
{
MOZ_ASSERT(mPipeOutputStream);
mPipeOutputStream->Close();
workers::AssertIsOnMainThread();
if (mPipeOutputStream) {
mPipeOutputStream->Close();
}
if (NS_FAILED(aStatusCode)) {
FailWithNetworkError();

View File

@ -65,10 +65,7 @@ private:
nsresult Fetch(bool aCORSFlag);
nsresult ContinueFetch(bool aCORSFlag);
nsresult BasicFetch();
nsresult HttpFetch(bool aCORSFlag = false, bool aPreflightCORSFlag = false, bool aAuthenticationFlag = false);
nsresult ContinueHttpFetchAfterServiceWorker();
nsresult ContinueHttpFetchAfterCORSPreflight();
nsresult HttpNetworkFetch();
nsresult HttpFetch(bool aCORSFlag = false, bool aCORSPreflightFlag = false, bool aAuthenticationFlag = false);
nsresult ContinueHttpFetchAfterNetworkFetch();
// Returns the filtered response sent to the observer.
already_AddRefed<InternalResponse>

View File

@ -303,10 +303,28 @@ InternalHeaders::CORSHeaders(InternalHeaders* aHeaders)
nsRefPtr<InternalHeaders> cors = new InternalHeaders(aHeaders->mGuard);
ErrorResult result;
nsAutoTArray<nsCString, 1> acExposedNames;
aHeaders->GetAll(NS_LITERAL_CSTRING("Access-Control-Expose-Headers"), acExposedNames, result);
nsAutoCString acExposedNames;
aHeaders->Get(NS_LITERAL_CSTRING("Access-Control-Expose-Headers"), acExposedNames, result);
MOZ_ASSERT(!result.Failed());
nsAutoTArray<nsCString, 5> exposeNamesArray;
nsCCharSeparatedTokenizer exposeTokens(acExposedNames, ',');
while (exposeTokens.hasMoreTokens()) {
const nsDependentCSubstring& token = exposeTokens.nextToken();
if (token.IsEmpty()) {
continue;
}
if (!NS_IsValidHTTPToken(token)) {
NS_WARNING("Got invalid HTTP token in Access-Control-Expose-Headers. Header value is:");
NS_WARNING(acExposedNames.get());
exposeNamesArray.Clear();
break;
}
exposeNamesArray.AppendElement(token);
}
nsCaseInsensitiveCStringArrayComparator comp;
for (uint32_t i = 0; i < aHeaders->mList.Length(); ++i) {
const Entry& entry = aHeaders->mList[i];
@ -316,7 +334,7 @@ InternalHeaders::CORSHeaders(InternalHeaders* aHeaders)
entry.mName.EqualsASCII("expires") ||
entry.mName.EqualsASCII("last-modified") ||
entry.mName.EqualsASCII("pragma") ||
acExposedNames.Contains(entry.mName, comp)) {
exposeNamesArray.Contains(entry.mName, comp)) {
cors->Append(entry.mName, entry.mValue, result);
MOZ_ASSERT(!result.Failed());
}
@ -331,5 +349,17 @@ InternalHeaders::GetEntries(nsTArray<InternalHeaders::Entry>& aEntries) const
MOZ_ASSERT(aEntries.IsEmpty());
aEntries.AppendElements(mList);
}
void
InternalHeaders::GetUnsafeHeaders(nsTArray<nsCString>& aNames) const
{
MOZ_ASSERT(aNames.IsEmpty());
for (uint32_t i = 0; i < mList.Length(); ++i) {
const Entry& header = mList[i];
if (!InternalHeaders::IsSimpleHeader(header.mName, header.mValue)) {
aNames.AppendElement(header.mName);
}
}
}
} // namespace dom
} // namespace mozilla

View File

@ -89,11 +89,12 @@ public:
void
GetEntries(nsTArray<InternalHeaders::Entry>& aEntries) const;
void
GetUnsafeHeaders(nsTArray<nsCString>& aNames) const;
private:
virtual ~InternalHeaders();
static bool IsSimpleHeader(const nsACString& aName,
const nsACString& aValue);
static bool IsInvalidName(const nsACString& aName, ErrorResult& aRv);
static bool IsInvalidValue(const nsACString& aValue, ErrorResult& aRv);
bool IsImmutable(ErrorResult& aRv) const;
@ -120,6 +121,9 @@ private:
IsForbiddenRequestNoCorsHeader(aName, aValue) ||
IsForbiddenResponseHeader(aName);
}
static bool IsSimpleHeader(const nsACString& aName,
const nsACString& aValue);
};
} // namespace dom

View File

@ -25,9 +25,16 @@ InternalRequest::GetRequestConstructorCopy(nsIGlobalObject* aGlobal, ErrorResult
copy->mURL.Assign(mURL);
copy->SetMethod(mMethod);
copy->mHeaders = new InternalHeaders(*mHeaders);
copy->SetUnsafeRequest();
copy->mBodyStream = mBodyStream;
copy->mForceOriginHeader = true;
// The "client" is not stored in our implementation. Fetch API users should
// use the appropriate window/document/principal and other Gecko security
// mechanisms as appropriate.
copy->mSameOriginDataURL = true;
copy->mPreserveContentCodings = true;
// The default referrer is already about:client.
copy->mContext = nsIContentPolicy::TYPE_FETCH;
copy->mMode = mMode;

View File

@ -184,6 +184,12 @@ public:
mMode = aMode;
}
RequestCredentials
GetCredentialsMode() const
{
return mCredentialsMode;
}
void
SetCredentialsMode(RequestCredentials aCredentialsMode)
{
@ -220,6 +226,12 @@ public:
return mUnsafeRequest;
}
void
SetUnsafeRequest()
{
mUnsafeRequest = true;
}
InternalHeaders*
Headers()
{

View File

@ -4,8 +4,10 @@ support-files =
test_headers_mainthread.js
worker_test_fetch_basic.js
worker_test_fetch_basic_http.js
worker_test_fetch_cors.js
worker_wrapper.js
[test_headers.html]
[test_fetch_basic.html]
[test_fetch_basic_http.html]
[test_fetch_cors.html]

View File

@ -0,0 +1,57 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<!DOCTYPE HTML>
<html>
<head>
<title>Bug 1039846 - Test fetch() CORS mode</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
var worker;
function testOnWorker(done) {
ok(true, "=== Start Worker Tests ===");
worker = new Worker("worker_test_fetch_cors.js");
worker.onmessage = function(event) {
if (event.data.type == "finish") {
ok(true, "=== Finish Worker Tests ===");
done();
} else if (event.data.type == "status") {
ok(event.data.status, event.data.msg);
}
}
worker.onerror = function(event) {
ok(false, "Worker had an error: " + event.message);
ok(true, "=== Finish Worker Tests ===");
done();
};
worker.postMessage("start");
}
//
// Driver
//
SpecialPowers.pushPrefEnv({"set": [
["dom.fetch.enabled", true]
]}, function() {
testOnWorker(function() {
SimpleTest.finish();
});
});
</script>
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,749 @@
if (typeof ok !== "function") {
function ok(a, msg) {
postMessage({type: 'status', status: !!a, msg: a + ": " + msg });
}
}
if (typeof is !== "function") {
function is(a, b, msg) {
postMessage({type: 'status', status: a === b, msg: a + " === " + b + ": " + msg });
}
}
var path = "/tests/dom/base/test/";
function isNetworkError(response) {
return response.type == "error" && response.status === 0 && response.statusText === "";
}
function isOpaqueResponse(response) {
return response.type == "opaque" && response.status === 0 && response.statusText === "";
}
function testModeSameOrigin() {
// Fetch spec Section 4, step 4, "request's mode is same-origin".
var req = new Request("http://example.com", { mode: "same-origin" });
return fetch(req).then(function(res) {
ok(isNetworkError(res), "Attempting to fetch a resource from a different origin with mode same-origin should fail.");
});
}
function testNoCorsCtor() {
// Request constructor Step 19.1
var simpleMethods = ["GET", "HEAD", "POST"];
for (var i = 0; i < simpleMethods.length; ++i) {
var r = new Request("http://example.com", { method: simpleMethods[i], mode: "no-cors" });
ok(true, "no-cors Request with simple method " + simpleMethods[i] + " is allowed.");
}
var otherMethods = ["DELETE", "OPTIONS", "PUT"];
for (var i = 0; i < otherMethods.length; ++i) {
try {
var r = new Request("http://example.com", { method: otherMethods[i], mode: "no-cors" });
ok(false, "no-cors Request with non-simple method " + otherMethods[i] + " is not allowed.");
} catch(e) {
ok(true, "no-cors Request with non-simple method " + otherMethods[i] + " is not allowed.");
}
}
// Request constructor Step 19.2, check guarded headers.
var r = new Request(".", { mode: "no-cors" });
r.headers.append("Content-Type", "multipart/form-data");
is(r.headers.get("content-type"), "multipart/form-data", "Appending simple header should succeed");
r.headers.append("custom", "value");
ok(!r.headers.has("custom"), "Appending custom header should fail");
r.headers.append("DNT", "value");
ok(!r.headers.has("DNT"), "Appending forbidden header should fail");
}
var corsServerPath = "/tests/dom/base/test/file_CrossSiteXHR_server.sjs?";
function testModeNoCors() {
// Fetch spec, section 4, step 4, response tainting should be set opaque, so
// that fetching leads to an opaque filtered response in step 8.
var r = new Request("http://example.com" + corsServerPath + "status=200&allowOrigin=*", { mode: "no-cors" });
return fetch(r).then(function(res) {
ok(isOpaqueResponse(res), "no-cors Request fetch should result in opaque response");
});
}
function testModeCors() {
var tests = [// Plain request
{ pass: 1,
method: "GET",
noAllowPreflight: 1,
},
// undefined username
{ pass: 1,
method: "GET",
noAllowPreflight: 1,
username: undefined
},
// undefined username and password
{ pass: 1,
method: "GET",
noAllowPreflight: 1,
username: undefined,
password: undefined
},
// nonempty username
{ pass: 0,
method: "GET",
noAllowPreflight: 1,
username: "user",
},
// nonempty password
// XXXbz this passes for now, because we ignore passwords
// without usernames in most cases.
{ pass: 1,
method: "GET",
noAllowPreflight: 1,
password: "password",
},
// Default allowed headers
{ pass: 1,
method: "GET",
headers: { "Content-Type": "text/plain",
"Accept": "foo/bar",
"Accept-Language": "sv-SE" },
noAllowPreflight: 1,
},
{ pass: 0,
method: "GET",
headers: { "Content-Type": "foo/bar",
"Accept": "foo/bar",
"Accept-Language": "sv-SE" },
noAllowPreflight: 1,
},
// Custom headers
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue" },
allowHeaders: "x-my-header",
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue" },
allowHeaders: "X-My-Header",
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue",
"long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header": "secondValue" },
allowHeaders: "x-my-header, long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header",
},
{ pass: 1,
method: "GET",
headers: { "x-my%-header": "myValue" },
allowHeaders: "x-my%-header",
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "myValue" },
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "" },
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "myValue" },
allowHeaders: "",
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "myValue" },
allowHeaders: "y-my-header",
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "myValue" },
allowHeaders: "x-my-header y-my-header",
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "myValue" },
allowHeaders: "x-my-header, y-my-header z",
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "myValue" },
allowHeaders: "x-my-header, y-my-he(ader",
},
{ pass: 0,
method: "GET",
headers: { "myheader": "" },
allowMethods: "myheader",
},
// Multiple custom headers
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue",
"second-header": "secondValue",
"third-header": "thirdValue" },
allowHeaders: "x-my-header, second-header, third-header",
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue",
"second-header": "secondValue",
"third-header": "thirdValue" },
allowHeaders: "x-my-header,second-header,third-header",
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue",
"second-header": "secondValue",
"third-header": "thirdValue" },
allowHeaders: "x-my-header ,second-header ,third-header",
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue",
"second-header": "secondValue",
"third-header": "thirdValue" },
allowHeaders: "x-my-header , second-header , third-header",
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue",
"second-header": "secondValue" },
allowHeaders: ", x-my-header, , ,, second-header, , ",
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "myValue",
"second-header": "secondValue" },
allowHeaders: "x-my-header, second-header, unused-header",
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "myValue",
"y-my-header": "secondValue" },
allowHeaders: "x-my-header",
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "",
"y-my-header": "" },
allowHeaders: "x-my-header",
},
// HEAD requests
{ pass: 1,
method: "HEAD",
noAllowPreflight: 1,
},
// HEAD with safe headers
{ pass: 1,
method: "HEAD",
headers: { "Content-Type": "text/plain",
"Accept": "foo/bar",
"Accept-Language": "sv-SE" },
noAllowPreflight: 1,
},
{ pass: 0,
method: "HEAD",
headers: { "Content-Type": "foo/bar",
"Accept": "foo/bar",
"Accept-Language": "sv-SE" },
noAllowPreflight: 1,
},
// HEAD with custom headers
{ pass: 1,
method: "HEAD",
headers: { "x-my-header": "myValue" },
allowHeaders: "x-my-header",
},
{ pass: 0,
method: "HEAD",
headers: { "x-my-header": "myValue" },
},
{ pass: 0,
method: "HEAD",
headers: { "x-my-header": "myValue" },
allowHeaders: "",
},
{ pass: 0,
method: "HEAD",
headers: { "x-my-header": "myValue" },
allowHeaders: "y-my-header",
},
{ pass: 0,
method: "HEAD",
headers: { "x-my-header": "myValue" },
allowHeaders: "x-my-header y-my-header",
},
// POST tests
{ pass: 1,
method: "POST",
body: "hi there",
noAllowPreflight: 1,
},
{ pass: 1,
method: "POST",
},
{ pass: 1,
method: "POST",
noAllowPreflight: 1,
},
// POST with standard headers
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain" },
noAllowPreflight: 1,
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "multipart/form-data" },
noAllowPreflight: 1,
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
noAllowPreflight: 1,
},
{ pass: 0,
method: "POST",
body: "hi there",
headers: { "Content-Type": "foo/bar" },
},
{ pass: 0,
method: "POST",
headers: { "Content-Type": "foo/bar" },
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain",
"Accept": "foo/bar",
"Accept-Language": "sv-SE" },
noAllowPreflight: 1,
},
// POST with custom headers
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Accept": "foo/bar",
"Accept-Language": "sv-SE",
"x-my-header": "myValue" },
allowHeaders: "x-my-header",
},
{ pass: 1,
method: "POST",
headers: { "Content-Type": "text/plain",
"x-my-header": "myValue" },
allowHeaders: "x-my-header",
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "text/plain",
"x-my-header": "myValue" },
allowHeaders: "x-my-header",
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "foo/bar",
"x-my-header": "myValue" },
allowHeaders: "x-my-header, content-type",
},
{ pass: 0,
method: "POST",
body: "hi there",
headers: { "Content-Type": "foo/bar" },
noAllowPreflight: 1,
},
{ pass: 0,
method: "POST",
body: "hi there",
headers: { "Content-Type": "foo/bar",
"x-my-header": "myValue" },
allowHeaders: "x-my-header",
},
{ pass: 1,
method: "POST",
headers: { "x-my-header": "myValue" },
allowHeaders: "x-my-header",
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "x-my-header": "myValue" },
allowHeaders: "x-my-header, $_%",
},
// Other methods
{ pass: 1,
method: "DELETE",
allowMethods: "DELETE",
},
{ pass: 0,
method: "DELETE",
allowHeaders: "DELETE",
},
{ pass: 0,
method: "DELETE",
},
{ pass: 0,
method: "DELETE",
allowMethods: "",
},
{ pass: 1,
method: "DELETE",
allowMethods: "POST, PUT, DELETE",
},
{ pass: 1,
method: "DELETE",
allowMethods: "POST, DELETE, PUT",
},
{ pass: 1,
method: "DELETE",
allowMethods: "DELETE, POST, PUT",
},
{ pass: 1,
method: "DELETE",
allowMethods: "POST ,PUT ,DELETE",
},
{ pass: 1,
method: "DELETE",
allowMethods: "POST,PUT,DELETE",
},
{ pass: 1,
method: "DELETE",
allowMethods: "POST , PUT , DELETE",
},
{ pass: 1,
method: "DELETE",
allowMethods: " ,, PUT ,, , , DELETE , ,",
},
{ pass: 0,
method: "DELETE",
allowMethods: "PUT",
},
{ pass: 0,
method: "DELETE",
allowMethods: "DELETEZ",
},
{ pass: 0,
method: "DELETE",
allowMethods: "DELETE PUT",
},
{ pass: 0,
method: "DELETE",
allowMethods: "DELETE, PUT Z",
},
{ pass: 0,
method: "DELETE",
allowMethods: "DELETE, PU(T",
},
{ pass: 0,
method: "DELETE",
allowMethods: "PUT DELETE",
},
{ pass: 0,
method: "DELETE",
allowMethods: "PUT Z, DELETE",
},
{ pass: 0,
method: "DELETE",
allowMethods: "PU(T, DELETE",
},
{ pass: 0,
method: "PUT",
allowMethods: "put",
},
// Status messages
{ pass: 1,
method: "GET",
noAllowPreflight: 1,
status: 404,
statusMessage: "nothin' here",
},
{ pass: 1,
method: "GET",
noAllowPreflight: 1,
status: 401,
statusMessage: "no can do",
},
{ pass: 1,
method: "POST",
body: "hi there",
headers: { "Content-Type": "foo/bar" },
allowHeaders: "content-type",
status: 500,
statusMessage: "server boo",
},
{ pass: 1,
method: "GET",
noAllowPreflight: 1,
status: 200,
statusMessage: "Yes!!",
},
{ pass: 0,
method: "GET",
headers: { "x-my-header": "header value" },
allowHeaders: "x-my-header",
preflightStatus: 400
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "header value" },
allowHeaders: "x-my-header",
preflightStatus: 200
},
{ pass: 1,
method: "GET",
headers: { "x-my-header": "header value" },
allowHeaders: "x-my-header",
preflightStatus: 204
},
// exposed headers
{ pass: 1,
method: "GET",
responseHeaders: { "x-my-header": "x header" },
exposeHeaders: "x-my-header",
expectedResponseHeaders: ["x-my-header"],
},
{ pass: 0,
method: "GET",
origin: "http://invalid",
responseHeaders: { "x-my-header": "x header" },
exposeHeaders: "x-my-header",
expectedResponseHeaders: [],
},
{ pass: 1,
method: "GET",
responseHeaders: { "x-my-header": "x header" },
expectedResponseHeaders: [],
},
{ pass: 1,
method: "GET",
responseHeaders: { "x-my-header": "x header" },
exposeHeaders: "x-my-header y",
expectedResponseHeaders: [],
},
{ pass: 1,
method: "GET",
responseHeaders: { "x-my-header": "x header" },
exposeHeaders: "y x-my-header",
expectedResponseHeaders: [],
},
{ pass: 1,
method: "GET",
responseHeaders: { "x-my-header": "x header" },
exposeHeaders: "x-my-header, y-my-header z",
expectedResponseHeaders: [],
},
{ pass: 1,
method: "GET",
responseHeaders: { "x-my-header": "x header" },
exposeHeaders: "x-my-header, y-my-hea(er",
expectedResponseHeaders: [],
},
{ pass: 1,
method: "GET",
responseHeaders: { "x-my-header": "x header",
"y-my-header": "y header" },
exposeHeaders: " , ,,y-my-header,z-my-header, ",
expectedResponseHeaders: ["y-my-header"],
},
{ pass: 1,
method: "GET",
responseHeaders: { "Cache-Control": "cacheControl header",
"Content-Language": "contentLanguage header",
"Expires":"expires header",
"Last-Modified":"lastModified header",
"Pragma":"pragma header",
"Unexpected":"unexpected header" },
expectedResponseHeaders: ["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"],
},
// Check that sending a body in the OPTIONS response works
{ pass: 1,
method: "DELETE",
allowMethods: "DELETE",
preflightBody: "I'm a preflight response body",
},
];
var baseURL = "http://example.org" + corsServerPath;
var origin = "http://mochi.test:8888";
var fetches = [];
for (test of tests) {
var req = {
url: baseURL + "allowOrigin=" + escape(test.origin || origin),
method: test.method,
headers: test.headers,
uploadProgress: test.uploadProgress,
body: test.body,
responseHeaders: test.responseHeaders,
};
if (test.pass) {
req.url += "&origin=" + escape(origin) +
"&requestMethod=" + test.method;
}
if ("username" in test) {
var u = new URL(req.url);
u.username = test.username || "";
req.url = u.href;
}
if ("password" in test) {
var u = new URL(req.url);
u.password = test.password || "";
req.url = u.href;
}
if (test.noAllowPreflight)
req.url += "&noAllowPreflight";
if (test.pass && "headers" in test) {
function isUnsafeHeader(name) {
lName = name.toLowerCase();
return lName != "accept" &&
lName != "accept-language" &&
(lName != "content-type" ||
["text/plain",
"multipart/form-data",
"application/x-www-form-urlencoded"]
.indexOf(test.headers[name].toLowerCase()) == -1);
}
req.url += "&headers=" + escape(test.headers.toSource());
reqHeaders =
escape([name for (name in test.headers)]
.filter(isUnsafeHeader)
.map(String.toLowerCase)
.sort()
.join(","));
req.url += reqHeaders ? "&requestHeaders=" + reqHeaders : "";
}
if ("allowHeaders" in test)
req.url += "&allowHeaders=" + escape(test.allowHeaders);
if ("allowMethods" in test)
req.url += "&allowMethods=" + escape(test.allowMethods);
if (test.body)
req.url += "&body=" + escape(test.body);
if (test.status) {
req.url += "&status=" + test.status;
req.url += "&statusMessage=" + escape(test.statusMessage);
}
if (test.preflightStatus)
req.url += "&preflightStatus=" + test.preflightStatus;
if (test.responseHeaders)
req.url += "&responseHeaders=" + escape(test.responseHeaders.toSource());
if (test.exposeHeaders)
req.url += "&exposeHeaders=" + escape(test.exposeHeaders);
if (test.preflightBody)
req.url += "&preflightBody=" + escape(test.preflightBody);
var request = new Request(req.url, { method: req.method, mode: "cors",
headers: req.headers, body: req.body });
fetches.push((function(test, request) {
return fetch(request).then(function(res) {
dump("Response for " + request.url + "\n");
if (test.pass) {
ok(!isNetworkError(res),
"shouldn't have failed in test for " + test.toSource());
if (test.status) {
is(res.status, test.status, "wrong status in test for " + test.toSource());
is(res.statusText, test.statusMessage, "wrong status text for " + test.toSource());
}
else {
is(res.status, 200, "wrong status in test for " + test.toSource());
is(res.statusText, "OK", "wrong status text for " + test.toSource());
}
if (test.responseHeaders) {
for (header in test.responseHeaders) {
if (test.expectedResponseHeaders.indexOf(header) == -1) {
is(res.headers.has(header), false,
"|Headers.has()|wrong response header (" + header + ") in test for " +
test.toSource());
}
else {
is(res.headers.get(header), test.responseHeaders[header],
"|Headers.get()|wrong response header (" + header + ") in test for " +
test.toSource());
}
}
}
return res.text().then(function(v) {
if (test.method !== "HEAD") {
is(v, "<res>hello pass</res>\n",
"wrong responseText in test for " + test.toSource());
}
else {
is(v, "",
"wrong responseText in HEAD test for " + test.toSource());
}
});
}
else {
ok(isNetworkError(res),
"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());
if (test.responseHeaders) {
for (header in test.responseHeaders) {
is(res.headers.get(header), null,
"wrong response header (" + header + ") in test for " +
test.toSource());
}
}
return res.text().then(function(v) {
is(v, "",
"wrong responseText in test for " + test.toSource());
});
}
});
})(test, request));
}
return Promise.all(fetches);
}
function runTest() {
var done = function() {
if (typeof SimpleTest === "object") {
SimpleTest.finish();
} else {
postMessage({ type: 'finish' });
}
}
testNoCorsCtor();
Promise.resolve()
.then(testModeSameOrigin)
.then(testModeNoCors)
.then(testModeCors)
// Put more promise based tests here.
.then(done)
.catch(function(e) {
ok(false, "Some test failed " + e);
done();
});
}
onmessage = runTest;