/* * Copyright GoInstant, Inc. and other contributors. All rights reserved. * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ 'use strict'; var net = require('net'); var urlParse = require('url').parse; var pubsuffix = require('./pubsuffix'); var Store = require('./store').Store; var punycode; try { punycode = require('punycode'); } catch(e) { console.warn("cookie: can't load punycode; won't use punycode for domain normalization"); } var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/; // From RFC2616 S2.2: var TOKEN = /[\x21\x23-\x26\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]/; // From RFC6265 S4.1.1 // note that it excludes \x3B ";" var COOKIE_OCTET = /[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/; var COOKIE_OCTETS = new RegExp('^'+COOKIE_OCTET.source+'$'); // The name/key cannot be empty but the value can (S5.2): var COOKIE_PAIR_STRICT = new RegExp('^('+TOKEN.source+'+)=("?)('+COOKIE_OCTET.source+'*)\\2$'); var COOKIE_PAIR = /^([^=\s]+)\s*=\s*("?)\s*(.*)\s*\2\s*$/; // RFC6265 S4.1.1 defines extension-av as 'any CHAR except CTLs or ";"' // Note ';' is \x3B var NON_CTL_SEMICOLON = /[\x20-\x3A\x3C-\x7E]+/; var EXTENSION_AV = NON_CTL_SEMICOLON; var PATH_VALUE = NON_CTL_SEMICOLON; // Used for checking whether or not there is a trailing semi-colon var TRAILING_SEMICOLON = /;+$/; /* RFC6265 S5.1.1.5: * [fail if] the day-of-month-value is less than 1 or greater than 31 */ var DAY_OF_MONTH = /^(0?[1-9]|[12][0-9]|3[01])$/; /* RFC6265 S5.1.1.5: * [fail if] * * the hour-value is greater than 23, * * the minute-value is greater than 59, or * * the second-value is greater than 59. */ var TIME = /(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/; var STRICT_TIME = /^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/; var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/i; var MONTH_TO_NUM = { jan:0, feb:1, mar:2, apr:3, may:4, jun:5, jul:6, aug:7, sep:8, oct:9, nov:10, dec:11 }; var NUM_TO_MONTH = [ 'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec' ]; var NUM_TO_DAY = [ 'Sun','Mon','Tue','Wed','Thu','Fri','Sat' ]; var YEAR = /^([1-9][0-9]{1,3})$/; // 2 to 4 digits var MAX_TIME = 2147483647000; // 31-bit max var MIN_TIME = 0; // 31-bit min // RFC6265 S5.1.1 date parser: function parseDate(str,strict) { if (!str) { return; } var found_time, found_dom, found_month, found_year; /* RFC6265 S5.1.1: * 2. Process each date-token sequentially in the order the date-tokens * appear in the cookie-date */ var tokens = str.split(DATE_DELIM); if (!tokens) { return; } var date = new Date(); date.setMilliseconds(0); for (var i=0; i= 10 ? d : '0'+d; var h = date.getUTCHours(); h = h >= 10 ? h : '0'+h; var m = date.getUTCMinutes(); m = m >= 10 ? m : '0'+m; var s = date.getUTCSeconds(); s = s >= 10 ? s : '0'+s; return NUM_TO_DAY[date.getUTCDay()] + ', ' + d+' '+ NUM_TO_MONTH[date.getUTCMonth()] +' '+ date.getUTCFullYear() +' '+ h+':'+m+':'+s+' GMT'; } // S5.1.2 Canonicalized Host Names function canonicalDomain(str) { if (str == null) { return null; } str = str.trim().replace(/^\./,''); // S4.1.2.3 & S5.2.3: ignore leading . // convert to IDN if any non-ASCII characters if (punycode && /[^\u0001-\u007f]/.test(str)) { str = punycode.toASCII(str); } return str.toLowerCase(); } // S5.1.3 Domain Matching function domainMatch(str, domStr, canonicalize) { if (str == null || domStr == null) { return null; } if (canonicalize !== false) { str = canonicalDomain(str); domStr = canonicalDomain(domStr); } /* * "The domain string and the string are identical. (Note that both the * domain string and the string will have been canonicalized to lower case at * this point)" */ if (str == domStr) { return true; } /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */ /* "* The string is a host name (i.e., not an IP address)." */ if (net.isIP(str)) { return false; } /* "* The domain string is a suffix of the string" */ var idx = str.indexOf(domStr); if (idx <= 0) { return false; // it's a non-match (-1) or prefix (0) } // e.g "a.b.c".indexOf("b.c") === 2 // 5 === 3+2 if (str.length !== domStr.length + idx) { // it's not a suffix return false; } /* "* The last character of the string that is not included in the domain * string is a %x2E (".") character." */ if (str.substr(idx-1,1) !== '.') { return false; } return true; } // RFC6265 S5.1.4 Paths and Path-Match /* * "The user agent MUST use an algorithm equivalent to the following algorithm * to compute the default-path of a cookie:" * * Assumption: the path (and not query part or absolute uri) is passed in. */ function defaultPath(path) { // "2. If the uri-path is empty or if the first character of the uri-path is not // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps. if (!path || path.substr(0,1) !== "/") { return "/"; } // "3. If the uri-path contains no more than one %x2F ("/") character, output // %x2F ("/") and skip the remaining step." if (path === "/") { return path; } var rightSlash = path.lastIndexOf("/"); if (rightSlash === 0) { return "/"; } // "4. Output the characters of the uri-path from the first character up to, // but not including, the right-most %x2F ("/")." return path.slice(0, rightSlash); } /* * "A request-path path-matches a given cookie-path if at least one of the * following conditions holds:" */ function pathMatch(reqPath,cookiePath) { // "o The cookie-path and the request-path are identical." if (cookiePath === reqPath) { return true; } var idx = reqPath.indexOf(cookiePath); if (idx === 0) { // "o The cookie-path is a prefix of the request-path, and the last // character of the cookie-path is %x2F ("/")." if (cookiePath.substr(-1) === "/") { return true; } // " o The cookie-path is a prefix of the request-path, and the first // character of the request-path that is not included in the cookie- path // is a %x2F ("/") character." if (reqPath.substr(cookiePath.length,1) === "/") { return true; } } return false; } function parse(str, strict) { str = str.trim(); // S4.1.1 Trailing semi-colons are not part of the specification. // If we are not in strict mode we remove the trailing semi-colons. var semiColonCheck = TRAILING_SEMICOLON.exec(str); if (semiColonCheck) { if (strict) { return; } str = str.slice(0, semiColonCheck.index); } // We use a regex to parse the "name-value-pair" part of S5.2 var firstSemi = str.indexOf(';'); // S5.2 step 1 var pairRx = strict ? COOKIE_PAIR_STRICT : COOKIE_PAIR; var result = pairRx.exec(firstSemi === -1 ? str : str.substr(0,firstSemi)); // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")" // constraints as well as trimming any whitespace. if (!result) { return; } var c = new Cookie(); c.key = result[1]; // the regexp should trim() already c.value = result[3]; // [2] is quotes or empty-string if (firstSemi === -1) { return c; } // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string // (including the %x3B (";") in question)." plus later on in the same section // "discard the first ";" and trim". var unparsed = str.slice(firstSemi).replace(/^\s*;\s*/,'').trim(); // "If the unparsed-attributes string is empty, skip the rest of these // steps." if (unparsed.length === 0) { return c; } /* * S5.2 says that when looping over the items "[p]rocess the attribute-name * and attribute-value according to the requirements in the following * subsections" for every item. Plus, for many of the individual attributes * in S5.3 it says to use the "attribute-value of the last attribute in the * cookie-attribute-list". Therefore, in this implementation, we overwrite * the previous value. */ var cookie_avs = unparsed.split(/\s*;\s*/); while (cookie_avs.length) { var av = cookie_avs.shift(); if (strict && !EXTENSION_AV.test(av)) { return; } var av_sep = av.indexOf('='); var av_key, av_value; if (av_sep === -1) { av_key = av; av_value = null; } else { av_key = av.substr(0,av_sep); av_value = av.substr(av_sep+1); } av_key = av_key.trim().toLowerCase(); if (av_value) { av_value = av_value.trim(); } switch(av_key) { case 'expires': // S5.2.1 if (!av_value) {if(strict){return;}else{break;} } var exp = parseDate(av_value,strict); // "If the attribute-value failed to parse as a cookie date, ignore the // cookie-av." if (exp == null) { if(strict){return;}else{break;} } c.expires = exp; // over and underflow not realistically a concern: V8's getTime() seems to // store something larger than a 32-bit time_t (even with 32-bit node) break; case 'max-age': // S5.2.2 if (!av_value) { if(strict){return;}else{break;} } // "If the first character of the attribute-value is not a DIGIT or a "-" // character ...[or]... If the remainder of attribute-value contains a // non-DIGIT character, ignore the cookie-av." if (!/^-?[0-9]+$/.test(av_value)) { if(strict){return;}else{break;} } var delta = parseInt(av_value,10); if (strict && delta <= 0) { return; // S4.1.1 } // "If delta-seconds is less than or equal to zero (0), let expiry-time // be the earliest representable date and time." c.setMaxAge(delta); break; case 'domain': // S5.2.3 // "If the attribute-value is empty, the behavior is undefined. However, // the user agent SHOULD ignore the cookie-av entirely." if (!av_value) { if(strict){return;}else{break;} } // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E // (".") character." var domain = av_value.trim().replace(/^\./,''); if (!domain) { if(strict){return;}else{break;} } // see "is empty" above // "Convert the cookie-domain to lower case." c.domain = domain.toLowerCase(); break; case 'path': // S5.2.4 /* * "If the attribute-value is empty or if the first character of the * attribute-value is not %x2F ("/"): * Let cookie-path be the default-path. * Otherwise: * Let cookie-path be the attribute-value." * * We'll represent the default-path as null since it depends on the * context of the parsing. */ if (!av_value || av_value.substr(0,1) != "/") { if(strict){return;}else{break;} } c.path = av_value; break; case 'secure': // S5.2.5 /* * "If the attribute-name case-insensitively matches the string "Secure", * the user agent MUST append an attribute to the cookie-attribute-list * with an attribute-name of Secure and an empty attribute-value." */ if (av_value != null) { if(strict){return;} } c.secure = true; break; case 'httponly': // S5.2.6 -- effectively the same as 'secure' if (av_value != null) { if(strict){return;} } c.httpOnly = true; break; default: c.extensions = c.extensions || []; c.extensions.push(av); break; } } // ensure a default date for sorting: c.creation = new Date(); return c; } function fromJSON(str) { if (!str) { return null; } var obj; try { obj = JSON.parse(str); } catch (e) { return null; } var c = new Cookie(); for (var i=0; i 1) { var lindex = path.lastIndexOf('/'); if (lindex === 0) { break; } path = path.substr(0,lindex); permutations.push(path); } permutations.push('/'); return permutations; } function Cookie (opts) { if (typeof opts !== "object") { return; } Object.keys(opts).forEach(function (key) { if (Cookie.prototype.hasOwnProperty(key)) { this[key] = opts[key] || Cookie.prototype[key]; } }.bind(this)); } Cookie.parse = parse; Cookie.fromJSON = fromJSON; Cookie.prototype.key = ""; Cookie.prototype.value = ""; // the order in which the RFC has them: Cookie.prototype.expires = "Infinity"; // coerces to literal Infinity Cookie.prototype.maxAge = null; // takes precedence over expires for TTL Cookie.prototype.domain = null; Cookie.prototype.path = null; Cookie.prototype.secure = false; Cookie.prototype.httpOnly = false; Cookie.prototype.extensions = null; // set by the CookieJar: Cookie.prototype.hostOnly = null; // boolean when set Cookie.prototype.pathIsDefault = null; // boolean when set Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse Cookie.prototype.lastAccessed = null; // Date when set var cookieProperties = Object.freeze(Object.keys(Cookie.prototype).map(function(p) { if (p instanceof Function) { return; } return p; })); var numCookieProperties = cookieProperties.length; Cookie.prototype.inspect = function inspect() { var now = Date.now(); return 'Cookie="'+this.toString() + '; hostOnly='+(this.hostOnly != null ? this.hostOnly : '?') + '; aAge='+(this.lastAccessed ? (now-this.lastAccessed.getTime())+'ms' : '?') + '; cAge='+(this.creation ? (now-this.creation.getTime())+'ms' : '?') + '"'; }; Cookie.prototype.validate = function validate() { if (!COOKIE_OCTETS.test(this.value)) { return false; } if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires,true)) { return false; } if (this.maxAge != null && this.maxAge <= 0) { return false; // "Max-Age=" non-zero-digit *DIGIT } if (this.path != null && !PATH_VALUE.test(this.path)) { return false; } var cdomain = this.cdomain(); if (cdomain) { if (cdomain.match(/\.$/)) { return false; // S4.1.2.3 suggests that this is bad. domainMatch() tests confirm this } var suffix = pubsuffix.getPublicSuffix(cdomain); if (suffix == null) { // it's a public suffix return false; } } return true; }; Cookie.prototype.setExpires = function setExpires(exp) { if (exp instanceof Date) { this.expires = exp; } else { this.expires = parseDate(exp) || "Infinity"; } }; Cookie.prototype.setMaxAge = function setMaxAge(age) { if (age === Infinity || age === -Infinity) { this.maxAge = age.toString(); // so JSON.stringify() works } else { this.maxAge = age; } }; // gives Cookie header format Cookie.prototype.cookieString = function cookieString() { var val = this.value; if (val == null) { val = ''; } return this.key+'='+val; }; // gives Set-Cookie header format Cookie.prototype.toString = function toString() { var str = this.cookieString(); if (this.expires != Infinity) { if (this.expires instanceof Date) { str += '; Expires='+formatDate(this.expires); } else { str += '; Expires='+this.expires; } } if (this.maxAge != null && this.maxAge != Infinity) { str += '; Max-Age='+this.maxAge; } if (this.domain && !this.hostOnly) { str += '; Domain='+this.domain; } if (this.path) { str += '; Path='+this.path; } if (this.secure) { str += '; Secure'; } if (this.httpOnly) { str += '; HttpOnly'; } if (this.extensions) { this.extensions.forEach(function(ext) { str += '; '+ext; }); } return str; }; // TTL() partially replaces the "expiry-time" parts of S5.3 step 3 (setCookie() // elsewhere) // S5.3 says to give the "latest representable date" for which we use Infinity // For "expired" we use 0 Cookie.prototype.TTL = function TTL(now) { /* RFC6265 S4.1.2.2 If a cookie has both the Max-Age and the Expires * attribute, the Max-Age attribute has precedence and controls the * expiration date of the cookie. * (Concurs with S5.3 step 3) */ if (this.maxAge != null) { return this.maxAge<=0 ? 0 : this.maxAge*1000; } var expires = this.expires; if (expires != Infinity) { if (!(expires instanceof Date)) { expires = parseDate(expires) || Infinity; } if (expires == Infinity) { return Infinity; } return expires.getTime() - (now || Date.now()); } return Infinity; }; // expiryTime() replaces the "expiry-time" parts of S5.3 step 3 (setCookie() // elsewhere) Cookie.prototype.expiryTime = function expiryTime(now) { if (this.maxAge != null) { var relativeTo = this.creation || now || new Date(); var age = (this.maxAge <= 0) ? -Infinity : this.maxAge*1000; return relativeTo.getTime() + age; } if (this.expires == Infinity) { return Infinity; } return this.expires.getTime(); }; // expiryDate() replaces the "expiry-time" parts of S5.3 step 3 (setCookie() // elsewhere), except it returns a Date Cookie.prototype.expiryDate = function expiryDate(now) { var millisec = this.expiryTime(now); if (millisec == Infinity) { return new Date(MAX_TIME); } else if (millisec == -Infinity) { return new Date(MIN_TIME); } else { return new Date(millisec); } }; // This replaces the "persistent-flag" parts of S5.3 step 3 Cookie.prototype.isPersistent = function isPersistent() { return (this.maxAge != null || this.expires != Infinity); }; // Mostly S5.1.2 and S5.2.3: Cookie.prototype.cdomain = Cookie.prototype.canonicalizedDomain = function canonicalizedDomain() { if (this.domain == null) { return null; } return canonicalDomain(this.domain); }; var memstore; function CookieJar(store, rejectPublicSuffixes) { if (rejectPublicSuffixes != null) { this.rejectPublicSuffixes = rejectPublicSuffixes; } if (!store) { memstore = memstore || require('./memstore'); store = new memstore.MemoryCookieStore(); } this.store = store; } CookieJar.prototype.store = null; CookieJar.prototype.rejectPublicSuffixes = true; var CAN_BE_SYNC = []; CAN_BE_SYNC.push('setCookie'); CookieJar.prototype.setCookie = function(cookie, url, options, cb) { var err; var context = (url instanceof Object) ? url : urlParse(url); if (options instanceof Function) { cb = options; options = {}; } var host = canonicalDomain(context.hostname); // S5.3 step 1 if (!(cookie instanceof Cookie)) { cookie = Cookie.parse(cookie, options.strict === true); } if (!cookie) { err = new Error("Cookie failed to parse"); return cb(options.ignoreError ? null : err); } // S5.3 step 2 var now = options.now || new Date(); // will assign later to save effort in the face of errors // S5.3 step 3: NOOP; persistent-flag and expiry-time is handled by getCookie() // S5.3 step 4: NOOP; domain is null by default // S5.3 step 5: public suffixes if (this.rejectPublicSuffixes && cookie.domain) { var suffix = pubsuffix.getPublicSuffix(cookie.cdomain()); if (suffix == null) { // e.g. "com" err = new Error("Cookie has domain set to a public suffix"); return cb(options.ignoreError ? null : err); } } // S5.3 step 6: if (cookie.domain) { if (!domainMatch(host, cookie.cdomain(), false)) { err = new Error("Cookie not in this host's domain. Cookie:"+cookie.cdomain()+" Request:"+host); return cb(options.ignoreError ? null : err); } if (cookie.hostOnly == null) { // don't reset if already set cookie.hostOnly = false; } } else { cookie.hostOnly = true; cookie.domain = host; } // S5.3 step 7: "Otherwise, set the cookie's path to the default-path of the // request-uri" if (!cookie.path) { cookie.path = defaultPath(context.pathname); cookie.pathIsDefault = true; } else { if (cookie.path.length > 1 && cookie.path.substr(-1) == '/') { cookie.path = cookie.path.slice(0,-1); } } // S5.3 step 8: NOOP; secure attribute // S5.3 step 9: NOOP; httpOnly attribute // S5.3 step 10 if (options.http === false && cookie.httpOnly) { err = new Error("Cookie is HttpOnly and this isn't an HTTP API"); return cb(options.ignoreError ? null : err); } var store = this.store; if (!store.updateCookie) { store.updateCookie = function(oldCookie, newCookie, cb) { this.putCookie(newCookie, cb); }; } function withCookie(err, oldCookie) { if (err) { return cb(err); } var next = function(err) { if (err) { return cb(err); } else { cb(null, cookie); } }; if (oldCookie) { // S5.3 step 11 - "If the cookie store contains a cookie with the same name, // domain, and path as the newly created cookie:" if (options.http === false && oldCookie.httpOnly) { // step 11.2 err = new Error("old Cookie is HttpOnly and this isn't an HTTP API"); return cb(options.ignoreError ? null : err); } cookie.creation = oldCookie.creation; // step 11.3 cookie.lastAccessed = now; // Step 11.4 (delete cookie) is implied by just setting the new one: store.updateCookie(oldCookie, cookie, next); // step 12 } else { cookie.creation = cookie.lastAccessed = now; store.putCookie(cookie, next); // step 12 } } store.findCookie(cookie.domain, cookie.path, cookie.key, withCookie); }; // RFC6365 S5.4 CAN_BE_SYNC.push('getCookies'); CookieJar.prototype.getCookies = function(url, options, cb) { var context = (url instanceof Object) ? url : urlParse(url); if (options instanceof Function) { cb = options; options = {}; } var host = canonicalDomain(context.hostname); var path = context.pathname || '/'; var secure = options.secure; if (secure == null && context.protocol && (context.protocol == 'https:' || context.protocol == 'wss:')) { secure = true; } var http = options.http; if (http == null) { http = true; } var now = options.now || Date.now(); var expireCheck = options.expire !== false; var allPaths = !!options.allPaths; var store = this.store; function matchingCookie(c) { // "Either: // The cookie's host-only-flag is true and the canonicalized // request-host is identical to the cookie's domain. // Or: // The cookie's host-only-flag is false and the canonicalized // request-host domain-matches the cookie's domain." if (c.hostOnly) { if (c.domain != host) { return false; } } else { if (!domainMatch(host, c.domain, false)) { return false; } } // "The request-uri's path path-matches the cookie's path." if (!allPaths && !pathMatch(path, c.path)) { return false; } // "If the cookie's secure-only-flag is true, then the request-uri's // scheme must denote a "secure" protocol" if (c.secure && !secure) { return false; } // "If the cookie's http-only-flag is true, then exclude the cookie if the // cookie-string is being generated for a "non-HTTP" API" if (c.httpOnly && !http) { return false; } // deferred from S5.3 // non-RFC: allow retention of expired cookies by choice if (expireCheck && c.expiryTime() <= now) { store.removeCookie(c.domain, c.path, c.key, function(){}); // result ignored return false; } return true; } store.findCookies(host, allPaths ? null : path, function(err,cookies) { if (err) { return cb(err); } cookies = cookies.filter(matchingCookie); // sorting of S5.4 part 2 if (options.sort !== false) { cookies = cookies.sort(cookieCompare); } // S5.4 part 3 var now = new Date(); cookies.forEach(function(c) { c.lastAccessed = now; }); // TODO persist lastAccessed cb(null,cookies); }); }; CAN_BE_SYNC.push('getCookieString'); CookieJar.prototype.getCookieString = function(/*..., cb*/) { var args = Array.prototype.slice.call(arguments,0); var cb = args.pop(); var next = function(err,cookies) { if (err) { cb(err); } else { cb(null, cookies.map(function(c){ return c.cookieString(); }).join('; ')); } }; args.push(next); this.getCookies.apply(this,args); }; CAN_BE_SYNC.push('getSetCookieStrings'); CookieJar.prototype.getSetCookieStrings = function(/*..., cb*/) { var args = Array.prototype.slice.call(arguments,0); var cb = args.pop(); var next = function(err,cookies) { if (err) { cb(err); } else { cb(null, cookies.map(function(c){ return c.toString(); })); } }; args.push(next); this.getCookies.apply(this,args); }; // Use a closure to provide a true imperative API for synchronous stores. function syncWrap(method) { return function() { if (!this.store.synchronous) { throw new Error('CookieJar store is not synchronous; use async API instead.'); } var args = Array.prototype.slice.call(arguments); var syncErr, syncResult; args.push(function syncCb(err, result) { syncErr = err; syncResult = result; }); this[method].apply(this, args); if (syncErr) { throw syncErr; } return syncResult; }; } // wrap all declared CAN_BE_SYNC methods in the sync wrapper CAN_BE_SYNC.forEach(function(method) { CookieJar.prototype[method+'Sync'] = syncWrap(method); }); module.exports = { CookieJar: CookieJar, Cookie: Cookie, Store: Store, parseDate: parseDate, formatDate: formatDate, parse: parse, fromJSON: fromJSON, domainMatch: domainMatch, defaultPath: defaultPath, pathMatch: pathMatch, getPublicSuffix: pubsuffix.getPublicSuffix, cookieCompare: cookieCompare, permuteDomain: permuteDomain, permutePath: permutePath, canonicalDomain: canonicalDomain, };