1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
content / browser / preloading / prerender / prerender_navigation_throttle.cc [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/preloading/prerender/prerender_navigation_throttle.h"
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "base/strings/string_split.h"
#include "content/browser/preloading/prerender/prerender_features.h"
#include "content/browser/preloading/prerender/prerender_final_status.h"
#include "content/browser/preloading/prerender/prerender_host.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/browser/preloading/prerender/prerender_metrics.h"
#include "content/browser/preloading/prerender/prerender_navigation_utils.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_delegate.h"
#include "content/public/browser/preloading_trigger_type.h"
#include "content/public/common/content_features.h"
#include "services/network/public/mojom/parsed_headers.mojom.h"
#include "third_party/blink/public/common/features.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content {
namespace {
// For the given two origins, analyze what kind of redirection happened.
void AnalyzeCrossOriginRedirection(const url::Origin& current_origin,
const url::Origin& initial_origin,
const std::string& histogram_suffix) {
CHECK_NE(initial_origin, current_origin);
CHECK(current_origin.GetURL().SchemeIsHTTPOrHTTPS());
CHECK(initial_origin.GetURL().SchemeIsHTTPOrHTTPS());
std::bitset<3> bits;
bits[2] = current_origin.scheme() != initial_origin.scheme();
bits[1] = current_origin.host() != initial_origin.host();
bits[0] = current_origin.port() != initial_origin.port();
CHECK(bits.any());
auto mismatch_type =
static_cast<PrerenderCrossOriginRedirectionMismatch>(bits.to_ulong());
RecordPrerenderRedirectionMismatchType(mismatch_type, histogram_suffix);
if (mismatch_type ==
PrerenderCrossOriginRedirectionMismatch::kSchemePortMismatch) {
RecordPrerenderRedirectionProtocolChange(
current_origin.scheme() == url::kHttpsScheme
? PrerenderCrossOriginRedirectionProtocolChange::
kHttpProtocolUpgrade
: PrerenderCrossOriginRedirectionProtocolChange::
kHttpProtocolDowngrade,
histogram_suffix);
return;
}
}
// Returns true if a host of the given url is on the predefined blocked list as
// they cannot support prerendering.
bool ShouldSkipHostInBlockList(const GURL& url) {
// Keep the blocked list as static because the blocked hosts are served via
// feature parameters and are never changed until browser restarts.
//
// Blocked hosts are expected to be passed as a comma separated string.
// e.g. example1.test,example2.test
const static base::NoDestructor<std::vector<std::string>>
embedder_blocked_hosts(base::SplitString(
features::kPrerender2EmbedderBlockedHostsParam.Get(), ",",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY));
return base::Contains(*embedder_blocked_hosts, url.host());
}
} // namespace
PrerenderNavigationThrottle::~PrerenderNavigationThrottle() = default;
// static
std::unique_ptr<PrerenderNavigationThrottle>
PrerenderNavigationThrottle::MaybeCreateThrottleFor(
NavigationHandle* navigation_handle) {
auto* navigation_request = NavigationRequest::From(navigation_handle);
FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node();
if (frame_tree_node->GetFrameType() == FrameType::kPrerenderMainFrame) {
return base::WrapUnique(
new PrerenderNavigationThrottle(navigation_request));
}
return nullptr;
}
const char* PrerenderNavigationThrottle::GetNameForLogging() {
return "PrerenderNavigationThrottle";
}
NavigationThrottle::ThrottleCheckResult
PrerenderNavigationThrottle::WillStartRequest() {
return WillStartOrRedirectRequest(/*is_redirection=*/false);
}
NavigationThrottle::ThrottleCheckResult
PrerenderNavigationThrottle::WillRedirectRequest() {
return WillStartOrRedirectRequest(/*is_redirection=*/true);
}
PrerenderNavigationThrottle::PrerenderNavigationThrottle(
NavigationRequest* navigation_request)
: NavigationThrottle(navigation_request),
prerender_host_(static_cast<PrerenderHost*>(
navigation_request->frame_tree_node()->frame_tree().delegate())) {
CHECK(prerender_host_);
// This throttle is responsible for setting the initial navigation id on the
// PrerenderHost, since the PrerenderHost obtains the NavigationRequest,
// which has the ID, only after the navigation throttles run.
if (prerender_host_->GetInitialNavigationId().has_value()) {
// If the host already has an initial navigation id, this throttle
// will later cancel the navigation in Will*Request(). Just do nothing
// until then.
} else {
prerender_host_->SetInitialNavigation(navigation_request);
}
}
NavigationThrottle::ThrottleCheckResult
PrerenderNavigationThrottle::WillStartOrRedirectRequest(bool is_redirection) {
GURL navigation_url = navigation_handle()->GetURL();
url::Origin navigation_origin = url::Origin::Create(navigation_url);
url::Origin initial_prerendering_origin =
url::Origin::Create(prerender_host_->GetInitialUrl());
// Reset the flags that should be calculated every time redirction happens.
is_same_site_cross_origin_prerender_ = false;
same_site_cross_origin_prerender_did_redirect_ = false;
if (prerender_host_->IsBrowserInitiated() &&
ShouldSkipHostInBlockList(navigation_url)) {
CancelPrerendering(PrerenderFinalStatus::kEmbedderHostDisallowed);
return CANCEL;
}
// Allow only HTTP(S) schemes.
// https://wicg.github.io/nav-speculation/prerendering.html#no-bad-navs
if (!navigation_url.SchemeIsHTTPOrHTTPS()) {
if (is_redirection) {
CancelPrerendering(PrerenderFinalStatus::kInvalidSchemeRedirect);
} else {
// For non-redirected initial navigation, this should be checked in
// PrerenderHostRegistry::CreateAndStartHost().
CHECK(!IsInitialNavigation());
CancelPrerendering(PrerenderFinalStatus::kInvalidSchemeNavigation);
}
return CANCEL;
}
// Disallow all pages that have an effective URL like hosted apps and NTP.
auto* browser_context =
navigation_handle()->GetStartingSiteInstance()->GetBrowserContext();
if (SiteInstanceImpl::HasEffectiveURL(browser_context, navigation_url)) {
CancelPrerendering(
is_redirection
? PrerenderFinalStatus::kRedirectedPrerenderingUrlHasEffectiveUrl
: PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl);
return CANCEL;
}
// Origin checks for the navigation (redirection), which varies depending on
// whether the navigation is initial one or not.
if (IsInitialNavigation()) {
// Origin checks for initial prerendering navigation (redirection).
//
// For non-embedder triggered prerendering, compare the origin of the
// initiator URL to the origin of navigation (redirection) URL.
//
// For embedder triggered prerendering, there is no initiator page, so
// initial prerendering navigation doesn't check origins and instead initial
// prerendering redirection compare the origin of initial prerendering URL
// to the origin of redirection URL.
if (prerender_host_->IsBrowserInitiated()) {
// Cancel an embedder triggered prerendering if it is redirected to a URL
// cross-site to the initial prerendering URL.
if (prerender_navigation_utils::IsCrossSite(
navigation_url, initial_prerendering_origin)) {
AnalyzeCrossOriginRedirection(navigation_origin,
initial_prerendering_origin,
prerender_host_->GetHistogramSuffix());
CancelPrerendering(
PrerenderFinalStatus::kCrossSiteRedirectInInitialNavigation);
return CANCEL;
}
// Skip the same-site check for non-redirected cases as the initiator
// origin is nullopt for browser-initiated prerendering.
CHECK(!prerender_host_->initiator_origin().has_value());
} else if (prerender_navigation_utils::IsCrossSite(
navigation_url,
prerender_host_->initiator_origin().value())) {
// TODO(crbug.com/40168192): Once cross-site prerendering is implemented,
// we'll need to enforce strict referrer policies
// (https://wicg.github.io/nav-speculation/prefetch.html#list-of-sufficiently-strict-speculative-navigation-referrer-policies).
//
// Cancel prerendering if this is cross-site prerendering, cross-site
// redirection during prerendering, or cross-site navigation from a
// prerendered page.
CancelPrerendering(
is_redirection
? PrerenderFinalStatus::kCrossSiteRedirectInInitialNavigation
: PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation);
return CANCEL;
} else if (prerender_navigation_utils::IsSameSiteCrossOrigin(
navigation_url,
prerender_host_->initiator_origin().value())) {
// Same-site cross-origin prerendering is allowed only when the opt-in
// header is specified on response. This will be checked on
// WillProcessResponse().
is_same_site_cross_origin_prerender_ = true;
same_site_cross_origin_prerender_did_redirect_ = is_redirection;
}
} else {
// Origin checks for the main frame navigation (redirection) happens after
// the initial prerendering navigation in a prerendered page. Compare the
// origin of the initial prerendering URL to the origin of navigation
// (redirection) URL.
if (!base::FeatureList::IsEnabled(
blink::features::kPrerender2MainFrameNavigation)) {
// Navigations after the initial prerendering navigation are disallowed
// when the kPrerender2MainFrameNavigation feature is disabled.
CancelPrerendering(PrerenderFinalStatus::kMainFrameNavigation);
return CANCEL;
}
// Cross-site navigations after the initial prerendering navigation are
// disallowed.
if (prerender_navigation_utils::IsCrossSite(navigation_url,
initial_prerendering_origin)) {
CancelPrerendering(
is_redirection
? PrerenderFinalStatus::kCrossSiteRedirectInMainFrameNavigation
: PrerenderFinalStatus::
kCrossSiteNavigationInMainFrameNavigation);
return CANCEL;
}
// Same-site cross-origin prerendering is allowed only when the opt-in
// header is specified on response. This will be checked on
// WillProcessResponse().
if (prerender_navigation_utils::IsSameSiteCrossOrigin(
navigation_url, initial_prerendering_origin)) {
is_same_site_cross_origin_prerender_ = true;
same_site_cross_origin_prerender_did_redirect_ = is_redirection;
}
}
return PROCEED;
}
NavigationThrottle::ThrottleCheckResult
PrerenderNavigationThrottle::WillProcessResponse() {
auto* navigation_request = NavigationRequest::From(navigation_handle());
// https://wicg.github.io/nav-speculation/prerendering.html#navigate-fetch-patch
// "1. If browsingContext is a prerendering browsing context and
// responseOrigin is not same origin with incumbentNavigationOrigin, then:"
// "1.1. Let loadingModes be the result of getting the supported loading
// modes for response."
// "1.2. If loadingModes does not contain `credentialed-prerender`, then
// set response to a network error."
bool is_credentialed_prerender =
navigation_request->response() &&
navigation_request->response()->parsed_headers &&
base::Contains(
navigation_request->response()->parsed_headers->supports_loading_mode,
network::mojom::LoadingMode::kCredentialedPrerender);
// Cancel prerendering when this is same-site cross-origin navigation but the
// opt-in header is not specified.
if (is_same_site_cross_origin_prerender_ && !is_credentialed_prerender) {
// Calculate the final status for cancellation.
PrerenderFinalStatus final_status = PrerenderFinalStatus::kDestroyed;
if (IsInitialNavigation()) {
final_status =
same_site_cross_origin_prerender_did_redirect_
? PrerenderFinalStatus::
kSameSiteCrossOriginRedirectNotOptInInInitialNavigation
: PrerenderFinalStatus::
kSameSiteCrossOriginNavigationNotOptInInInitialNavigation;
} else {
final_status =
same_site_cross_origin_prerender_did_redirect_
? PrerenderFinalStatus::
kSameSiteCrossOriginRedirectNotOptInInMainFrameNavigation
: PrerenderFinalStatus::
kSameSiteCrossOriginNavigationNotOptInInMainFrameNavigation;
}
CancelPrerendering(final_status);
return CANCEL;
}
std::optional<PrerenderFinalStatus> cancel_reason;
// TODO(crbug.com/40222993): Delay until activation instead of cancellation.
if (navigation_handle()->IsDownload()) {
// Disallow downloads during prerendering and cancel the prerender.
cancel_reason = PrerenderFinalStatus::kDownload;
} else if (prerender_navigation_utils::IsDisallowedHttpResponseCode(
navigation_request->commit_params().http_response_code)) {
// There's no point in trying to prerender failed navigations.
cancel_reason = PrerenderFinalStatus::kNavigationBadHttpStatus;
}
if (cancel_reason.has_value()) {
CancelPrerendering(cancel_reason.value());
return CANCEL;
}
return PROCEED;
}
bool PrerenderNavigationThrottle::IsInitialNavigation() const {
return prerender_host_->IsInitialNavigation(
*NavigationRequest::From(navigation_handle()));
}
void PrerenderNavigationThrottle::CancelPrerendering(
PrerenderFinalStatus final_status) {
auto* navigation_request = NavigationRequest::From(navigation_handle());
FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node();
CHECK_EQ(frame_tree_node->GetFrameType(), FrameType::kPrerenderMainFrame);
PrerenderHostRegistry* prerender_host_registry =
frame_tree_node->current_frame_host()
->delegate()
->GetPrerenderHostRegistry();
prerender_host_registry->CancelHost(prerender_host_->frame_tree_node_id(),
final_status);
}
} // namespace content