Implement cookieless requests for Access-Control. b=389508 r=biesi sr=jst

This commit is contained in:
Jonas Sicking 2008-09-30 17:52:52 -07:00
parent 7a655650a9
commit cce6fbac9b
12 changed files with 292 additions and 21 deletions

View File

@ -101,7 +101,7 @@ interface nsIXMLHttpRequestUpload : nsIXMLHttpRequestEventTarget {
* you're aware of all the security implications. And then think twice about
* it.
*/
[scriptable, uuid(acda85ab-d06c-4176-b834-6d129ca97ca3)]
[scriptable, uuid(48ce10a0-c585-4e8f-a5f5-1ac1e47cc501)]
interface nsIXMLHttpRequest : nsISupports
{
/**
@ -321,6 +321,16 @@ interface nsIXMLHttpRequest : nsISupports
*/
attribute boolean mozBackgroundRequest;
/**
* When set to true attempts to make cross-site Access-Control requests
* with credentials such as cookies and authorization headers.
*
* Never affects same-site requests.
*
* Defaults to false.
*/
attribute boolean withCredentials;
/**
* Initialize the object for use from C++ code with the principal, script
* context, and owner window that should be used.

View File

@ -63,9 +63,11 @@ NS_IMPL_ISUPPORTS4(nsCrossSiteListenerProxy, nsIStreamListener,
nsCrossSiteListenerProxy::nsCrossSiteListenerProxy(nsIStreamListener* aOuter,
nsIPrincipal* aRequestingPrincipal,
nsIChannel* aChannel,
PRBool aWithCredentials,
nsresult* aResult)
: mOuterListener(aOuter),
mRequestingPrincipal(aRequestingPrincipal),
mWithCredentials(aWithCredentials),
mRequestApproved(PR_FALSE),
mHasBeenCrossSite(PR_FALSE),
mIsPreflight(PR_FALSE)
@ -80,11 +82,13 @@ nsCrossSiteListenerProxy::nsCrossSiteListenerProxy(nsIStreamListener* aOuter,
nsCrossSiteListenerProxy::nsCrossSiteListenerProxy(nsIStreamListener* aOuter,
nsIPrincipal* aRequestingPrincipal,
nsIChannel* aChannel,
PRBool aWithCredentials,
const nsCString& aPreflightMethod,
const nsTArray<nsCString>& aPreflightHeaders,
nsresult* aResult)
: mOuterListener(aOuter),
mRequestingPrincipal(aRequestingPrincipal),
mWithCredentials(aWithCredentials),
mRequestApproved(PR_FALSE),
mHasBeenCrossSite(PR_FALSE),
mIsPreflight(PR_TRUE),
@ -219,7 +223,7 @@ nsCrossSiteListenerProxy::CheckRequestApproved(nsIRequest* aRequest)
NS_LITERAL_CSTRING("Access-Control-Allow-Origin"), allowedOriginHeader);
NS_ENSURE_SUCCESS(rv, rv);
if (!allowedOriginHeader.EqualsLiteral("*")) {
if (mWithCredentials || !allowedOriginHeader.EqualsLiteral("*")) {
nsCAutoString origin;
rv = GetOrigin(mRequestingPrincipal, origin);
NS_ENSURE_SUCCESS(rv, rv);
@ -229,6 +233,17 @@ nsCrossSiteListenerProxy::CheckRequestApproved(nsIRequest* aRequest)
}
}
// Check Access-Control-Allow-Credentials header
if (mWithCredentials) {
nsCAutoString allowCredentialsHeader;
rv = http->GetResponseHeader(
NS_LITERAL_CSTRING("Access-Control-Allow-Credentials"), allowCredentialsHeader);
NS_ENSURE_SUCCESS(rv, rv);
if (!allowCredentialsHeader.EqualsLiteral("true")) {
return NS_ERROR_DOM_BAD_URI;
}
}
if (mIsPreflight) {
nsCAutoString headerVal;
@ -391,5 +406,16 @@ nsCrossSiteListenerProxy::UpdateChannel(nsIChannel* aChannel)
}
}
// Make cookie-less if needed
if (mIsPreflight || !mWithCredentials) {
nsLoadFlags flags;
rv = http->GetLoadFlags(&flags);
NS_ENSURE_SUCCESS(rv, rv);
flags |= nsIRequest::LOAD_ANONYMOUS;
rv = http->SetLoadFlags(flags);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}

View File

@ -62,10 +62,12 @@ public:
nsCrossSiteListenerProxy(nsIStreamListener* aOuter,
nsIPrincipal* aRequestingPrincipal,
nsIChannel* aChannel,
PRBool aWithCredentials,
nsresult* aResult);
nsCrossSiteListenerProxy(nsIStreamListener* aOuter,
nsIPrincipal* aRequestingPrincipal,
nsIChannel* aChannel,
PRBool aWithCredentials,
const nsCString& aPreflightMethod,
const nsTArray<nsCString>& aPreflightHeaders,
nsresult* aResult);
@ -83,6 +85,7 @@ private:
nsCOMPtr<nsIStreamListener> mOuterListener;
nsCOMPtr<nsIPrincipal> mRequestingPrincipal;
nsCOMPtr<nsIInterfaceRequestor> mOuterNotificationCallbacks;
PRBool mWithCredentials;
PRBool mRequestApproved;
PRBool mHasBeenCrossSite;
PRBool mIsPreflight;

View File

@ -221,7 +221,7 @@ nsSyncLoader::LoadDocument(nsIChannel* aChannel,
if (aLoaderPrincipal) {
listener = new nsCrossSiteListenerProxy(listener, aLoaderPrincipal,
mChannel, &rv);
mChannel, PR_FALSE, &rv);
NS_ENSURE_TRUE(listener, NS_ERROR_OUT_OF_MEMORY);
NS_ENSURE_SUCCESS(rv, rv);
}

View File

@ -127,6 +127,7 @@
#define XML_HTTP_REQUEST_MPART_HEADERS (1 << 15) // Internal
#define XML_HTTP_REQUEST_USE_XSITE_AC (1 << 16) // Internal
#define XML_HTTP_REQUEST_NEED_AC_PREFLIGHT (1 << 17) // Internal
#define XML_HTTP_REQUEST_AC_WITH_CREDENTIALS (1 << 18) // Internal
#define XML_HTTP_REQUEST_LOADSTATES \
(XML_HTTP_REQUEST_UNINITIALIZED | \
@ -293,10 +294,11 @@ public:
nsIStreamListener* aOuterListener,
nsISupports* aOuterContext,
nsIPrincipal* aReferrerPrincipal,
const nsACString& aRequestMethod)
const nsACString& aRequestMethod,
PRBool aWithCredentials)
: mOuterChannel(aOuterChannel), mOuterListener(aOuterListener),
mOuterContext(aOuterContext), mReferrerPrincipal(aReferrerPrincipal),
mRequestMethod(aRequestMethod)
mRequestMethod(aRequestMethod), mWithCredentials(aWithCredentials)
{ }
NS_DECL_ISUPPORTS
@ -313,6 +315,7 @@ private:
nsCOMPtr<nsISupports> mOuterContext;
nsCOMPtr<nsIPrincipal> mReferrerPrincipal;
nsCString mRequestMethod;
PRBool mWithCredentials;
};
NS_IMPL_ISUPPORTS4(nsACProxyListener, nsIStreamListener, nsIRequestObserver,
@ -366,7 +369,7 @@ nsACProxyListener::AddResultToCache(nsIRequest *aRequest)
nsAccessControlLRUCache::CacheEntry* entry =
nsXMLHttpRequest::sAccessControlCache->
GetEntry(uri, mReferrerPrincipal, PR_TRUE);
GetEntry(uri, mReferrerPrincipal, mWithCredentials, PR_TRUE);
if (!entry) {
return;
}
@ -886,10 +889,11 @@ nsAccessControlLRUCache::CacheEntry::CheckRequest(const nsCString& aMethod,
nsAccessControlLRUCache::CacheEntry*
nsAccessControlLRUCache::GetEntry(nsIURI* aURI,
nsIPrincipal* aPrincipal,
PRBool aWithCredentials,
PRBool aCreate)
{
nsCString key;
if (!GetCacheKey(aURI, aPrincipal, key)) {
if (!GetCacheKey(aURI, aPrincipal, aWithCredentials, key)) {
NS_WARNING("Invalid cache key!");
return nsnull;
}
@ -983,6 +987,7 @@ nsAccessControlLRUCache::RemoveExpiredEntries(const nsACString& aKey,
/* static */ PRBool
nsAccessControlLRUCache::GetCacheKey(nsIURI* aURI,
nsIPrincipal* aPrincipal,
PRBool aWithCredentials,
nsACString& _retval)
{
NS_ASSERTION(aURI, "Null uri!");
@ -1002,11 +1007,20 @@ nsAccessControlLRUCache::GetCacheKey(nsIURI* aURI,
port.AppendInt(portInt);
}
nsCAutoString cred;
if (aWithCredentials) {
_retval.AssignLiteral("cred");
}
else {
_retval.AssignLiteral("nocred");
}
nsCAutoString spec;
rv = aURI->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, PR_FALSE);
_retval.Assign(scheme + space + host + space + port + space + spec);
_retval.Assign(cred + space + scheme + space + host + space + port + space +
spec);
return PR_TRUE;
}
@ -2572,6 +2586,8 @@ nsXMLHttpRequest::Send(nsIVariant *aBody)
rv = CheckChannelForCrossSiteRequest(mChannel);
NS_ENSURE_SUCCESS(rv, rv);
PRBool withCredentials = !!(mState & XML_HTTP_REQUEST_AC_WITH_CREDENTIALS);
if (mState & XML_HTTP_REQUEST_USE_XSITE_AC) {
// Check if we need to do a preflight request.
NS_ENSURE_TRUE(httpChannel, NS_ERROR_DOM_BAD_URI);
@ -2610,7 +2626,7 @@ nsXMLHttpRequest::Send(nsIVariant *aBody)
nsAccessControlLRUCache::CacheEntry* entry =
sAccessControlCache ?
sAccessControlCache->GetEntry(uri, mPrincipal, PR_FALSE) :
sAccessControlCache->GetEntry(uri, mPrincipal, withCredentials, PR_FALSE) :
nsnull;
if (!entry || !entry->CheckRequest(method, mACUnsafeHeaders)) {
@ -2653,7 +2669,7 @@ nsXMLHttpRequest::Send(nsIVariant *aBody)
// Always create a nsCrossSiteListenerProxy here even if it's
// a same-origin request right now, since it could be redirected.
listener = new nsCrossSiteListenerProxy(listener, mPrincipal, mChannel,
&rv);
withCredentials, &rv);
NS_ENSURE_TRUE(listener, NS_ERROR_OUT_OF_MEMORY);
NS_ENSURE_SUCCESS(rv, rv);
}
@ -2689,12 +2705,14 @@ nsXMLHttpRequest::Send(nsIVariant *aBody)
// a GET request to the same URI. Set that up if needed
if (mACGetChannel) {
nsCOMPtr<nsIStreamListener> acProxyListener =
new nsACProxyListener(mChannel, listener, nsnull, mPrincipal, method);
new nsACProxyListener(mChannel, listener, nsnull, mPrincipal, method,
withCredentials);
NS_ENSURE_TRUE(acProxyListener, NS_ERROR_OUT_OF_MEMORY);
acProxyListener =
new nsCrossSiteListenerProxy(acProxyListener, mPrincipal, mACGetChannel,
method, mACUnsafeHeaders, &rv);
withCredentials, method, mACUnsafeHeaders,
&rv);
NS_ENSURE_TRUE(acProxyListener, NS_ERROR_OUT_OF_MEMORY);
NS_ENSURE_SUCCESS(rv, rv);
@ -2924,6 +2942,33 @@ nsXMLHttpRequest::SetMozBackgroundRequest(PRBool aMozBackgroundRequest)
return NS_OK;
}
/* attribute boolean withCredentials; */
NS_IMETHODIMP
nsXMLHttpRequest::GetWithCredentials(PRBool *_retval)
{
*_retval = !!(mState & XML_HTTP_REQUEST_AC_WITH_CREDENTIALS);
return NS_OK;
}
/* attribute boolean withCredentials; */
NS_IMETHODIMP
nsXMLHttpRequest::SetWithCredentials(PRBool aWithCredentials)
{
// Return error if we're already processing a request
if (XML_HTTP_REQUEST_SENT & mState) {
return NS_ERROR_FAILURE;
}
if (aWithCredentials) {
mState |= XML_HTTP_REQUEST_AC_WITH_CREDENTIALS;
}
else {
mState &= ~XML_HTTP_REQUEST_AC_WITH_CREDENTIALS;
}
return NS_OK;
}
// nsIDOMEventListener
nsresult

View File

@ -124,7 +124,7 @@ public:
}
CacheEntry* GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal,
PRBool aCreate);
PRBool aWithCredentials, PRBool aCreate);
void Clear();
@ -134,7 +134,7 @@ private:
void* aUserData);
static PRBool GetCacheKey(nsIURI* aURI, nsIPrincipal* aPrincipal,
nsACString& _retval);
PRBool aWithCredentials, nsACString& _retval);
nsClassHashtable<nsCStringHashKey, CacheEntry> mTable;
PRCList mList;

View File

@ -55,6 +55,9 @@ window.addEventListener('message', function(e) {
post(e, res);
}
if (req.withCred)
xhr.withCredentials = true;
res.events.push("opening");
xhr.open(req.method, req.url, true);

View File

@ -3,7 +3,7 @@ function handleRequest(request, response)
try {
var query = {};
request.queryString.split('&').forEach(function (val) {
var [name, value] = val.split('=');
[name, value] = val.split('=');
query[name] = unescape(value);
});
@ -35,6 +35,24 @@ function handleRequest(request, response)
throw "Origin had wrong value. Expected " + query.origin + " got " +
request.getHeader("Origin");
}
if ("cookie" in query) {
cookies = {};
request.getHeader("Cookie").split(/ *; */).forEach(function (val) {
[name, value] = val.split('=');
cookies[name] = unescape(value);
});
query.cookie.split(",").forEach(function (val) {
[name, value] = val.split('=');
if (cookies[name] != value) {
throw "Cookie " + name + " had wrong value. Expected " + value +
" got " + cookies[name];
}
});
}
if ("noCookie" in query && request.hasHeader("Cookie")) {
throw "Got cookies when didn't expect to";
}
// Send response
@ -42,6 +60,12 @@ function handleRequest(request, response)
if (query.allowOrigin && (!isPreflight || !query.noAllowPreflight))
response.setHeader("Access-Control-Allow-Origin", query.allowOrigin);
if (query.allowCred)
response.setHeader("Access-Control-Allow-Credentials", "true");
if (query.setCookie)
response.setHeader("Set-Cookie", query.setCookie + "; path=/");
if (isPreflight) {
if (query.allowHeaders)

View File

@ -436,6 +436,133 @@ function runTest() {
}
// Test cookie behavior
tests = [{ pass: 1,
method: "GET",
withCred: 1,
allowCred: 1,
},
{ pass: 0,
method: "GET",
withCred: 1,
allowCred: 0,
},
{ pass: 0,
method: "GET",
withCred: 1,
allowCred: 1,
origin: "*",
},
{ pass: 1,
method: "GET",
withCred: 0,
allowCred: 1,
origin: "*",
},
{ pass: 1,
method: "GET",
setCookie: "a=1",
withCred: 1,
allowCred: 1,
},
{ pass: 1,
method: "GET",
cookie: "a=1",
withCred: 1,
allowCred: 1,
},
{ pass: 1,
method: "GET",
noCookie: 1,
withCred: 0,
allowCred: 1,
},
{ pass: 0,
method: "GET",
noCookie: 1,
withCred: 1,
allowCred: 1,
},
{ pass: 1,
method: "GET",
setCookie: "a=2",
withCred: 0,
allowCred: 1,
},
{ pass: 1,
method: "GET",
cookie: "a=1",
withCred: 1,
allowCred: 1,
},
{ pass: 1,
method: "GET",
setCookie: "a=2",
withCred: 1,
allowCred: 1,
},
{ pass: 1,
method: "GET",
cookie: "a=2",
withCred: 1,
allowCred: 1,
},
];
for each(test in tests) {
req = {
url: baseURL + "allowOrigin=" + escape(test.origin || origin),
method: test.method,
headers: test.headers,
withCred: test.withCred,
};
if (test.allowCred)
req.url += "&allowCred";
if (test.setCookie)
req.url += "&setCookie=" + escape(test.setCookie);
if (test.cookie)
req.url += "&cookie=" + escape(test.cookie);
if (test.noCookie)
req.url += "&noCookie";
if ("allowHeaders" in test)
req.url += "&allowHeaders=" + escape(test.allowHeaders);
if ("allowMethods" in test)
req.url += "&allowMethods=" + escape(test.allowMethods);
loaderWindow.postMessage(req.toSource(), origin);
res = eval(yield);
if (test.pass) {
is(res.didFail, false,
"shouldn't have failed in test for " + test.toSource());
is(res.status, 200, "wrong status in test for " + test.toSource());
is(res.responseXML, "<res>hello pass</res>",
"wrong responseXML in test for " + test.toSource());
is(res.responseText, "<res>hello pass</res>\n",
"wrong responseText in test for " + test.toSource());
is(res.events.join(","),
"opening,rs1,sending,rs1,loadstart,rs2,rs3,rs4,load",
"wrong responseText in test for " + test.toSource());
}
else {
is(res.didFail, true,
"should have failed in test for " + test.toSource());
is(res.status, 0, "wrong status in test for " + test.toSource());
is(res.responseXML, null,
"wrong responseXML in test for " + test.toSource());
is(res.responseText, "",
"wrong responseText in test for " + test.toSource());
is(res.events.join(","),
"opening,rs1,sending,rs1,loadstart,rs2,rs4,error",
"wrong events in test for " + test.toSource());
is(res.progressEvents, 0,
"wrong events in test for " + test.toSource());
}
}
SimpleTest.finish();
yield;

View File

@ -539,7 +539,8 @@ txCompileObserver::startLoad(nsIURI* aUri, txStylesheetCompiler* aCompiler,
// Always install in case of redirects
nsCOMPtr<nsIStreamListener> listener =
new nsCrossSiteListenerProxy(sink, aReferrerPrincipal, channel, &rv);
new nsCrossSiteListenerProxy(sink, aReferrerPrincipal, channel,
PR_FALSE, &rv);
NS_ENSURE_TRUE(listener, NS_ERROR_OUT_OF_MEMORY);
NS_ENSURE_SUCCESS(rv, rv);

View File

@ -214,4 +214,11 @@ interface nsIRequest : nsISupports
const unsigned long VALIDATE_ALWAYS = 1 << 11;
const unsigned long VALIDATE_NEVER = 1 << 12;
const unsigned long VALIDATE_ONCE_PER_SESSION = 1 << 13;
/**
* When set, this flag indicates that no user-specific data should be added
* to the request when opened. This means that things like authorization
* tokens or cookie headers should not be added.
*/
const unsigned long LOAD_ANONYMOUS = 1 << 14;
};

View File

@ -663,6 +663,10 @@ nsHttpChannel::SetupTransaction()
void
nsHttpChannel::AddCookiesToRequest()
{
if (mLoadFlags & LOAD_ANONYMOUS) {
return;
}
nsXPIDLCString cookie;
nsICookieService *cs = gHttpHandler->GetCookieService();
@ -1746,13 +1750,22 @@ nsHttpChannel::OpenOfflineCacheEntryForWriting()
nsresult
nsHttpChannel::GenerateCacheKey(nsACString &cacheKey)
{
cacheKey.Truncate();
if (mLoadFlags & LOAD_ANONYMOUS) {
cacheKey.AssignLiteral("anon&");
}
if (mPostID) {
char buf[32];
PR_snprintf(buf, sizeof(buf), "id=%x&uri=", mPostID);
cacheKey.Assign(buf);
} else
cacheKey.Truncate();
PR_snprintf(buf, sizeof(buf), "id=%x&", mPostID);
cacheKey.Append(buf);
}
if (!cacheKey.IsEmpty()) {
cacheKey.AppendLiteral("uri=");
}
// Strip any trailing #ref from the URL before using it as the key
const char *spec = mFallbackChannel ? mFallbackKey.get() : mSpec.get();
const char *p = strchr(spec, '#');
@ -2888,6 +2901,10 @@ nsHttpChannel::ProcessAuthentication(PRUint32 httpStatus)
LOG(("nsHttpChannel::ProcessAuthentication [this=%x code=%u]\n",
this, httpStatus));
if (mLoadFlags & LOAD_ANONYMOUS) {
return NS_ERROR_NOT_AVAILABLE;
}
const char *challenges;
PRBool proxyAuth = (httpStatus == 407);
@ -3608,6 +3625,10 @@ nsHttpChannel::AddAuthorizationHeaders()
{
LOG(("nsHttpChannel::AddAuthorizationHeaders? [this=%x]\n", this));
if (mLoadFlags & LOAD_ANONYMOUS) {
return;
}
// this getter never fails
nsHttpAuthCache *authCache = gHttpHandler->AuthCache();
@ -4500,6 +4521,10 @@ nsHttpChannel::GetResponseVersion(PRUint32 *major, PRUint32 *minor)
NS_IMETHODIMP
nsHttpChannel::SetCookie(const char *aCookieHeader)
{
if (mLoadFlags & LOAD_ANONYMOUS) {
return NS_OK;
}
// empty header isn't an error
if (!(aCookieHeader && *aCookieHeader))
return NS_OK;