1'use strict' 2 3const assert = require('assert') 4const { kHeadersList } = require('../core/symbols') 5 6function 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 */ 33function 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 */ 70function 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 */ 91function 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 */ 106function 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 */ 157function 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 */ 190function 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 */ 200function 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 266let kHeadersListNode 267 268function 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 287module.exports = { 288 isCTLExcludingHtab, 289 stringify, 290 getHeadersList 291} 292