util.js 7.2 KB

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