Spaces:
Runtime error
Runtime error
const assert = require('assert') | |
const { kHeadersList } = require('../core/symbols') | |
function isCTLExcludingHtab (value) { | |
if (value.length === 0) { | |
return false | |
} | |
for (const char of value) { | |
const code = char.charCodeAt(0) | |
if ( | |
(code >= 0x00 || code <= 0x08) || | |
(code >= 0x0A || code <= 0x1F) || | |
code === 0x7F | |
) { | |
return false | |
} | |
} | |
} | |
/** | |
CHAR = <any US-ASCII character (octets 0 - 127)> | |
token = 1*<any CHAR except CTLs or separators> | |
separators = "(" | ")" | "<" | ">" | "@" | |
| "," | ";" | ":" | "\" | <"> | |
| "/" | "[" | "]" | "?" | "=" | |
| "{" | "}" | SP | HT | |
* @param {string} name | |
*/ | |
function validateCookieName (name) { | |
for (const char of name) { | |
const code = char.charCodeAt(0) | |
if ( | |
(code <= 0x20 || code > 0x7F) || | |
char === '(' || | |
char === ')' || | |
char === '>' || | |
char === '<' || | |
char === '@' || | |
char === ',' || | |
char === ';' || | |
char === ':' || | |
char === '\\' || | |
char === '"' || | |
char === '/' || | |
char === '[' || | |
char === ']' || | |
char === '?' || | |
char === '=' || | |
char === '{' || | |
char === '}' | |
) { | |
throw new Error('Invalid cookie name') | |
} | |
} | |
} | |
/** | |
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 | |
* @param {string} value | |
*/ | |
function validateCookieValue (value) { | |
for (const char of value) { | |
const code = char.charCodeAt(0) | |
if ( | |
code < 0x21 || // exclude CTLs (0-31) | |
code === 0x22 || | |
code === 0x2C || | |
code === 0x3B || | |
code === 0x5C || | |
code > 0x7E // non-ascii | |
) { | |
throw new Error('Invalid header value') | |
} | |
} | |
} | |
/** | |
* path-value = <any CHAR except CTLs or ";"> | |
* @param {string} path | |
*/ | |
function validateCookiePath (path) { | |
for (const char of path) { | |
const code = char.charCodeAt(0) | |
if (code < 0x21 || char === ';') { | |
throw new Error('Invalid cookie path') | |
} | |
} | |
} | |
/** | |
* I have no idea why these values aren't allowed to be honest, | |
* but Deno tests these. - Khafra | |
* @param {string} domain | |
*/ | |
function validateCookieDomain (domain) { | |
if ( | |
domain.startsWith('-') || | |
domain.endsWith('.') || | |
domain.endsWith('-') | |
) { | |
throw new Error('Invalid cookie domain') | |
} | |
} | |
/** | |
* @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 | |
* @param {number|Date} date | |
IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT | |
; fixed length/zone/capitalization subset of the format | |
; see Section 3.3 of [RFC5322] | |
day-name = %x4D.6F.6E ; "Mon", case-sensitive | |
/ %x54.75.65 ; "Tue", case-sensitive | |
/ %x57.65.64 ; "Wed", case-sensitive | |
/ %x54.68.75 ; "Thu", case-sensitive | |
/ %x46.72.69 ; "Fri", case-sensitive | |
/ %x53.61.74 ; "Sat", case-sensitive | |
/ %x53.75.6E ; "Sun", case-sensitive | |
date1 = day SP month SP year | |
; e.g., 02 Jun 1982 | |
day = 2DIGIT | |
month = %x4A.61.6E ; "Jan", case-sensitive | |
/ %x46.65.62 ; "Feb", case-sensitive | |
/ %x4D.61.72 ; "Mar", case-sensitive | |
/ %x41.70.72 ; "Apr", case-sensitive | |
/ %x4D.61.79 ; "May", case-sensitive | |
/ %x4A.75.6E ; "Jun", case-sensitive | |
/ %x4A.75.6C ; "Jul", case-sensitive | |
/ %x41.75.67 ; "Aug", case-sensitive | |
/ %x53.65.70 ; "Sep", case-sensitive | |
/ %x4F.63.74 ; "Oct", case-sensitive | |
/ %x4E.6F.76 ; "Nov", case-sensitive | |
/ %x44.65.63 ; "Dec", case-sensitive | |
year = 4DIGIT | |
GMT = %x47.4D.54 ; "GMT", case-sensitive | |
time-of-day = hour ":" minute ":" second | |
; 00:00:00 - 23:59:60 (leap second) | |
hour = 2DIGIT | |
minute = 2DIGIT | |
second = 2DIGIT | |
*/ | |
function toIMFDate (date) { | |
if (typeof date === 'number') { | |
date = new Date(date) | |
} | |
const days = [ | |
'Sun', 'Mon', 'Tue', 'Wed', | |
'Thu', 'Fri', 'Sat' | |
] | |
const months = [ | |
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', | |
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' | |
] | |
const dayName = days[date.getUTCDay()] | |
const day = date.getUTCDate().toString().padStart(2, '0') | |
const month = months[date.getUTCMonth()] | |
const year = date.getUTCFullYear() | |
const hour = date.getUTCHours().toString().padStart(2, '0') | |
const minute = date.getUTCMinutes().toString().padStart(2, '0') | |
const second = date.getUTCSeconds().toString().padStart(2, '0') | |
return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT` | |
} | |
/** | |
max-age-av = "Max-Age=" non-zero-digit *DIGIT | |
; In practice, both expires-av and max-age-av | |
; are limited to dates representable by the | |
; user agent. | |
* @param {number} maxAge | |
*/ | |
function validateCookieMaxAge (maxAge) { | |
if (maxAge < 0) { | |
throw new Error('Invalid cookie max-age') | |
} | |
} | |
/** | |
* @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 | |
* @param {import('./index').Cookie} cookie | |
*/ | |
function stringify (cookie) { | |
if (cookie.name.length === 0) { | |
return null | |
} | |
validateCookieName(cookie.name) | |
validateCookieValue(cookie.value) | |
const out = [`${cookie.name}=${cookie.value}`] | |
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1 | |
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2 | |
if (cookie.name.startsWith('__Secure-')) { | |
cookie.secure = true | |
} | |
if (cookie.name.startsWith('__Host-')) { | |
cookie.secure = true | |
cookie.domain = null | |
cookie.path = '/' | |
} | |
if (cookie.secure) { | |
out.push('Secure') | |
} | |
if (cookie.httpOnly) { | |
out.push('HttpOnly') | |
} | |
if (typeof cookie.maxAge === 'number') { | |
validateCookieMaxAge(cookie.maxAge) | |
out.push(`Max-Age=${cookie.maxAge}`) | |
} | |
if (cookie.domain) { | |
validateCookieDomain(cookie.domain) | |
out.push(`Domain=${cookie.domain}`) | |
} | |
if (cookie.path) { | |
validateCookiePath(cookie.path) | |
out.push(`Path=${cookie.path}`) | |
} | |
if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') { | |
out.push(`Expires=${toIMFDate(cookie.expires)}`) | |
} | |
if (cookie.sameSite) { | |
out.push(`SameSite=${cookie.sameSite}`) | |
} | |
for (const part of cookie.unparsed) { | |
if (!part.includes('=')) { | |
throw new Error('Invalid unparsed') | |
} | |
const [key, ...value] = part.split('=') | |
out.push(`${key.trim()}=${value.join('=')}`) | |
} | |
return out.join('; ') | |
} | |
let kHeadersListNode | |
function getHeadersList (headers) { | |
if (headers[kHeadersList]) { | |
return headers[kHeadersList] | |
} | |
if (!kHeadersListNode) { | |
kHeadersListNode = Object.getOwnPropertySymbols(headers).find( | |
(symbol) => symbol.description === 'headers list' | |
) | |
assert(kHeadersListNode, 'Headers cannot be parsed') | |
} | |
const headersList = headers[kHeadersListNode] | |
assert(headersList) | |
return headersList | |
} | |
module.exports = { | |
isCTLExcludingHtab, | |
stringify, | |
getHeadersList | |
} | |