Thu, 15 Jan 2015 21:03:48 +0100
Integrate friendly tips from Tor colleagues to make (or not) 4.5 alpha 3;
This includes removal of overloaded (but unused) methods, and addition of
a overlooked call to DataStruct::SetData(nsISupports, uint32_t, bool.)
1 /* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozilla/Assertions.h"
7 #include "mozilla/LinkedList.h"
9 #include "nsCrossSiteListenerProxy.h"
10 #include "nsIChannel.h"
11 #include "nsIHttpChannel.h"
12 #include "nsError.h"
13 #include "nsContentUtils.h"
14 #include "nsIScriptSecurityManager.h"
15 #include "nsNetUtil.h"
16 #include "nsMimeTypes.h"
17 #include "nsIStreamConverterService.h"
18 #include "nsStringStream.h"
19 #include "nsGkAtoms.h"
20 #include "nsWhitespaceTokenizer.h"
21 #include "nsIChannelEventSink.h"
22 #include "nsIAsyncVerifyRedirectCallback.h"
23 #include "nsCharSeparatedTokenizer.h"
24 #include "nsAsyncRedirectVerifyHelper.h"
25 #include "nsClassHashtable.h"
26 #include "nsHashKeys.h"
27 #include "nsStreamUtils.h"
28 #include "mozilla/Preferences.h"
29 #include "nsIScriptError.h"
30 #include "nsILoadGroup.h"
31 #include "nsILoadContext.h"
32 #include "nsIConsoleService.h"
33 #include "nsIDOMWindowUtils.h"
34 #include "nsIDOMWindow.h"
35 #include <algorithm>
37 using namespace mozilla;
39 #define PREFLIGHT_CACHE_SIZE 100
41 static bool gDisableCORS = false;
42 static bool gDisableCORSPrivateData = false;
44 static nsresult
45 LogBlockedRequest(nsIRequest* aRequest)
46 {
47 nsresult rv = NS_OK;
49 // Get the innerWindowID associated with the XMLHTTPRequest
50 PRUint64 innerWindowID = 0;
52 nsCOMPtr<nsILoadGroup> loadGroup;
53 aRequest->GetLoadGroup(getter_AddRefs(loadGroup));
54 if (loadGroup) {
55 nsCOMPtr<nsIInterfaceRequestor> callbacks;
56 loadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks));
57 if (callbacks) {
58 nsCOMPtr<nsILoadContext> loadContext = do_GetInterface(callbacks);
59 if(loadContext) {
60 nsCOMPtr<nsIDOMWindow> window;
61 loadContext->GetAssociatedWindow(getter_AddRefs(window));
62 if (window) {
63 nsCOMPtr<nsIDOMWindowUtils> du = do_GetInterface(window);
64 du->GetCurrentInnerWindowID(&innerWindowID);
65 }
66 }
67 }
68 }
70 if (!innerWindowID) {
71 return NS_ERROR_FAILURE;
72 }
74 nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
75 nsCOMPtr<nsIURI> aUri;
76 channel->GetURI(getter_AddRefs(aUri));
77 nsAutoCString spec;
78 if (aUri) {
79 aUri->GetSpec(spec);
80 }
82 // Generate the error message
83 nsXPIDLString blockedMessage;
84 NS_ConvertUTF8toUTF16 specUTF16(spec);
85 const char16_t* params[] = { specUTF16.get() };
86 rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES,
87 "CrossSiteRequestBlocked",
88 params,
89 blockedMessage);
91 // Build the error object and log it to the console
92 nsCOMPtr<nsIConsoleService> console(do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv));
93 NS_ENSURE_SUCCESS(rv, rv);
95 nsCOMPtr<nsIScriptError> scriptError = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv);
96 NS_ENSURE_SUCCESS(rv, rv);
98 nsAutoString msg(blockedMessage.get());
99 rv = scriptError->InitWithWindowID(msg,
100 NS_ConvertUTF8toUTF16(spec),
101 EmptyString(),
102 0,
103 0,
104 nsIScriptError::warningFlag,
105 "CORS",
106 innerWindowID);
107 NS_ENSURE_SUCCESS(rv, rv);
109 rv = console->LogMessage(scriptError);
110 return rv;
111 }
113 //////////////////////////////////////////////////////////////////////////
114 // Preflight cache
116 class nsPreflightCache
117 {
118 public:
119 struct TokenTime
120 {
121 nsCString token;
122 TimeStamp expirationTime;
123 };
125 struct CacheEntry : public LinkedListElement<CacheEntry>
126 {
127 CacheEntry(nsCString& aKey)
128 : mKey(aKey)
129 {
130 MOZ_COUNT_CTOR(nsPreflightCache::CacheEntry);
131 }
133 ~CacheEntry()
134 {
135 MOZ_COUNT_DTOR(nsPreflightCache::CacheEntry);
136 }
138 void PurgeExpired(TimeStamp now);
139 bool CheckRequest(const nsCString& aMethod,
140 const nsTArray<nsCString>& aCustomHeaders);
142 nsCString mKey;
143 nsTArray<TokenTime> mMethods;
144 nsTArray<TokenTime> mHeaders;
145 };
147 nsPreflightCache()
148 {
149 MOZ_COUNT_CTOR(nsPreflightCache);
150 }
152 ~nsPreflightCache()
153 {
154 Clear();
155 MOZ_COUNT_DTOR(nsPreflightCache);
156 }
158 bool Initialize()
159 {
160 return true;
161 }
163 CacheEntry* GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal,
164 bool aWithCredentials, bool aCreate);
165 void RemoveEntries(nsIURI* aURI, nsIPrincipal* aPrincipal);
167 void Clear();
169 private:
170 static PLDHashOperator
171 RemoveExpiredEntries(const nsACString& aKey, nsAutoPtr<CacheEntry>& aValue,
172 void* aUserData);
174 static bool GetCacheKey(nsIURI* aURI, nsIPrincipal* aPrincipal,
175 bool aWithCredentials, nsACString& _retval);
177 nsClassHashtable<nsCStringHashKey, CacheEntry> mTable;
178 LinkedList<CacheEntry> mList;
179 };
181 // Will be initialized in EnsurePreflightCache.
182 static nsPreflightCache* sPreflightCache = nullptr;
184 static bool EnsurePreflightCache()
185 {
186 if (sPreflightCache)
187 return true;
189 nsAutoPtr<nsPreflightCache> newCache(new nsPreflightCache());
191 if (newCache->Initialize()) {
192 sPreflightCache = newCache.forget();
193 return true;
194 }
196 return false;
197 }
199 void
200 nsPreflightCache::CacheEntry::PurgeExpired(TimeStamp now)
201 {
202 uint32_t i;
203 for (i = 0; i < mMethods.Length(); ++i) {
204 if (now >= mMethods[i].expirationTime) {
205 mMethods.RemoveElementAt(i--);
206 }
207 }
208 for (i = 0; i < mHeaders.Length(); ++i) {
209 if (now >= mHeaders[i].expirationTime) {
210 mHeaders.RemoveElementAt(i--);
211 }
212 }
213 }
215 bool
216 nsPreflightCache::CacheEntry::CheckRequest(const nsCString& aMethod,
217 const nsTArray<nsCString>& aHeaders)
218 {
219 PurgeExpired(TimeStamp::NowLoRes());
221 if (!aMethod.EqualsLiteral("GET") && !aMethod.EqualsLiteral("POST")) {
222 uint32_t i;
223 for (i = 0; i < mMethods.Length(); ++i) {
224 if (aMethod.Equals(mMethods[i].token))
225 break;
226 }
227 if (i == mMethods.Length()) {
228 return false;
229 }
230 }
232 for (uint32_t i = 0; i < aHeaders.Length(); ++i) {
233 uint32_t j;
234 for (j = 0; j < mHeaders.Length(); ++j) {
235 if (aHeaders[i].Equals(mHeaders[j].token,
236 nsCaseInsensitiveCStringComparator())) {
237 break;
238 }
239 }
240 if (j == mHeaders.Length()) {
241 return false;
242 }
243 }
245 return true;
246 }
248 nsPreflightCache::CacheEntry*
249 nsPreflightCache::GetEntry(nsIURI* aURI,
250 nsIPrincipal* aPrincipal,
251 bool aWithCredentials,
252 bool aCreate)
253 {
254 nsCString key;
255 if (!GetCacheKey(aURI, aPrincipal, aWithCredentials, key)) {
256 NS_WARNING("Invalid cache key!");
257 return nullptr;
258 }
260 CacheEntry* entry;
262 if (mTable.Get(key, &entry)) {
263 // Entry already existed so just return it. Also update the LRU list.
265 // Move to the head of the list.
266 entry->removeFrom(mList);
267 mList.insertFront(entry);
269 return entry;
270 }
272 if (!aCreate) {
273 return nullptr;
274 }
276 // This is a new entry, allocate and insert into the table now so that any
277 // failures don't cause items to be removed from a full cache.
278 entry = new CacheEntry(key);
279 if (!entry) {
280 NS_WARNING("Failed to allocate new cache entry!");
281 return nullptr;
282 }
284 NS_ASSERTION(mTable.Count() <= PREFLIGHT_CACHE_SIZE,
285 "Something is borked, too many entries in the cache!");
287 // Now enforce the max count.
288 if (mTable.Count() == PREFLIGHT_CACHE_SIZE) {
289 // Try to kick out all the expired entries.
290 TimeStamp now = TimeStamp::NowLoRes();
291 mTable.Enumerate(RemoveExpiredEntries, &now);
293 // If that didn't remove anything then kick out the least recently used
294 // entry.
295 if (mTable.Count() == PREFLIGHT_CACHE_SIZE) {
296 CacheEntry* lruEntry = static_cast<CacheEntry*>(mList.popLast());
297 MOZ_ASSERT(lruEntry);
299 // This will delete 'lruEntry'.
300 mTable.Remove(lruEntry->mKey);
302 NS_ASSERTION(mTable.Count() == PREFLIGHT_CACHE_SIZE - 1,
303 "Somehow tried to remove an entry that was never added!");
304 }
305 }
307 mTable.Put(key, entry);
308 mList.insertFront(entry);
310 return entry;
311 }
313 void
314 nsPreflightCache::RemoveEntries(nsIURI* aURI, nsIPrincipal* aPrincipal)
315 {
316 CacheEntry* entry;
317 nsCString key;
318 if (GetCacheKey(aURI, aPrincipal, true, key) &&
319 mTable.Get(key, &entry)) {
320 entry->removeFrom(mList);
321 mTable.Remove(key);
322 }
324 if (GetCacheKey(aURI, aPrincipal, false, key) &&
325 mTable.Get(key, &entry)) {
326 entry->removeFrom(mList);
327 mTable.Remove(key);
328 }
329 }
331 void
332 nsPreflightCache::Clear()
333 {
334 mList.clear();
335 mTable.Clear();
336 }
338 /* static */ PLDHashOperator
339 nsPreflightCache::RemoveExpiredEntries(const nsACString& aKey,
340 nsAutoPtr<CacheEntry>& aValue,
341 void* aUserData)
342 {
343 TimeStamp* now = static_cast<TimeStamp*>(aUserData);
345 aValue->PurgeExpired(*now);
347 if (aValue->mHeaders.IsEmpty() &&
348 aValue->mMethods.IsEmpty()) {
349 // Expired, remove from the list as well as the hash table.
350 aValue->removeFrom(sPreflightCache->mList);
351 return PL_DHASH_REMOVE;
352 }
354 return PL_DHASH_NEXT;
355 }
357 /* static */ bool
358 nsPreflightCache::GetCacheKey(nsIURI* aURI,
359 nsIPrincipal* aPrincipal,
360 bool aWithCredentials,
361 nsACString& _retval)
362 {
363 NS_ASSERTION(aURI, "Null uri!");
364 NS_ASSERTION(aPrincipal, "Null principal!");
366 NS_NAMED_LITERAL_CSTRING(space, " ");
368 nsCOMPtr<nsIURI> uri;
369 nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri));
370 NS_ENSURE_SUCCESS(rv, false);
372 nsAutoCString scheme, host, port;
373 if (uri) {
374 uri->GetScheme(scheme);
375 uri->GetHost(host);
376 port.AppendInt(NS_GetRealPort(uri));
377 }
379 nsAutoCString cred;
380 if (aWithCredentials) {
381 _retval.AssignLiteral("cred");
382 }
383 else {
384 _retval.AssignLiteral("nocred");
385 }
387 nsAutoCString spec;
388 rv = aURI->GetSpec(spec);
389 NS_ENSURE_SUCCESS(rv, false);
391 _retval.Assign(cred + space + scheme + space + host + space + port + space +
392 spec);
394 return true;
395 }
397 //////////////////////////////////////////////////////////////////////////
398 // nsCORSListenerProxy
400 NS_IMPL_ISUPPORTS(nsCORSListenerProxy, nsIStreamListener,
401 nsIRequestObserver, nsIChannelEventSink,
402 nsIInterfaceRequestor, nsIAsyncVerifyRedirectCallback)
404 /* static */
405 void
406 nsCORSListenerProxy::Startup()
407 {
408 Preferences::AddBoolVarCache(&gDisableCORS,
409 "content.cors.disable");
410 Preferences::AddBoolVarCache(&gDisableCORSPrivateData,
411 "content.cors.no_private_data");
412 }
414 /* static */
415 void
416 nsCORSListenerProxy::Shutdown()
417 {
418 delete sPreflightCache;
419 sPreflightCache = nullptr;
420 }
422 nsCORSListenerProxy::nsCORSListenerProxy(nsIStreamListener* aOuter,
423 nsIPrincipal* aRequestingPrincipal,
424 bool aWithCredentials)
425 : mOuterListener(aOuter),
426 mRequestingPrincipal(aRequestingPrincipal),
427 mOriginHeaderPrincipal(aRequestingPrincipal),
428 mWithCredentials(aWithCredentials && !gDisableCORSPrivateData),
429 mRequestApproved(false),
430 mHasBeenCrossSite(false),
431 mIsPreflight(false)
432 {
433 }
435 nsCORSListenerProxy::nsCORSListenerProxy(nsIStreamListener* aOuter,
436 nsIPrincipal* aRequestingPrincipal,
437 bool aWithCredentials,
438 const nsCString& aPreflightMethod,
439 const nsTArray<nsCString>& aPreflightHeaders)
440 : mOuterListener(aOuter),
441 mRequestingPrincipal(aRequestingPrincipal),
442 mOriginHeaderPrincipal(aRequestingPrincipal),
443 mWithCredentials(aWithCredentials && !gDisableCORSPrivateData),
444 mRequestApproved(false),
445 mHasBeenCrossSite(false),
446 mIsPreflight(true),
447 mPreflightMethod(aPreflightMethod),
448 mPreflightHeaders(aPreflightHeaders)
449 {
450 for (uint32_t i = 0; i < mPreflightHeaders.Length(); ++i) {
451 ToLowerCase(mPreflightHeaders[i]);
452 }
453 mPreflightHeaders.Sort();
454 }
456 nsresult
457 nsCORSListenerProxy::Init(nsIChannel* aChannel, bool aAllowDataURI)
458 {
459 aChannel->GetNotificationCallbacks(getter_AddRefs(mOuterNotificationCallbacks));
460 aChannel->SetNotificationCallbacks(this);
462 nsresult rv = UpdateChannel(aChannel, aAllowDataURI);
463 if (NS_FAILED(rv)) {
464 mOuterListener = nullptr;
465 mRequestingPrincipal = nullptr;
466 mOriginHeaderPrincipal = nullptr;
467 mOuterNotificationCallbacks = nullptr;
468 }
469 return rv;
470 }
472 NS_IMETHODIMP
473 nsCORSListenerProxy::OnStartRequest(nsIRequest* aRequest,
474 nsISupports* aContext)
475 {
476 nsresult rv = CheckRequestApproved(aRequest);
477 mRequestApproved = NS_SUCCEEDED(rv);
478 if (!mRequestApproved) {
479 rv = LogBlockedRequest(aRequest);
480 NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to log blocked cross-site request");
481 if (sPreflightCache) {
482 nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
483 if (channel) {
484 nsCOMPtr<nsIURI> uri;
485 NS_GetFinalChannelURI(channel, getter_AddRefs(uri));
486 if (uri) {
487 // OK to use mRequestingPrincipal since preflights never get
488 // redirected.
489 sPreflightCache->RemoveEntries(uri, mRequestingPrincipal);
490 }
491 }
492 }
494 aRequest->Cancel(NS_ERROR_DOM_BAD_URI);
495 mOuterListener->OnStartRequest(aRequest, aContext);
497 return NS_ERROR_DOM_BAD_URI;
498 }
500 return mOuterListener->OnStartRequest(aRequest, aContext);
501 }
503 bool
504 IsValidHTTPToken(const nsCSubstring& aToken)
505 {
506 if (aToken.IsEmpty()) {
507 return false;
508 }
510 nsCSubstring::const_char_iterator iter, end;
512 aToken.BeginReading(iter);
513 aToken.EndReading(end);
515 while (iter != end) {
516 if (*iter <= 32 ||
517 *iter >= 127 ||
518 *iter == '(' ||
519 *iter == ')' ||
520 *iter == '<' ||
521 *iter == '>' ||
522 *iter == '@' ||
523 *iter == ',' ||
524 *iter == ';' ||
525 *iter == ':' ||
526 *iter == '\\' ||
527 *iter == '\"' ||
528 *iter == '/' ||
529 *iter == '[' ||
530 *iter == ']' ||
531 *iter == '?' ||
532 *iter == '=' ||
533 *iter == '{' ||
534 *iter == '}') {
535 return false;
536 }
537 ++iter;
538 }
540 return true;
541 }
543 nsresult
544 nsCORSListenerProxy::CheckRequestApproved(nsIRequest* aRequest)
545 {
546 // Check if this was actually a cross domain request
547 if (!mHasBeenCrossSite) {
548 return NS_OK;
549 }
551 if (gDisableCORS) {
552 return NS_ERROR_DOM_BAD_URI;
553 }
555 // Check if the request failed
556 nsresult status;
557 nsresult rv = aRequest->GetStatus(&status);
558 NS_ENSURE_SUCCESS(rv, rv);
559 NS_ENSURE_SUCCESS(status, status);
561 // Test that things worked on a HTTP level
562 nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest);
563 NS_ENSURE_TRUE(http, NS_ERROR_DOM_BAD_URI);
565 // Check the Access-Control-Allow-Origin header
566 nsAutoCString allowedOriginHeader;
567 rv = http->GetResponseHeader(
568 NS_LITERAL_CSTRING("Access-Control-Allow-Origin"), allowedOriginHeader);
569 NS_ENSURE_SUCCESS(rv, rv);
571 if (mWithCredentials || !allowedOriginHeader.EqualsLiteral("*")) {
572 nsAutoCString origin;
573 rv = nsContentUtils::GetASCIIOrigin(mOriginHeaderPrincipal, origin);
574 NS_ENSURE_SUCCESS(rv, rv);
576 if (!allowedOriginHeader.Equals(origin)) {
577 return NS_ERROR_DOM_BAD_URI;
578 }
579 }
581 // Check Access-Control-Allow-Credentials header
582 if (mWithCredentials) {
583 nsAutoCString allowCredentialsHeader;
584 rv = http->GetResponseHeader(
585 NS_LITERAL_CSTRING("Access-Control-Allow-Credentials"), allowCredentialsHeader);
586 NS_ENSURE_SUCCESS(rv, rv);
588 if (!allowCredentialsHeader.EqualsLiteral("true")) {
589 return NS_ERROR_DOM_BAD_URI;
590 }
591 }
593 if (mIsPreflight) {
594 bool succeedded;
595 rv = http->GetRequestSucceeded(&succeedded);
596 NS_ENSURE_SUCCESS(rv, rv);
597 if (!succeedded) {
598 return NS_ERROR_DOM_BAD_URI;
599 }
601 nsAutoCString headerVal;
602 // The "Access-Control-Allow-Methods" header contains a comma separated
603 // list of method names.
604 http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Methods"),
605 headerVal);
606 bool foundMethod = mPreflightMethod.EqualsLiteral("GET") ||
607 mPreflightMethod.EqualsLiteral("HEAD") ||
608 mPreflightMethod.EqualsLiteral("POST");
609 nsCCharSeparatedTokenizer methodTokens(headerVal, ',');
610 while(methodTokens.hasMoreTokens()) {
611 const nsDependentCSubstring& method = methodTokens.nextToken();
612 if (method.IsEmpty()) {
613 continue;
614 }
615 if (!IsValidHTTPToken(method)) {
616 return NS_ERROR_DOM_BAD_URI;
617 }
618 foundMethod |= mPreflightMethod.Equals(method);
619 }
620 NS_ENSURE_TRUE(foundMethod, NS_ERROR_DOM_BAD_URI);
622 // The "Access-Control-Allow-Headers" header contains a comma separated
623 // list of header names.
624 headerVal.Truncate();
625 http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Headers"),
626 headerVal);
627 nsTArray<nsCString> headers;
628 nsCCharSeparatedTokenizer headerTokens(headerVal, ',');
629 while(headerTokens.hasMoreTokens()) {
630 const nsDependentCSubstring& header = headerTokens.nextToken();
631 if (header.IsEmpty()) {
632 continue;
633 }
634 if (!IsValidHTTPToken(header)) {
635 return NS_ERROR_DOM_BAD_URI;
636 }
637 headers.AppendElement(header);
638 }
639 for (uint32_t i = 0; i < mPreflightHeaders.Length(); ++i) {
640 if (!headers.Contains(mPreflightHeaders[i],
641 nsCaseInsensitiveCStringArrayComparator())) {
642 return NS_ERROR_DOM_BAD_URI;
643 }
644 }
645 }
647 return NS_OK;
648 }
650 NS_IMETHODIMP
651 nsCORSListenerProxy::OnStopRequest(nsIRequest* aRequest,
652 nsISupports* aContext,
653 nsresult aStatusCode)
654 {
655 nsresult rv = mOuterListener->OnStopRequest(aRequest, aContext, aStatusCode);
656 mOuterListener = nullptr;
657 mOuterNotificationCallbacks = nullptr;
658 mRedirectCallback = nullptr;
659 mOldRedirectChannel = nullptr;
660 mNewRedirectChannel = nullptr;
661 return rv;
662 }
664 NS_IMETHODIMP
665 nsCORSListenerProxy::OnDataAvailable(nsIRequest* aRequest,
666 nsISupports* aContext,
667 nsIInputStream* aInputStream,
668 uint64_t aOffset,
669 uint32_t aCount)
670 {
671 if (!mRequestApproved) {
672 return NS_ERROR_DOM_BAD_URI;
673 }
674 return mOuterListener->OnDataAvailable(aRequest, aContext, aInputStream,
675 aOffset, aCount);
676 }
678 NS_IMETHODIMP
679 nsCORSListenerProxy::GetInterface(const nsIID & aIID, void **aResult)
680 {
681 if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
682 *aResult = static_cast<nsIChannelEventSink*>(this);
683 NS_ADDREF_THIS();
685 return NS_OK;
686 }
688 return mOuterNotificationCallbacks ?
689 mOuterNotificationCallbacks->GetInterface(aIID, aResult) :
690 NS_ERROR_NO_INTERFACE;
691 }
693 NS_IMETHODIMP
694 nsCORSListenerProxy::AsyncOnChannelRedirect(nsIChannel *aOldChannel,
695 nsIChannel *aNewChannel,
696 uint32_t aFlags,
697 nsIAsyncVerifyRedirectCallback *cb)
698 {
699 nsresult rv;
700 if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags)) {
701 rv = CheckRequestApproved(aOldChannel);
702 if (NS_FAILED(rv)) {
703 rv = LogBlockedRequest(aOldChannel);
704 NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to log blocked cross-site request");
706 if (sPreflightCache) {
707 nsCOMPtr<nsIURI> oldURI;
708 NS_GetFinalChannelURI(aOldChannel, getter_AddRefs(oldURI));
709 if (oldURI) {
710 // OK to use mRequestingPrincipal since preflights never get
711 // redirected.
712 sPreflightCache->RemoveEntries(oldURI, mRequestingPrincipal);
713 }
714 }
715 aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI);
716 return NS_ERROR_DOM_BAD_URI;
717 }
719 if (mHasBeenCrossSite) {
720 // Once we've been cross-site, cross-origin redirects reset our source
721 // origin.
722 nsCOMPtr<nsIPrincipal> oldChannelPrincipal;
723 nsContentUtils::GetSecurityManager()->
724 GetChannelPrincipal(aOldChannel, getter_AddRefs(oldChannelPrincipal));
725 nsCOMPtr<nsIPrincipal> newChannelPrincipal;
726 nsContentUtils::GetSecurityManager()->
727 GetChannelPrincipal(aNewChannel, getter_AddRefs(newChannelPrincipal));
728 if (!oldChannelPrincipal || !newChannelPrincipal) {
729 rv = NS_ERROR_OUT_OF_MEMORY;
730 }
732 if (NS_SUCCEEDED(rv)) {
733 bool equal;
734 rv = oldChannelPrincipal->Equals(newChannelPrincipal, &equal);
735 if (NS_SUCCEEDED(rv)) {
736 if (!equal) {
737 // Spec says to set our source origin to a unique origin.
738 mOriginHeaderPrincipal = do_CreateInstance("@mozilla.org/nullprincipal;1");
739 if (!mOriginHeaderPrincipal) {
740 rv = NS_ERROR_OUT_OF_MEMORY;
741 }
742 }
743 }
744 }
746 if (NS_FAILED(rv)) {
747 aOldChannel->Cancel(rv);
748 return rv;
749 }
750 }
751 }
753 // Prepare to receive callback
754 mRedirectCallback = cb;
755 mOldRedirectChannel = aOldChannel;
756 mNewRedirectChannel = aNewChannel;
758 nsCOMPtr<nsIChannelEventSink> outer =
759 do_GetInterface(mOuterNotificationCallbacks);
760 if (outer) {
761 rv = outer->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, this);
762 if (NS_FAILED(rv)) {
763 aOldChannel->Cancel(rv); // is this necessary...?
764 mRedirectCallback = nullptr;
765 mOldRedirectChannel = nullptr;
766 mNewRedirectChannel = nullptr;
767 }
768 return rv;
769 }
771 (void) OnRedirectVerifyCallback(NS_OK);
772 return NS_OK;
773 }
775 NS_IMETHODIMP
776 nsCORSListenerProxy::OnRedirectVerifyCallback(nsresult result)
777 {
778 NS_ASSERTION(mRedirectCallback, "mRedirectCallback not set in callback");
779 NS_ASSERTION(mOldRedirectChannel, "mOldRedirectChannel not set in callback");
780 NS_ASSERTION(mNewRedirectChannel, "mNewRedirectChannel not set in callback");
782 if (NS_SUCCEEDED(result)) {
783 nsresult rv = UpdateChannel(mNewRedirectChannel);
784 if (NS_FAILED(rv)) {
785 NS_WARNING("nsCORSListenerProxy::OnRedirectVerifyCallback: "
786 "UpdateChannel() returned failure");
787 }
788 result = rv;
789 }
791 if (NS_FAILED(result)) {
792 mOldRedirectChannel->Cancel(result);
793 }
795 mOldRedirectChannel = nullptr;
796 mNewRedirectChannel = nullptr;
797 mRedirectCallback->OnRedirectVerifyCallback(result);
798 mRedirectCallback = nullptr;
799 return NS_OK;
800 }
802 nsresult
803 nsCORSListenerProxy::UpdateChannel(nsIChannel* aChannel, bool aAllowDataURI)
804 {
805 nsCOMPtr<nsIURI> uri, originalURI;
806 nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
807 NS_ENSURE_SUCCESS(rv, rv);
808 rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI));
809 NS_ENSURE_SUCCESS(rv, rv);
811 // exempt data URIs from the same origin check.
812 if (aAllowDataURI && originalURI == uri) {
813 bool dataScheme = false;
814 rv = uri->SchemeIs("data", &dataScheme);
815 NS_ENSURE_SUCCESS(rv, rv);
816 if (dataScheme) {
817 return NS_OK;
818 }
819 }
821 // Check that the uri is ok to load
822 rv = nsContentUtils::GetSecurityManager()->
823 CheckLoadURIWithPrincipal(mRequestingPrincipal, uri,
824 nsIScriptSecurityManager::STANDARD);
825 NS_ENSURE_SUCCESS(rv, rv);
827 if (originalURI != uri) {
828 rv = nsContentUtils::GetSecurityManager()->
829 CheckLoadURIWithPrincipal(mRequestingPrincipal, originalURI,
830 nsIScriptSecurityManager::STANDARD);
831 NS_ENSURE_SUCCESS(rv, rv);
832 }
834 if (!mHasBeenCrossSite &&
835 NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(uri, false, false)) &&
836 (originalURI == uri ||
837 NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(originalURI,
838 false, false)))) {
839 return NS_OK;
840 }
842 // It's a cross site load
843 mHasBeenCrossSite = true;
845 nsCString userpass;
846 uri->GetUserPass(userpass);
847 NS_ENSURE_TRUE(userpass.IsEmpty(), NS_ERROR_DOM_BAD_URI);
849 // Add the Origin header
850 nsAutoCString origin;
851 rv = nsContentUtils::GetASCIIOrigin(mOriginHeaderPrincipal, origin);
852 NS_ENSURE_SUCCESS(rv, rv);
854 nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel);
855 NS_ENSURE_TRUE(http, NS_ERROR_FAILURE);
857 rv = http->SetRequestHeader(NS_LITERAL_CSTRING("Origin"), origin, false);
858 NS_ENSURE_SUCCESS(rv, rv);
860 // Add preflight headers if this is a preflight request
861 if (mIsPreflight) {
862 rv = http->
863 SetRequestHeader(NS_LITERAL_CSTRING("Access-Control-Request-Method"),
864 mPreflightMethod, false);
865 NS_ENSURE_SUCCESS(rv, rv);
867 if (!mPreflightHeaders.IsEmpty()) {
868 nsAutoCString headers;
869 for (uint32_t i = 0; i < mPreflightHeaders.Length(); ++i) {
870 if (i != 0) {
871 headers += ',';
872 }
873 headers += mPreflightHeaders[i];
874 }
875 rv = http->
876 SetRequestHeader(NS_LITERAL_CSTRING("Access-Control-Request-Headers"),
877 headers, false);
878 NS_ENSURE_SUCCESS(rv, rv);
879 }
880 }
882 // Make cookie-less if needed
883 if (mIsPreflight || !mWithCredentials) {
884 nsLoadFlags flags;
885 rv = http->GetLoadFlags(&flags);
886 NS_ENSURE_SUCCESS(rv, rv);
888 flags |= nsIRequest::LOAD_ANONYMOUS;
889 rv = http->SetLoadFlags(flags);
890 NS_ENSURE_SUCCESS(rv, rv);
891 }
893 return NS_OK;
894 }
896 //////////////////////////////////////////////////////////////////////////
897 // Preflight proxy
899 // Class used as streamlistener and notification callback when
900 // doing the initial OPTIONS request for a CORS check
901 class nsCORSPreflightListener MOZ_FINAL : public nsIStreamListener,
902 public nsIInterfaceRequestor,
903 public nsIChannelEventSink
904 {
905 public:
906 nsCORSPreflightListener(nsIChannel* aOuterChannel,
907 nsIStreamListener* aOuterListener,
908 nsISupports* aOuterContext,
909 nsIPrincipal* aReferrerPrincipal,
910 const nsACString& aRequestMethod,
911 bool aWithCredentials)
912 : mOuterChannel(aOuterChannel), mOuterListener(aOuterListener),
913 mOuterContext(aOuterContext), mReferrerPrincipal(aReferrerPrincipal),
914 mRequestMethod(aRequestMethod), mWithCredentials(aWithCredentials)
915 { }
917 NS_DECL_ISUPPORTS
918 NS_DECL_NSISTREAMLISTENER
919 NS_DECL_NSIREQUESTOBSERVER
920 NS_DECL_NSIINTERFACEREQUESTOR
921 NS_DECL_NSICHANNELEVENTSINK
923 private:
924 void AddResultToCache(nsIRequest* aRequest);
926 nsCOMPtr<nsIChannel> mOuterChannel;
927 nsCOMPtr<nsIStreamListener> mOuterListener;
928 nsCOMPtr<nsISupports> mOuterContext;
929 nsCOMPtr<nsIPrincipal> mReferrerPrincipal;
930 nsCString mRequestMethod;
931 bool mWithCredentials;
932 };
934 NS_IMPL_ISUPPORTS(nsCORSPreflightListener, nsIStreamListener,
935 nsIRequestObserver, nsIInterfaceRequestor,
936 nsIChannelEventSink)
938 void
939 nsCORSPreflightListener::AddResultToCache(nsIRequest *aRequest)
940 {
941 nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest);
942 NS_ASSERTION(http, "Request was not http");
944 // The "Access-Control-Max-Age" header should return an age in seconds.
945 nsAutoCString headerVal;
946 http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Max-Age"),
947 headerVal);
948 if (headerVal.IsEmpty()) {
949 return;
950 }
952 // Sanitize the string. We only allow 'delta-seconds' as specified by
953 // http://dev.w3.org/2006/waf/access-control (digits 0-9 with no leading or
954 // trailing non-whitespace characters).
955 uint32_t age = 0;
956 nsCSubstring::const_char_iterator iter, end;
957 headerVal.BeginReading(iter);
958 headerVal.EndReading(end);
959 while (iter != end) {
960 if (*iter < '0' || *iter > '9') {
961 return;
962 }
963 age = age * 10 + (*iter - '0');
964 // Cap at 24 hours. This also avoids overflow
965 age = std::min(age, 86400U);
966 ++iter;
967 }
969 if (!age || !EnsurePreflightCache()) {
970 return;
971 }
974 // String seems fine, go ahead and cache.
975 // Note that we have already checked that these headers follow the correct
976 // syntax.
978 nsCOMPtr<nsIURI> uri;
979 NS_GetFinalChannelURI(http, getter_AddRefs(uri));
981 TimeStamp expirationTime = TimeStamp::NowLoRes() + TimeDuration::FromSeconds(age);
983 nsPreflightCache::CacheEntry* entry =
984 sPreflightCache->GetEntry(uri, mReferrerPrincipal, mWithCredentials,
985 true);
986 if (!entry) {
987 return;
988 }
990 // The "Access-Control-Allow-Methods" header contains a comma separated
991 // list of method names.
992 http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Methods"),
993 headerVal);
995 nsCCharSeparatedTokenizer methods(headerVal, ',');
996 while(methods.hasMoreTokens()) {
997 const nsDependentCSubstring& method = methods.nextToken();
998 if (method.IsEmpty()) {
999 continue;
1000 }
1001 uint32_t i;
1002 for (i = 0; i < entry->mMethods.Length(); ++i) {
1003 if (entry->mMethods[i].token.Equals(method)) {
1004 entry->mMethods[i].expirationTime = expirationTime;
1005 break;
1006 }
1007 }
1008 if (i == entry->mMethods.Length()) {
1009 nsPreflightCache::TokenTime* newMethod =
1010 entry->mMethods.AppendElement();
1011 if (!newMethod) {
1012 return;
1013 }
1015 newMethod->token = method;
1016 newMethod->expirationTime = expirationTime;
1017 }
1018 }
1020 // The "Access-Control-Allow-Headers" header contains a comma separated
1021 // list of method names.
1022 http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Headers"),
1023 headerVal);
1025 nsCCharSeparatedTokenizer headers(headerVal, ',');
1026 while(headers.hasMoreTokens()) {
1027 const nsDependentCSubstring& header = headers.nextToken();
1028 if (header.IsEmpty()) {
1029 continue;
1030 }
1031 uint32_t i;
1032 for (i = 0; i < entry->mHeaders.Length(); ++i) {
1033 if (entry->mHeaders[i].token.Equals(header)) {
1034 entry->mHeaders[i].expirationTime = expirationTime;
1035 break;
1036 }
1037 }
1038 if (i == entry->mHeaders.Length()) {
1039 nsPreflightCache::TokenTime* newHeader =
1040 entry->mHeaders.AppendElement();
1041 if (!newHeader) {
1042 return;
1043 }
1045 newHeader->token = header;
1046 newHeader->expirationTime = expirationTime;
1047 }
1048 }
1049 }
1051 NS_IMETHODIMP
1052 nsCORSPreflightListener::OnStartRequest(nsIRequest *aRequest,
1053 nsISupports *aContext)
1054 {
1055 nsresult status;
1056 nsresult rv = aRequest->GetStatus(&status);
1058 if (NS_SUCCEEDED(rv)) {
1059 rv = status;
1060 }
1062 if (NS_SUCCEEDED(rv)) {
1063 // Everything worked, try to cache and then fire off the actual request.
1064 AddResultToCache(aRequest);
1066 rv = mOuterChannel->AsyncOpen(mOuterListener, mOuterContext);
1067 }
1069 if (NS_FAILED(rv)) {
1070 mOuterChannel->Cancel(rv);
1071 mOuterListener->OnStartRequest(mOuterChannel, mOuterContext);
1072 mOuterListener->OnStopRequest(mOuterChannel, mOuterContext, rv);
1074 return rv;
1075 }
1077 return NS_OK;
1078 }
1080 NS_IMETHODIMP
1081 nsCORSPreflightListener::OnStopRequest(nsIRequest *aRequest,
1082 nsISupports *aContext,
1083 nsresult aStatus)
1084 {
1085 mOuterChannel = nullptr;
1086 mOuterListener = nullptr;
1087 mOuterContext = nullptr;
1088 return NS_OK;
1089 }
1091 /** nsIStreamListener methods **/
1093 NS_IMETHODIMP
1094 nsCORSPreflightListener::OnDataAvailable(nsIRequest *aRequest,
1095 nsISupports *ctxt,
1096 nsIInputStream *inStr,
1097 uint64_t sourceOffset,
1098 uint32_t count)
1099 {
1100 uint32_t totalRead;
1101 return inStr->ReadSegments(NS_DiscardSegment, nullptr, count, &totalRead);
1102 }
1104 NS_IMETHODIMP
1105 nsCORSPreflightListener::AsyncOnChannelRedirect(nsIChannel *aOldChannel,
1106 nsIChannel *aNewChannel,
1107 uint32_t aFlags,
1108 nsIAsyncVerifyRedirectCallback *callback)
1109 {
1110 // Only internal redirects allowed for now.
1111 if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags))
1112 return NS_ERROR_DOM_BAD_URI;
1114 callback->OnRedirectVerifyCallback(NS_OK);
1115 return NS_OK;
1116 }
1118 NS_IMETHODIMP
1119 nsCORSPreflightListener::GetInterface(const nsIID & aIID, void **aResult)
1120 {
1121 return QueryInterface(aIID, aResult);
1122 }
1125 nsresult
1126 NS_StartCORSPreflight(nsIChannel* aRequestChannel,
1127 nsIStreamListener* aListener,
1128 nsIPrincipal* aPrincipal,
1129 bool aWithCredentials,
1130 nsTArray<nsCString>& aUnsafeHeaders,
1131 nsIChannel** aPreflightChannel)
1132 {
1133 *aPreflightChannel = nullptr;
1135 nsAutoCString method;
1136 nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aRequestChannel));
1137 NS_ENSURE_TRUE(httpChannel, NS_ERROR_UNEXPECTED);
1138 httpChannel->GetRequestMethod(method);
1140 nsCOMPtr<nsIURI> uri;
1141 nsresult rv = NS_GetFinalChannelURI(aRequestChannel, getter_AddRefs(uri));
1142 NS_ENSURE_SUCCESS(rv, rv);
1144 nsPreflightCache::CacheEntry* entry =
1145 sPreflightCache ?
1146 sPreflightCache->GetEntry(uri, aPrincipal, aWithCredentials, false) :
1147 nullptr;
1149 if (entry && entry->CheckRequest(method, aUnsafeHeaders)) {
1150 // We have a cached preflight result, just start the original channel
1151 return aRequestChannel->AsyncOpen(aListener, nullptr);
1152 }
1154 // Either it wasn't cached or the cached result has expired. Build a
1155 // channel for the OPTIONS request.
1157 nsCOMPtr<nsILoadGroup> loadGroup;
1158 rv = aRequestChannel->GetLoadGroup(getter_AddRefs(loadGroup));
1159 NS_ENSURE_SUCCESS(rv, rv);
1161 nsLoadFlags loadFlags;
1162 rv = aRequestChannel->GetLoadFlags(&loadFlags);
1163 NS_ENSURE_SUCCESS(rv, rv);
1165 nsCOMPtr<nsIChannel> preflightChannel;
1166 rv = NS_NewChannel(getter_AddRefs(preflightChannel), uri, nullptr,
1167 loadGroup, nullptr, loadFlags);
1168 NS_ENSURE_SUCCESS(rv, rv);
1170 nsCOMPtr<nsIHttpChannel> preHttp = do_QueryInterface(preflightChannel);
1171 NS_ASSERTION(preHttp, "Failed to QI to nsIHttpChannel!");
1173 rv = preHttp->SetRequestMethod(NS_LITERAL_CSTRING("OPTIONS"));
1174 NS_ENSURE_SUCCESS(rv, rv);
1176 // Set up listener which will start the original channel
1177 nsCOMPtr<nsIStreamListener> preflightListener =
1178 new nsCORSPreflightListener(aRequestChannel, aListener, nullptr, aPrincipal,
1179 method, aWithCredentials);
1180 NS_ENSURE_TRUE(preflightListener, NS_ERROR_OUT_OF_MEMORY);
1182 nsRefPtr<nsCORSListenerProxy> corsListener =
1183 new nsCORSListenerProxy(preflightListener, aPrincipal,
1184 aWithCredentials, method,
1185 aUnsafeHeaders);
1186 rv = corsListener->Init(preflightChannel);
1187 NS_ENSURE_SUCCESS(rv, rv);
1188 preflightListener = corsListener;
1190 // Start preflight
1191 rv = preflightChannel->AsyncOpen(preflightListener, nullptr);
1192 NS_ENSURE_SUCCESS(rv, rv);
1194 // Return newly created preflight channel
1195 preflightChannel.forget(aPreflightChannel);
1197 return NS_OK;
1198 }