diff --git a/content/base/src/nsCrossSiteListenerProxy.cpp b/content/base/src/nsCrossSiteListenerProxy.cpp index 560686bbf82..ab48e85bc5a 100644 --- a/content/base/src/nsCrossSiteListenerProxy.cpp +++ b/content/base/src/nsCrossSiteListenerProxy.cpp @@ -53,110 +53,8 @@ #include "nsWhitespaceTokenizer.h" #include "nsIChannelEventSink.h" #include "nsCommaSeparatedTokenizer.h" -#include "prclist.h" -#include "nsAutoPtr.h" -#include "nsClassHashtable.h" -#include "nsHashKeys.h" -#include "prtime.h" -// ========================================================================= -// Support classes - -// Class implementing the preflight request cache -#define ACCESS_CONTROL_CACHE_SIZE 100 - -class nsACPreflightCache -{ -public: - static void Shutdown() - { - PR_INIT_CLIST(&mList); - delete mTable; - mTable = nsnull; - } - - struct TokenTime - { - nsCString token; - PRTime expirationTime; - }; - - class CacheEntry : public PRCList - { - public: - CacheEntry(nsCString& aKey) - : mKey(aKey) - { - MOZ_COUNT_CTOR(nsACPreflightCache::CacheEntry); - } - - ~CacheEntry() - { - MOZ_COUNT_DTOR(nsACPreflightCache::CacheEntry); - } - - void PurgeExpired(PRTime now); - PRBool CheckRequest(const nsCString& aMethod, - const nsTArray& aCustomHeaders); - - nsCString mKey; - nsTArray mMethods; - nsTArray mHeaders; - }; - - - static CacheEntry* GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal, - PRBool aWithCredentials, PRBool aCreate); - -private: - PR_STATIC_CALLBACK(PLDHashOperator) - RemoveExpiredEntries(const nsACString& aKey, nsAutoPtr& aValue, - void* aUserData); - - static PRBool GetCacheKey(nsIURI* aURI, nsIPrincipal* aPrincipal, - PRBool aWithCredentials, nsACString& _retval); - - static nsClassHashtable* mTable; - static PRCList mList; -}; - - -// Class used as streamlistener and notification callback when -// doing the initial GET request for an access-control check - -class nsACPreflightListener : public nsIStreamListener, - public nsIInterfaceRequestor, - public nsIChannelEventSink -{ -public: - nsACPreflightListener(nsIChannel* aOuterChannel, - nsIStreamListener* aOuterListener, - nsISupports* aOuterContext, - nsIPrincipal* aReferrerPrincipal, - PRBool aWithCredentials) - : mOuterChannel(aOuterChannel), mOuterListener(aOuterListener), - mOuterContext(aOuterContext), mReferrerPrincipal(aReferrerPrincipal), - mWithCredentials(aWithCredentials) - { } - - NS_DECL_ISUPPORTS - NS_DECL_NSISTREAMLISTENER - NS_DECL_NSIREQUESTOBSERVER - NS_DECL_NSIINTERFACEREQUESTOR - NS_DECL_NSICHANNELEVENTSINK - -private: - void AddResultToCache(nsIRequest* aRequest); - - nsCOMPtr mOuterChannel; - nsCOMPtr mOuterListener; - nsCOMPtr mOuterContext; - nsCOMPtr mReferrerPrincipal; - PRBool mWithCredentials; -}; - -// ========================================================================= -// nsCrossSiteListenerProxy +static NS_DEFINE_CID(kCParserCID, NS_PARSER_CID); NS_IMPL_ISUPPORTS4(nsCrossSiteListenerProxy, nsIStreamListener, nsIRequestObserver, nsIChannelEventSink, @@ -203,128 +101,6 @@ nsCrossSiteListenerProxy::nsCrossSiteListenerProxy(nsIStreamListener* aOuter, *aResult = UpdateChannel(aChannel); } -nsresult -nsCrossSiteListenerProxy::CheckPreflight(nsIHttpChannel* aRequestChannel, - nsIStreamListener* aRequestListener, - nsISupports* aRequestContext, - nsIPrincipal* aRequestingPrincipal, - PRBool aForcePreflight, - nsTArray& aUnsafeHeaders, - PRBool aWithCredentials, - PRBool* aPreflighted, - nsIChannel** aPreflightChannel, - nsIStreamListener** aPreflightListener) -{ - *aPreflighted = PR_FALSE; - - nsCAutoString method; - aRequestChannel->GetRequestMethod(method); - - if (!aUnsafeHeaders.IsEmpty() || aForcePreflight) { - *aPreflighted = PR_TRUE; - } - else if (method.LowerCaseEqualsLiteral("post")) { - nsCAutoString contentTypeHeader; - aRequestChannel->GetRequestHeader(NS_LITERAL_CSTRING("Content-Type"), - contentTypeHeader); - - nsCAutoString contentType, charset; - NS_ParseContentType(contentTypeHeader, contentType, charset); - if (!contentType.LowerCaseEqualsLiteral("text/plain")) { - *aPreflighted = PR_TRUE; - } - } - else if (!method.LowerCaseEqualsLiteral("get")) { - *aPreflighted = PR_TRUE; - } - - if (!*aPreflighted) { - return NS_OK; - } - - // If so, set up the preflight - - // Check to see if this initial OPTIONS request has already been cached - // in our special Access Control Cache. - nsCOMPtr uri; - nsresult rv = aRequestChannel->GetURI(getter_AddRefs(uri)); - NS_ENSURE_SUCCESS(rv, rv); - - nsACPreflightCache::CacheEntry* entry = - nsACPreflightCache::GetEntry(uri, aRequestingPrincipal, - aWithCredentials, PR_FALSE); - - if (entry && entry->CheckRequest(method, aUnsafeHeaders)) { - return NS_OK; - } - - // Either it wasn't cached or the cached result has expired. Build a - // channel for the OPTIONS request. - nsCOMPtr loadGroup; - rv = aRequestChannel->GetLoadGroup(getter_AddRefs(loadGroup)); - NS_ENSURE_SUCCESS(rv, rv); - - nsLoadFlags loadFlags; - rv = aRequestChannel->GetLoadFlags(&loadFlags); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr preflightChannel; - rv = NS_NewChannel(getter_AddRefs(preflightChannel), uri, nsnull, - loadGroup, nsnull, loadFlags); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr preflightHttp = do_QueryInterface(preflightChannel); - NS_ASSERTION(preflightHttp, "Failed to QI to nsIHttpChannel!"); - - rv = preflightHttp->SetRequestMethod(NS_LITERAL_CSTRING("OPTIONS")); - NS_ENSURE_SUCCESS(rv, rv); - - // Add channel headers - rv = preflightHttp-> - SetRequestHeader(NS_LITERAL_CSTRING("Access-Control-Request-Method"), - method, PR_FALSE); - NS_ENSURE_SUCCESS(rv, rv); - - if (!aUnsafeHeaders.IsEmpty()) { - nsCAutoString headers; - for (PRUint32 i = 0; i < aUnsafeHeaders.Length(); ++i) { - if (i != 0) { - headers += ','; - } - headers += aUnsafeHeaders[i]; - } - rv = preflightHttp-> - SetRequestHeader(NS_LITERAL_CSTRING("Access-Control-Request-Headers"), - headers, PR_FALSE); - NS_ENSURE_SUCCESS(rv, rv); - } - - // Set up listener - nsCOMPtr preflightListener = - new nsACPreflightListener(aRequestChannel, aRequestListener, - aRequestContext, aRequestingPrincipal, - aWithCredentials); - NS_ENSURE_TRUE(preflightListener, NS_ERROR_OUT_OF_MEMORY); - - preflightListener = - new nsCrossSiteListenerProxy(preflightListener, aRequestingPrincipal, - preflightChannel, aWithCredentials, method, - aUnsafeHeaders, &rv); - NS_ENSURE_TRUE(preflightListener, NS_ERROR_OUT_OF_MEMORY); - NS_ENSURE_SUCCESS(rv, rv); - - preflightChannel.swap(*aPreflightChannel); - preflightListener.swap(*aPreflightListener); - - return NS_OK; -} - -void -nsCrossSiteListenerProxy::ShutdownPreflightCache() -{ - nsACPreflightCache::Shutdown(); -} - NS_IMETHODIMP nsCrossSiteListenerProxy::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) @@ -608,6 +384,28 @@ nsCrossSiteListenerProxy::UpdateChannel(nsIChannel* aChannel) rv = http->SetRequestHeader(NS_LITERAL_CSTRING("Origin"), origin, PR_FALSE); NS_ENSURE_SUCCESS(rv, rv); + // Add preflight headers if this is a preflight request + if (mIsPreflight) { + rv = http-> + SetRequestHeader(NS_LITERAL_CSTRING("Access-Control-Request-Method"), + mPreflightMethod, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mPreflightHeaders.IsEmpty()) { + nsCAutoString headers; + for (PRUint32 i = 0; i < mPreflightHeaders.Length(); ++i) { + if (i != 0) { + headers += ','; + } + headers += mPreflightHeaders[i]; + } + rv = http-> + SetRequestHeader(NS_LITERAL_CSTRING("Access-Control-Request-Headers"), + headers, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + } + } + // Make cookie-less if needed if (mIsPreflight || !mWithCredentials) { nsLoadFlags flags; @@ -621,387 +419,3 @@ nsCrossSiteListenerProxy::UpdateChannel(nsIChannel* aChannel) return NS_OK; } - -// ========================================================================= -// nsACPreflightListener - -NS_IMPL_ISUPPORTS4(nsACPreflightListener, nsIStreamListener, nsIRequestObserver, - nsIInterfaceRequestor, nsIChannelEventSink) - -void -nsACPreflightListener::AddResultToCache(nsIRequest *aRequest) -{ - nsCOMPtr http = do_QueryInterface(aRequest); - NS_ASSERTION(http, "Request was not http"); - - // The "Access-Control-Max-Age" header should return an age in seconds. - nsCAutoString headerVal; - http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Max-Age"), - headerVal); - if (headerVal.IsEmpty()) { - return; - } - - // Sanitize the string. We only allow 'delta-seconds' as specified by - // http://dev.w3.org/2006/waf/access-control (digits 0-9 with no leading or - // trailing non-whitespace characters). - PRUint32 age = 0; - nsCSubstring::const_char_iterator iter, end; - headerVal.BeginReading(iter); - headerVal.EndReading(end); - while (iter != end) { - if (*iter < '0' || *iter > '9') { - return; - } - age = age * 10 + (*iter - '0'); - // Cap at 24 hours. This also avoids overflow - age = PR_MIN(age, 86400); - ++iter; - } - - if (!age) { - return; - } - - - // String seems fine, go ahead and cache. - // Note that we have already checked that these headers follow the correct - // syntax. - - nsCOMPtr uri; - http->GetURI(getter_AddRefs(uri)); - - // PR_Now gives microseconds - PRTime expirationTime = PR_Now() + (PRUint64)age * PR_USEC_PER_SEC; - - nsACPreflightCache::CacheEntry* entry = - nsACPreflightCache::GetEntry(uri, mReferrerPrincipal, mWithCredentials, - PR_TRUE); - if (!entry) { - return; - } - - // The "Access-Control-Allow-Methods" header contains a comma separated - // list of method names. - http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Methods"), - headerVal); - - nsCCommaSeparatedTokenizer methods(headerVal); - while(methods.hasMoreTokens()) { - const nsDependentCSubstring& method = methods.nextToken(); - if (method.IsEmpty()) { - continue; - } - PRUint32 i; - for (i = 0; i < entry->mMethods.Length(); ++i) { - if (entry->mMethods[i].token.Equals(method)) { - entry->mMethods[i].expirationTime = expirationTime; - break; - } - } - if (i == entry->mMethods.Length()) { - nsACPreflightCache::TokenTime* newMethod = - entry->mMethods.AppendElement(); - if (!newMethod) { - return; - } - - newMethod->token = method; - newMethod->expirationTime = expirationTime; - } - } - - // The "Access-Control-Allow-Headers" header contains a comma separated - // list of method names. - http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Headers"), - headerVal); - - nsCCommaSeparatedTokenizer headers(headerVal); - while(headers.hasMoreTokens()) { - const nsDependentCSubstring& header = headers.nextToken(); - if (header.IsEmpty()) { - continue; - } - PRUint32 i; - for (i = 0; i < entry->mHeaders.Length(); ++i) { - if (entry->mHeaders[i].token.Equals(header)) { - entry->mHeaders[i].expirationTime = expirationTime; - break; - } - } - if (i == entry->mHeaders.Length()) { - nsACPreflightCache::TokenTime* newHeader = - entry->mHeaders.AppendElement(); - if (!newHeader) { - return; - } - - newHeader->token = header; - newHeader->expirationTime = expirationTime; - } - } -} - -NS_IMETHODIMP -nsACPreflightListener::OnStartRequest(nsIRequest *aRequest, - nsISupports *aContext) -{ - nsresult status; - nsresult rv = aRequest->GetStatus(&status); - - if (NS_SUCCEEDED(rv)) { - rv = status; - } - - if (NS_SUCCEEDED(rv)) { - // Everything worked, try to cache and then fire off the actual request. - AddResultToCache(aRequest); - - rv = mOuterChannel->AsyncOpen(mOuterListener, mOuterContext); - } - - if (NS_FAILED(rv)) { - mOuterChannel->Cancel(rv); - mOuterListener->OnStartRequest(mOuterChannel, mOuterContext); - mOuterListener->OnStopRequest(mOuterChannel, mOuterContext, rv); - - return rv; - } - - return NS_OK; -} - -NS_IMETHODIMP -nsACPreflightListener::OnStopRequest(nsIRequest *aRequest, - nsISupports *aContext, - nsresult aStatus) -{ - return NS_OK; -} - -/** nsIStreamListener methods **/ - -NS_IMETHODIMP -nsACPreflightListener::OnDataAvailable(nsIRequest *aRequest, - nsISupports *ctxt, - nsIInputStream *inStr, - PRUint32 sourceOffset, - PRUint32 count) -{ - return NS_OK; -} - -NS_IMETHODIMP -nsACPreflightListener::OnChannelRedirect(nsIChannel *aOldChannel, - nsIChannel *aNewChannel, - PRUint32 aFlags) -{ - // No redirects allowed for now. - return NS_ERROR_DOM_BAD_URI; -} - -NS_IMETHODIMP -nsACPreflightListener::GetInterface(const nsIID & aIID, void **aResult) -{ - return QueryInterface(aIID, aResult); -} - -// ========================================================================= -// nsACPreflightCache - -nsClassHashtable* - nsACPreflightCache::mTable; -PRCList nsACPreflightCache::mList; - -void -nsACPreflightCache::CacheEntry::PurgeExpired(PRTime now) -{ - PRUint32 i; - for (i = 0; i < mMethods.Length(); ++i) { - if (now >= mMethods[i].expirationTime) { - mMethods.RemoveElementAt(i--); - } - } - for (i = 0; i < mHeaders.Length(); ++i) { - if (now >= mHeaders[i].expirationTime) { - mHeaders.RemoveElementAt(i--); - } - } -} - -PRBool -nsACPreflightCache::CacheEntry::CheckRequest(const nsCString& aMethod, - const nsTArray& aHeaders) -{ - PurgeExpired(PR_Now()); - - if (!aMethod.EqualsLiteral("GET") && !aMethod.EqualsLiteral("POST")) { - PRUint32 i; - for (i = 0; i < mMethods.Length(); ++i) { - if (aMethod.Equals(mMethods[i].token)) - break; - } - if (i == mMethods.Length()) { - return PR_FALSE; - } - } - - for (PRUint32 i = 0; i < aHeaders.Length(); ++i) { - PRUint32 j; - for (j = 0; j < mHeaders.Length(); ++j) { - if (aHeaders[i].Equals(mHeaders[j].token, - nsCaseInsensitiveCStringComparator())) { - break; - } - } - if (j == mHeaders.Length()) { - return PR_FALSE; - } - } - - return PR_TRUE; -} - -nsACPreflightCache::CacheEntry* -nsACPreflightCache::GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal, - PRBool aWithCredentials, PRBool aCreate) -{ - if (!mTable) { - if (!aCreate) { - return nsnull; - } - - PR_INIT_CLIST(&mList); - - mTable = new nsClassHashtable; - NS_ENSURE_TRUE(mTable, nsnull); - - if (!mTable->Init()) { - delete mTable; - mTable = nsnull; - return nsnull; - } - } - - nsCString key; - if (!GetCacheKey(aURI, aPrincipal, aWithCredentials, key)) { - NS_WARNING("Invalid cache key!"); - return nsnull; - } - - CacheEntry* entry; - - if (mTable->Get(key, &entry)) { - // Entry already existed so just return it. Also update the LRU list. - - // Move to the head of the list. - PR_REMOVE_LINK(entry); - PR_INSERT_LINK(entry, &mList); - - return entry; - } - - if (!aCreate) { - return nsnull; - } - - // This is a new entry, allocate and insert into the table now so that any - // failures don't cause items to be removed from a full cache. - entry = new CacheEntry(key); - if (!entry) { - NS_WARNING("Failed to allocate new cache entry!"); - return nsnull; - } - - if (!mTable->Put(key, entry)) { - // Failed, clean up the new entry. - delete entry; - - NS_WARNING("Failed to add entry to the access control cache!"); - return nsnull; - } - - PR_INSERT_LINK(entry, &mList); - - NS_ASSERTION(mTable->Count() <= ACCESS_CONTROL_CACHE_SIZE + 1, - "Something is borked, too many entries in the cache!"); - - // Now enforce the max count. - if (mTable->Count() > ACCESS_CONTROL_CACHE_SIZE) { - // Try to kick out all the expired entries. - PRTime now = PR_Now(); - mTable->Enumerate(RemoveExpiredEntries, &now); - - // If that didn't remove anything then kick out the least recently used - // entry. - if (mTable->Count() > ACCESS_CONTROL_CACHE_SIZE) { - CacheEntry* lruEntry = static_cast(PR_LIST_TAIL(&mList)); - PR_REMOVE_LINK(lruEntry); - - // This will delete 'lruEntry'. - mTable->Remove(lruEntry->mKey); - - NS_ASSERTION(mTable->Count() >= ACCESS_CONTROL_CACHE_SIZE, - "Somehow tried to remove an entry that was never added!"); - } - } - - return entry; -} - -/* static */ PR_CALLBACK PLDHashOperator -nsACPreflightCache::RemoveExpiredEntries(const nsACString& aKey, - nsAutoPtr& aValue, - void* aUserData) -{ - PRTime* now = static_cast(aUserData); - - aValue->PurgeExpired(*now); - - if (aValue->mHeaders.IsEmpty() && - aValue->mHeaders.IsEmpty()) { - // Expired, remove from the list as well as the hash table. - PR_REMOVE_LINK(aValue); - return PL_DHASH_REMOVE; - } - - return PL_DHASH_NEXT; -} - -/* static */ PRBool -nsACPreflightCache::GetCacheKey(nsIURI* aURI, nsIPrincipal* aPrincipal, - PRBool aWithCredentials, nsACString& _retval) -{ - NS_ASSERTION(aURI, "Null uri!"); - NS_ASSERTION(aPrincipal, "Null principal!"); - - NS_NAMED_LITERAL_CSTRING(space, " "); - - nsCOMPtr uri; - nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri)); - NS_ENSURE_SUCCESS(rv, PR_FALSE); - - nsCAutoString scheme, host, port; - if (uri) { - uri->GetScheme(scheme); - uri->GetHost(host); - port.AppendInt(NS_GetRealPort(uri)); - } - - 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(cred + space + scheme + space + host + space + port + space + - spec); - - return PR_TRUE; -} diff --git a/content/base/src/nsCrossSiteListenerProxy.h b/content/base/src/nsCrossSiteListenerProxy.h index e79f019c87e..c889b78527a 100644 --- a/content/base/src/nsCrossSiteListenerProxy.h +++ b/content/base/src/nsCrossSiteListenerProxy.h @@ -39,13 +39,17 @@ #include "nsIInterfaceRequestor.h" #include "nsCOMPtr.h" #include "nsString.h" +#include "nsIURI.h" #include "nsTArray.h" +#include "nsIContentSink.h" +#include "nsIXMLContentSink.h" +#include "nsIExpatSink.h" +#include "nsIInterfaceRequestor.h" #include "nsIChannelEventSink.h" class nsIURI; class nsIParser; class nsIPrincipal; -class nsIHttpChannel; extern PRBool IsValidHTTPToken(const nsCSubstring& aToken); @@ -68,19 +72,6 @@ public: const nsTArray& aPreflightHeaders, nsresult* aResult); - static nsresult CheckPreflight(nsIHttpChannel* aRequestChannel, - nsIStreamListener* aRequestListener, - nsISupports* aRequestContext, - nsIPrincipal* aRequestingPrincipal, - PRBool aForcePreflight, - nsTArray& aUnsafeHeaders, - PRBool aWithCredentials, - PRBool* aPreflighted, - nsIChannel** aPreflightChannel, - nsIStreamListener** aPreflightListener); - - static void ShutdownPreflightCache(); - NS_DECL_ISUPPORTS NS_DECL_NSIREQUESTOBSERVER NS_DECL_NSISTREAMLISTENER diff --git a/content/base/src/nsXMLHttpRequest.cpp b/content/base/src/nsXMLHttpRequest.cpp index 81e11aaacd9..5ef831b1e04 100644 --- a/content/base/src/nsXMLHttpRequest.cpp +++ b/content/base/src/nsXMLHttpRequest.cpp @@ -138,6 +138,8 @@ XML_HTTP_REQUEST_SENT | \ XML_HTTP_REQUEST_STOPPED) +#define ACCESS_CONTROL_CACHE_SIZE 100 + #define NS_BADCERTHANDLER_CONTRACTID \ "@mozilla.org/content/xmlhttprequest-bad-cert-handler;1" @@ -281,6 +283,220 @@ nsMultipartProxyListener::OnDataAvailable(nsIRequest *aRequest, count); } +// Class used as streamlistener and notification callback when +// doing the initial GET request for an access-control check +class nsACProxyListener : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink +{ +public: + nsACProxyListener(nsIChannel* aOuterChannel, + nsIStreamListener* aOuterListener, + nsISupports* aOuterContext, + nsIPrincipal* aReferrerPrincipal, + const nsACString& aRequestMethod, + PRBool aWithCredentials) + : mOuterChannel(aOuterChannel), mOuterListener(aOuterListener), + mOuterContext(aOuterContext), mReferrerPrincipal(aReferrerPrincipal), + mRequestMethod(aRequestMethod), mWithCredentials(aWithCredentials) + { } + + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + +private: + void AddResultToCache(nsIRequest* aRequest); + + nsCOMPtr mOuterChannel; + nsCOMPtr mOuterListener; + nsCOMPtr mOuterContext; + nsCOMPtr mReferrerPrincipal; + nsCString mRequestMethod; + PRBool mWithCredentials; +}; + +NS_IMPL_ISUPPORTS4(nsACProxyListener, nsIStreamListener, nsIRequestObserver, + nsIInterfaceRequestor, nsIChannelEventSink) + +void +nsACProxyListener::AddResultToCache(nsIRequest *aRequest) +{ + nsCOMPtr http = do_QueryInterface(aRequest); + NS_ASSERTION(http, "Request was not http"); + + // The "Access-Control-Max-Age" header should return an age in seconds. + nsCAutoString headerVal; + http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Max-Age"), + headerVal); + if (headerVal.IsEmpty()) { + return; + } + + // Sanitize the string. We only allow 'delta-seconds' as specified by + // http://dev.w3.org/2006/waf/access-control (digits 0-9 with no leading or + // trailing non-whitespace characters). + PRUint32 age = 0; + nsCSubstring::const_char_iterator iter, end; + headerVal.BeginReading(iter); + headerVal.EndReading(end); + while (iter != end) { + if (*iter < '0' || *iter > '9') { + return; + } + age = age * 10 + (*iter - '0'); + // Cap at 24 hours. This also avoids overflow + age = PR_MIN(age, 86400); + ++iter; + } + + if (!age || !nsXMLHttpRequest::EnsureACCache()) { + return; + } + + + // String seems fine, go ahead and cache. + // Note that we have already checked that these headers follow the correct + // syntax. + + nsCOMPtr uri; + http->GetURI(getter_AddRefs(uri)); + + // PR_Now gives microseconds + PRTime expirationTime = PR_Now() + (PRUint64)age * PR_USEC_PER_SEC; + + nsAccessControlLRUCache::CacheEntry* entry = + nsXMLHttpRequest::sAccessControlCache-> + GetEntry(uri, mReferrerPrincipal, mWithCredentials, PR_TRUE); + if (!entry) { + return; + } + + // The "Access-Control-Allow-Methods" header contains a comma separated + // list of method names. + http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Methods"), + headerVal); + + nsCCommaSeparatedTokenizer methods(headerVal); + while(methods.hasMoreTokens()) { + const nsDependentCSubstring& method = methods.nextToken(); + if (method.IsEmpty()) { + continue; + } + PRUint32 i; + for (i = 0; i < entry->mMethods.Length(); ++i) { + if (entry->mMethods[i].token.Equals(method)) { + entry->mMethods[i].expirationTime = expirationTime; + break; + } + } + if (i == entry->mMethods.Length()) { + nsAccessControlLRUCache::TokenTime* newMethod = + entry->mMethods.AppendElement(); + if (!newMethod) { + return; + } + + newMethod->token = method; + newMethod->expirationTime = expirationTime; + } + } + + // The "Access-Control-Allow-Headers" header contains a comma separated + // list of method names. + http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Headers"), + headerVal); + + nsCCommaSeparatedTokenizer headers(headerVal); + while(headers.hasMoreTokens()) { + const nsDependentCSubstring& header = headers.nextToken(); + if (header.IsEmpty()) { + continue; + } + PRUint32 i; + for (i = 0; i < entry->mHeaders.Length(); ++i) { + if (entry->mHeaders[i].token.Equals(header)) { + entry->mHeaders[i].expirationTime = expirationTime; + break; + } + } + if (i == entry->mHeaders.Length()) { + nsAccessControlLRUCache::TokenTime* newHeader = + entry->mHeaders.AppendElement(); + if (!newHeader) { + return; + } + + newHeader->token = header; + newHeader->expirationTime = expirationTime; + } + } +} + +NS_IMETHODIMP +nsACProxyListener::OnStartRequest(nsIRequest *aRequest, nsISupports *aContext) +{ + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + + if (NS_SUCCEEDED(rv)) { + rv = status; + } + + if (NS_SUCCEEDED(rv)) { + // Everything worked, try to cache and then fire off the actual request. + AddResultToCache(aRequest); + + rv = mOuterChannel->AsyncOpen(mOuterListener, mOuterContext); + } + + if (NS_FAILED(rv)) { + mOuterChannel->Cancel(rv); + mOuterListener->OnStartRequest(mOuterChannel, mOuterContext); + mOuterListener->OnStopRequest(mOuterChannel, mOuterContext, rv); + + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsACProxyListener::OnStopRequest(nsIRequest *aRequest, nsISupports *aContext, + nsresult aStatus) +{ + return NS_OK; +} + +/** nsIStreamListener methods **/ + +NS_IMETHODIMP +nsACProxyListener::OnDataAvailable(nsIRequest *aRequest, + nsISupports *ctxt, + nsIInputStream *inStr, + PRUint32 sourceOffset, + PRUint32 count) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsACProxyListener::OnChannelRedirect(nsIChannel *aOldChannel, + nsIChannel *aNewChannel, + PRUint32 aFlags) +{ + // No redirects allowed for now. + return NS_ERROR_DOM_BAD_URI; +} + +NS_IMETHODIMP +nsACProxyListener::GetInterface(const nsIID & aIID, void **aResult) +{ + return QueryInterface(aIID, aResult); +} + /** * Gets the nsIDocument given the script context. Will return nsnull on failure. * @@ -628,12 +844,201 @@ NS_INTERFACE_MAP_END_INHERITING(nsXHREventTarget) NS_IMPL_ADDREF_INHERITED(nsXMLHttpRequestUpload, nsXHREventTarget) NS_IMPL_RELEASE_INHERITED(nsXMLHttpRequestUpload, nsXHREventTarget) +void +nsAccessControlLRUCache::CacheEntry::PurgeExpired(PRTime now) +{ + PRUint32 i; + for (i = 0; i < mMethods.Length(); ++i) { + if (now >= mMethods[i].expirationTime) { + mMethods.RemoveElementAt(i--); + } + } + for (i = 0; i < mHeaders.Length(); ++i) { + if (now >= mHeaders[i].expirationTime) { + mHeaders.RemoveElementAt(i--); + } + } +} + +PRBool +nsAccessControlLRUCache::CacheEntry::CheckRequest(const nsCString& aMethod, + const nsTArray& aHeaders) +{ + PurgeExpired(PR_Now()); + + if (!aMethod.EqualsLiteral("GET") && !aMethod.EqualsLiteral("POST")) { + PRUint32 i; + for (i = 0; i < mMethods.Length(); ++i) { + if (aMethod.Equals(mMethods[i].token)) + break; + } + if (i == mMethods.Length()) { + return PR_FALSE; + } + } + + for (PRUint32 i = 0; i < aHeaders.Length(); ++i) { + PRUint32 j; + for (j = 0; j < mHeaders.Length(); ++j) { + if (aHeaders[i].Equals(mHeaders[j].token, + nsCaseInsensitiveCStringComparator())) { + break; + } + } + if (j == mHeaders.Length()) { + return PR_FALSE; + } + } + + return PR_TRUE; +} + +nsAccessControlLRUCache::CacheEntry* +nsAccessControlLRUCache::GetEntry(nsIURI* aURI, + nsIPrincipal* aPrincipal, + PRBool aWithCredentials, + PRBool aCreate) +{ + nsCString key; + if (!GetCacheKey(aURI, aPrincipal, aWithCredentials, key)) { + NS_WARNING("Invalid cache key!"); + return nsnull; + } + + CacheEntry* entry; + + if (mTable.Get(key, &entry)) { + // Entry already existed so just return it. Also update the LRU list. + + // Move to the head of the list. + PR_REMOVE_LINK(entry); + PR_INSERT_LINK(entry, &mList); + + return entry; + } + + if (!aCreate) { + return nsnull; + } + + // This is a new entry, allocate and insert into the table now so that any + // failures don't cause items to be removed from a full cache. + entry = new CacheEntry(key); + if (!entry) { + NS_WARNING("Failed to allocate new cache entry!"); + return nsnull; + } + + if (!mTable.Put(key, entry)) { + // Failed, clean up the new entry. + delete entry; + + NS_WARNING("Failed to add entry to the access control cache!"); + return nsnull; + } + + PR_INSERT_LINK(entry, &mList); + + NS_ASSERTION(mTable.Count() <= ACCESS_CONTROL_CACHE_SIZE + 1, + "Something is borked, too many entries in the cache!"); + + // Now enforce the max count. + if (mTable.Count() > ACCESS_CONTROL_CACHE_SIZE) { + // Try to kick out all the expired entries. + PRTime now = PR_Now(); + mTable.Enumerate(RemoveExpiredEntries, &now); + + // If that didn't remove anything then kick out the least recently used + // entry. + if (mTable.Count() > ACCESS_CONTROL_CACHE_SIZE) { + CacheEntry* lruEntry = static_cast(PR_LIST_TAIL(&mList)); + PR_REMOVE_LINK(lruEntry); + + // This will delete 'lruEntry'. + mTable.Remove(lruEntry->mKey); + + NS_ASSERTION(mTable.Count() >= ACCESS_CONTROL_CACHE_SIZE, + "Somehow tried to remove an entry that was never added!"); + } + } + + return entry; +} + +void +nsAccessControlLRUCache::Clear() +{ + PR_INIT_CLIST(&mList); + mTable.Clear(); +} + +/* static */ PR_CALLBACK PLDHashOperator +nsAccessControlLRUCache::RemoveExpiredEntries(const nsACString& aKey, + nsAutoPtr& aValue, + void* aUserData) +{ + PRTime* now = static_cast(aUserData); + + aValue->PurgeExpired(*now); + + if (aValue->mHeaders.IsEmpty() && + aValue->mHeaders.IsEmpty()) { + // Expired, remove from the list as well as the hash table. + PR_REMOVE_LINK(aValue); + return PL_DHASH_REMOVE; + } + + return PL_DHASH_NEXT; +} + +/* static */ PRBool +nsAccessControlLRUCache::GetCacheKey(nsIURI* aURI, + nsIPrincipal* aPrincipal, + PRBool aWithCredentials, + nsACString& _retval) +{ + NS_ASSERTION(aURI, "Null uri!"); + NS_ASSERTION(aPrincipal, "Null principal!"); + + NS_NAMED_LITERAL_CSTRING(space, " "); + + nsCOMPtr uri; + nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, PR_FALSE); + + nsCAutoString scheme, host, port; + if (uri) { + uri->GetScheme(scheme); + uri->GetHost(host); + port.AppendInt(NS_GetRealPort(uri)); + } + + 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(cred + space + scheme + space + host + space + port + space + + spec); + + return PR_TRUE; +} ///////////////////////////////////////////// // // ///////////////////////////////////////////// +// Will be initialized in nsXMLHttpRequest::EnsureACCache. +nsAccessControlLRUCache* nsXMLHttpRequest::sAccessControlCache = nsnull; + nsXMLHttpRequest::nsXMLHttpRequest() : mRequestObserver(nsnull), mState(XML_HTTP_REQUEST_UNINITIALIZED), mUploadTransferred(0), mUploadTotal(0), mUploadComplete(PR_TRUE), @@ -1078,8 +1483,8 @@ nsXMLHttpRequest::Abort() if (mChannel) { mChannel->Cancel(NS_BINDING_ABORTED); } - if (mACPreflightChannel) { - mACPreflightChannel->Cancel(NS_BINDING_ABORTED); + if (mACGetChannel) { + mACGetChannel->Cancel(NS_BINDING_ABORTED); } mResponseXML = nsnull; mResponseBody.Truncate(); @@ -1198,6 +1603,24 @@ nsXMLHttpRequest::GetResponseHeader(const nsACString& header, return rv; } +nsresult +nsXMLHttpRequest::GetLoadGroup(nsILoadGroup **aLoadGroup) +{ + NS_ENSURE_ARG_POINTER(aLoadGroup); + *aLoadGroup = nsnull; + + if (mState & XML_HTTP_REQUEST_BACKGROUND) { + return NS_OK; + } + + nsCOMPtr doc = GetDocumentFromScriptContext(mScriptContext); + if (doc) { + *aLoadGroup = doc->GetDocumentLoadGroup().get(); // already_AddRefed + } + + return NS_OK; +} + nsresult nsXMLHttpRequest::CreateReadystatechangeEvent(nsIDOMEvent** aDOMEvent) { @@ -1422,12 +1845,11 @@ nsXMLHttpRequest::OpenRequest(const nsACString& method, authp = PR_TRUE; } - // Find the load group for the page, and add ourselves to it. This way any - // pending requests will be automatically aborted if the user leaves the page. + // When we are called from JS we can find the load group for the page, + // and add ourselves to it. This way any pending requests + // will be automatically aborted if the user leaves the page. nsCOMPtr loadGroup; - if (doc && !(mState & XML_HTTP_REQUEST_BACKGROUND)) { - loadGroup = doc->GetDocumentLoadGroup(); - } + GetLoadGroup(getter_AddRefs(loadGroup)); // nsIRequest::LOAD_BACKGROUND prevents throbber from becoming active, which // in turn keeps STOP button from becoming active. If the consumer passed in @@ -1443,7 +1865,7 @@ nsXMLHttpRequest::OpenRequest(const nsACString& method, } rv = NS_NewChannel(getter_AddRefs(mChannel), uri, nsnull, loadGroup, nsnull, loadFlags); - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) return rv; // Check if we're doing a cross-origin request. if (IsSystemPrincipal(mPrincipal)) { @@ -1948,174 +2370,6 @@ nsXMLHttpRequest::SendAsBinary(const nsAString &aBody) return Send(variant); } -nsresult -nsXMLHttpRequest::SetUploadDataAndType(nsIVariant* aBody) -{ - nsCOMPtr httpChannel(do_QueryInterface(mChannel)); - NS_ASSERTION(httpChannel, "Should be http channel to get here"); - - nsXPIDLString serial; - nsCOMPtr postDataStream; - nsCAutoString charset(NS_LITERAL_CSTRING("UTF-8")); - - PRUint16 dataType; - nsresult rv = aBody->GetDataType(&dataType); - NS_ENSURE_SUCCESS(rv, rv); - - switch (dataType) { - case nsIDataType::VTYPE_INTERFACE: - case nsIDataType::VTYPE_INTERFACE_IS: - { - nsCOMPtr supports; - nsID *iid; - rv = aBody->GetAsInterface(&iid, getter_AddRefs(supports)); - if (NS_FAILED(rv)) - return rv; - if (iid) - nsMemory::Free(iid); - - // document? - nsCOMPtr doc(do_QueryInterface(supports)); - if (doc) { - nsCOMPtr serializer = - do_CreateInstance(NS_XMLSERIALIZER_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr dom3doc(do_QueryInterface(doc)); - if (dom3doc) { - nsAutoString inputEncoding; - dom3doc->GetInputEncoding(inputEncoding); - if (DOMStringIsNull(inputEncoding)) { - charset.AssignLiteral("UTF-8"); - } else { - CopyUTF16toUTF8(inputEncoding, charset); - } - } - - // Serialize to a stream so that the encoding used will - // match the document's. - nsCOMPtr storStream; - rv = NS_NewStorageStream(4096, PR_UINT32_MAX, - getter_AddRefs(storStream)); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr output; - rv = storStream->GetOutputStream(0, getter_AddRefs(output)); - NS_ENSURE_SUCCESS(rv, rv); - - // Empty string for encoding means to use document's current - // encoding. - rv = serializer->SerializeToStream(doc, output, EmptyCString()); - NS_ENSURE_SUCCESS(rv, rv); - - output->Close(); - rv = storStream->NewInputStream(0, getter_AddRefs(postDataStream)); - NS_ENSURE_SUCCESS(rv, rv); - } else { - // nsISupportsString? - nsCOMPtr wstr(do_QueryInterface(supports)); - if (wstr) { - wstr->GetData(serial); - } else { - // stream? - nsCOMPtr stream(do_QueryInterface(supports)); - if (stream) { - postDataStream = stream; - charset.Truncate(); - } - } - } - } - break; - case nsIDataType::VTYPE_VOID: - case nsIDataType::VTYPE_EMPTY: - // Makes us act as if !aBody, don't upload anything - break; - case nsIDataType::VTYPE_EMPTY_ARRAY: - case nsIDataType::VTYPE_ARRAY: - // IE6 throws error here, so we do that as well - return NS_ERROR_INVALID_ARG; - default: - // try variant string - rv = aBody->GetAsWString(getter_Copies(serial)); - NS_ENSURE_SUCCESS(rv, rv); - break; - } - - if (serial) { - // Convert to a byte stream - nsCOMPtr converter = - do_CreateInstance("@mozilla.org/intl/scriptableunicodeconverter", &rv); - NS_ENSURE_SUCCESS(rv, rv); - - rv = converter->SetCharset("UTF-8"); - NS_ENSURE_SUCCESS(rv, rv); - - rv = converter->ConvertToInputStream(serial, - getter_AddRefs(postDataStream)); - NS_ENSURE_SUCCESS(rv, rv); - } - - if (postDataStream) { - nsCOMPtr uploadChannel(do_QueryInterface(httpChannel)); - NS_ASSERTION(uploadChannel, "http must support nsIUploadChannel"); - - // If no content type header was set by the client, we set it to - // application/xml. - nsCAutoString contentType; - if (NS_FAILED(httpChannel-> - GetRequestHeader(NS_LITERAL_CSTRING("Content-Type"), - contentType)) || - contentType.IsEmpty()) { - contentType = NS_LITERAL_CSTRING("application/xml"); - } - - // We don't want to set a charset for streams. - if (!charset.IsEmpty()) { - nsCAutoString specifiedCharset; - PRBool haveCharset; - PRInt32 charsetStart, charsetEnd; - rv = NS_ExtractCharsetFromContentType(contentType, specifiedCharset, - &haveCharset, &charsetStart, - &charsetEnd); - if (NS_FAILED(rv)) { - contentType.AssignLiteral("application/xml"); - specifiedCharset.Truncate(); - charsetStart = charsetEnd = contentType.Length(); - } - - // If the content-type the page set already has a charset parameter, - // and it's the same charset, up to case, as |charset|, just send the - // page-set content-type header. Apparently at least - // google-web-toolkit is broken and relies on the exact case of its - // charset parameter, which makes things break if we use |charset| - // (which is always a fully resolved charset per our charset alias - // table, hence might be differently cased). - if (!specifiedCharset.Equals(charset, - nsCaseInsensitiveCStringComparator())) { - nsCAutoString newCharset("; charset="); - newCharset.Append(charset); - contentType.Replace(charsetStart, charsetEnd - charsetStart, - newCharset); - } - } - - mUploadComplete = PR_FALSE; - - nsCAutoString method; - httpChannel->GetRequestMethod(method); - - postDataStream->Available(&mUploadTotal); - rv = uploadChannel->SetUploadStream(postDataStream, contentType, -1); - NS_ENSURE_SUCCESS(rv, rv); - - // Reset the method to its original value - httpChannel->SetRequestMethod(method); - } - - return NS_OK; -} - /* void send (in nsIVariant aBody); */ NS_IMETHODIMP nsXMLHttpRequest::Send(nsIVariant *aBody) @@ -2145,8 +2399,7 @@ nsXMLHttpRequest::Send(nsIVariant *aBody) nsCOMPtr httpChannel(do_QueryInterface(mChannel)); if (httpChannel) { - // If GET, method name will be uppercase - httpChannel->GetRequestMethod(method); + httpChannel->GetRequestMethod(method); // If GET, method name will be uppercase if (!IsSystemPrincipal(mPrincipal)) { nsCOMPtr codebase; @@ -2162,31 +2415,161 @@ nsXMLHttpRequest::Send(nsIVariant *aBody) mUploadComplete = PR_TRUE; mErrorLoad = PR_FALSE; if (aBody && httpChannel && !method.EqualsLiteral("GET")) { - SetUploadDataAndType(aBody); - } + nsXPIDLString serial; + nsCOMPtr postDataStream; + nsCAutoString charset(NS_LITERAL_CSTRING("UTF-8")); - // Bypass the network cache in cases where it makes no sense: - // 1) Multipart responses are very large and would likely be doomed by the - // cache once they grow too large, so they are not worth caching. - // 2) POST responses are always unique, and we provide no API that would - // allow our consumers to specify a "cache key" to access old POST - // responses, so they are not worth caching. - if ((mState & XML_HTTP_REQUEST_MULTIPART) || method.EqualsLiteral("POST")) { - AddLoadFlags(mChannel, - nsIRequest::LOAD_BYPASS_CACHE | nsIRequest::INHIBIT_CACHING); - } - // When we are sync loading, we need to bypass the local cache when it would - // otherwise block us waiting for exclusive access to the cache. If we don't - // do this, then we could dead lock in some cases (see bug 309424). - else if (!(mState & XML_HTTP_REQUEST_ASYNC)) { - AddLoadFlags(mChannel, - nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY); - } + PRUint16 dataType; + rv = aBody->GetDataType(&dataType); + if (NS_FAILED(rv)) + return rv; - // Since we expect XML data, set the type hint accordingly - // This means that we always try to parse local files as XML - // ignoring return value, as this is not critical - mChannel->SetContentType(NS_LITERAL_CSTRING("application/xml")); + switch (dataType) { + case nsIDataType::VTYPE_INTERFACE: + case nsIDataType::VTYPE_INTERFACE_IS: + { + nsCOMPtr supports; + nsID *iid; + rv = aBody->GetAsInterface(&iid, getter_AddRefs(supports)); + if (NS_FAILED(rv)) + return rv; + if (iid) + nsMemory::Free(iid); + + // document? + nsCOMPtr doc(do_QueryInterface(supports)); + if (doc) { + nsCOMPtr serializer(do_CreateInstance(NS_XMLSERIALIZER_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr dom3doc(do_QueryInterface(doc)); + if (dom3doc) { + nsAutoString inputEncoding; + dom3doc->GetInputEncoding(inputEncoding); + if (DOMStringIsNull(inputEncoding)) { + charset.AssignLiteral("UTF-8"); + } else { + CopyUTF16toUTF8(inputEncoding, charset); + } + } + + // Serialize to a stream so that the encoding used will + // match the document's. + nsCOMPtr storStream; + rv = NS_NewStorageStream(4096, PR_UINT32_MAX, getter_AddRefs(storStream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr output; + rv = storStream->GetOutputStream(0, getter_AddRefs(output)); + NS_ENSURE_SUCCESS(rv, rv); + + // Empty string for encoding means to use document's current + // encoding. + rv = serializer->SerializeToStream(doc, output, EmptyCString()); + NS_ENSURE_SUCCESS(rv, rv); + + output->Close(); + rv = storStream->NewInputStream(0, getter_AddRefs(postDataStream)); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // nsISupportsString? + nsCOMPtr wstr(do_QueryInterface(supports)); + if (wstr) { + wstr->GetData(serial); + } else { + // stream? + nsCOMPtr stream(do_QueryInterface(supports)); + if (stream) { + postDataStream = stream; + charset.Truncate(); + } + } + } + } + break; + case nsIDataType::VTYPE_VOID: + case nsIDataType::VTYPE_EMPTY: + // Makes us act as if !aBody, don't upload anything + break; + case nsIDataType::VTYPE_EMPTY_ARRAY: + case nsIDataType::VTYPE_ARRAY: + // IE6 throws error here, so we do that as well + return NS_ERROR_INVALID_ARG; + default: + // try variant string + rv = aBody->GetAsWString(getter_Copies(serial)); + if (NS_FAILED(rv)) + return rv; + break; + } + + if (serial) { + // Convert to a byte stream + nsCOMPtr converter = + do_CreateInstance("@mozilla.org/intl/scriptableunicodeconverter", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = converter->SetCharset("UTF-8"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = converter->ConvertToInputStream(serial, + getter_AddRefs(postDataStream)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (postDataStream) { + nsCOMPtr uploadChannel(do_QueryInterface(httpChannel)); + NS_ASSERTION(uploadChannel, "http must support nsIUploadChannel"); + + // If no content type header was set by the client, we set it to + // application/xml. + nsCAutoString contentType; + if (NS_FAILED(httpChannel-> + GetRequestHeader(NS_LITERAL_CSTRING("Content-Type"), + contentType)) || + contentType.IsEmpty()) { + contentType = NS_LITERAL_CSTRING("application/xml"); + } + + // We don't want to set a charset for streams. + if (!charset.IsEmpty()) { + nsCAutoString specifiedCharset; + PRBool haveCharset; + PRInt32 charsetStart, charsetEnd; + rv = NS_ExtractCharsetFromContentType(contentType, specifiedCharset, + &haveCharset, &charsetStart, + &charsetEnd); + if (NS_FAILED(rv)) { + contentType.AssignLiteral("application/xml"); + specifiedCharset.Truncate(); + charsetStart = charsetEnd = contentType.Length(); + } + + // If the content-type the page set already has a charset parameter, + // and it's the same charset, up to case, as |charset|, just send the + // page-set content-type header. Apparently at least + // google-web-toolkit is broken and relies on the exact case of its + // charset parameter, which makes things break if we use |charset| + // (which is always a fully resolved charset per our charset alias + // table, hence might be differently cased). + if (!specifiedCharset.Equals(charset, + nsCaseInsensitiveCStringComparator())) { + nsCAutoString newCharset("; charset="); + newCharset.Append(charset); + contentType.Replace(charsetStart, charsetEnd - charsetStart, + newCharset); + } + } + + mUploadComplete = PR_FALSE; + postDataStream->Available(&mUploadTotal); + rv = uploadChannel->SetUploadStream(postDataStream, contentType, -1); + // Reset the method to its original value + if (httpChannel) { + httpChannel->SetRequestMethod(method); + } + } + } // Reset responseBody mResponseBody.Truncate(); @@ -2197,9 +2580,72 @@ nsXMLHttpRequest::Send(nsIVariant *aBody) rv = CheckChannelForCrossSiteRequest(mChannel); NS_ENSURE_SUCCESS(rv, rv); - // Start request, or preflight request if one is needed. 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); + + nsCAutoString method; + httpChannel->GetRequestMethod(method); + if (!mACUnsafeHeaders.IsEmpty() || + HasListenersFor(NS_LITERAL_STRING(UPLOADPROGRESS_STR)) || + (mUpload && mUpload->HasListeners())) { + mState |= XML_HTTP_REQUEST_NEED_AC_PREFLIGHT; + } + else if (method.LowerCaseEqualsLiteral("post")) { + nsCAutoString contentTypeHeader; + httpChannel->GetRequestHeader(NS_LITERAL_CSTRING("Content-Type"), + contentTypeHeader); + + nsCAutoString contentType, charset; + NS_ParseContentType(contentTypeHeader, contentType, charset); + + NS_ENSURE_SUCCESS(rv, rv); + if (!contentType.LowerCaseEqualsLiteral("text/plain")) { + mState |= XML_HTTP_REQUEST_NEED_AC_PREFLIGHT; + } + } + else if (!method.LowerCaseEqualsLiteral("get")) { + mState |= XML_HTTP_REQUEST_NEED_AC_PREFLIGHT; + } + + // If so, set up the preflight + if (mState & XML_HTTP_REQUEST_NEED_AC_PREFLIGHT) { + // Check to see if this initial OPTIONS request has already been cached + // in our special Access Control Cache. + nsCOMPtr uri; + rv = mChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAccessControlLRUCache::CacheEntry* entry = + sAccessControlCache ? + sAccessControlCache->GetEntry(uri, mPrincipal, withCredentials, PR_FALSE) : + nsnull; + + if (!entry || !entry->CheckRequest(method, mACUnsafeHeaders)) { + // Either it wasn't cached or the cached result has expired. Build a + // channel for the OPTIONS request. + nsCOMPtr loadGroup; + GetLoadGroup(getter_AddRefs(loadGroup)); + + nsLoadFlags loadFlags; + rv = mChannel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewChannel(getter_AddRefs(mACGetChannel), uri, nsnull, + loadGroup, nsnull, loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr acHttp = do_QueryInterface(mACGetChannel); + NS_ASSERTION(acHttp, "Failed to QI to nsIHttpChannel!"); + + rv = acHttp->SetRequestMethod(NS_LITERAL_CSTRING("OPTIONS")); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + // Hook us up to listen to redirects and the like mChannel->GetNotificationCallbacks(getter_AddRefs(mNotificationCallbacks)); mChannel->SetNotificationCallbacks(this); @@ -2222,31 +2668,49 @@ nsXMLHttpRequest::Send(nsIVariant *aBody) NS_ENSURE_SUCCESS(rv, rv); } - nsCOMPtr preflightListener; - 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); - - PRBool force = HasListenersFor(NS_LITERAL_STRING(UPLOADPROGRESS_STR)) || - (mUpload && mUpload->HasListeners()); - - PRBool preflighted; - rv = nsCrossSiteListenerProxy:: - CheckPreflight(httpChannel, listener, nsnull, mPrincipal, force, - mACUnsafeHeaders, withCredentials, &preflighted, - getter_AddRefs(mACPreflightChannel), - getter_AddRefs(preflightListener)); - NS_ENSURE_SUCCESS(rv, rv); - - if (preflighted) { - mState |= XML_HTTP_REQUEST_NEED_AC_PREFLIGHT; + // Bypass the network cache in cases where it makes no sense: + // 1) Multipart responses are very large and would likely be doomed by the + // cache once they grow too large, so they are not worth caching. + // 2) POST responses are always unique, and we provide no API that would + // allow our consumers to specify a "cache key" to access old POST + // responses, so they are not worth caching. + if ((mState & XML_HTTP_REQUEST_MULTIPART) || method.EqualsLiteral("POST")) { + AddLoadFlags(mChannel, + nsIRequest::LOAD_BYPASS_CACHE | nsIRequest::INHIBIT_CACHING); + } + // When we are sync loading, we need to bypass the local cache when it would + // otherwise block us waiting for exclusive access to the cache. If we don't + // do this, then we could dead lock in some cases (see bug 309424). + else if (!(mState & XML_HTTP_REQUEST_ASYNC)) { + AddLoadFlags(mChannel, + nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY); + if (mACGetChannel) { + AddLoadFlags(mACGetChannel, + nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY); } } - if (mACPreflightChannel) { - // Start preflight. Preflight channel will start mChannel - // if preflight succeeds. - rv = mACPreflightChannel->AsyncOpen(preflightListener, nsnull); + // Since we expect XML data, set the type hint accordingly + // This means that we always try to parse local files as XML + // ignoring return value, as this is not critical + mChannel->SetContentType(NS_LITERAL_CSTRING("application/xml")); + + // If we're doing a cross-site non-GET request we need to first do + // a GET request to the same URI. Set that up if needed + if (mACGetChannel) { + nsCOMPtr acProxyListener = + new nsACProxyListener(mChannel, listener, nsnull, mPrincipal, method, + withCredentials); + NS_ENSURE_TRUE(acProxyListener, NS_ERROR_OUT_OF_MEMORY); + + acProxyListener = + new nsCrossSiteListenerProxy(acProxyListener, mPrincipal, mACGetChannel, + withCredentials, method, mACUnsafeHeaders, + &rv); + NS_ENSURE_TRUE(acProxyListener, NS_ERROR_OUT_OF_MEMORY); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mACGetChannel->AsyncOpen(acProxyListener, nsnull); } else { // Start reading from the channel @@ -2256,7 +2720,7 @@ nsXMLHttpRequest::Send(nsIVariant *aBody) if (NS_FAILED(rv)) { // Drop our ref to the channel to avoid cycles mChannel = nsnull; - mACPreflightChannel = nsnull; + mACGetChannel = nsnull; return rv; } @@ -2306,12 +2770,12 @@ nsXMLHttpRequest::SetRequestHeader(const nsACString& header, // Check that we haven't already opened the channel. We can't rely on // the channel throwing from mChannel->SetRequestHeader since we might - // still be waiting for mACPreflightChannel to actually open mChannel - if (mACPreflightChannel) { + // still be waiting for mACGetChannel to actually open mChannel + if (mACGetChannel) { PRBool pending; - rv = mACPreflightChannel->IsPending(&pending); + rv = mACGetChannel->IsPending(&pending); NS_ENSURE_SUCCESS(rv, rv); - + if (pending) { return NS_ERROR_IN_PROGRESS; } diff --git a/content/base/src/nsXMLHttpRequest.h b/content/base/src/nsXMLHttpRequest.h index 130abf3e762..bffb33e1a32 100644 --- a/content/base/src/nsXMLHttpRequest.h +++ b/content/base/src/nsXMLHttpRequest.h @@ -64,6 +64,9 @@ #include "nsIJSNativeInitializer.h" #include "nsPIDOMWindow.h" #include "nsIDOMLSProgressEvent.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "prclist.h" #include "prtime.h" #include "nsIEventListenerManager.h" #include "nsIDOMNSEvent.h" @@ -72,6 +75,71 @@ class nsILoadGroup; +class nsAccessControlLRUCache +{ +public: + struct TokenTime + { + nsCString token; + PRTime expirationTime; + }; + + struct CacheEntry : public PRCList + { + CacheEntry(nsCString& aKey) + : mKey(aKey) + { + MOZ_COUNT_CTOR(nsAccessControlLRUCache::CacheEntry); + } + + ~CacheEntry() + { + MOZ_COUNT_DTOR(nsAccessControlLRUCache::CacheEntry); + } + + void PurgeExpired(PRTime now); + PRBool CheckRequest(const nsCString& aMethod, + const nsTArray& aCustomHeaders); + + nsCString mKey; + nsTArray mMethods; + nsTArray mHeaders; + }; + + nsAccessControlLRUCache() + { + MOZ_COUNT_CTOR(nsAccessControlLRUCache); + PR_INIT_CLIST(&mList); + } + + ~nsAccessControlLRUCache() + { + Clear(); + MOZ_COUNT_DTOR(nsAccessControlLRUCache); + } + + PRBool Initialize() + { + return mTable.Init(); + } + + CacheEntry* GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal, + PRBool aWithCredentials, PRBool aCreate); + + void Clear(); + +private: + PR_STATIC_CALLBACK(PLDHashOperator) + RemoveExpiredEntries(const nsACString& aKey, nsAutoPtr& aValue, + void* aUserData); + + static PRBool GetCacheKey(nsIURI* aURI, nsIPrincipal* aPrincipal, + PRBool aWithCredentials, nsACString& _retval); + + nsClassHashtable mTable; + PRCList mList; +}; + class nsDOMEventListenerWrapper : public nsIDOMEventListener { public: @@ -265,12 +333,35 @@ public: NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsXMLHttpRequest, nsXHREventTarget) + static PRBool EnsureACCache() + { + if (sAccessControlCache) + return PR_TRUE; + + nsAutoPtr newCache(new nsAccessControlLRUCache()); + NS_ENSURE_TRUE(newCache, PR_FALSE); + + if (newCache->Initialize()) { + sAccessControlCache = newCache.forget(); + return PR_TRUE; + } + + return PR_FALSE; + } + + static void ShutdownACCache() + { + delete sAccessControlCache; + sAccessControlCache = nsnull; + } + PRBool AllowUploadProgress(); + static nsAccessControlLRUCache* sAccessControlCache; + protected: friend class nsMultipartProxyListener; - nsresult SetUploadDataAndType(nsIVariant* aBody); nsresult DetectCharset(nsACString& aCharset); nsresult ConvertBodyToText(nsAString& aOutBuffer); static NS_METHOD StreamReaderFunc(nsIInputStream* in, @@ -283,6 +374,8 @@ protected: // determines if the onreadystatechange listener should be called. nsresult ChangeState(PRUint32 aState, PRBool aBroadcast = PR_TRUE); nsresult RequestCompleted(); + nsresult GetLoadGroup(nsILoadGroup **aLoadGroup); + nsIURI *GetBaseURI(); nsresult RemoveAddEventListener(const nsAString& aType, nsRefPtr& aCurrent, @@ -307,7 +400,7 @@ protected: // mReadRequest is different from mChannel for multipart requests nsCOMPtr mReadRequest; nsCOMPtr mResponseXML; - nsCOMPtr mACPreflightChannel; + nsCOMPtr mACGetChannel; nsTArray mACUnsafeHeaders; nsRefPtr mOnUploadProgressListener; diff --git a/layout/build/nsLayoutStatics.cpp b/layout/build/nsLayoutStatics.cpp index 4f5109e4dd9..9ad425e7cbc 100644 --- a/layout/build/nsLayoutStatics.cpp +++ b/layout/build/nsLayoutStatics.cpp @@ -83,7 +83,6 @@ #include "nsXMLHttpRequest.h" #include "nsIFocusEventSuppressor.h" #include "nsDOMThreadService.h" -#include "nsCrossSiteListenerProxy.h" #ifdef MOZ_XUL #include "nsXULPopupManager.h" @@ -347,7 +346,7 @@ nsLayoutStatics::Shutdown() nsAudioStream::ShutdownLibrary(); #endif - nsCrossSiteListenerProxy::ShutdownPreflightCache(); + nsXMLHttpRequest::ShutdownACCache(); } void