-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate to our own raw-content cookie parser
Useful to give us cookie + set-cookie parsing with the same format, and avoid various automatic format handling (Date etc) that set-cookie-parser does unhelpfully.
- Loading branch information
Showing
5 changed files
with
228 additions
and
46 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// Partially taken from https://github.com/jshttp/cookie under MIT license, | ||
// heavily rewritten with setCookie support added. | ||
|
||
export interface Cookie { | ||
name: string; | ||
value: string; | ||
path?: string; | ||
httponly?: boolean; | ||
secure?: boolean; | ||
samesite?: string; | ||
domain?: string; | ||
expires?: string; | ||
'max-age'?: string; | ||
[key: string]: string | boolean | undefined; | ||
} | ||
|
||
export function parseSetCookieHeader( | ||
headers: string | string[] | ||
) { | ||
if (!Array.isArray(headers)) { | ||
headers = [headers]; | ||
} | ||
|
||
const cookies: Array<Cookie> = []; | ||
|
||
for (const header of headers) { | ||
const [cookieKV, ...parts] = header.split(";"); | ||
|
||
const [name, value] = (cookieKV?.split("=") ?? []); | ||
if (!name || value === undefined) continue; | ||
|
||
const cookie: Cookie = { | ||
name, | ||
value | ||
}; | ||
|
||
for (const part of parts) { | ||
let [key, val] = part.split("="); | ||
key = key.trim().toLowerCase(); | ||
val = val?.trim() ?? true; | ||
cookie[key] = val; | ||
} | ||
|
||
cookies.push(cookie); | ||
} | ||
|
||
return cookies; | ||
} | ||
|
||
export function parseCookieHeader( | ||
str: string | ||
): Array<Cookie> { | ||
const cookies: Array<Cookie> = []; | ||
const len = str.length; | ||
// RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='. | ||
if (len < 2) return cookies; | ||
|
||
let index = 0; | ||
|
||
do { | ||
const eqIdx = str.indexOf("=", index); | ||
if (eqIdx === -1) break; // No more cookie pairs. | ||
|
||
const colonIdx = str.indexOf(";", index); | ||
const endIdx = colonIdx === -1 ? len : colonIdx; | ||
|
||
if (eqIdx > endIdx) { | ||
// backtrack on prior semicolon | ||
index = str.lastIndexOf(";", eqIdx - 1) + 1; | ||
continue; | ||
} | ||
|
||
const nameStartIdx = startIndex(str, index, eqIdx); | ||
const nameEndIdx = endIndex(str, eqIdx, nameStartIdx); | ||
const name = str.slice(nameStartIdx, nameEndIdx); | ||
|
||
const valStartIdx = startIndex(str, eqIdx + 1, endIdx); | ||
const valEndIdx = endIndex(str, endIdx, valStartIdx); | ||
const value = decode(str.slice(valStartIdx, valEndIdx)); | ||
|
||
cookies.push({ name: name, value }); | ||
|
||
index = endIdx + 1; | ||
} while (index < len); | ||
|
||
return cookies; | ||
} | ||
|
||
function startIndex(str: string, index: number, max: number) { | ||
do { | ||
const code = str.charCodeAt(index); | ||
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index; | ||
} while (++index < max); | ||
return max; | ||
} | ||
|
||
function endIndex(str: string, index: number, min: number) { | ||
while (index > min) { | ||
const code = str.charCodeAt(--index); | ||
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index + 1; | ||
} | ||
return min; | ||
} | ||
|
||
function decode(str: string): string { | ||
if (str.indexOf("%") === -1) return str; | ||
|
||
try { | ||
return decodeURIComponent(str); | ||
} catch (e) { | ||
return str; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { expect } from 'chai'; | ||
|
||
import { | ||
parseCookieHeader, | ||
parseSetCookieHeader | ||
} from '../../../../src/model/http/cookies'; | ||
|
||
describe('Cookie parsing', () => { | ||
describe('parseCookieHeader', () => { | ||
it('should parse a simple cookie', () => { | ||
const result = parseCookieHeader('name=value'); | ||
expect(result).to.deep.equal([{ name: 'name', value: 'value' }]); | ||
}); | ||
|
||
it('should parse multiple cookies', () => { | ||
const result = parseCookieHeader('name1=value1; name2=value2'); | ||
expect(result).to.deep.equal([ | ||
{ name: 'name1', value: 'value1' }, | ||
{ name: 'name2', value: 'value2' } | ||
]); | ||
}); | ||
|
||
it('should handle URL encoded values', () => { | ||
const result = parseCookieHeader('name=hello%20world'); | ||
expect(result).to.deep.equal([{ name: 'name', value: 'hello world' }]); | ||
}); | ||
|
||
it('should return empty array for invalid input', () => { | ||
expect(parseCookieHeader('')).to.deep.equal([]); | ||
expect(parseCookieHeader('invalid')).to.deep.equal([]); | ||
}); | ||
}); | ||
|
||
describe('parseSetCookieHeader', () => { | ||
it('should parse a simple Set-Cookie header', () => { | ||
const result = parseSetCookieHeader('name=value'); | ||
expect(result).to.deep.equal([{ name: 'name', value: 'value' }]); | ||
}); | ||
|
||
it('should parse Set-Cookie with attributes', () => { | ||
const result = parseSetCookieHeader( | ||
'name=value; Path=/; Domain=example.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT' | ||
); | ||
expect(result).to.deep.equal([{ | ||
name: 'name', | ||
value: 'value', | ||
path: '/', | ||
domain: 'example.com', | ||
expires: 'Wed, 21 Oct 2015 07:28:00 GMT' | ||
}]); | ||
}); | ||
|
||
it('should parse boolean flags', () => { | ||
const result = parseSetCookieHeader('name=value; httponly; Secure'); | ||
expect(result).to.deep.equal([{ | ||
name: 'name', | ||
value: 'value', | ||
httponly: true, | ||
secure: true | ||
}]); | ||
}); | ||
|
||
it('should parse multiple Set-Cookie headers', () => { | ||
const result = parseSetCookieHeader([ | ||
'name1=value1; Path=/', | ||
'name2=value2;httponly;UnknownOther=hello' | ||
]); | ||
expect(result).to.deep.equal([ | ||
{ name: 'name1', value: 'value1', path: '/' }, | ||
{ name: 'name2', value: 'value2', httponly: true, unknownother: 'hello' } | ||
]); | ||
}); | ||
|
||
it('should handle case-insensitive attribute names', () => { | ||
const result = parseSetCookieHeader([ | ||
'name=value; PATH=/test; httponly; DOMAIN=example.com; SecURE', | ||
'other=value; samesite=Strict; MAX-AGE=3600' | ||
]); | ||
|
||
expect(result).to.deep.equal([{ | ||
name: 'name', | ||
value: 'value', | ||
path: '/test', // Standardized casing | ||
httponly: true, // Standardized casing | ||
domain: 'example.com', | ||
secure: true | ||
}, { | ||
name: 'other', | ||
value: 'value', | ||
samesite: 'Strict', | ||
'max-age': '3600' | ||
}]); | ||
}); | ||
|
||
it('should handle empty/invalid headers', () => { | ||
expect(parseSetCookieHeader('')).to.deep.equal([]); | ||
expect(parseSetCookieHeader([])).to.deep.equal([]); | ||
expect(parseSetCookieHeader([';', 'invalid'])).to.deep.equal([]); | ||
}); | ||
}); | ||
}); |