[d24f17c] | 1 | 'use strict'
|
---|
| 2 |
|
---|
| 3 | const assert = require('assert')
|
---|
| 4 | const { kHeadersList } = require('../core/symbols')
|
---|
| 5 |
|
---|
| 6 | function isCTLExcludingHtab (value) {
|
---|
| 7 | if (value.length === 0) {
|
---|
| 8 | return false
|
---|
| 9 | }
|
---|
| 10 |
|
---|
| 11 | for (const char of value) {
|
---|
| 12 | const code = char.charCodeAt(0)
|
---|
| 13 |
|
---|
| 14 | if (
|
---|
| 15 | (code >= 0x00 || code <= 0x08) ||
|
---|
| 16 | (code >= 0x0A || code <= 0x1F) ||
|
---|
| 17 | code === 0x7F
|
---|
| 18 | ) {
|
---|
| 19 | return false
|
---|
| 20 | }
|
---|
| 21 | }
|
---|
| 22 | }
|
---|
| 23 |
|
---|
| 24 | /**
|
---|
| 25 | CHAR = <any US-ASCII character (octets 0 - 127)>
|
---|
| 26 | token = 1*<any CHAR except CTLs or separators>
|
---|
| 27 | separators = "(" | ")" | "<" | ">" | "@"
|
---|
| 28 | | "," | ";" | ":" | "\" | <">
|
---|
| 29 | | "/" | "[" | "]" | "?" | "="
|
---|
| 30 | | "{" | "}" | SP | HT
|
---|
| 31 | * @param {string} name
|
---|
| 32 | */
|
---|
| 33 | function validateCookieName (name) {
|
---|
| 34 | for (const char of name) {
|
---|
| 35 | const code = char.charCodeAt(0)
|
---|
| 36 |
|
---|
| 37 | if (
|
---|
| 38 | (code <= 0x20 || code > 0x7F) ||
|
---|
| 39 | char === '(' ||
|
---|
| 40 | char === ')' ||
|
---|
| 41 | char === '>' ||
|
---|
| 42 | char === '<' ||
|
---|
| 43 | char === '@' ||
|
---|
| 44 | char === ',' ||
|
---|
| 45 | char === ';' ||
|
---|
| 46 | char === ':' ||
|
---|
| 47 | char === '\\' ||
|
---|
| 48 | char === '"' ||
|
---|
| 49 | char === '/' ||
|
---|
| 50 | char === '[' ||
|
---|
| 51 | char === ']' ||
|
---|
| 52 | char === '?' ||
|
---|
| 53 | char === '=' ||
|
---|
| 54 | char === '{' ||
|
---|
| 55 | char === '}'
|
---|
| 56 | ) {
|
---|
| 57 | throw new Error('Invalid cookie name')
|
---|
| 58 | }
|
---|
| 59 | }
|
---|
| 60 | }
|
---|
| 61 |
|
---|
| 62 | /**
|
---|
| 63 | cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
|
---|
| 64 | cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
---|
| 65 | ; US-ASCII characters excluding CTLs,
|
---|
| 66 | ; whitespace DQUOTE, comma, semicolon,
|
---|
| 67 | ; and backslash
|
---|
| 68 | * @param {string} value
|
---|
| 69 | */
|
---|
| 70 | function validateCookieValue (value) {
|
---|
| 71 | for (const char of value) {
|
---|
| 72 | const code = char.charCodeAt(0)
|
---|
| 73 |
|
---|
| 74 | if (
|
---|
| 75 | code < 0x21 || // exclude CTLs (0-31)
|
---|
| 76 | code === 0x22 ||
|
---|
| 77 | code === 0x2C ||
|
---|
| 78 | code === 0x3B ||
|
---|
| 79 | code === 0x5C ||
|
---|
| 80 | code > 0x7E // non-ascii
|
---|
| 81 | ) {
|
---|
| 82 | throw new Error('Invalid header value')
|
---|
| 83 | }
|
---|
| 84 | }
|
---|
| 85 | }
|
---|
| 86 |
|
---|
| 87 | /**
|
---|
| 88 | * path-value = <any CHAR except CTLs or ";">
|
---|
| 89 | * @param {string} path
|
---|
| 90 | */
|
---|
| 91 | function validateCookiePath (path) {
|
---|
| 92 | for (const char of path) {
|
---|
| 93 | const code = char.charCodeAt(0)
|
---|
| 94 |
|
---|
| 95 | if (code < 0x21 || char === ';') {
|
---|
| 96 | throw new Error('Invalid cookie path')
|
---|
| 97 | }
|
---|
| 98 | }
|
---|
| 99 | }
|
---|
| 100 |
|
---|
| 101 | /**
|
---|
| 102 | * I have no idea why these values aren't allowed to be honest,
|
---|
| 103 | * but Deno tests these. - Khafra
|
---|
| 104 | * @param {string} domain
|
---|
| 105 | */
|
---|
| 106 | function validateCookieDomain (domain) {
|
---|
| 107 | if (
|
---|
| 108 | domain.startsWith('-') ||
|
---|
| 109 | domain.endsWith('.') ||
|
---|
| 110 | domain.endsWith('-')
|
---|
| 111 | ) {
|
---|
| 112 | throw new Error('Invalid cookie domain')
|
---|
| 113 | }
|
---|
| 114 | }
|
---|
| 115 |
|
---|
| 116 | /**
|
---|
| 117 | * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
|
---|
| 118 | * @param {number|Date} date
|
---|
| 119 | IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
|
---|
| 120 | ; fixed length/zone/capitalization subset of the format
|
---|
| 121 | ; see Section 3.3 of [RFC5322]
|
---|
| 122 |
|
---|
| 123 | day-name = %x4D.6F.6E ; "Mon", case-sensitive
|
---|
| 124 | / %x54.75.65 ; "Tue", case-sensitive
|
---|
| 125 | / %x57.65.64 ; "Wed", case-sensitive
|
---|
| 126 | / %x54.68.75 ; "Thu", case-sensitive
|
---|
| 127 | / %x46.72.69 ; "Fri", case-sensitive
|
---|
| 128 | / %x53.61.74 ; "Sat", case-sensitive
|
---|
| 129 | / %x53.75.6E ; "Sun", case-sensitive
|
---|
| 130 | date1 = day SP month SP year
|
---|
| 131 | ; e.g., 02 Jun 1982
|
---|
| 132 |
|
---|
| 133 | day = 2DIGIT
|
---|
| 134 | month = %x4A.61.6E ; "Jan", case-sensitive
|
---|
| 135 | / %x46.65.62 ; "Feb", case-sensitive
|
---|
| 136 | / %x4D.61.72 ; "Mar", case-sensitive
|
---|
| 137 | / %x41.70.72 ; "Apr", case-sensitive
|
---|
| 138 | / %x4D.61.79 ; "May", case-sensitive
|
---|
| 139 | / %x4A.75.6E ; "Jun", case-sensitive
|
---|
| 140 | / %x4A.75.6C ; "Jul", case-sensitive
|
---|
| 141 | / %x41.75.67 ; "Aug", case-sensitive
|
---|
| 142 | / %x53.65.70 ; "Sep", case-sensitive
|
---|
| 143 | / %x4F.63.74 ; "Oct", case-sensitive
|
---|
| 144 | / %x4E.6F.76 ; "Nov", case-sensitive
|
---|
| 145 | / %x44.65.63 ; "Dec", case-sensitive
|
---|
| 146 | year = 4DIGIT
|
---|
| 147 |
|
---|
| 148 | GMT = %x47.4D.54 ; "GMT", case-sensitive
|
---|
| 149 |
|
---|
| 150 | time-of-day = hour ":" minute ":" second
|
---|
| 151 | ; 00:00:00 - 23:59:60 (leap second)
|
---|
| 152 |
|
---|
| 153 | hour = 2DIGIT
|
---|
| 154 | minute = 2DIGIT
|
---|
| 155 | second = 2DIGIT
|
---|
| 156 | */
|
---|
| 157 | function toIMFDate (date) {
|
---|
| 158 | if (typeof date === 'number') {
|
---|
| 159 | date = new Date(date)
|
---|
| 160 | }
|
---|
| 161 |
|
---|
| 162 | const days = [
|
---|
| 163 | 'Sun', 'Mon', 'Tue', 'Wed',
|
---|
| 164 | 'Thu', 'Fri', 'Sat'
|
---|
| 165 | ]
|
---|
| 166 |
|
---|
| 167 | const months = [
|
---|
| 168 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
---|
| 169 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
---|
| 170 | ]
|
---|
| 171 |
|
---|
| 172 | const dayName = days[date.getUTCDay()]
|
---|
| 173 | const day = date.getUTCDate().toString().padStart(2, '0')
|
---|
| 174 | const month = months[date.getUTCMonth()]
|
---|
| 175 | const year = date.getUTCFullYear()
|
---|
| 176 | const hour = date.getUTCHours().toString().padStart(2, '0')
|
---|
| 177 | const minute = date.getUTCMinutes().toString().padStart(2, '0')
|
---|
| 178 | const second = date.getUTCSeconds().toString().padStart(2, '0')
|
---|
| 179 |
|
---|
| 180 | return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT`
|
---|
| 181 | }
|
---|
| 182 |
|
---|
| 183 | /**
|
---|
| 184 | max-age-av = "Max-Age=" non-zero-digit *DIGIT
|
---|
| 185 | ; In practice, both expires-av and max-age-av
|
---|
| 186 | ; are limited to dates representable by the
|
---|
| 187 | ; user agent.
|
---|
| 188 | * @param {number} maxAge
|
---|
| 189 | */
|
---|
| 190 | function validateCookieMaxAge (maxAge) {
|
---|
| 191 | if (maxAge < 0) {
|
---|
| 192 | throw new Error('Invalid cookie max-age')
|
---|
| 193 | }
|
---|
| 194 | }
|
---|
| 195 |
|
---|
| 196 | /**
|
---|
| 197 | * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
|
---|
| 198 | * @param {import('./index').Cookie} cookie
|
---|
| 199 | */
|
---|
| 200 | function stringify (cookie) {
|
---|
| 201 | if (cookie.name.length === 0) {
|
---|
| 202 | return null
|
---|
| 203 | }
|
---|
| 204 |
|
---|
| 205 | validateCookieName(cookie.name)
|
---|
| 206 | validateCookieValue(cookie.value)
|
---|
| 207 |
|
---|
| 208 | const out = [`${cookie.name}=${cookie.value}`]
|
---|
| 209 |
|
---|
| 210 | // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
|
---|
| 211 | // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2
|
---|
| 212 | if (cookie.name.startsWith('__Secure-')) {
|
---|
| 213 | cookie.secure = true
|
---|
| 214 | }
|
---|
| 215 |
|
---|
| 216 | if (cookie.name.startsWith('__Host-')) {
|
---|
| 217 | cookie.secure = true
|
---|
| 218 | cookie.domain = null
|
---|
| 219 | cookie.path = '/'
|
---|
| 220 | }
|
---|
| 221 |
|
---|
| 222 | if (cookie.secure) {
|
---|
| 223 | out.push('Secure')
|
---|
| 224 | }
|
---|
| 225 |
|
---|
| 226 | if (cookie.httpOnly) {
|
---|
| 227 | out.push('HttpOnly')
|
---|
| 228 | }
|
---|
| 229 |
|
---|
| 230 | if (typeof cookie.maxAge === 'number') {
|
---|
| 231 | validateCookieMaxAge(cookie.maxAge)
|
---|
| 232 | out.push(`Max-Age=${cookie.maxAge}`)
|
---|
| 233 | }
|
---|
| 234 |
|
---|
| 235 | if (cookie.domain) {
|
---|
| 236 | validateCookieDomain(cookie.domain)
|
---|
| 237 | out.push(`Domain=${cookie.domain}`)
|
---|
| 238 | }
|
---|
| 239 |
|
---|
| 240 | if (cookie.path) {
|
---|
| 241 | validateCookiePath(cookie.path)
|
---|
| 242 | out.push(`Path=${cookie.path}`)
|
---|
| 243 | }
|
---|
| 244 |
|
---|
| 245 | if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
|
---|
| 246 | out.push(`Expires=${toIMFDate(cookie.expires)}`)
|
---|
| 247 | }
|
---|
| 248 |
|
---|
| 249 | if (cookie.sameSite) {
|
---|
| 250 | out.push(`SameSite=${cookie.sameSite}`)
|
---|
| 251 | }
|
---|
| 252 |
|
---|
| 253 | for (const part of cookie.unparsed) {
|
---|
| 254 | if (!part.includes('=')) {
|
---|
| 255 | throw new Error('Invalid unparsed')
|
---|
| 256 | }
|
---|
| 257 |
|
---|
| 258 | const [key, ...value] = part.split('=')
|
---|
| 259 |
|
---|
| 260 | out.push(`${key.trim()}=${value.join('=')}`)
|
---|
| 261 | }
|
---|
| 262 |
|
---|
| 263 | return out.join('; ')
|
---|
| 264 | }
|
---|
| 265 |
|
---|
| 266 | let kHeadersListNode
|
---|
| 267 |
|
---|
| 268 | function getHeadersList (headers) {
|
---|
| 269 | if (headers[kHeadersList]) {
|
---|
| 270 | return headers[kHeadersList]
|
---|
| 271 | }
|
---|
| 272 |
|
---|
| 273 | if (!kHeadersListNode) {
|
---|
| 274 | kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
|
---|
| 275 | (symbol) => symbol.description === 'headers list'
|
---|
| 276 | )
|
---|
| 277 |
|
---|
| 278 | assert(kHeadersListNode, 'Headers cannot be parsed')
|
---|
| 279 | }
|
---|
| 280 |
|
---|
| 281 | const headersList = headers[kHeadersListNode]
|
---|
| 282 | assert(headersList)
|
---|
| 283 |
|
---|
| 284 | return headersList
|
---|
| 285 | }
|
---|
| 286 |
|
---|
| 287 | module.exports = {
|
---|
| 288 | isCTLExcludingHtab,
|
---|
| 289 | stringify,
|
---|
| 290 | getHeadersList
|
---|
| 291 | }
|
---|