-
Notifications
You must be signed in to change notification settings - Fork 159
/
Copy pathcookieManager.js
251 lines (231 loc) · 8.34 KB
/
cookieManager.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
// "use strict" is not required in ES6 modules as they are always in strict mode
// Exporting functions and constants as named exports and default export
/**
* RegExp to match cookie-name in RFC 6265 sec 4.1.1
* This refers out to the obsoleted definition of token in RFC 2616 sec 2.2
* which has been replaced by the token definition in RFC 7230 appendix B.
*
* cookie-name = token
* token = 1*tchar
* tchar = "!" / "#" / "$" / "%" / "&" / "'" /
* "*" / "+" / "-" / "." / "^" / "_" /
* "`" / "|" / "~" / DIGIT / ALPHA
*
* Note: Allowing more characters - https://github.com/jshttp/cookie/issues/191
* Allow same range as cookie value, except `=`, which delimits end of name.
*/
const cookieNameRegExp = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
/**
* RegExp to match cookie-value in RFC 6265 sec 4.1.1
*
* cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
* ; US-ASCII characters excluding CTLs,
* ; whitespace DQUOTE, comma, semicolon,
* ; and backslash
*
* Allowing more characters: https://github.com/jshttp/cookie/issues/191
* Comma, backslash, and DQUOTE are not part of the parsing algorithm.
*/
const cookieValueRegExp = /^[\u0021-\u003A\u003C-\u007E]*$/;
/**
* RegExp to match domain-value in RFC 6265 sec 4.1.1
*
* domain-value = <subdomain>
* ; defined in [RFC1034], Section 3.5, as
* ; enhanced by [RFC1123], Section 2.1
* <subdomain> = <label> | <subdomain> "." <label>
* <label> = <let-dig> [ [ <ldh-str> ] <let-dig> ]
* Labels must be 63 characters or less.
* 'let-dig' not 'letter' in the first char, per RFC1123
* <ldh-str> = <let-dig-hyp> | <let-dig-hyp> <ldh-str>
* <let-dig-hyp> = <let-dig> | "-"
* <let-dig> = <letter> | <digit>
* <letter> = any one of the 52 alphabetic characters A through Z in
* upper case and a through z in lower case
* <digit> = any one of the ten digits 0 through 9
*
* Keep support for leading dot: https://github.com/jshttp/cookie/issues/173
*
* > (Note that a leading %x2E ("."), if present, is ignored even though that
* character is not permitted, but a trailing %x2E ("."), if present, will
* cause the user agent to ignore the attribute.)
*/
const domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
/**
* RegExp to match path-value in RFC 6265 sec 4.1.1
*
* path-value = <any CHAR except CTLs or ";">
* CHAR = %x01-7F
* ; defined in RFC 5234 appendix B.1
*/
const pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
const __toString = Object.prototype.toString;
const NullObject = (() => {
const C = function () {
};
C.prototype = Object.create(null);
return C;
})();
/**
* Parse a cookie header.
*
* Parse the given cookie header string into an object
* The object has the various cookies as keys(names) => values
*/
export function parse(input, options) {
const obj = new NullObject();
const dec = options?.decode || decode;
// If input is an array, process each string
const headers = Array.isArray(input) ? input : [input];
// console.log(headers);
for (const str of headers) {
const len = str.length;
if (len < 2) continue;
let index = 0;
do {
const eqIdx = str.indexOf("=", index);
if (eqIdx === -1) break;
const colonIdx = str.indexOf(";", index);
const endIdx = colonIdx === -1 ? len : colonIdx;
if (eqIdx > endIdx) {
index = str.lastIndexOf(";", eqIdx - 1) + 1;
continue;
}
const keyStartIdx = startIndex(str, index, eqIdx);
const keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
const key = str.slice(keyStartIdx, keyEndIdx); // Keep original case
let valStartIdx = startIndex(str, eqIdx + 1, endIdx);
let valEndIdx = endIndex(str, endIdx, valStartIdx);
const value = dec(str.slice(valStartIdx, valEndIdx));
// Always overwrite with the latest value for the same key
obj[key] = value;
index = endIdx + 1;
} while (index < len);
}
return obj;
}
function startIndex(str, index, max) {
do {
const code = str.charCodeAt(index);
if (code !== 0x20 && code !== 0x09) return index;
} while (++index < max);
return max;
}
function endIndex(str, index, min) {
while (index > min) {
const code = str.charCodeAt(--index);
if (code !== 0x20 && code !== 0x09) return index + 1;
}
return min;
}
/**
* Serialize data into a cookie header.
*
* Serialize a name value pair into a cookie string suitable for
* http headers. An optional options object specifies cookie parameters.
*/
export function serialize(name, val, options) {
const enc = options?.encode || encodeURIComponent;
if (!cookieNameRegExp.test(name)) {
throw new TypeError(`argument name is invalid: ${name}`);
}
const value = enc(val);
if (!cookieValueRegExp.test(value)) {
throw new TypeError(`argument val is invalid: ${val}`);
}
let str = name + "=" + value;
if (!options) return str;
if (options.maxAge !== undefined) {
if (!Number.isInteger(options.maxAge)) {
throw new TypeError(`option maxAge is invalid: ${options.maxAge}`);
}
str += "; Max-Age=" + options.maxAge;
}
if (options.domain) {
if (!domainValueRegExp.test(options.domain)) {
throw new TypeError(`option domain is invalid: ${options.domain}`);
}
str += "; Domain=" + options.domain;
}
if (options.path) {
if (!pathValueRegExp.test(options.path)) {
throw new TypeError(`option path is invalid: ${options.path}`);
}
str += "; Path=" + options.path;
}
if (options.expires) {
if (!isDate(options.expires) || !Number.isFinite(options.expires.valueOf())) {
throw new TypeError(`option expires is invalid: ${options.expires}`);
}
str += "; Expires=" + options.expires.toUTCString();
}
if (options.httpOnly) {
str += "; HttpOnly";
}
if (options.secure) {
str += "; Secure";
}
if (options.partitioned) {
str += "; Partitioned";
}
if (options.priority) {
const priority = typeof options.priority === "string" ? options.priority.toLowerCase() : undefined;
switch (priority) {
case "low":
str += "; Priority=Low";
break;
case "medium":
str += "; Priority=Medium";
break;
case "high":
str += "; Priority=High";
break;
default:
throw new TypeError(`option priority is invalid: ${options.priority}`);
}
}
if (options.sameSite) {
const sameSite = typeof options.sameSite === "string" ? options.sameSite.toLowerCase() : options.sameSite;
switch (sameSite) {
case true:
case "strict":
str += "; SameSite=Strict";
break;
case "lax":
str += "; SameSite=Lax";
break;
case "none":
str += "; SameSite=None";
break;
default:
throw new TypeError(`option sameSite is invalid: ${options.sameSite}`);
}
}
return str;
}
/**
* URL-decode string value. Optimized to skip native call when no %.
*/
function decode(str) {
if (str.indexOf("%") === -1) return str;
try {
return decodeURIComponent(str);
} catch (e) {
return str;
}
}
/**
* Determine if value is a Date.
*/
function isDate(val) {
return __toString.call(val) === "[object Date]";
}
export function stringify(obj, encode = 0) {
return Object.entries(obj)
.map(([key, value]) => encode ? serialize(key, value) : decodeURIComponent(serialize(key, value)))
.join("; ")
}
// Export as default object for named and default usage
const COOKIE = {parse, serialize, stringify};
export default COOKIE;