| '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 |
| } |