-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
Copy pathcookies.js
295 lines (279 loc) · 9.04 KB
/
cookies.js
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
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {endsWith} from './string';
import {
getSourceOrigin,
isProxyOrigin,
parseUrlDeprecated,
tryDecodeUriComponent,
} from './url';
import {urls} from './config';
import {userAssert} from './log';
const TEST_COOKIE_NAME = '-test-amp-cookie-tmp';
/** @enum {string} */
export const SameSite = {
LAX: 'Lax',
STRICT: 'Strict',
NONE: 'None',
};
/**
* Returns the value of the cookie. The cookie access is restricted and must
* go through the privacy review. Before using this method please file a
* GitHub issue with "Privacy Review" label.
*
* Returns the cookie's value or `null`.
*
* @param {!Window} win
* @param {string} name
* @return {?string}
*/
export function getCookie(win, name) {
const cookieString = tryGetDocumentCookie_(win);
if (!cookieString) {
return null;
}
const cookies = cookieString.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
const eq = cookie.indexOf('=');
if (eq == -1) {
continue;
}
if (tryDecodeUriComponent(cookie.substring(0, eq).trim()) == name) {
const value = cookie.substring(eq + 1).trim();
return tryDecodeUriComponent(value, value);
}
}
return null;
}
/**
* This method should not be inlined to prevent TryCatch deoptimization.
* @param {!Window} win
* @return {string}
* @noinline
*/
function tryGetDocumentCookie_(win) {
try {
return win.document.cookie;
} catch (e) {
// Act as if no cookie is available. Exceptions can be thrown when
// AMP docs are opened on origins that do not allow setting
// cookies such as null origins.
return '';
}
}
/**
* Sets the value of the cookie. The cookie access is restricted and must
* go through the privacy review. Before using this method please file a
* GitHub issue with "Privacy Review" label.
*
* @param {!Window} win
* @param {string} name
* @param {string} value
* @param {time} expirationTime
* @param {{
* highestAvailableDomain:(boolean|undefined),
* domain:(string|undefined),
* sameSite: (!SameSite|undefined),
* secure: (boolean|undefined),
* }=} options
* - highestAvailableDomain: If true, set the cookie at the widest domain
* scope allowed by the browser. E.g. on example.com if we are currently
* on www.example.com.
* - domain: Explicit domain to set. domain overrides HigestAvailableDomain
* - allowOnProxyOrigin: Allow setting a cookie on the AMP Cache.
* - sameSite: The SameSite value to use when setting the cookie.
* - secure: Whether the cookie should contain Secure (only sent over https).
*/
export function setCookie(win, name, value, expirationTime, options = {}) {
checkOriginForSettingCookie(win, options, name);
let domain = undefined;
// Respect explicitly set domain over higestAvailabeDomain
if (options.domain) {
domain = options.domain;
} else if (options.highestAvailableDomain) {
domain = /** @type {string} */ (getHighestAvailableDomain(win));
}
trySetCookie(
win,
name,
value,
expirationTime,
domain,
options.sameSite,
options.secure
);
}
/**
* Attemp to find the HighestAvailableDomain on
* @param {!Window} win
* @return {?string}
*/
export function getHighestAvailableDomain(win) {
// <meta name='amp-cookie-scope'>. Need to respect the meta first.
// Note: The same logic applies to shadow docs. Where all shadow docs are
// considered to be in the same origin. And only the <meta> from
// shell will be respected. (Header from shadow doc will be removed)
const metaTag =
win.document.head &&
win.document.head.querySelector("meta[name='amp-cookie-scope']");
if (metaTag) {
// The content value could be an empty string. Return null instead
const cookieScope = metaTag.getAttribute('content') || '';
// Verify the validness of the amp-cookie-scope meta value
const sourceOrigin = getSourceOrigin(win.location.href);
// Verify the meta tag content value is valid
if (endsWith(sourceOrigin, '.' + cookieScope)) {
return cookieScope;
} else {
// When the amp-cookie-scope value is invalid, fallback to the exact origin
// the document is contained in.
// sourceOrigin in the format of 'https://xxx or http://xxx'
return sourceOrigin.split('://')[1];
}
}
if (!isProxyOrigin(win.location.href)) {
const parts = win.location.hostname.split('.');
let domain = parts[parts.length - 1];
const testCookieName = getTempCookieName(win);
for (let i = parts.length - 2; i >= 0; i--) {
domain = parts[i] + '.' + domain;
// Try set a cookie for testing only, expire after 1 sec
trySetCookie(win, testCookieName, 'delete', Date.now() + 1000, domain);
if (getCookie(win, testCookieName) == 'delete') {
// Remove the cookie for testing
trySetCookie(win, testCookieName, 'delete', Date.now() - 1000, domain);
return domain;
}
}
}
// Proxy origin w/o <meta name='amp-cookie-scope>
// We cannot calculate the etld+1 without the public suffix list.
// Return null instead.
// Note: This should not affect cookie writing because we don't allow writing
// cookie to highestAvailableDomain on proxy origin
// In the case of link decoration on proxy origin,
// we expect the correct meta tag to be
// set by publisher or cache order for AMP runtime to find all subdomains.
return null;
}
/**
* Attempt to set a cookie with the given params.
*
* @param {!Window} win
* @param {string} name
* @param {string} value
* @param {time} expirationTime
* @param {string|undefined} domain
* @param {!SameSite=} sameSite
* @param {boolean|undefined=} secure
*/
function trySetCookie(
win,
name,
value,
expirationTime,
domain,
sameSite,
secure
) {
// We do not allow setting cookies on the domain that contains both
// the cdn. and www. hosts.
// Note: we need to allow cdn.ampproject.org in order to optin to experiments
if (domain == 'ampproject.org') {
// Actively delete them.
value = 'delete';
expirationTime = 0;
}
const cookie =
encodeURIComponent(name) +
'=' +
encodeURIComponent(value) +
'; path=/' +
(domain ? '; domain=' + domain : '') +
'; expires=' +
new Date(expirationTime).toUTCString() +
getSameSiteString(win, sameSite) +
(secure ? '; Secure' : '');
try {
win.document.cookie = cookie;
} catch (ignore) {
// Do not throw if setting the cookie failed Exceptions can be thrown
// when AMP docs are opened on origins that do not allow setting
// cookies such as null origins.
}
}
/**
* Gets the cookie string to use for SameSite. This only sets the SameSite
* value if specified, falling back to the browser default. The default value
* is equivalent to SameSite.NONE, but is planned to be set to SameSite.LAX in
* Chrome 80.
*
* Note: In Safari 12, if the value is set to SameSite.NONE, it is treated by
* the browser as SameSite.STRICT.
* @param {Window} win
* @param {!SameSite|undefined} sameSite
* @return {string} The string to use when setting the cookie.
*/
function getSameSiteString(win, sameSite) {
if (!sameSite) {
return '';
}
return `; SameSite=${sameSite}`;
}
/**
* Throws if a given cookie should not be set on the given origin.
* This is a defense-in-depth. Callers should never run into this.
*
* @param {!Window} win
* @param {!Object} options
* @param {string} name For the error message.
*/
function checkOriginForSettingCookie(win, options, name) {
if (options.allowOnProxyOrigin) {
userAssert(
!options.highestAvailableDomain,
'Could not support highestAvailable Domain on proxy origin, ' +
'specify domain explicitly'
);
return;
}
userAssert(
!isProxyOrigin(win.location.href),
`Should never attempt to set cookie on proxy origin: ${name}`
);
const current = parseUrlDeprecated(win.location.href).hostname.toLowerCase();
const proxy = parseUrlDeprecated(urls.cdn).hostname.toLowerCase();
userAssert(
!(current == proxy || endsWith(current, '.' + proxy)),
'Should never attempt to set cookie on proxy origin. (in depth check): ' +
name
);
}
/**
* Return a temporary cookie name for testing only
* @param {!Window} win
* @return {string}
*/
function getTempCookieName(win) {
let testCookieName = TEST_COOKIE_NAME;
const counter = 0;
while (getCookie(win, testCookieName)) {
// test cookie name conflict, append counter to test cookie name
testCookieName = TEST_COOKIE_NAME + counter;
}
return testCookieName;
}