|
1 | 1 | /** |
2 | | - * This code was originally copied from the 'cookie` module at v0.5.0 and was simplified for our use case. |
| 2 | + * The value decoding in `cookiePairsToRecord` was originally copied from the 'cookie` module at v0.5.0. |
3 | 3 | * https://github.com/jshttp/cookie/blob/a0c84147aab6266bdb3996cf4062e93907c0b0fc/index.js |
4 | 4 | * It had the following license: |
5 | 5 | * |
|
28 | 28 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
29 | 29 | */ |
30 | 30 |
|
31 | | -/** |
32 | | - * Parses a cookie string |
33 | | - */ |
34 | | -export function parseCookie(str: string): Record<string, string> { |
35 | | - const obj: Record<string, string> = {}; |
36 | | - let index = 0; |
| 31 | +import { FILTERED_VALUE } from './data-collection/filtering-snippets'; |
37 | 32 |
|
38 | | - while (index < str.length) { |
39 | | - const eqIdx = str.indexOf('=', index); |
| 33 | +/** A cookie's name and raw value. A nameless cookie (RFC 6265bis) has the name `''`. */ |
| 34 | +export type CookiePair = [name: string, value: string]; |
40 | 35 |
|
41 | | - // no more cookie pairs |
42 | | - if (eqIdx === -1) { |
43 | | - break; |
| 36 | +/** |
| 37 | + * Splits a `Cookie` / `Set-Cookie` header into its ordered name-value pairs. Values are trimmed, but not |
| 38 | + * decoded or unquoted. |
| 39 | + * |
| 40 | + * A segment without an `=` is a nameless cookie, so the bare token is its value (RFC 6265bis). |
| 41 | + */ |
| 42 | +export function parseCookieHeader(value: string | string[], headerName: 'cookie' | 'set-cookie'): CookiePair[] { |
| 43 | + // Set-Cookie: one cookie per header, followed by attributes ("name=value; HttpOnly; Secure") |
| 44 | + // Cookie: multiple cookies separated by ";" (the space after ";" is not guaranteed on the wire) |
| 45 | + const segments = (Array.isArray(value) ? value : [value]).flatMap(headerValue => { |
| 46 | + if (typeof headerValue !== 'string') { |
| 47 | + return []; |
44 | 48 | } |
| 49 | + return headerName === 'set-cookie' ? [headerValue.split(';')[0]!] : headerValue.split(';'); |
| 50 | + }); |
45 | 51 |
|
46 | | - let endIdx = str.indexOf(';', index); |
47 | | - |
48 | | - if (endIdx === -1) { |
49 | | - endIdx = str.length; |
50 | | - } else if (endIdx < eqIdx) { |
51 | | - // backtrack on prior semicolon |
52 | | - index = str.lastIndexOf(';', eqIdx - 1) + 1; |
53 | | - continue; |
54 | | - } |
| 52 | + return ( |
| 53 | + segments |
| 54 | + .map(segment => segment.trim()) |
| 55 | + // ";;" and trailing ";" leave empty segments. "=" has neither name nor value, so RFC 6265bis ignores it. |
| 56 | + .filter(segment => segment !== '' && segment !== '=') |
| 57 | + .map((segment): CookiePair => { |
| 58 | + // Only first "=" separates name from value: "jwt=eyJhbGc=" has value "eyJhbGc=" |
| 59 | + const equalSignIndex = segment.indexOf('='); |
| 60 | + return equalSignIndex === -1 |
| 61 | + ? // No "=": nameless cookie, the whole segment is the value |
| 62 | + ['', segment] |
| 63 | + : // Trim both parts, so that "theme = dark" is named "theme", not "theme " |
| 64 | + [segment.slice(0, equalSignIndex).trim(), segment.slice(equalSignIndex + 1).trim()]; |
| 65 | + }) |
| 66 | + ); |
| 67 | +} |
55 | 68 |
|
56 | | - const key = str.slice(index, eqIdx).trim(); |
| 69 | +/** |
| 70 | + * Converts cookie pairs to a record with decoded values. The first cookie of a name wins. |
| 71 | + * |
| 72 | + * A nameless cookie's token is its value, and no name-based denylist can match it. So it is stored |
| 73 | + * under the name `''` and its value is always filtered. |
| 74 | + */ |
| 75 | +export function cookiePairsToRecord(pairs: CookiePair[]): Record<string, string> { |
| 76 | + const record: Record<string, string> = {}; |
57 | 77 |
|
58 | | - // only assign once |
59 | | - if (undefined === obj[key]) { |
60 | | - let val = str.slice(eqIdx + 1, endIdx).trim(); |
| 78 | + for (const [name, value] of pairs) { |
| 79 | + if (record[name] === undefined) { |
| 80 | + record[name] = name === '' ? FILTERED_VALUE : decodeCookieValue(value); |
| 81 | + } |
| 82 | + } |
61 | 83 |
|
62 | | - // quoted values |
63 | | - if (val.charCodeAt(0) === 0x22) { |
64 | | - val = val.slice(1, -1); |
65 | | - } |
| 84 | + return record; |
| 85 | +} |
66 | 86 |
|
67 | | - try { |
68 | | - obj[key] = val.indexOf('%') !== -1 ? decodeURIComponent(val) : val; |
69 | | - } catch { |
70 | | - obj[key] = val; |
71 | | - } |
72 | | - } |
| 87 | +function decodeCookieValue(value: string): string { |
| 88 | + const unquoted = value.length > 1 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value; |
73 | 89 |
|
74 | | - index = endIdx + 1; |
| 90 | + try { |
| 91 | + return unquoted.indexOf('%') !== -1 ? decodeURIComponent(unquoted) : unquoted; |
| 92 | + } catch { |
| 93 | + return unquoted; |
75 | 94 | } |
76 | | - |
77 | | - return obj; |
78 | 95 | } |
0 commit comments