index.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. var cache = {}
  12. /**
  13. * RegExp to match field-content in RFC 7230 sec 3.2
  14. *
  15. * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
  16. * field-vchar = VCHAR / obs-text
  17. * obs-text = %x80-FF
  18. */
  19. var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
  20. /**
  21. * RegExp to match Same-Site cookie attribute value.
  22. */
  23. var sameSiteRegExp = /^(?:lax|strict)$/i
  24. function Cookies(request, response, options) {
  25. if (!(this instanceof Cookies)) return new Cookies(request, response, options)
  26. this.secure = undefined
  27. this.request = request
  28. this.response = response
  29. if (options) {
  30. if (Array.isArray(options)) {
  31. // array of key strings
  32. deprecate('"keys" argument; provide using options {"keys": [...]}')
  33. this.keys = new Keygrip(options)
  34. } else if (options.constructor && options.constructor.name === 'Keygrip') {
  35. // any keygrip constructor to allow different versions
  36. deprecate('"keys" argument; provide using options {"keys": keygrip}')
  37. this.keys = options
  38. } else {
  39. this.keys = Array.isArray(options.keys) ? new Keygrip(options.keys) : options.keys
  40. this.secure = options.secure
  41. }
  42. }
  43. }
  44. Cookies.prototype.get = function(name, opts) {
  45. var sigName = name + ".sig"
  46. , header, match, value, remote, data, index
  47. , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys
  48. header = this.request.headers["cookie"]
  49. if (!header) return
  50. match = header.match(getPattern(name))
  51. if (!match) return
  52. value = match[1]
  53. if (!opts || !signed) return value
  54. remote = this.get(sigName)
  55. if (!remote) return
  56. data = name + "=" + value
  57. if (!this.keys) throw new Error('.keys required for signed cookies');
  58. index = this.keys.index(data, remote)
  59. if (index < 0) {
  60. this.set(sigName, null, {path: "/", signed: false })
  61. } else {
  62. index && this.set(sigName, this.keys.sign(data), { signed: false })
  63. return value
  64. }
  65. };
  66. Cookies.prototype.set = function(name, value, opts) {
  67. var res = this.response
  68. , req = this.request
  69. , headers = res.getHeader("Set-Cookie") || []
  70. , secure = this.secure !== undefined ? !!this.secure : req.protocol === 'https' || req.connection.encrypted
  71. , cookie = new Cookie(name, value, opts)
  72. , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys
  73. if (typeof headers == "string") headers = [headers]
  74. if (!secure && opts && opts.secure) {
  75. throw new Error('Cannot send secure cookie over unencrypted connection')
  76. }
  77. cookie.secure = secure
  78. if (opts && "secure" in opts) cookie.secure = opts.secure
  79. if (opts && "secureProxy" in opts) {
  80. deprecate('"secureProxy" option; use "secure" option, provide "secure" to constructor if needed')
  81. cookie.secure = opts.secureProxy
  82. }
  83. headers = pushCookie(headers, cookie)
  84. if (opts && signed) {
  85. if (!this.keys) throw new Error('.keys required for signed cookies');
  86. cookie.value = this.keys.sign(cookie.toString())
  87. cookie.name += ".sig"
  88. headers = pushCookie(headers, cookie)
  89. }
  90. var setHeader = res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader
  91. setHeader.call(res, 'Set-Cookie', headers)
  92. return this
  93. };
  94. function Cookie(name, value, attrs) {
  95. if (!fieldContentRegExp.test(name)) {
  96. throw new TypeError('argument name is invalid');
  97. }
  98. if (value && !fieldContentRegExp.test(value)) {
  99. throw new TypeError('argument value is invalid');
  100. }
  101. value || (this.expires = new Date(0))
  102. this.name = name
  103. this.value = value || ""
  104. for (var name in attrs) {
  105. this[name] = attrs[name]
  106. }
  107. if (this.path && !fieldContentRegExp.test(this.path)) {
  108. throw new TypeError('option path is invalid');
  109. }
  110. if (this.domain && !fieldContentRegExp.test(this.domain)) {
  111. throw new TypeError('option domain is invalid');
  112. }
  113. if (this.sameSite && this.sameSite !== true && !sameSiteRegExp.test(this.sameSite)) {
  114. throw new TypeError('option sameSite is invalid')
  115. }
  116. }
  117. Cookie.prototype.path = "/";
  118. Cookie.prototype.expires = undefined;
  119. Cookie.prototype.domain = undefined;
  120. Cookie.prototype.httpOnly = true;
  121. Cookie.prototype.sameSite = false;
  122. Cookie.prototype.secure = false;
  123. Cookie.prototype.overwrite = false;
  124. Cookie.prototype.toString = function() {
  125. return this.name + "=" + this.value
  126. };
  127. Cookie.prototype.toHeader = function() {
  128. var header = this.toString()
  129. if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge);
  130. if (this.path ) header += "; path=" + this.path
  131. if (this.expires ) header += "; expires=" + this.expires.toUTCString()
  132. if (this.domain ) header += "; domain=" + this.domain
  133. if (this.sameSite ) header += "; samesite=" + (this.sameSite === true ? 'strict' : this.sameSite.toLowerCase())
  134. if (this.secure ) header += "; secure"
  135. if (this.httpOnly ) header += "; httponly"
  136. return header
  137. };
  138. // back-compat so maxage mirrors maxAge
  139. Object.defineProperty(Cookie.prototype, 'maxage', {
  140. configurable: true,
  141. enumerable: true,
  142. get: function () { return this.maxAge },
  143. set: function (val) { return this.maxAge = val }
  144. });
  145. deprecate.property(Cookie.prototype, 'maxage', '"maxage"; use "maxAge" instead')
  146. function getPattern(name) {
  147. if (cache[name]) return cache[name]
  148. return cache[name] = new RegExp(
  149. "(?:^|;) *" +
  150. name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") +
  151. "=([^;]*)"
  152. )
  153. }
  154. function pushCookie(cookies, cookie) {
  155. if (cookie.overwrite) {
  156. cookies = cookies.filter(function(c) { return c.indexOf(cookie.name+'=') !== 0 })
  157. }
  158. cookies.push(cookie.toHeader())
  159. return cookies
  160. }
  161. Cookies.connect = Cookies.express = function(keys) {
  162. return function(req, res, next) {
  163. req.cookies = res.cookies = new Cookies(req, res, {
  164. keys: keys
  165. })
  166. next()
  167. }
  168. }
  169. Cookies.Cookie = Cookie
  170. module.exports = Cookies