Skip to content
Snippets Groups Projects
index.js 8.81 MiB
Newer Older
  • Learn to ignore specific revisions
  •     /* 2.4. If the found-year flag is not set and the date-token matches the
         * year production, set the found-year flag and set the year-value to the
         * number denoted by the date-token.  Skip the remaining sub-steps and
         * continue to the next date-token.
         */
        if (year === null) {
          // "year = 2*4DIGIT ( non-digit *OCTET )"
          result = parseDigits(token, 2, 4, true);
          if (result !== null) {
            year = result;
            /* From S5.1.1:
             * 3.  If the year-value is greater than or equal to 70 and less
             * than or equal to 99, increment the year-value by 1900.
             * 4.  If the year-value is greater than or equal to 0 and less
             * than or equal to 69, increment the year-value by 2000.
             */
            if (year >= 70 && year <= 99) {
              year += 1900;
            } else if (year >= 0 && year <= 69) {
              year += 2000;
            }
          }
        }
      }
    
      /* RFC 6265 S5.1.1
       * "5. Abort these steps and fail to parse the cookie-date if:
       *     *  at least one of the found-day-of-month, found-month, found-
       *        year, or found-time flags is not set,
       *     *  the day-of-month-value is less than 1 or greater than 31,
       *     *  the year-value is less than 1601,
       *     *  the hour-value is greater than 23,
       *     *  the minute-value is greater than 59, or
       *     *  the second-value is greater than 59.
       *     (Note that leap seconds cannot be represented in this syntax.)"
       *
       * So, in order as above:
       */
      if (
        dayOfMonth === null || month === null || year === null || second === null ||
        dayOfMonth < 1 || dayOfMonth > 31 ||
        year < 1601 ||
        hour > 23 ||
        minute > 59 ||
        second > 59
      ) {
        return;
      }
    
      return new Date(Date.UTC(year, month, dayOfMonth, hour, minute, second));
    }
    
    function formatDate(date) {
      var d = date.getUTCDate(); d = d >= 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);
    
    function trimTerminator(str) {
      for (var t = 0; t < TERMINATORS.length; t++) {
        var terminatorIdx = str.indexOf(TERMINATORS[t]);
        if (terminatorIdx !== -1) {
          str = str.substr(0,terminatorIdx);
    
    function parseCookiePair(cookiePair, looseMode) {
      cookiePair = trimTerminator(cookiePair);
    
      var firstEq = cookiePair.indexOf('=');
      if (looseMode) {
        if (firstEq === 0) { // '=' is immediately at start
          cookiePair = cookiePair.substr(1);
          firstEq = cookiePair.indexOf('='); // might still need to split on '='
    
      } else { // non-loose mode
        if (firstEq <= 0) { // no '=' or is at start
          return; // needs to have non-empty "cookie-name"
    
      }
    
      var cookieName, cookieValue;
      if (firstEq <= 0) {
        cookieName = "";
        cookieValue = cookiePair.trim();
      } else {
        cookieName = cookiePair.substr(0, firstEq).trim();
        cookieValue = cookiePair.substr(firstEq+1).trim();
      }
    
      if (CONTROL_CHARS.test(cookieName) || CONTROL_CHARS.test(cookieValue)) {
        return;
      }
    
      var c = new Cookie();
      c.key = cookieName;
      c.value = cookieValue;
      return c;
    
    function parse(str, options) {
      if (!options || typeof options !== 'object') {
        options = {};
      }
      str = str.trim();
    
      // We use a regex to parse the "name-value-pair" part of S5.2
      var firstSemi = str.indexOf(';'); // S5.2 step 1
      var cookiePair = (firstSemi === -1) ? str : str.substr(0, firstSemi);
      var c = parseCookiePair(cookiePair, !!options.loose);
      if (!c) {
        return;
      }
    
      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 + 1).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(';');
      while (cookie_avs.length) {
        var av = cookie_avs.shift().trim();
        if (av.length === 0) { // happens if ";;" appears
          continue;
    
        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) {
            var exp = parseDate(av_value);
            // "If the attribute-value failed to parse as a cookie date, ignore the
            // cookie-av."
            if (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)
              c.expires = exp;
            }
          }
          break;
    
        case 'max-age': // S5.2.2
          if (av_value) {
            // "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)) {
              var delta = parseInt(av_value, 10);
              // "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) {
            // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E
            // (".") character."
            var domain = av_value.trim().replace(/^\./, '');
            if (domain) {
              // "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.
           */
          c.path = av_value && av_value[0] === "/" ? av_value : null;
          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."
           */
          c.secure = true;
          break;
    
        case 'httponly': // S5.2.6 -- effectively the same as 'secure'
          c.httpOnly = true;
          break;
    
        default:
          c.extensions = c.extensions || [];
          c.extensions.push(av);
          break;
        }
      }
    
    // avoid the V8 deoptimization monster!
    function jsonParse(str) {
      var obj;
      try {
        obj = JSON.parse(str);
      } catch (e) {
        return e;
      }
      return obj;
    }
    
    function fromJSON(str) {
      if (!str) {
        return null;
      }
    
      var obj;
      if (typeof str === 'string') {
        obj = jsonParse(str);
        if (obj instanceof Error) {
          return null;
    
      } else {
        // assume it's an Object
        obj = str;
      }
    
      var c = new Cookie();
      for (var i=0; i<Cookie.serializableProperties.length; i++) {
        var prop = Cookie.serializableProperties[i];
        if (obj[prop] === undefined ||
            obj[prop] === Cookie.prototype[prop])
        {
          continue; // leave as prototype default
    
    
        if (prop === 'expires' ||
            prop === 'creation' ||
            prop === 'lastAccessed')
        {
          if (obj[prop] === null) {
            c[prop] = null;
          } else {
            c[prop] = obj[prop] == "Infinity" ?
              "Infinity" : new Date(obj[prop]);
          }
        } else {
          c[prop] = obj[prop];
    
    /* Section 5.4 part 2:
     * "*  Cookies with longer paths are listed before cookies with
     *     shorter paths.
     *
     *  *  Among cookies that have equal-length path fields, cookies with
     *     earlier creation-times are listed before cookies with later
     *     creation-times."
     */
    
    function cookieCompare(a,b) {
      var cmp = 0;
    
      // descending for length: b CMP a
      var aPathLen = a.path ? a.path.length : 0;
      var bPathLen = b.path ? b.path.length : 0;
      cmp = bPathLen - aPathLen;
      if (cmp !== 0) {
        return cmp;
      }
    
      // ascending for time: a CMP b
      var aTime = a.creation ? a.creation.getTime() : MAX_TIME;
      var bTime = b.creation ? b.creation.getTime() : MAX_TIME;
      cmp = aTime - bTime;
      if (cmp !== 0) {
        return cmp;
      }
    
      // break ties for the same millisecond (precision of JavaScript's clock)
      cmp = a.creationIndex - b.creationIndex;
    
    // Gives the permutation of all possible pathMatch()es of a given path. The
    // array is in longest-to-shortest order.  Handy for indexing.
    function permutePath(path) {
      if (path === '/') {
        return ['/'];
      }
      if (path.lastIndexOf('/') === path.length-1) {
        path = path.substr(0,path.length-1);
      }
      var permutations = [path];
      while (path.length > 1) {
        var lindex = path.lastIndexOf('/');
        if (lindex === 0) {
          break;
    
        path = path.substr(0,lindex);
        permutations.push(path);
      }
      permutations.push('/');
      return permutations;
    }
    
    function getCookieContext(url) {
      if (url instanceof Object) {
        return url;
      }
      // NOTE: decodeURI will throw on malformed URIs (see GH-32).
      // Therefore, we will just skip decoding for such URIs.
      try {
        url = decodeURI(url);
      }
      catch(err) {
        // Silently swallow error
      }
    
      return urlParse(url);
    }
    
    function Cookie(options) {
      options = options || {};
    
      Object.keys(options).forEach(function(prop) {
        if (Cookie.prototype.hasOwnProperty(prop) &&
            Cookie.prototype[prop] !== options[prop] &&
            prop.substr(0,1) !== '_')
        {
          this[prop] = options[prop];
        }
      }, this);
    
      this.creation = this.creation || new Date();
    
      // used to break creation ties in cookieCompare():
      Object.defineProperty(this, 'creationIndex', {
        configurable: false,
        enumerable: false, // important for assert.deepEqual checks
        writable: true,
        value: ++Cookie.cookiesCreated
      });
    
    Cookie.cookiesCreated = 0; // incremented each time a cookie is created
    
    Cookie.parse = parse;
    Cookie.fromJSON = fromJSON;
    
    Cookie.prototype.key = "";
    Cookie.prototype.value = "";
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // 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;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // 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
    Object.defineProperty(Cookie.prototype, 'creationIndex', {
      configurable: true,
      enumerable: false,
      writable: true,
      value: 0
    });
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Cookie.serializableProperties = Object.keys(Cookie.prototype)
      .filter(function(prop) {
        return !(
          Cookie.prototype[prop] instanceof Function ||
          prop === 'creationIndex' ||
          prop.substr(0,1) === '_'
        );
      });
    
    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' : '?') +
        '"';
    };
    
    // Use the new custom inspection symbol to add the custom inspect function if
    // available.
    if (util.inspect.custom) {
      Cookie.prototype[util.inspect.custom] = Cookie.prototype.inspect;
    
    Cookie.prototype.toJSON = function() {
      var obj = {};
    
      var props = Cookie.serializableProperties;
      for (var i=0; i<props.length; i++) {
        var prop = props[i];
        if (this[prop] === Cookie.prototype[prop]) {
          continue; // leave as prototype default
        }
    
        if (prop === 'expires' ||
            prop === 'creation' ||
            prop === 'lastAccessed')
        {
          if (this[prop] === null) {
            obj[prop] = null;
          } else {
            obj[prop] = this[prop] == "Infinity" ? // intentionally not ===
              "Infinity" : this[prop].toISOString();
          }
        } else if (prop === 'maxAge') {
          if (this[prop] !== null) {
            // again, intentionally not ===
            obj[prop] = (this[prop] == Infinity || this[prop] == -Infinity) ?
              this[prop].toString() : this[prop];
          }
    
        } else {
    
          if (this[prop] !== Cookie.prototype[prop]) {
            obj[prop] = this[prop];
          }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Cookie.prototype.clone = function() {
      return fromJSON(this.toJSON());
    
    Cookie.prototype.validate = function validate() {
      if (!COOKIE_OCTETS.test(this.value)) {
        return false;
      }
      if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires)) {
        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;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      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;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Cookie.prototype.setExpires = function setExpires(exp) {
      if (exp instanceof Date) {
        this.expires = exp;
      } else {
        this.expires = parseDate(exp) || "Infinity";
      }
    };
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Cookie.prototype.setMaxAge = function setMaxAge(age) {
      if (age === Infinity || age === -Infinity) {
        this.maxAge = age.toString(); // so JSON.stringify() works
      } else {
        this.maxAge = age;
      }
    };
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // gives Cookie header format
    Cookie.prototype.cookieString = function cookieString() {
      var val = this.value;
      if (val == null) {
        val = '';
      }
      if (this.key === '') {
        return 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;
        });
      }
    
    // 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;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var expires = this.expires;
      if (expires != Infinity) {
        if (!(expires instanceof Date)) {
          expires = parseDate(expires) || Infinity;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        if (expires == Infinity) {
          return Infinity;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        return expires.getTime() - (now || Date.now());
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      return Infinity;
    };
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // 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 = now || this.creation || new Date();
        var age = (this.maxAge <= 0) ? -Infinity : this.maxAge*1000;
        return relativeTo.getTime() + age;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      if (this.expires == Infinity) {
        return Infinity;
      }
      return this.expires.getTime();
    };
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // 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);
      }
    };
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // This replaces the "persistent-flag" parts of S5.3 step 3
    Cookie.prototype.isPersistent = function isPersistent() {
      return (this.maxAge != null || this.expires != Infinity);
    };
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // 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);
    };
    
    function CookieJar(store, options) {
      if (typeof options === "boolean") {
        options = {rejectPublicSuffixes: options};
      } else if (options == null) {
        options = {};
      }
      if (options.rejectPublicSuffixes != null) {
        this.rejectPublicSuffixes = options.rejectPublicSuffixes;
      }
      if (options.looseMode != null) {
        this.enableLooseMode = options.looseMode;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      if (!store) {
        store = new MemoryCookieStore();
      }
      this.store = store;
    }
    CookieJar.prototype.store = null;
    CookieJar.prototype.rejectPublicSuffixes = true;
    CookieJar.prototype.enableLooseMode = false;
    var CAN_BE_SYNC = [];
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    CAN_BE_SYNC.push('setCookie');
    CookieJar.prototype.setCookie = function(cookie, url, options, cb) {
      var err;
      var context = getCookieContext(url);
      if (options instanceof Function) {
        cb = options;
        options = {};
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var host = canonicalDomain(context.hostname);
      var loose = this.enableLooseMode;
      if (options.loose != null) {
        loose = options.loose;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // S5.3 step 1
      if (!(cookie instanceof Cookie)) {
        cookie = Cookie.parse(cookie, { loose: loose });
      }
      if (!cookie) {
        err = new Error("Cookie failed to parse");
        return cb(options.ignoreError ? null : err);
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // S5.3 step 2
      var now = options.now || new Date(); // will assign later to save effort in the face of errors
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // S5.3 step 3: NOOP; persistent-flag and expiry-time is handled by getCookie()
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // S5.3 step 4: NOOP; domain is null by default
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // 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);
        }
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // 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);
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        if (cookie.hostOnly == null) { // don't reset if already set
          cookie.hostOnly = false;
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      } else {
        cookie.hostOnly = true;
        cookie.domain = host;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      //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.
      if (!cookie.path || cookie.path[0] !== '/') {
        cookie.path = defaultPath(context.pathname);
        cookie.pathIsDefault = true;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // S5.3 step 8: NOOP; secure attribute
      // S5.3 step 9: NOOP; httpOnly attribute
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // 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);
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var store = this.store;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      if (!store.updateCookie) {
        store.updateCookie = function(oldCookie, newCookie, cb) {
          this.putCookie(newCookie, cb);
        };
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      function withCookie(err, oldCookie) {
        if (err) {
          return cb(err);
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        var next = function(err) {
          if (err) {
            return cb(err);
          } else {
            cb(null, cookie);
          }
        };
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        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.creationIndex = oldCookie.creationIndex; // preserve tie-breaker
          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
        }
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      store.findCookie(cookie.domain, cookie.path, cookie.key, withCookie);
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // RFC6365 S5.4
    CAN_BE_SYNC.push('getCookies');
    CookieJar.prototype.getCookies = function(url, options, cb) {
      var context = getCookieContext(url);
      if (options instanceof Function) {
        cb = options;
        options = {};
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var host = canonicalDomain(context.hostname);
      var path = context.pathname || '/';
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var secure = options.secure;
      if (secure == null && context.protocol &&
          (context.protocol == 'https:' || context.protocol == 'wss:'))
      {
        secure = true;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var http = options.http;
      if (http == null) {
        http = true;
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var now = options.now || Date.now();
      var expireCheck = options.expire !== false;
      var allPaths = !!options.allPaths;
      var store = this.store;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      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;
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        // "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;
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        // "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;
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        // 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;
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      store.findCookies(host, allPaths ? null : path, function(err,cookies) {
        if (err) {
          return cb(err);
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        cookies = cookies.filter(matchingCookie);
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        // sorting of S5.4 part 2
        if (options.sort !== false) {
          cookies = cookies.sort(cookieCompare);
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        // S5.4 part 3
        var now = new Date();
        cookies.forEach(function(c) {
          c.lastAccessed = now;
        });
        // TODO persist lastAccessed
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        cb(null,cookies);
      });
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    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
            .sort(cookieCompare)
            .map(function(c){
              return c.cookieString();
            })
            .join('; '));
        }
      };
      args.push(next);