| 'use strict'; |
| |
| // module to handle cookies |
| |
| const urllib = require('url'); |
| |
| const SESSION_TIMEOUT = 1800; // 30 min |
| |
| /** |
| * Creates a biskviit cookie jar for managing cookie values in memory |
| * |
| * @constructor |
| * @param {Object} [options] Optional options object |
| */ |
| class Cookies { |
| constructor(options) { |
| this.options = options || {}; |
| this.cookies = []; |
| } |
| |
| /** |
| * Stores a cookie string to the cookie storage |
| * |
| * @param {String} cookieStr Value from the 'Set-Cookie:' header |
| * @param {String} url Current URL |
| */ |
| set(cookieStr, url) { |
| let urlparts = urllib.parse(url || ''); |
| let cookie = this.parse(cookieStr); |
| let domain; |
| |
| if (cookie.domain) { |
| domain = cookie.domain.replace(/^\./, ''); |
| |
| // do not allow cross origin cookies |
| if ( |
| // can't be valid if the requested domain is shorter than current hostname |
| urlparts.hostname.length < domain.length || |
| |
| // prefix domains with dot to be sure that partial matches are not used |
| ('.' + urlparts.hostname).substr(-domain.length + 1) !== ('.' + domain)) { |
| cookie.domain = urlparts.hostname; |
| } |
| } else { |
| cookie.domain = urlparts.hostname; |
| } |
| |
| if (!cookie.path) { |
| cookie.path = this.getPath(urlparts.pathname); |
| } |
| |
| // if no expire date, then use sessionTimeout value |
| if (!cookie.expires) { |
| cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000); |
| } |
| |
| return this.add(cookie); |
| } |
| |
| /** |
| * Returns cookie string for the 'Cookie:' header. |
| * |
| * @param {String} url URL to check for |
| * @returns {String} Cookie header or empty string if no matches were found |
| */ |
| get(url) { |
| return this.list(url).map(cookie => |
| cookie.name + '=' + cookie.value).join('; '); |
| } |
| |
| /** |
| * Lists all valied cookie objects for the specified URL |
| * |
| * @param {String} url URL to check for |
| * @returns {Array} An array of cookie objects |
| */ |
| list(url) { |
| let result = []; |
| let i; |
| let cookie; |
| |
| for (i = this.cookies.length - 1; i >= 0; i--) { |
| cookie = this.cookies[i]; |
| |
| if (this.isExpired(cookie)) { |
| this.cookies.splice(i, i); |
| continue; |
| } |
| |
| if (this.match(cookie, url)) { |
| result.unshift(cookie); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Parses cookie string from the 'Set-Cookie:' header |
| * |
| * @param {String} cookieStr String from the 'Set-Cookie:' header |
| * @returns {Object} Cookie object |
| */ |
| parse(cookieStr) { |
| let cookie = {}; |
| |
| (cookieStr || '').toString().split(';').forEach(cookiePart => { |
| let valueParts = cookiePart.split('='); |
| let key = valueParts.shift().trim().toLowerCase(); |
| let value = valueParts.join('=').trim(); |
| let domain; |
| |
| if (!key) { |
| // skip empty parts |
| return; |
| } |
| |
| switch (key) { |
| |
| case 'expires': |
| value = new Date(value); |
| // ignore date if can not parse it |
| if (value.toString() !== 'Invalid Date') { |
| cookie.expires = value; |
| } |
| break; |
| |
| case 'path': |
| cookie.path = value; |
| break; |
| |
| case 'domain': |
| domain = value.toLowerCase(); |
| if (domain.length && domain.charAt(0) !== '.') { |
| domain = '.' + domain; // ensure preceeding dot for user set domains |
| } |
| cookie.domain = domain; |
| break; |
| |
| case 'max-age': |
| cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000); |
| break; |
| |
| case 'secure': |
| cookie.secure = true; |
| break; |
| |
| case 'httponly': |
| cookie.httponly = true; |
| break; |
| |
| default: |
| if (!cookie.name) { |
| cookie.name = key; |
| cookie.value = value; |
| } |
| } |
| }); |
| |
| return cookie; |
| } |
| |
| /** |
| * Checks if a cookie object is valid for a specified URL |
| * |
| * @param {Object} cookie Cookie object |
| * @param {String} url URL to check for |
| * @returns {Boolean} true if cookie is valid for specifiec URL |
| */ |
| match(cookie, url) { |
| let urlparts = urllib.parse(url || ''); |
| |
| // check if hostname matches |
| // .foo.com also matches subdomains, foo.com does not |
| if (urlparts.hostname !== cookie.domain && (cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) { |
| return false; |
| } |
| |
| // check if path matches |
| let path = this.getPath(urlparts.pathname); |
| if (path.substr(0, cookie.path.length) !== cookie.path) { |
| return false; |
| } |
| |
| // check secure argument |
| if (cookie.secure && urlparts.protocol !== 'https:') { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Adds (or updates/removes if needed) a cookie object to the cookie storage |
| * |
| * @param {Object} cookie Cookie value to be stored |
| */ |
| add(cookie) { |
| let i; |
| let len; |
| |
| // nothing to do here |
| if (!cookie || !cookie.name) { |
| return false; |
| } |
| |
| // overwrite if has same params |
| for (i = 0, len = this.cookies.length; i < len; i++) { |
| if (this.compare(this.cookies[i], cookie)) { |
| |
| // check if the cookie needs to be removed instead |
| if (this.isExpired(cookie)) { |
| this.cookies.splice(i, 1); // remove expired/unset cookie |
| return false; |
| } |
| |
| this.cookies[i] = cookie; |
| return true; |
| } |
| } |
| |
| // add as new if not already expired |
| if (!this.isExpired(cookie)) { |
| this.cookies.push(cookie); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Checks if two cookie objects are the same |
| * |
| * @param {Object} a Cookie to check against |
| * @param {Object} b Cookie to check against |
| * @returns {Boolean} True, if the cookies are the same |
| */ |
| compare(a, b) { |
| return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === a.httponly; |
| } |
| |
| /** |
| * Checks if a cookie is expired |
| * |
| * @param {Object} cookie Cookie object to check against |
| * @returns {Boolean} True, if the cookie is expired |
| */ |
| isExpired(cookie) { |
| return (cookie.expires && cookie.expires < new Date()) || !cookie.value; |
| } |
| |
| /** |
| * Returns normalized cookie path for an URL path argument |
| * |
| * @param {String} pathname |
| * @returns {String} Normalized path |
| */ |
| getPath(pathname) { |
| let path = (pathname || '/').split('/'); |
| path.pop(); // remove filename part |
| path = path.join('/').trim(); |
| |
| // ensure path prefix / |
| if (path.charAt(0) !== '/') { |
| path = '/' + path; |
| } |
| |
| // ensure path suffix / |
| if (path.substr(-1) !== '/') { |
| path += '/'; |
| } |
| |
| return path; |
| } |
| } |
| |
| module.exports = Cookies; |