1'use strict' 2 3const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants') 4const { isCTLExcludingHtab } = require('./util') 5const { collectASequenceOfCodePointsFast } = require('../fetch/dataURL') 6const assert = require('assert') 7 8/** 9 * @description Parses the field-value attributes of a set-cookie header string. 10 * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 11 * @param {string} header 12 * @returns if the header is invalid, null will be returned 13 */ 14function parseSetCookie (header) { 15 // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F 16 // character (CTL characters excluding HTAB): Abort these steps and 17 // ignore the set-cookie-string entirely. 18 if (isCTLExcludingHtab(header)) { 19 return null 20 } 21 22 let nameValuePair = '' 23 let unparsedAttributes = '' 24 let name = '' 25 let value = '' 26 27 // 2. If the set-cookie-string contains a %x3B (";") character: 28 if (header.includes(';')) { 29 // 1. The name-value-pair string consists of the characters up to, 30 // but not including, the first %x3B (";"), and the unparsed- 31 // attributes consist of the remainder of the set-cookie-string 32 // (including the %x3B (";") in question). 33 const position = { position: 0 } 34 35 nameValuePair = collectASequenceOfCodePointsFast(';', header, position) 36 unparsedAttributes = header.slice(position.position) 37 } else { 38 // Otherwise: 39 40 // 1. The name-value-pair string consists of all the characters 41 // contained in the set-cookie-string, and the unparsed- 42 // attributes is the empty string. 43 nameValuePair = header 44 } 45 46 // 3. If the name-value-pair string lacks a %x3D ("=") character, then 47 // the name string is empty, and the value string is the value of 48 // name-value-pair. 49 if (!nameValuePair.includes('=')) { 50 value = nameValuePair 51 } else { 52 // Otherwise, the name string consists of the characters up to, but 53 // not including, the first %x3D ("=") character, and the (possibly 54 // empty) value string consists of the characters after the first 55 // %x3D ("=") character. 56 const position = { position: 0 } 57 name = collectASequenceOfCodePointsFast( 58 '=', 59 nameValuePair, 60 position 61 ) 62 value = nameValuePair.slice(position.position + 1) 63 } 64 65 // 4. Remove any leading or trailing WSP characters from the name 66 // string and the value string. 67 name = name.trim() 68 value = value.trim() 69 70 // 5. If the sum of the lengths of the name string and the value string 71 // is more than 4096 octets, abort these steps and ignore the set- 72 // cookie-string entirely. 73 if (name.length + value.length > maxNameValuePairSize) { 74 return null 75 } 76 77 // 6. The cookie-name is the name string, and the cookie-value is the 78 // value string. 79 return { 80 name, value, ...parseUnparsedAttributes(unparsedAttributes) 81 } 82} 83 84/** 85 * Parses the remaining attributes of a set-cookie header 86 * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 87 * @param {string} unparsedAttributes 88 * @param {[Object.<string, unknown>]={}} cookieAttributeList 89 */ 90function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) { 91 // 1. If the unparsed-attributes string is empty, skip the rest of 92 // these steps. 93 if (unparsedAttributes.length === 0) { 94 return cookieAttributeList 95 } 96 97 // 2. Discard the first character of the unparsed-attributes (which 98 // will be a %x3B (";") character). 99 assert(unparsedAttributes[0] === ';') 100 unparsedAttributes = unparsedAttributes.slice(1) 101 102 let cookieAv = '' 103 104 // 3. If the remaining unparsed-attributes contains a %x3B (";") 105 // character: 106 if (unparsedAttributes.includes(';')) { 107 // 1. Consume the characters of the unparsed-attributes up to, but 108 // not including, the first %x3B (";") character. 109 cookieAv = collectASequenceOfCodePointsFast( 110 ';', 111 unparsedAttributes, 112 { position: 0 } 113 ) 114 unparsedAttributes = unparsedAttributes.slice(cookieAv.length) 115 } else { 116 // Otherwise: 117 118 // 1. Consume the remainder of the unparsed-attributes. 119 cookieAv = unparsedAttributes 120 unparsedAttributes = '' 121 } 122 123 // Let the cookie-av string be the characters consumed in this step. 124 125 let attributeName = '' 126 let attributeValue = '' 127 128 // 4. If the cookie-av string contains a %x3D ("=") character: 129 if (cookieAv.includes('=')) { 130 // 1. The (possibly empty) attribute-name string consists of the 131 // characters up to, but not including, the first %x3D ("=") 132 // character, and the (possibly empty) attribute-value string 133 // consists of the characters after the first %x3D ("=") 134 // character. 135 const position = { position: 0 } 136 137 attributeName = collectASequenceOfCodePointsFast( 138 '=', 139 cookieAv, 140 position 141 ) 142 attributeValue = cookieAv.slice(position.position + 1) 143 } else { 144 // Otherwise: 145 146 // 1. The attribute-name string consists of the entire cookie-av 147 // string, and the attribute-value string is empty. 148 attributeName = cookieAv 149 } 150 151 // 5. Remove any leading or trailing WSP characters from the attribute- 152 // name string and the attribute-value string. 153 attributeName = attributeName.trim() 154 attributeValue = attributeValue.trim() 155 156 // 6. If the attribute-value is longer than 1024 octets, ignore the 157 // cookie-av string and return to Step 1 of this algorithm. 158 if (attributeValue.length > maxAttributeValueSize) { 159 return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) 160 } 161 162 // 7. Process the attribute-name and attribute-value according to the 163 // requirements in the following subsections. (Notice that 164 // attributes with unrecognized attribute-names are ignored.) 165 const attributeNameLowercase = attributeName.toLowerCase() 166 167 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 168 // If the attribute-name case-insensitively matches the string 169 // "Expires", the user agent MUST process the cookie-av as follows. 170 if (attributeNameLowercase === 'expires') { 171 // 1. Let the expiry-time be the result of parsing the attribute-value 172 // as cookie-date (see Section 5.1.1). 173 const expiryTime = new Date(attributeValue) 174 175 // 2. If the attribute-value failed to parse as a cookie date, ignore 176 // the cookie-av. 177 178 cookieAttributeList.expires = expiryTime 179 } else if (attributeNameLowercase === 'max-age') { 180 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 181 // If the attribute-name case-insensitively matches the string "Max- 182 // Age", the user agent MUST process the cookie-av as follows. 183 184 // 1. If the first character of the attribute-value is not a DIGIT or a 185 // "-" character, ignore the cookie-av. 186 const charCode = attributeValue.charCodeAt(0) 187 188 if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { 189 return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) 190 } 191 192 // 2. If the remainder of attribute-value contains a non-DIGIT 193 // character, ignore the cookie-av. 194 if (!/^\d+$/.test(attributeValue)) { 195 return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) 196 } 197 198 // 3. Let delta-seconds be the attribute-value converted to an integer. 199 const deltaSeconds = Number(attributeValue) 200 201 // 4. Let cookie-age-limit be the maximum age of the cookie (which 202 // SHOULD be 400 days or less, see Section 4.1.2.2). 203 204 // 5. Set delta-seconds to the smaller of its present value and cookie- 205 // age-limit. 206 // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) 207 208 // 6. If delta-seconds is less than or equal to zero (0), let expiry- 209 // time be the earliest representable date and time. Otherwise, let 210 // the expiry-time be the current date and time plus delta-seconds 211 // seconds. 212 // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds 213 214 // 7. Append an attribute to the cookie-attribute-list with an 215 // attribute-name of Max-Age and an attribute-value of expiry-time. 216 cookieAttributeList.maxAge = deltaSeconds 217 } else if (attributeNameLowercase === 'domain') { 218 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3 219 // If the attribute-name case-insensitively matches the string "Domain", 220 // the user agent MUST process the cookie-av as follows. 221 222 // 1. Let cookie-domain be the attribute-value. 223 let cookieDomain = attributeValue 224 225 // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be 226 // cookie-domain without its leading %x2E ("."). 227 if (cookieDomain[0] === '.') { 228 cookieDomain = cookieDomain.slice(1) 229 } 230 231 // 3. Convert the cookie-domain to lower case. 232 cookieDomain = cookieDomain.toLowerCase() 233 234 // 4. Append an attribute to the cookie-attribute-list with an 235 // attribute-name of Domain and an attribute-value of cookie-domain. 236 cookieAttributeList.domain = cookieDomain 237 } else if (attributeNameLowercase === 'path') { 238 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4 239 // If the attribute-name case-insensitively matches the string "Path", 240 // the user agent MUST process the cookie-av as follows. 241 242 // 1. If the attribute-value is empty or if the first character of the 243 // attribute-value is not %x2F ("/"): 244 let cookiePath = '' 245 if (attributeValue.length === 0 || attributeValue[0] !== '/') { 246 // 1. Let cookie-path be the default-path. 247 cookiePath = '/' 248 } else { 249 // Otherwise: 250 251 // 1. Let cookie-path be the attribute-value. 252 cookiePath = attributeValue 253 } 254 255 // 2. Append an attribute to the cookie-attribute-list with an 256 // attribute-name of Path and an attribute-value of cookie-path. 257 cookieAttributeList.path = cookiePath 258 } else if (attributeNameLowercase === 'secure') { 259 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5 260 // If the attribute-name case-insensitively matches the string "Secure", 261 // the user agent MUST append an attribute to the cookie-attribute-list 262 // with an attribute-name of Secure and an empty attribute-value. 263 264 cookieAttributeList.secure = true 265 } else if (attributeNameLowercase === 'httponly') { 266 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6 267 // If the attribute-name case-insensitively matches the string 268 // "HttpOnly", the user agent MUST append an attribute to the cookie- 269 // attribute-list with an attribute-name of HttpOnly and an empty 270 // attribute-value. 271 272 cookieAttributeList.httpOnly = true 273 } else if (attributeNameLowercase === 'samesite') { 274 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7 275 // If the attribute-name case-insensitively matches the string 276 // "SameSite", the user agent MUST process the cookie-av as follows: 277 278 // 1. Let enforcement be "Default". 279 let enforcement = 'Default' 280 281 const attributeValueLowercase = attributeValue.toLowerCase() 282 // 2. If cookie-av's attribute-value is a case-insensitive match for 283 // "None", set enforcement to "None". 284 if (attributeValueLowercase.includes('none')) { 285 enforcement = 'None' 286 } 287 288 // 3. If cookie-av's attribute-value is a case-insensitive match for 289 // "Strict", set enforcement to "Strict". 290 if (attributeValueLowercase.includes('strict')) { 291 enforcement = 'Strict' 292 } 293 294 // 4. If cookie-av's attribute-value is a case-insensitive match for 295 // "Lax", set enforcement to "Lax". 296 if (attributeValueLowercase.includes('lax')) { 297 enforcement = 'Lax' 298 } 299 300 // 5. Append an attribute to the cookie-attribute-list with an 301 // attribute-name of "SameSite" and an attribute-value of 302 // enforcement. 303 cookieAttributeList.sameSite = enforcement 304 } else { 305 cookieAttributeList.unparsed ??= [] 306 307 cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) 308 } 309 310 // 8. Return to Step 1 of this algorithm. 311 return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) 312} 313 314module.exports = { 315 parseSetCookie, 316 parseUnparsedAttributes 317} 318