1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * You can obtain one at http://mozilla.org/MPL/2.0/. */
8 #include "FetchPreloader.h"
10 #include "mozilla/Assertions.h"
11 #include "mozilla/CORSMode.h"
12 #include "mozilla/DebugOnly.h"
13 #include "mozilla/dom/Document.h"
14 #include "mozilla/LoadInfo.h"
15 #include "mozilla/ScopeExit.h"
16 #include "mozilla/Unused.h"
17 #include "mozilla/dom/ReferrerInfo.h"
18 #include "nsContentPolicyUtils.h"
19 #include "nsContentSecurityManager.h"
20 #include "nsContentUtils.h"
21 #include "nsIChannel.h"
22 #include "nsIChildChannel.h"
23 #include "nsIClassOfService.h"
24 #include "nsIHttpChannel.h"
25 #include "nsIHttpChannelInternal.h"
26 #include "nsISupportsPriority.h"
27 #include "nsITimedChannel.h"
28 #include "nsNetUtil.h"
29 #include "nsStringStream.h"
30 #include "nsIDocShell.h"
34 NS_IMPL_ISUPPORTS(FetchPreloader
, nsIStreamListener
, nsIRequestObserver
)
36 FetchPreloader::FetchPreloader()
37 : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD
) {}
39 FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType
)
40 : mContentPolicyType(aContentPolicyType
) {}
42 nsresult
FetchPreloader::OpenChannel(const PreloadHashKey
& aKey
, nsIURI
* aURI
,
43 const CORSMode aCORSMode
,
44 const dom::ReferrerPolicy
& aReferrerPolicy
,
45 dom::Document
* aDocument
,
46 uint64_t aEarlyHintPreloaderId
,
47 int32_t aSupportsPriorityValue
) {
49 nsCOMPtr
<nsIChannel
> channel
;
51 auto notify
= MakeScopeExit([&]() {
53 // Make sure we notify any <link preload> elements when opening fails
54 // because of various technical or security reasons.
56 // Using the non-channel overload of this method to make it work even
57 // before NotifyOpen has been called on this preload. We are not
58 // switching between channels, so it's safe to do so.
63 nsCOMPtr
<nsILoadGroup
> loadGroup
= aDocument
->GetDocumentLoadGroup();
64 nsCOMPtr
<nsPIDOMWindowOuter
> window
= aDocument
->GetWindow();
65 nsCOMPtr
<nsIInterfaceRequestor
> prompter
;
67 nsIDocShell
* docshell
= window
->GetDocShell();
68 prompter
= do_QueryInterface(docshell
);
71 rv
= CreateChannel(getter_AddRefs(channel
), aURI
, aCORSMode
, aReferrerPolicy
,
72 aDocument
, loadGroup
, prompter
, aEarlyHintPreloaderId
,
73 aSupportsPriorityValue
);
74 NS_ENSURE_SUCCESS(rv
, rv
);
76 // Doing this now so that we have the channel and tainting set on it properly
77 // to notify the proper event (load or error) on the associated preload tags
78 // when the CSP check fails.
79 rv
= CheckContentPolicy(aURI
, aDocument
);
84 FetchPreloader::PrioritizeAsPreload(channel
);
85 AddLoadBackgroundFlag(channel
);
87 NotifyOpen(aKey
, channel
, aDocument
, true);
89 if (aEarlyHintPreloaderId
) {
90 nsCOMPtr
<nsIHttpChannelInternal
> channelInternal
=
91 do_QueryInterface(channel
);
92 NS_ENSURE_TRUE(channelInternal
!= nullptr, NS_ERROR_FAILURE
);
94 rv
= channelInternal
->SetEarlyHintPreloaderId(aEarlyHintPreloaderId
);
95 NS_ENSURE_SUCCESS(rv
, rv
);
97 return mAsyncConsumeResult
= rv
= channel
->AsyncOpen(this);
100 nsresult
FetchPreloader::CreateChannel(
101 nsIChannel
** aChannel
, nsIURI
* aURI
, const CORSMode aCORSMode
,
102 const dom::ReferrerPolicy
& aReferrerPolicy
, dom::Document
* aDocument
,
103 nsILoadGroup
* aLoadGroup
, nsIInterfaceRequestor
* aCallbacks
,
104 uint64_t aEarlyHintPreloaderId
, int32_t aSupportsPriorityValue
) {
107 nsSecurityFlags securityFlags
=
108 nsContentSecurityManager::ComputeSecurityFlags(
109 aCORSMode
, nsContentSecurityManager::CORSSecurityMapping::
110 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS
);
112 nsCOMPtr
<nsIChannel
> channel
;
113 rv
= NS_NewChannelWithTriggeringPrincipal(
114 getter_AddRefs(channel
), aURI
, aDocument
, aDocument
->NodePrincipal(),
115 securityFlags
, nsIContentPolicy::TYPE_FETCH
, nullptr, aLoadGroup
,
121 AdjustPriority(channel
, aSupportsPriorityValue
);
123 if (nsCOMPtr
<nsIHttpChannel
> httpChannel
= do_QueryInterface(channel
)) {
124 nsCOMPtr
<nsIReferrerInfo
> referrerInfo
= new dom::ReferrerInfo(
125 aDocument
->GetDocumentURIAsReferrer(), aReferrerPolicy
);
126 rv
= httpChannel
->SetReferrerInfoWithoutClone(referrerInfo
);
127 MOZ_ASSERT(NS_SUCCEEDED(rv
));
130 if (nsCOMPtr
<nsITimedChannel
> timedChannel
= do_QueryInterface(channel
)) {
131 if (aEarlyHintPreloaderId
) {
132 timedChannel
->SetInitiatorType(u
"early-hints"_ns
);
134 timedChannel
->SetInitiatorType(u
"link"_ns
);
138 channel
.forget(aChannel
);
143 void FetchPreloader::AdjustPriority(nsIChannel
* aChannel
,
144 int32_t aSupportsPriorityValue
) {
145 if (nsCOMPtr
<nsISupportsPriority
> supportsPriority
{
146 do_QueryInterface(aChannel
)}) {
147 supportsPriority
->SetPriority(aSupportsPriorityValue
);
151 nsresult
FetchPreloader::CheckContentPolicy(nsIURI
* aURI
,
152 dom::Document
* aDocument
) {
157 nsCOMPtr
<nsILoadInfo
> secCheckLoadInfo
= new net::LoadInfo(
158 aDocument
->NodePrincipal(), aDocument
->NodePrincipal(), aDocument
,
159 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK
, mContentPolicyType
);
161 int16_t shouldLoad
= nsIContentPolicy::ACCEPT
;
162 nsresult rv
= NS_CheckContentLoadPolicy(aURI
, secCheckLoadInfo
, &shouldLoad
,
163 nsContentUtils::GetContentPolicy());
164 if (NS_SUCCEEDED(rv
) && NS_CP_ACCEPTED(shouldLoad
)) {
168 return NS_ERROR_CONTENT_BLOCKED
;
173 nsresult
FetchPreloader::AsyncConsume(nsIStreamListener
* aListener
) {
174 if (NS_FAILED(mAsyncConsumeResult
)) {
175 // Already being consumed or failed to open.
176 return mAsyncConsumeResult
;
179 // Prevent duplicate calls.
180 mAsyncConsumeResult
= NS_ERROR_NOT_AVAILABLE
;
182 if (!mConsumeListener
) {
183 // Called before we are getting response from the channel.
184 mConsumeListener
= aListener
;
186 // Channel already started, push cached calls to this listener.
187 // Can't be anything else than the `Cache`, hence a safe static_cast.
188 Cache
* cache
= static_cast<Cache
*>(mConsumeListener
.get());
189 cache
->AsyncConsume(aListener
);
196 void FetchPreloader::PrioritizeAsPreload(nsIChannel
* aChannel
) {
197 if (nsCOMPtr
<nsIClassOfService
> cos
= do_QueryInterface(aChannel
)) {
198 cos
->AddClassFlags(nsIClassOfService::Unblocked
);
202 // nsIRequestObserver + nsIStreamListener
204 NS_IMETHODIMP
FetchPreloader::OnStartRequest(nsIRequest
* request
) {
205 NotifyStart(request
);
207 if (!mConsumeListener
) {
208 // AsyncConsume not called yet.
209 mConsumeListener
= new Cache();
212 return mConsumeListener
->OnStartRequest(request
);
215 NS_IMETHODIMP
FetchPreloader::OnDataAvailable(nsIRequest
* request
,
216 nsIInputStream
* input
,
217 uint64_t offset
, uint32_t count
) {
218 return mConsumeListener
->OnDataAvailable(request
, input
, offset
, count
);
221 NS_IMETHODIMP
FetchPreloader::OnStopRequest(nsIRequest
* request
,
223 mConsumeListener
->OnStopRequest(request
, status
);
225 // We want 404 or other types of server responses to be treated as 'error'.
226 if (nsCOMPtr
<nsIHttpChannel
> httpChannel
= do_QueryInterface(request
)) {
227 uint32_t responseStatus
= 0;
228 Unused
<< httpChannel
->GetResponseStatus(&responseStatus
);
229 if (responseStatus
/ 100 != 2) {
230 status
= NS_ERROR_FAILURE
;
234 // Fetch preloader wants to keep the channel around so that consumers like XHR
235 // can access it even after the preload is done.
236 nsCOMPtr
<nsIChannel
> channel
= mChannel
;
237 NotifyStop(request
, status
);
238 mChannel
.swap(channel
);
242 // FetchPreloader::Cache
244 NS_IMPL_ISUPPORTS(FetchPreloader::Cache
, nsIStreamListener
, nsIRequestObserver
)
246 NS_IMETHODIMP
FetchPreloader::Cache::OnStartRequest(nsIRequest
* request
) {
249 if (mFinalListener
) {
250 return mFinalListener
->OnStartRequest(mRequest
);
253 mCalls
.AppendElement(Call
{VariantIndex
<0>{}, StartRequest
{}});
257 NS_IMETHODIMP
FetchPreloader::Cache::OnDataAvailable(nsIRequest
* request
,
258 nsIInputStream
* input
,
261 if (mFinalListener
) {
262 return mFinalListener
->OnDataAvailable(mRequest
, input
, offset
, count
);
266 if (!data
.mData
.SetLength(count
, fallible
)) {
267 return NS_ERROR_OUT_OF_MEMORY
;
271 nsresult rv
= input
->Read(data
.mData
.BeginWriting(), count
, &read
);
276 mCalls
.AppendElement(Call
{VariantIndex
<1>{}, std::move(data
)});
280 NS_IMETHODIMP
FetchPreloader::Cache::OnStopRequest(nsIRequest
* request
,
282 if (mFinalListener
) {
283 return mFinalListener
->OnStopRequest(mRequest
, status
);
286 mCalls
.AppendElement(Call
{VariantIndex
<2>{}, StopRequest
{status
}});
290 void FetchPreloader::Cache::AsyncConsume(nsIStreamListener
* aListener
) {
291 // Must dispatch for two reasons:
292 // 1. This is called directly from FetchLoader::AsyncConsume, which must
293 // behave the same way as nsIChannel::AsyncOpen.
294 // 2. In case there are already stream listener events scheduled on the main
295 // thread we preserve the order - those will still end up in Cache.
297 // * The `Cache` object is fully main thread only for now, doesn't support
298 // retargeting, but it can be improved to allow it.
300 nsCOMPtr
<nsIStreamListener
> listener(aListener
);
301 NS_DispatchToMainThread(NewRunnableMethod
<nsCOMPtr
<nsIStreamListener
>>(
302 "FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume
,
306 void FetchPreloader::Cache::Consume(nsCOMPtr
<nsIStreamListener
> aListener
) {
307 MOZ_ASSERT(!mFinalListener
, "Duplicate call");
309 mFinalListener
= std::move(aListener
);
311 // Status of the channel read after each call.
312 nsresult status
= NS_OK
;
313 nsCOMPtr
<nsIChannel
> channel(do_QueryInterface(mRequest
));
315 RefPtr
<Cache
> self(this);
316 for (auto& call
: mCalls
) {
317 nsresult rv
= call
.match(
318 [&](const StartRequest
& startRequest
) mutable {
319 return self
->mFinalListener
->OnStartRequest(self
->mRequest
);
321 [&](const DataAvailable
& dataAvailable
) mutable {
322 if (NS_FAILED(status
)) {
323 // Channel has been cancelled during this mCalls loop.
327 nsCOMPtr
<nsIInputStream
> input
;
328 rv
= NS_NewCStringInputStream(getter_AddRefs(input
),
329 dataAvailable
.mData
);
334 return self
->mFinalListener
->OnDataAvailable(
335 self
->mRequest
, input
, 0, dataAvailable
.mData
.Length());
337 [&](const StopRequest
& stopRequest
) {
338 // First cancellation overrides mStatus in nsHttpChannel.
339 nsresult stopStatus
=
340 NS_FAILED(status
) ? status
: stopRequest
.mStatus
;
341 self
->mFinalListener
->OnStopRequest(self
->mRequest
, stopStatus
);
342 self
->mFinalListener
= nullptr;
343 self
->mRequest
= nullptr;
352 bool isCancelled
= false;
353 Unused
<< channel
->GetCanceled(&isCancelled
);
355 mRequest
->GetStatus(&status
);
356 } else if (NS_FAILED(rv
)) {
358 mRequest
->Cancel(status
);
365 } // namespace mozilla