index.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /*!
  2. * cookies
  3. * Copyright(c) 2014 Jed Schmidt, http://jed.is/
  4. * Copyright(c) 2015-2016 Douglas Christopher Wilson
  5. * MIT Licensed
  6. */
  7. 'use strict'
  8. var deprecate = require('depd')('cookies')
  9. var Keygrip = require('keygrip')
  10. var http = require('http')
  11. /**
  12. * RegExp to match field-content in RFC 7230 sec 3.2
  13. *
  14. * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
  15. * field-vchar = VCHAR / obs-text
  16. * obs-text = %x80-FF
  17. */
  18. var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
  19. /**
  20. * RegExp to match Priority cookie attribute value.
  21. */
  22. var PRIORITY_REGEXP = /^(?:low|medium|high)$/i
  23. /**
  24. * Cache for generated name regular expressions.
  25. */
  26. var REGEXP_CACHE = Object.create(null)
  27. /**
  28. * RegExp to match all characters to escape in a RegExp.
  29. */
  30. var REGEXP_ESCAPE_CHARS_REGEXP = /[\^$\\.*+?()[\]{}|]/g
  31. /**
  32. * RegExp to match basic restricted name characters for loose validation.
  33. */
  34. var RESTRICTED_NAME_CHARS_REGEXP = /[;=]/
  35. /**
  36. * RegExp to match basic restricted value characters for loose validation.
  37. */
  38. var RESTRICTED_VALUE_CHARS_REGEXP = /[;]/
  39. /**
  40. * RegExp to match Same-Site cookie attribute value.
  41. */
  42. var SAME_SITE_REGEXP = /^(?:lax|none|strict)$/i
  43. function Cookies(request, response, options) {
  44. if (!(this instanceof Cookies)) return new Cookies(request, response, options)
  45. this.secure = undefined
  46. this.request = request
  47. this.response = response
  48. if (options) {
  49. if (Array.isArray(options)) {
  50. // array of key strings
  51. deprecate('"keys" argument; provide using options {"keys": [...]}')
  52. this.keys = new Keygrip(options)
  53. } else if (options.constructor && options.constructor.name === 'Keygrip') {
  54. // any keygrip constructor to allow different versions
  55. deprecate('"keys" argument; provide using options {"keys": keygrip}')
  56. this.keys = options
  57. } else {
  58. this.keys = Array.isArray(options.keys) ? new Keygrip(options.keys) : options.keys
  59. this.secure = options.secure
  60. }
  61. }
  62. }
  63. Cookies.prototype.get = function(name, opts) {
  64. var sigName = name + ".sig"
  65. , header, match, value, remote, data, index
  66. , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys
  67. header = this.request.headers["cookie"]
  68. if (!header) return
  69. match = header.match(getPattern(name))
  70. if (!match) return
  71. value = match[1]
  72. if (value[0] === '"') value = value.slice(1, -1)
  73. if (!opts || !signed) return value
  74. remote = this.get(sigName)
  75. if (!remote) return
  76. data = name + "=" + value
  77. if (!this.keys) throw new Error('.keys required for signed cookies');
  78. index = this.keys.index(data, remote)
  79. if (index < 0) {
  80. this.set(sigName, null, {path: "/", signed: false })
  81. } else {
  82. index && this.set(sigName, this.keys.sign(data), { signed: false })
  83. return value
  84. }
  85. };
  86. Cookies.prototype.set = function(name, value, opts) {
  87. var res = this.response
  88. , req = this.request
  89. , headers = res.getHeader("Set-Cookie") || []
  90. , cookie = new Cookie(name, value, opts)
  91. , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys
  92. var secure = this.secure === undefined
  93. ? req.protocol === 'https' || isRequestEncrypted(req)
  94. : Boolean(this.secure)
  95. if (typeof headers == "string") headers = [headers]
  96. if (!secure && opts && opts.secure) {
  97. throw new Error('Cannot send secure cookie over unencrypted connection')
  98. }
  99. cookie.secure = opts && opts.secure !== undefined
  100. ? opts.secure
  101. : secure
  102. if (opts && "secureProxy" in opts) {
  103. deprecate('"secureProxy" option; use "secure" option, provide "secure" to constructor if needed')
  104. cookie.secure = opts.secureProxy
  105. }
  106. pushCookie(headers, cookie)
  107. if (opts && signed) {
  108. if (!this.keys) throw new Error('.keys required for signed cookies');
  109. cookie.value = this.keys.sign(cookie.toString())
  110. cookie.name += ".sig"
  111. pushCookie(headers, cookie)
  112. }
  113. var setHeader = res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader
  114. setHeader.call(res, 'Set-Cookie', headers)
  115. return this
  116. };
  117. function Cookie(name, value, attrs) {
  118. if (!fieldContentRegExp.test(name) || RESTRICTED_NAME_CHARS_REGEXP.test(name)) {
  119. throw new TypeError('argument name is invalid');
  120. }
  121. if (value && (!fieldContentRegExp.test(value) || RESTRICTED_VALUE_CHARS_REGEXP.test(value))) {
  122. throw new TypeError('argument value is invalid');
  123. }
  124. this.name = name
  125. this.value = value || ""
  126. for (var name in attrs) {
  127. this[name] = attrs[name]
  128. }
  129. if (!this.value) {
  130. this.expires = new Date(0)
  131. this.maxAge = null
  132. }
  133. if (this.path && !fieldContentRegExp.test(this.path)) {
  134. throw new TypeError('option path is invalid');
  135. }
  136. if (this.domain && !fieldContentRegExp.test(this.domain)) {
  137. throw new TypeError('option domain is invalid');
  138. }
  139. if (typeof this.maxAge === 'number' ? (isNaN(this.maxAge) || !isFinite(this.maxAge)) : this.maxAge) {
  140. throw new TypeError('option maxAge is invalid')
  141. }
  142. if (this.priority && !PRIORITY_REGEXP.test(this.priority)) {
  143. throw new TypeError('option priority is invalid')
  144. }
  145. if (this.sameSite && this.sameSite !== true && !SAME_SITE_REGEXP.test(this.sameSite)) {
  146. throw new TypeError('option sameSite is invalid')
  147. }
  148. }
  149. Cookie.prototype.path = "/";
  150. Cookie.prototype.expires = undefined;
  151. Cookie.prototype.domain = undefined;
  152. Cookie.prototype.httpOnly = true;
  153. Cookie.prototype.partitioned = false
  154. Cookie.prototype.priority = undefined
  155. Cookie.prototype.sameSite = false;
  156. Cookie.prototype.secure = false;
  157. Cookie.prototype.overwrite = false;
  158. Cookie.prototype.toString = function() {
  159. return this.name + "=" + this.value
  160. };
  161. Cookie.prototype.toHeader = function() {
  162. var header = this.toString()
  163. if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge);
  164. if (this.path ) header += "; path=" + this.path
  165. if (this.expires ) header += "; expires=" + this.expires.toUTCString()
  166. if (this.domain ) header += "; domain=" + this.domain
  167. if (this.priority ) header += "; priority=" + this.priority.toLowerCase()
  168. if (this.sameSite ) header += "; samesite=" + (this.sameSite === true ? 'strict' : this.sameSite.toLowerCase())
  169. if (this.secure ) header += "; secure"
  170. if (this.httpOnly ) header += "; httponly"
  171. if (this.partitioned) header += '; partitioned'
  172. return header
  173. };
  174. // back-compat so maxage mirrors maxAge
  175. Object.defineProperty(Cookie.prototype, 'maxage', {
  176. configurable: true,
  177. enumerable: true,
  178. get: function () { return this.maxAge },
  179. set: function (val) { return this.maxAge = val }
  180. });
  181. deprecate.property(Cookie.prototype, 'maxage', '"maxage"; use "maxAge" instead')
  182. /**
  183. * Get the pattern to search for a cookie in a string.
  184. * @param {string} name
  185. * @private
  186. */
  187. function getPattern (name) {
  188. if (!REGEXP_CACHE[name]) {
  189. REGEXP_CACHE[name] = new RegExp(
  190. '(?:^|;) *' +
  191. name.replace(REGEXP_ESCAPE_CHARS_REGEXP, '\\$&') +
  192. '=([^;]*)'
  193. )
  194. }
  195. return REGEXP_CACHE[name]
  196. }
  197. /**
  198. * Get the encrypted status for a request.
  199. *
  200. * @param {object} req
  201. * @return {string}
  202. * @private
  203. */
  204. function isRequestEncrypted (req) {
  205. return req.socket
  206. ? req.socket.encrypted
  207. : req.connection.encrypted
  208. }
  209. function pushCookie(headers, cookie) {
  210. if (cookie.overwrite) {
  211. for (var i = headers.length - 1; i >= 0; i--) {
  212. if (headers[i].indexOf(cookie.name + '=') === 0) {
  213. headers.splice(i, 1)
  214. }
  215. }
  216. }
  217. headers.push(cookie.toHeader())
  218. }
  219. Cookies.connect = Cookies.express = function(keys) {
  220. return function(req, res, next) {
  221. req.cookies = res.cookies = new Cookies(req, res, {
  222. keys: keys
  223. })
  224. next()
  225. }
  226. }
  227. Cookies.Cookie = Cookie
  228. module.exports = Cookies