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