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
|
/**
* Utilities for initiating prefetch via speculation rules.
*/
// Resolved URL to find this script.
const SR_PREFETCH_UTILS_URL = new URL(document.currentScript.src, document.baseURI);
// Hostname for cross origin urls.
const PREFETCH_PROXY_BYPASS_HOST = "{{hosts[alt][]}}";
class PrefetchAgent extends RemoteContext {
constructor(uuid, t) {
super(uuid);
this.t = t;
}
getExecutorURL(options = {}) {
let {hostname, username, password, protocol, executor, ...extra} = options;
let params = new URLSearchParams({uuid: this.context_id, ...extra});
if(executor === undefined) {
executor = "executor.sub.html";
}
let url = new URL(`${executor}?${params}`, SR_PREFETCH_UTILS_URL);
if(hostname !== undefined) {
url.hostname = hostname;
}
if(username !== undefined) {
url.username = username;
}
if(password !== undefined) {
url.password = password;
}
if(protocol !== undefined) {
url.protocol = protocol;
url.port = protocol === "https" ? "{{ports[https][0]}}" : "{{ports[http][0]}}";
}
return url;
}
// Requests prefetch via speculation rules.
//
// In the future, this should also use browser hooks to force the prefetch to
// occur despite heuristic matching, etc., and await the completion of the
// prefetch.
async forceSinglePrefetch(url, extra = {}) {
await this.execute_script((url, extra) => {
insertSpeculationRules({ prefetch: [{source: 'list', urls: [url], ...extra}] });
}, [url, extra]);
return new Promise(resolve => this.t.step_timeout(resolve, 2000));
}
async navigate(url) {
await this.execute_script((url) => {
window.executor.suspend(() => {
location.href = url;
});
}, [url]);
url.username = '';
url.password = '';
assert_equals(
await this.execute_script(() => location.href),
url.toString(),
"expected navigation to reach destination URL");
await this.execute_script(() => {});
}
async getRequestHeaders() {
return this.execute_script(() => requestHeaders);
}
async getResponseCookies() {
return this.execute_script(() => {
let cookie = {};
document.cookie.split(/\s*;\s*/).forEach((kv)=>{
let [key, value] = kv.split(/\s*=\s*/);
cookie[key] = value;
});
return cookie;
});
}
async getRequestCookies() {
return this.execute_script(() => window.requestCookies);
}
async getRequestCredentials() {
return this.execute_script(() => window.requestCredentials);
}
async setReferrerPolicy(referrerPolicy) {
return this.execute_script(referrerPolicy => {
const meta = document.createElement("meta");
meta.name = "referrer";
meta.content = referrerPolicy;
document.head.append(meta);
}, [referrerPolicy]);
}
async getDeliveryType(){
return this.execute_script(() => {
return performance.getEntriesByType("navigation")[0].deliveryType;
});
}
}
// Produces a URL with a UUID which will record when it's prefetched.
// |extra_params| can be specified to add extra search params to the generated
// URL.
function getPrefetchUrl(extra_params={}) {
let params = new URLSearchParams({ uuid: token(), ...extra_params });
return new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL);
}
// Produces n URLs with unique UUIDs which will record when they are prefetched.
function getPrefetchUrlList(n) {
return Array.from({ length: n }, () => getPrefetchUrl());
}
function getRedirectUrl() {
let params = new URLSearchParams({uuid: token()});
return new URL(`redirect.py?${params}`, SR_PREFETCH_UTILS_URL);
}
async function isUrlPrefetched(url) {
let response = await fetch(url, {redirect: 'follow'});
assert_true(response.ok);
return response.json();
}
// Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this.
async function spawnWindow(t, options = {}, uuid = token()) {
let agent = new PrefetchAgent(uuid, t);
let w = window.open(agent.getExecutorURL(options), '_blank', options);
t.add_cleanup(() => w.close());
return agent;
}
function insertSpeculationRules(body) {
let script = document.createElement('script');
script.type = 'speculationrules';
script.textContent = JSON.stringify(body);
document.head.appendChild(script);
}
// Creates and appends <a href=|href|> to |insertion point|. If
// |insertion_point| is not specified, document.body is used.
function addLink(href, insertion_point=document.body) {
const a = document.createElement('a');
a.href = href;
insertion_point.appendChild(a);
return a;
}
// Inserts a prefetch document rule with |predicate|. |predicate| can be
// undefined, in which case the default predicate will be used (i.e. all links
// in document will match).
function insertDocumentRule(predicate, extra_options={}) {
insertSpeculationRules({
prefetch: [{
source: 'document',
eagerness: 'eager',
where: predicate,
...extra_options
}]
});
}
function assert_prefetched (requestHeaders, description) {
assert_in_array(requestHeaders.purpose, ["", "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'.");
assert_in_array(requestHeaders.sec_purpose,
["prefetch", "prefetch;anonymous-client-ip"], description);
}
function assert_not_prefetched (requestHeaders, description){
assert_equals(requestHeaders.purpose, "", description);
assert_equals(requestHeaders.sec_purpose, "", description);
}
|