cookies.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. 'use strict';
  2. // module to handle cookies
  3. const urllib = require('url');
  4. const SESSION_TIMEOUT = 1800; // 30 min
  5. /**
  6. * Creates a biskviit cookie jar for managing cookie values in memory
  7. *
  8. * @constructor
  9. * @param {Object} [options] Optional options object
  10. */
  11. class Cookies {
  12. constructor(options) {
  13. this.options = options || {};
  14. this.cookies = [];
  15. }
  16. /**
  17. * Stores a cookie string to the cookie storage
  18. *
  19. * @param {String} cookieStr Value from the 'Set-Cookie:' header
  20. * @param {String} url Current URL
  21. */
  22. set(cookieStr, url) {
  23. let urlparts = urllib.parse(url || '');
  24. let cookie = this.parse(cookieStr);
  25. let domain;
  26. if (cookie.domain) {
  27. domain = cookie.domain.replace(/^\./, '');
  28. // do not allow cross origin cookies
  29. if (
  30. // can't be valid if the requested domain is shorter than current hostname
  31. urlparts.hostname.length < domain.length ||
  32. // prefix domains with dot to be sure that partial matches are not used
  33. ('.' + urlparts.hostname).substr(-domain.length + 1) !== '.' + domain
  34. ) {
  35. cookie.domain = urlparts.hostname;
  36. }
  37. } else {
  38. cookie.domain = urlparts.hostname;
  39. }
  40. if (!cookie.path) {
  41. cookie.path = this.getPath(urlparts.pathname);
  42. }
  43. // if no expire date, then use sessionTimeout value
  44. if (!cookie.expires) {
  45. cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000);
  46. }
  47. return this.add(cookie);
  48. }
  49. /**
  50. * Returns cookie string for the 'Cookie:' header.
  51. *
  52. * @param {String} url URL to check for
  53. * @returns {String} Cookie header or empty string if no matches were found
  54. */
  55. get(url) {
  56. return this.list(url)
  57. .map(cookie => cookie.name + '=' + cookie.value)
  58. .join('; ');
  59. }
  60. /**
  61. * Lists all valied cookie objects for the specified URL
  62. *
  63. * @param {String} url URL to check for
  64. * @returns {Array} An array of cookie objects
  65. */
  66. list(url) {
  67. let result = [];
  68. let i;
  69. let cookie;
  70. for (i = this.cookies.length - 1; i >= 0; i--) {
  71. cookie = this.cookies[i];
  72. if (this.isExpired(cookie)) {
  73. this.cookies.splice(i, i);
  74. continue;
  75. }
  76. if (this.match(cookie, url)) {
  77. result.unshift(cookie);
  78. }
  79. }
  80. return result;
  81. }
  82. /**
  83. * Parses cookie string from the 'Set-Cookie:' header
  84. *
  85. * @param {String} cookieStr String from the 'Set-Cookie:' header
  86. * @returns {Object} Cookie object
  87. */
  88. parse(cookieStr) {
  89. let cookie = {};
  90. (cookieStr || '')
  91. .toString()
  92. .split(';')
  93. .forEach(cookiePart => {
  94. let valueParts = cookiePart.split('=');
  95. let key = valueParts
  96. .shift()
  97. .trim()
  98. .toLowerCase();
  99. let value = valueParts.join('=').trim();
  100. let domain;
  101. if (!key) {
  102. // skip empty parts
  103. return;
  104. }
  105. switch (key) {
  106. case 'expires':
  107. value = new Date(value);
  108. // ignore date if can not parse it
  109. if (value.toString() !== 'Invalid Date') {
  110. cookie.expires = value;
  111. }
  112. break;
  113. case 'path':
  114. cookie.path = value;
  115. break;
  116. case 'domain':
  117. domain = value.toLowerCase();
  118. if (domain.length && domain.charAt(0) !== '.') {
  119. domain = '.' + domain; // ensure preceeding dot for user set domains
  120. }
  121. cookie.domain = domain;
  122. break;
  123. case 'max-age':
  124. cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
  125. break;
  126. case 'secure':
  127. cookie.secure = true;
  128. break;
  129. case 'httponly':
  130. cookie.httponly = true;
  131. break;
  132. default:
  133. if (!cookie.name) {
  134. cookie.name = key;
  135. cookie.value = value;
  136. }
  137. }
  138. });
  139. return cookie;
  140. }
  141. /**
  142. * Checks if a cookie object is valid for a specified URL
  143. *
  144. * @param {Object} cookie Cookie object
  145. * @param {String} url URL to check for
  146. * @returns {Boolean} true if cookie is valid for specifiec URL
  147. */
  148. match(cookie, url) {
  149. let urlparts = urllib.parse(url || '');
  150. // check if hostname matches
  151. // .foo.com also matches subdomains, foo.com does not
  152. if (
  153. urlparts.hostname !== cookie.domain &&
  154. (cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)
  155. ) {
  156. return false;
  157. }
  158. // check if path matches
  159. let path = this.getPath(urlparts.pathname);
  160. if (path.substr(0, cookie.path.length) !== cookie.path) {
  161. return false;
  162. }
  163. // check secure argument
  164. if (cookie.secure && urlparts.protocol !== 'https:') {
  165. return false;
  166. }
  167. return true;
  168. }
  169. /**
  170. * Adds (or updates/removes if needed) a cookie object to the cookie storage
  171. *
  172. * @param {Object} cookie Cookie value to be stored
  173. */
  174. add(cookie) {
  175. let i;
  176. let len;
  177. // nothing to do here
  178. if (!cookie || !cookie.name) {
  179. return false;
  180. }
  181. // overwrite if has same params
  182. for (i = 0, len = this.cookies.length; i < len; i++) {
  183. if (this.compare(this.cookies[i], cookie)) {
  184. // check if the cookie needs to be removed instead
  185. if (this.isExpired(cookie)) {
  186. this.cookies.splice(i, 1); // remove expired/unset cookie
  187. return false;
  188. }
  189. this.cookies[i] = cookie;
  190. return true;
  191. }
  192. }
  193. // add as new if not already expired
  194. if (!this.isExpired(cookie)) {
  195. this.cookies.push(cookie);
  196. }
  197. return true;
  198. }
  199. /**
  200. * Checks if two cookie objects are the same
  201. *
  202. * @param {Object} a Cookie to check against
  203. * @param {Object} b Cookie to check against
  204. * @returns {Boolean} True, if the cookies are the same
  205. */
  206. compare(a, b) {
  207. return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === a.httponly;
  208. }
  209. /**
  210. * Checks if a cookie is expired
  211. *
  212. * @param {Object} cookie Cookie object to check against
  213. * @returns {Boolean} True, if the cookie is expired
  214. */
  215. isExpired(cookie) {
  216. return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
  217. }
  218. /**
  219. * Returns normalized cookie path for an URL path argument
  220. *
  221. * @param {String} pathname
  222. * @returns {String} Normalized path
  223. */
  224. getPath(pathname) {
  225. let path = (pathname || '/').split('/');
  226. path.pop(); // remove filename part
  227. path = path.join('/').trim();
  228. // ensure path prefix /
  229. if (path.charAt(0) !== '/') {
  230. path = '/' + path;
  231. }
  232. // ensure path suffix /
  233. if (path.substr(-1) !== '/') {
  234. path += '/';
  235. }
  236. return path;
  237. }
  238. }
  239. module.exports = Cookies;