292 lines
7.2 KiB
JavaScript
292 lines
7.2 KiB
JavaScript
'use strict'
|
|
|
|
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
|
|
}
|