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/CORSMode.h"
11 #include "mozilla/DebugOnly.h"
12 #include "mozilla/dom/Document.h"
13 #include "mozilla/LoadInfo.h"
14 #include "mozilla/ScopeExit.h"
15 #include "mozilla/Unused.h"
16 #include "mozilla/dom/ReferrerInfo.h"
17 #include "nsContentPolicyUtils.h"
18 #include "nsContentSecurityManager.h"
19 #include "nsContentUtils.h"
20 #include "nsIChannel.h"
21 #include "nsIChildChannel.h"
22 #include "nsIClassOfService.h"
23 #include "nsIHttpChannel.h"
24 #include "nsIHttpChannelInternal.h"
25 #include "nsITimedChannel.h"
26 #include "nsNetUtil.h"
27 #include "nsStringStream.h"
28 #include "nsIDocShell.h"
32 NS_IMPL_ISUPPORTS(FetchPreloader
, nsIStreamListener
, nsIRequestObserver
)
34 FetchPreloader::FetchPreloader()
35 : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD
) {}
37 FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType
)
38 : mContentPolicyType(aContentPolicyType
) {}
40 nsresult
FetchPreloader::OpenChannel(const PreloadHashKey
& aKey
, nsIURI
* aURI
,
41 const CORSMode aCORSMode
,
42 const dom::ReferrerPolicy
& aReferrerPolicy
,
43 dom::Document
* aDocument
,
44 uint64_t aEarlyHintPreloaderId
) {
46 nsCOMPtr
<nsIChannel
> channel
;
48 auto notify
= MakeScopeExit([&]() {
50 // Make sure we notify any <link preload> elements when opening fails
51 // because of various technical or security reasons.
53 // Using the non-channel overload of this method to make it work even
54 // before NotifyOpen has been called on this preload. We are not
55 // switching between channels, so it's safe to do so.
60 nsCOMPtr
<nsILoadGroup
> loadGroup
= aDocument
->GetDocumentLoadGroup();
61 nsCOMPtr
<nsPIDOMWindowOuter
> window
= aDocument
->GetWindow();
62 nsCOMPtr
<nsIInterfaceRequestor
> prompter
;
64 nsIDocShell
* docshell
= window
->GetDocShell();
65 prompter
= do_QueryInterface(docshell
);
68 rv
= CreateChannel(getter_AddRefs(channel
), aURI
, aCORSMode
, aReferrerPolicy
,
69 aDocument
, loadGroup
, prompter
, aEarlyHintPreloaderId
);
70 NS_ENSURE_SUCCESS(rv
, rv
);
72 // Doing this now so that we have the channel and tainting set on it properly
73 // to notify the proper event (load or error) on the associated preload tags
74 // when the CSP check fails.
75 rv
= CheckContentPolicy(aURI
, aDocument
);
80 PrioritizeAsPreload(channel
);
81 AddLoadBackgroundFlag(channel
);
83 NotifyOpen(aKey
, channel
, aDocument
, true);
85 if (aEarlyHintPreloaderId
) {
86 nsCOMPtr
<nsIHttpChannelInternal
> channelInternal
=
87 do_QueryInterface(channel
);
88 NS_ENSURE_TRUE(channelInternal
!= nullptr, NS_ERROR_FAILURE
);
90 rv
= channelInternal
->SetEarlyHintPreloaderId(aEarlyHintPreloaderId
);
91 NS_ENSURE_SUCCESS(rv
, rv
);
93 return mAsyncConsumeResult
= rv
= channel
->AsyncOpen(this);
96 nsresult
FetchPreloader::CreateChannel(
97 nsIChannel
** aChannel
, nsIURI
* aURI
, const CORSMode aCORSMode
,
98 const dom::ReferrerPolicy
& aReferrerPolicy
, dom::Document
* aDocument
,
99 nsILoadGroup
* aLoadGroup
, nsIInterfaceRequestor
* aCallbacks
,
100 uint64_t aEarlyHintPreloaderId
) {
103 nsSecurityFlags securityFlags
=
104 nsContentSecurityManager::ComputeSecurityFlags(
105 aCORSMode
, nsContentSecurityManager::CORSSecurityMapping::
106 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS
);
108 nsCOMPtr
<nsIChannel
> channel
;
109 rv
= NS_NewChannelWithTriggeringPrincipal(
110 getter_AddRefs(channel
), aURI
, aDocument
, aDocument
->NodePrincipal(),
111 securityFlags
, nsIContentPolicy::TYPE_FETCH
, nullptr, aLoadGroup
,
117 if (nsCOMPtr
<nsIHttpChannel
> httpChannel
= do_QueryInterface(channel
)) {
118 nsCOMPtr
<nsIReferrerInfo
> referrerInfo
= new dom::ReferrerInfo(
119 aDocument
->GetDocumentURIAsReferrer(), aReferrerPolicy
);
120 rv
= httpChannel
->SetReferrerInfoWithoutClone(referrerInfo
);
121 MOZ_ASSERT(NS_SUCCEEDED(rv
));
124 if (nsCOMPtr
<nsITimedChannel
> timedChannel
= do_QueryInterface(channel
)) {
125 if (aEarlyHintPreloaderId
) {
126 timedChannel
->SetInitiatorType(u
"early-hints"_ns
);
128 timedChannel
->SetInitiatorType(u
"link"_ns
);
132 channel
.forget(aChannel
);
136 nsresult
FetchPreloader::CheckContentPolicy(nsIURI
* aURI
,
137 dom::Document
* aDocument
) {
142 nsCOMPtr
<nsILoadInfo
> secCheckLoadInfo
= new net::LoadInfo(
143 aDocument
->NodePrincipal(), aDocument
->NodePrincipal(), aDocument
,
144 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK
, mContentPolicyType
);
146 int16_t shouldLoad
= nsIContentPolicy::ACCEPT
;
148 NS_CheckContentLoadPolicy(aURI
, secCheckLoadInfo
, ""_ns
, &shouldLoad
,
149 nsContentUtils::GetContentPolicy());
150 if (NS_SUCCEEDED(rv
) && NS_CP_ACCEPTED(shouldLoad
)) {
154 return NS_ERROR_CONTENT_BLOCKED
;
159 nsresult
FetchPreloader::AsyncConsume(nsIStreamListener
* aListener
) {
160 if (NS_FAILED(mAsyncConsumeResult
)) {
161 // Already being consumed or failed to open.
162 return mAsyncConsumeResult
;
165 // Prevent duplicate calls.
166 mAsyncConsumeResult
= NS_ERROR_NOT_AVAILABLE
;
168 if (!mConsumeListener
) {
169 // Called before we are getting response from the channel.
170 mConsumeListener
= aListener
;
172 // Channel already started, push cached calls to this listener.
173 // Can't be anything else than the `Cache`, hence a safe static_cast.
174 Cache
* cache
= static_cast<Cache
*>(mConsumeListener
.get());
175 cache
->AsyncConsume(aListener
);
182 void FetchPreloader::PrioritizeAsPreload(nsIChannel
* aChannel
) {
183 if (nsCOMPtr
<nsIClassOfService
> cos
= do_QueryInterface(aChannel
)) {
184 cos
->AddClassFlags(nsIClassOfService::Unblocked
);
188 void FetchPreloader::PrioritizeAsPreload() { PrioritizeAsPreload(Channel()); }
190 // nsIRequestObserver + nsIStreamListener
192 NS_IMETHODIMP
FetchPreloader::OnStartRequest(nsIRequest
* request
) {
193 NotifyStart(request
);
195 if (!mConsumeListener
) {
196 // AsyncConsume not called yet.
197 mConsumeListener
= new Cache();
200 return mConsumeListener
->OnStartRequest(request
);
203 NS_IMETHODIMP
FetchPreloader::OnDataAvailable(nsIRequest
* request
,
204 nsIInputStream
* input
,
205 uint64_t offset
, uint32_t count
) {
206 return mConsumeListener
->OnDataAvailable(request
, input
, offset
, count
);
209 NS_IMETHODIMP
FetchPreloader::OnStopRequest(nsIRequest
* request
,
211 mConsumeListener
->OnStopRequest(request
, status
);
213 // We want 404 or other types of server responses to be treated as 'error'.
214 if (nsCOMPtr
<nsIHttpChannel
> httpChannel
= do_QueryInterface(request
)) {
215 uint32_t responseStatus
= 0;
216 Unused
<< httpChannel
->GetResponseStatus(&responseStatus
);
217 if (responseStatus
/ 100 != 2) {
218 status
= NS_ERROR_FAILURE
;
222 // Fetch preloader wants to keep the channel around so that consumers like XHR
223 // can access it even after the preload is done.
224 nsCOMPtr
<nsIChannel
> channel
= mChannel
;
225 NotifyStop(request
, status
);
226 mChannel
.swap(channel
);
230 // FetchPreloader::Cache
232 NS_IMPL_ISUPPORTS(FetchPreloader::Cache
, nsIStreamListener
, nsIRequestObserver
)
234 NS_IMETHODIMP
FetchPreloader::Cache::OnStartRequest(nsIRequest
* request
) {
237 if (mFinalListener
) {
238 return mFinalListener
->OnStartRequest(mRequest
);
241 mCalls
.AppendElement(Call
{VariantIndex
<0>{}, StartRequest
{}});
245 NS_IMETHODIMP
FetchPreloader::Cache::OnDataAvailable(nsIRequest
* request
,
246 nsIInputStream
* input
,
249 if (mFinalListener
) {
250 return mFinalListener
->OnDataAvailable(mRequest
, input
, offset
, count
);
254 if (!data
.mData
.SetLength(count
, fallible
)) {
255 return NS_ERROR_OUT_OF_MEMORY
;
259 nsresult rv
= input
->Read(data
.mData
.BeginWriting(), count
, &read
);
264 mCalls
.AppendElement(Call
{VariantIndex
<1>{}, std::move(data
)});
268 NS_IMETHODIMP
FetchPreloader::Cache::OnStopRequest(nsIRequest
* request
,
270 if (mFinalListener
) {
271 return mFinalListener
->OnStopRequest(mRequest
, status
);
274 mCalls
.AppendElement(Call
{VariantIndex
<2>{}, StopRequest
{status
}});
278 void FetchPreloader::Cache::AsyncConsume(nsIStreamListener
* aListener
) {
279 // Must dispatch for two reasons:
280 // 1. This is called directly from FetchLoader::AsyncConsume, which must
281 // behave the same way as nsIChannel::AsyncOpen.
282 // 2. In case there are already stream listener events scheduled on the main
283 // thread we preserve the order - those will still end up in Cache.
285 // * The `Cache` object is fully main thread only for now, doesn't support
286 // retargeting, but it can be improved to allow it.
288 nsCOMPtr
<nsIStreamListener
> listener(aListener
);
289 NS_DispatchToMainThread(NewRunnableMethod
<nsCOMPtr
<nsIStreamListener
>>(
290 "FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume
,
294 void FetchPreloader::Cache::Consume(nsCOMPtr
<nsIStreamListener
> aListener
) {
295 MOZ_ASSERT(!mFinalListener
, "Duplicate call");
297 mFinalListener
= std::move(aListener
);
299 // Status of the channel read after each call.
300 nsresult status
= NS_OK
;
301 nsCOMPtr
<nsIChannel
> channel(do_QueryInterface(mRequest
));
303 RefPtr
<Cache
> self(this);
304 for (auto& call
: mCalls
) {
305 nsresult rv
= call
.match(
306 [&](const StartRequest
& startRequest
) mutable {
307 return self
->mFinalListener
->OnStartRequest(self
->mRequest
);
309 [&](const DataAvailable
& dataAvailable
) mutable {
310 if (NS_FAILED(status
)) {
311 // Channel has been cancelled during this mCalls loop.
315 nsCOMPtr
<nsIInputStream
> input
;
316 rv
= NS_NewCStringInputStream(getter_AddRefs(input
),
317 dataAvailable
.mData
);
322 return self
->mFinalListener
->OnDataAvailable(
323 self
->mRequest
, input
, 0, dataAvailable
.mData
.Length());
325 [&](const StopRequest
& stopRequest
) {
326 // First cancellation overrides mStatus in nsHttpChannel.
327 nsresult stopStatus
=
328 NS_FAILED(status
) ? status
: stopRequest
.mStatus
;
329 self
->mFinalListener
->OnStopRequest(self
->mRequest
, stopStatus
);
330 self
->mFinalListener
= nullptr;
331 self
->mRequest
= nullptr;
340 bool isCancelled
= false;
341 Unused
<< channel
->GetCanceled(&isCancelled
);
343 mRequest
->GetStatus(&status
);
344 } else if (NS_FAILED(rv
)) {
346 mRequest
->Cancel(status
);
353 } // namespace mozilla