util.js 7.2 KB

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