Bug 1829125 - Align the PHC area to the jemalloc chunk size r=glandium
[gecko.git] / uriloader / preload / FetchPreloader.cpp
blob6e848bbee84c1620c8524f784d7f02b8214b9e55
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
5 * file,
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"
30 namespace mozilla {
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) {
45 nsresult rv;
46 nsCOMPtr<nsIChannel> channel;
48 auto notify = MakeScopeExit([&]() {
49 if (NS_FAILED(rv)) {
50 // Make sure we notify any <link preload> elements when opening fails
51 // because of various technical or security reasons.
52 NotifyStart(channel);
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.
56 NotifyStop(rv);
58 });
60 nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup();
61 nsCOMPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow();
62 nsCOMPtr<nsIInterfaceRequestor> prompter;
63 if (window) {
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);
76 if (NS_FAILED(rv)) {
77 return rv;
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) {
101 nsresult rv;
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,
112 aCallbacks);
113 if (NS_FAILED(rv)) {
114 return rv;
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);
127 } else {
128 timedChannel->SetInitiatorType(u"link"_ns);
132 channel.forget(aChannel);
133 return NS_OK;
136 nsresult FetchPreloader::CheckContentPolicy(nsIURI* aURI,
137 dom::Document* aDocument) {
138 if (!aDocument) {
139 return NS_OK;
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;
147 nsresult rv =
148 NS_CheckContentLoadPolicy(aURI, secCheckLoadInfo, ""_ns, &shouldLoad,
149 nsContentUtils::GetContentPolicy());
150 if (NS_SUCCEEDED(rv) && NS_CP_ACCEPTED(shouldLoad)) {
151 return NS_OK;
154 return NS_ERROR_CONTENT_BLOCKED;
157 // PreloaderBase
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;
171 } else {
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);
178 return NS_OK;
181 // static
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,
210 nsresult status) {
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);
227 return NS_OK;
230 // FetchPreloader::Cache
232 NS_IMPL_ISUPPORTS(FetchPreloader::Cache, nsIStreamListener, nsIRequestObserver)
234 NS_IMETHODIMP FetchPreloader::Cache::OnStartRequest(nsIRequest* request) {
235 mRequest = request;
237 if (mFinalListener) {
238 return mFinalListener->OnStartRequest(mRequest);
241 mCalls.AppendElement(Call{VariantIndex<0>{}, StartRequest{}});
242 return NS_OK;
245 NS_IMETHODIMP FetchPreloader::Cache::OnDataAvailable(nsIRequest* request,
246 nsIInputStream* input,
247 uint64_t offset,
248 uint32_t count) {
249 if (mFinalListener) {
250 return mFinalListener->OnDataAvailable(mRequest, input, offset, count);
253 DataAvailable data;
254 if (!data.mData.SetLength(count, fallible)) {
255 return NS_ERROR_OUT_OF_MEMORY;
258 uint32_t read;
259 nsresult rv = input->Read(data.mData.BeginWriting(), count, &read);
260 if (NS_FAILED(rv)) {
261 return rv;
264 mCalls.AppendElement(Call{VariantIndex<1>{}, std::move(data)});
265 return NS_OK;
268 NS_IMETHODIMP FetchPreloader::Cache::OnStopRequest(nsIRequest* request,
269 nsresult status) {
270 if (mFinalListener) {
271 return mFinalListener->OnStopRequest(mRequest, status);
274 mCalls.AppendElement(Call{VariantIndex<2>{}, StopRequest{status}});
275 return NS_OK;
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,
291 listener));
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.
312 return NS_OK;
315 nsCOMPtr<nsIInputStream> input;
316 rv = NS_NewCStringInputStream(getter_AddRefs(input),
317 dataAvailable.mData);
318 if (NS_FAILED(rv)) {
319 return rv;
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;
332 return NS_OK;
335 if (!mRequest) {
336 // We are done!
337 break;
340 bool isCancelled = false;
341 Unused << channel->GetCanceled(&isCancelled);
342 if (isCancelled) {
343 mRequest->GetStatus(&status);
344 } else if (NS_FAILED(rv)) {
345 status = rv;
346 mRequest->Cancel(status);
350 mCalls.Clear();
353 } // namespace mozilla