tough-cookie
Advanced tools
Comparing version 1.2.0 to 2.0.0
@@ -38,2 +38,3 @@ /*! | ||
var pathMatch = require('./pathMatch').pathMatch; | ||
var VERSION = require('../package.json').version; | ||
@@ -88,5 +89,3 @@ var punycode; | ||
var cookiesCreated = 0; // Number of cookies created in runtime | ||
// RFC6265 S5.1.1 date parser: | ||
@@ -463,11 +462,16 @@ function parseDate(str) { | ||
// ensure a default date for sorting: | ||
c.creation = new Date(); | ||
//NOTE: add runtime index for the cookieCompare() to resolve the situation when Date's precision is not enough . | ||
//Store initial UTC time as well, so we will be able to determine if we need to fallback to the Date object. | ||
c._creationRuntimeIdx = ++cookiesCreated; | ||
c._initialCreationTime = c.creation.getTime(); | ||
return c; | ||
} | ||
// avoid the V8 deoptimization monster! | ||
function jsonParse(str) { | ||
var obj; | ||
try { | ||
obj = JSON.parse(str); | ||
} catch (e) { | ||
return e; | ||
} | ||
return obj; | ||
} | ||
function fromJSON(str) { | ||
@@ -479,14 +483,21 @@ if (!str) { | ||
var obj; | ||
try { | ||
obj = JSON.parse(str); | ||
} catch (e) { | ||
return null; | ||
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<numCookieProperties; i++) { | ||
var prop = cookieProperties[i]; | ||
if (obj[prop] == null) { | ||
continue; | ||
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' || | ||
@@ -496,3 +507,8 @@ prop === 'creation' || | ||
{ | ||
c[prop] = obj[prop] == "Infinity" ? "Infinity" : new Date(obj[prop]); | ||
if (obj[prop] === null) { | ||
c[prop] = null; | ||
} else { | ||
c[prop] = obj[prop] == "Infinity" ? | ||
"Infinity" : new Date(obj[prop]); | ||
} | ||
} else { | ||
@@ -503,6 +519,2 @@ c[prop] = obj[prop]; | ||
// ensure a default date for sorting: | ||
c.creation = c.creation || new Date(); | ||
return c; | ||
@@ -521,19 +533,24 @@ } | ||
function cookieCompare(a,b) { | ||
var cmp = 0; | ||
// descending for length: b CMP a | ||
var deltaLen = (b.path ? b.path.length : 0) - (a.path ? a.path.length : 0); | ||
if (deltaLen !== 0) { | ||
return deltaLen; | ||
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; | ||
// NOTE: if creation dates are equal and they were not modified from the outside, | ||
// then use _creationRuntimeIdx for the comparison. | ||
if(aTime === bTime && aTime === a._initialCreationTime && bTime === b._initialCreationTime) { | ||
return a._creationRuntimeIdx - b._creationRuntimeIdx; | ||
cmp = aTime - bTime; | ||
if (cmp !== 0) { | ||
return cmp; | ||
} | ||
// ascending for time: a CMP b | ||
return aTime - bTime; | ||
// break ties for the same millisecond (precision of JavaScript's clock) | ||
cmp = a.creationIndex - b.creationIndex; | ||
return cmp; | ||
} | ||
@@ -579,13 +596,27 @@ | ||
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]; | ||
function Cookie(opts) { | ||
opts = opts || {}; | ||
Object.keys(opts).forEach(function(prop) { | ||
if (Cookie.prototype.hasOwnProperty(prop) && | ||
Cookie.prototype[prop] !== opts[prop] && | ||
prop.substr(0,1) !== '_') | ||
{ | ||
this[prop] = opts[prop]; | ||
} | ||
}.bind(this)); | ||
}, 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; | ||
@@ -610,13 +641,18 @@ Cookie.fromJSON = fromJSON; | ||
Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse | ||
Cookie.prototype._initialCreationTime = null; // Used to determine if cookie.creation was modified | ||
Cookie.prototype._creationRuntimeIdx = null; // Runtime index of the created cookie, used in cookieCompare() | ||
Cookie.prototype.lastAccessed = null; // Date when set | ||
Object.defineProperty(Cookie.prototype, 'creationIndex', { | ||
configurable: true, | ||
enumerable: false, | ||
writable: true, | ||
value: 0 | ||
}); | ||
var cookieProperties = Object.freeze(Object.keys(Cookie.prototype).map(function(p) { | ||
if (p instanceof Function) { | ||
return; | ||
} | ||
return p; | ||
})); | ||
var numCookieProperties = cookieProperties.length; | ||
Cookie.serializableProperties = Object.keys(Cookie.prototype) | ||
.filter(function(prop) { | ||
return !( | ||
Cookie.prototype[prop] instanceof Function || | ||
prop === 'creationIndex' || | ||
prop.substr(0,1) === '_' | ||
); | ||
}); | ||
@@ -632,2 +668,42 @@ Cookie.prototype.inspect = function 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]; | ||
} | ||
} | ||
} | ||
return obj; | ||
}; | ||
Cookie.prototype.clone = function() { | ||
return fromJSON(this.toJSON()); | ||
}; | ||
Cookie.prototype.validate = function validate() { | ||
@@ -757,3 +833,3 @@ if (!COOKIE_OCTETS.test(this.value)) { | ||
if (this.maxAge != null) { | ||
var relativeTo = this.creation || now || new Date(); | ||
var relativeTo = now || this.creation || new Date(); | ||
var age = (this.maxAge <= 0) ? -Infinity : this.maxAge*1000; | ||
@@ -796,3 +872,2 @@ return relativeTo.getTime() + age; | ||
function CookieJar(store, rejectPublicSuffixes) { | ||
@@ -910,2 +985,3 @@ if (rejectPublicSuffixes != null) { | ||
cookie.creation = oldCookie.creation; // step 11.3 | ||
cookie.creationIndex = oldCookie.creationIndex; // preserve tie-breaker | ||
cookie.lastAccessed = now; | ||
@@ -1057,2 +1133,147 @@ // Step 11.4 (delete cookie) is implied by just setting the new one: | ||
CAN_BE_SYNC.push('serialize'); | ||
CookieJar.prototype.serialize = function(cb) { | ||
var type = this.store.constructor.name; | ||
if (type === 'Object') { | ||
type = null; | ||
} | ||
// update README.md "Serialization Format" if you change this, please! | ||
var serialized = { | ||
// The version of tough-cookie that serialized this jar. Generally a good | ||
// practice since future versions can make data import decisions based on | ||
// known past behavior. When/if this matters, use `semver`. | ||
version: 'tough-cookie@'+VERSION, | ||
// add the store type, to make humans happy: | ||
storeType: type, | ||
// CookieJar configuration: | ||
rejectPublicSuffixes: !!this.rejectPublicSuffixes, | ||
// this gets filled from getAllCookies: | ||
cookies: [] | ||
}; | ||
if (!(this.store.getAllCookies && | ||
typeof this.store.getAllCookies === 'function')) | ||
{ | ||
return cb(new Error('store does not support getAllCookies and cannot be serialized')); | ||
} | ||
this.store.getAllCookies(function(err,cookies) { | ||
if (err) { | ||
return cb(err); | ||
} | ||
serialized.cookies = cookies.map(function(cookie) { | ||
// convert to serialized 'raw' cookies | ||
cookie = (cookie instanceof Cookie) ? cookie.toJSON() : cookie; | ||
// Remove the index so new ones get assigned during deserialization | ||
delete cookie.creationIndex; | ||
return cookie; | ||
}); | ||
return cb(null, serialized); | ||
}); | ||
}; | ||
// well-known name that JSON.stringify calls | ||
CookieJar.prototype.toJSON = function() { | ||
return this.serializeSync(); | ||
}; | ||
// use the class method CookieJar.deserialize instead of calling this directly | ||
CAN_BE_SYNC.push('_importCookies'); | ||
CookieJar.prototype._importCookies = function(serialized, cb) { | ||
var jar = this; | ||
var cookies = serialized.cookies; | ||
if (!cookies || !Array.isArray(cookies)) { | ||
return cb(new Error('serialized jar has no cookies array')); | ||
} | ||
function putNext(err) { | ||
if (err) { | ||
return cb(err); | ||
} | ||
if (!cookies.length) { | ||
return cb(err, jar); | ||
} | ||
var cookie; | ||
try { | ||
cookie = fromJSON(cookies.shift()); | ||
} catch (e) { | ||
return cb(e); | ||
} | ||
if (cookie === null) { | ||
return putNext(null); // skip this cookie | ||
} | ||
jar.store.putCookie(cookie, putNext); | ||
} | ||
putNext(); | ||
}; | ||
CookieJar.deserialize = function(strOrObj, store, cb) { | ||
if (arguments.length !== 3) { | ||
// store is optional | ||
cb = store; | ||
store = null; | ||
} | ||
var serialized; | ||
if (typeof strOrObj === 'string') { | ||
serialized = jsonParse(strOrObj); | ||
if (serialized instanceof Error) { | ||
return cb(serialized); | ||
} | ||
} else { | ||
serialized = strOrObj; | ||
} | ||
var jar = new CookieJar(store, serialized.rejectPublicSuffixes); | ||
jar._importCookies(serialized, function(err) { | ||
if (err) { | ||
return cb(err); | ||
} | ||
cb(null, jar); | ||
}); | ||
}; | ||
CookieJar.fromJSON = CookieJar.deserializeSync; | ||
CookieJar.deserializeSync = function(strOrObj, store) { | ||
var serialized = typeof strOrObj === 'string' ? | ||
JSON.parse(strOrObj) : strOrObj; | ||
var jar = new CookieJar(store, serialized.rejectPublicSuffixes); | ||
// catch this mistake early: | ||
if (!jar.store.synchronous) { | ||
throw new Error('CookieJar store is not synchronous; use async API instead.'); | ||
} | ||
jar._importCookiesSync(serialized); | ||
return jar; | ||
}; | ||
CAN_BE_SYNC.push('clone'); | ||
CookieJar.prototype.clone = function(newStore, cb) { | ||
if (arguments.length === 1) { | ||
cb = newStore; | ||
newStore = null; | ||
} | ||
this.serialize(function(err,serialized) { | ||
if (err) { | ||
return cb(err); | ||
} | ||
CookieJar.deserialize(newStore, serialized, cb); | ||
}); | ||
}; | ||
// Use a closure to provide a true imperative API for synchronous stores. | ||
@@ -1059,0 +1280,0 @@ function syncWrap(method) { |
@@ -44,2 +44,4 @@ /*! | ||
MemoryCookieStore.prototype.idx = null; | ||
// Since it's just a struct in RAM, this Store is synchronous | ||
MemoryCookieStore.prototype.synchronous = true; | ||
@@ -120,3 +122,3 @@ | ||
MemoryCookieStore.prototype.updateCookie = function updateCookie(oldCookie, newCookie, cb) { | ||
MemoryCookieStore.prototype.updateCookie = function(oldCookie, newCookie, cb) { | ||
// updateCookie() may avoid updating cookies that are identical. For example, | ||
@@ -128,3 +130,3 @@ // lastAccessed may not be important to some stores and an equality | ||
MemoryCookieStore.prototype.removeCookie = function removeCookie(domain, path, key, cb) { | ||
MemoryCookieStore.prototype.removeCookie = function(domain, path, key, cb) { | ||
if (this.idx[domain] && this.idx[domain][path] && this.idx[domain][path][key]) { | ||
@@ -136,3 +138,3 @@ delete this.idx[domain][path][key]; | ||
MemoryCookieStore.prototype.removeCookies = function removeCookies(domain, path, cb) { | ||
MemoryCookieStore.prototype.removeCookies = function(domain, path, cb) { | ||
if (this.idx[domain]) { | ||
@@ -147,1 +149,27 @@ if (path) { | ||
}; | ||
MemoryCookieStore.prototype.getAllCookies = function(cb) { | ||
var cookies = []; | ||
var idx = this.idx; | ||
var domains = Object.keys(idx); | ||
domains.forEach(function(domain) { | ||
var paths = Object.keys(idx[domain]); | ||
paths.forEach(function(path) { | ||
var keys = Object.keys(idx[domain][path]); | ||
keys.forEach(function(key) { | ||
if (key !== null) { | ||
cookies.push(idx[domain][path][key]); | ||
} | ||
}); | ||
}); | ||
}); | ||
// Sort by creationIndex so deserializing retains the creation order. | ||
// When implementing your own store, this SHOULD retain the order too | ||
cookies.sort(function(a,b) { | ||
return (a.creationIndex||0) - (b.creationIndex||0); | ||
}); | ||
cb(null, cookies); | ||
}; |
@@ -65,4 +65,8 @@ /*! | ||
Store.prototype.removeCookies = function removeCookies(domain, path, cb) { | ||
Store.prototype.removeCookies = function(domain, path, cb) { | ||
throw new Error('removeCookies is not implemented'); | ||
}; | ||
Store.prototype.getAllCookies = function(cb) { | ||
throw new Error('getAllCookies is not implemented (therefore jar cannot be serialized)'); | ||
}; |
@@ -16,3 +16,3 @@ { | ||
], | ||
"version": "1.2.0", | ||
"version": "2.0.0", | ||
"homepage": "https://github.com/SalesforceEng/tough-cookie", | ||
@@ -19,0 +19,0 @@ "repository": { |
296
README.md
@@ -11,3 +11,3 @@ [RFC6265](https://tools.ietf.org/html/rfc6265) Cookies and CookieJar for Node.js | ||
``` javascript | ||
var tough = require('tough-cookie'); // note: not 'cookie', 'cookies' or 'node-cookie' | ||
var tough = require('tough-cookie'); | ||
var Cookie = tough.Cookie; | ||
@@ -36,4 +36,3 @@ var cookie = Cookie.parse(header); | ||
tough | ||
===== | ||
## tough | ||
@@ -44,19 +43,15 @@ Functions on the module you get from `require('tough-cookie')`. All can be used as pure functions and don't need to be "bound". | ||
parseDate(string) | ||
----------------- | ||
### `parseDate(string)` | ||
Parse a cookie date string into a `Date`. Parses according to RFC6265 Section 5.1.1, not `Date.parse()`. | ||
formatDate(date) | ||
---------------- | ||
### `formatDate(date)` | ||
Format a Date into a RFC1123 string (the RFC6265-recommended format). | ||
canonicalDomain(str) | ||
-------------------- | ||
### `canonicalDomain(str)` | ||
Transforms a domain-name into a canonical domain-name. The canonical domain-name is a trimmed, lowercased, stripped-of-leading-dot and optionally punycode-encoded domain-name (Section 5.1.2 of RFC6265). For the most part, this function is idempotent (can be run again on its output without ill effects). | ||
domainMatch(str,domStr[,canonicalize=true]) | ||
------------------------------------------- | ||
### `domainMatch(str,domStr[,canonicalize=true])` | ||
@@ -67,4 +62,3 @@ Answers "does this real domain match the domain in a cookie?". The `str` is the "current" domain-name and the `domStr` is the "cookie" domain-name. Matches according to RFC6265 Section 5.1.3, but it helps to think of it as a "suffix match". | ||
defaultPath(path) | ||
----------------- | ||
### `defaultPath(path)` | ||
@@ -75,4 +69,3 @@ Given a current request/response path, gives the Path apropriate for storing in a cookie. This is basically the "directory" of a "file" in the path, but is specified by Section 5.1.4 of the RFC. | ||
pathMatch(reqPath,cookiePath) | ||
----------------------------- | ||
### `pathMatch(reqPath,cookiePath)` | ||
@@ -83,14 +76,11 @@ Answers "does the request-path path-match a given cookie-path?" as per RFC6265 Section 5.1.4. Returns a boolean. | ||
parse(header) | ||
---------------------------- | ||
### `parse(header)` | ||
alias for `Cookie.parse(header)` | ||
fromJSON(string) | ||
---------------- | ||
### `fromJSON(string)` | ||
alias for `Cookie.fromJSON(string)` | ||
getPublicSuffix(hostname) | ||
------------------------- | ||
### `getPublicSuffix(hostname)` | ||
@@ -103,7 +93,10 @@ Returns the public suffix of this hostname. The public suffix is the shortest domain-name upon which a cookie can be set. Returns `null` if the hostname cannot have cookies set for it. | ||
cookieCompare(a,b) | ||
------------------ | ||
### `cookieCompare(a,b)` | ||
For use with `.sort()`, sorts a list of cookies into the recommended order given in the RFC (Section 5.4 step 2). Longest `.path`s go first, then sorted oldest to youngest. | ||
For use with `.sort()`, sorts a list of cookies into the recommended order given in the RFC (Section 5.4 step 2). The sort algorithm is, in order of precedence: | ||
* Longest `.path` | ||
* oldest `.creation` (which has a 1ms precision, same as `Date`) | ||
* lowest `.creationIndex` (to get beyond the 1ms precision) | ||
``` javascript | ||
@@ -114,19 +107,19 @@ var cookies = [ /* unsorted array of Cookie objects */ ]; | ||
permuteDomain(domain) | ||
--------------------- | ||
**Note**: Since JavaScript's `Date` is limited to a 1ms precision, cookies within the same milisecond are entirely possible. This is especially true when using the `now` option to `.setCookie()`. The `.creationIndex` property is a per-process global counter, assigned during construction with `new Cookie()`. This preserves the spirit of the RFC sorting: older cookies go first. This works great for `MemoryCookieStore`, since `Set-Cookie` headers are parsed in order, but may not be so great for distributed systems. Sophisticated `Store`s may wish to set this to some other _logical clock_ such that if cookies A and B are created in the same millisecond, but cookie A is created before cookie B, then `A.creationIndex < B.creationIndex`. If you want to alter the global counter, which you probably _shouldn't_ do, it's stored in `Cookie.cookiesCreated`. | ||
### `permuteDomain(domain)` | ||
Generates a list of all possible domains that `domainMatch()` the parameter. May be handy for implementing cookie stores. | ||
### `permutePath(path)` | ||
permutePath(path) | ||
----------------- | ||
Generates a list of all possible paths that `pathMatch()` the parameter. May be handy for implementing cookie stores. | ||
Cookie | ||
====== | ||
Cookie.parse(header) | ||
----------------------------------- | ||
## Cookie | ||
Exported via `tough.Cookie`. | ||
### `Cookie.parse(header)` | ||
Parses a single Cookie or Set-Cookie HTTP header into a `Cookie` object. Returns `undefined` if the string can't be parsed. | ||
@@ -143,10 +136,6 @@ | ||
Cookie.fromJSON(string) | ||
----------------------- | ||
### Properties | ||
Convert a JSON string to a `Cookie` object. Does a `JSON.parse()` and converts the `.created`, `.lastAccessed` and `.expires` properties into `Date` objects. | ||
Cookie object properties: | ||
Properties | ||
========== | ||
* _key_ - string - the name or key of the cookie (default "") | ||
@@ -161,2 +150,4 @@ * _value_ - string - the value of the cookie (default "") | ||
* _extensions_ - `Array` - any unrecognized cookie attributes as strings (even if equal-signs inside) | ||
* _creation_ - `Date` - when this cookie was constructed | ||
* _creationIndex_ - number - set at construction, used to provide greater sort precision (please see `cookieCompare(a,b)` for a full explanation) | ||
@@ -167,39 +158,32 @@ After a cookie has been passed through `CookieJar.setCookie()` it will have the following additional attributes: | ||
* _pathIsDefault_ - boolean - if true, there was no Path field on the cookie and `defaultPath()` was used to derive one. | ||
* _created_ - `Date` - when this cookie was added to the jar | ||
* _creation_ - `Date` - **modified** from construction to when the cookie was added to the jar | ||
* _lastAccessed_ - `Date` - last time the cookie got accessed. Will affect cookie cleaning once implemented. Using `cookiejar.getCookies(...)` will update this attribute. | ||
Construction([{options}]) | ||
------------ | ||
### `Cookie([{properties}])` | ||
Receives an options object that can contain any Cookie properties, uses the default for unspecified properties. | ||
Receives an options object that can contain any of the above Cookie properties, uses the default for unspecified properties. | ||
.toString() | ||
----------- | ||
### `.toString()` | ||
encode to a Set-Cookie header value. The Expires cookie field is set using `formatDate()`, but is omitted entirely if `.expires` is `Infinity`. | ||
.cookieString() | ||
--------------- | ||
### `.cookieString()` | ||
encode to a Cookie header value (i.e. the `.key` and `.value` properties joined with '='). | ||
.setExpires(String) | ||
------------------- | ||
### `.setExpires(String)` | ||
sets the expiry based on a date-string passed through `parseDate()`. If parseDate returns `null` (i.e. can't parse this date string), `.expires` is set to `"Infinity"` (a string) is set. | ||
.setMaxAge(number) | ||
------------------- | ||
### `.setMaxAge(number)` | ||
sets the maxAge in seconds. Coerces `-Infinity` to `"-Infinity"` and `Infinity` to `"Infinity"` so it JSON serializes correctly. | ||
.expiryTime([now=Date.now()]) | ||
----------------------------- | ||
### `.expiryTime([now=Date.now()])` | ||
.expiryDate([now=Date.now()]) | ||
----------------------------- | ||
### `.expiryDate([now=Date.now()])` | ||
expiryTime() Computes the absolute unix-epoch milliseconds that this cookie expires. expiryDate() works similarly, except it returns a `Date` object. Note that in both cases the `now` parameter should be milliseconds. | ||
Max-Age takes precedence over Expires (as per the RFC). The `.created` attribute -- or, by default, the `now` paramter -- is used to offset the `.maxAge` attribute. | ||
Max-Age takes precedence over Expires (as per the RFC). The `.creation` attribute -- or, by default, the `now` paramter -- is used to offset the `.maxAge` attribute. | ||
@@ -210,4 +194,3 @@ If Expires (`.expires`) is set, that's returned. | ||
.TTL([now=Date.now()]) | ||
--------- | ||
### `.TTL([now=Date.now()])` | ||
@@ -218,13 +201,30 @@ compute the TTL relative to `now` (milliseconds). The same precedence rules as for `expiryTime`/`expiryDate` apply. | ||
.canonicalizedDoman() | ||
--------------------- | ||
### `.canonicalizedDoman()` | ||
.cdomain() | ||
---------- | ||
### `.cdomain()` | ||
return the canonicalized `.domain` field. This is lower-cased and punycode (RFC3490) encoded if the domain has any non-ASCII characters. | ||
.validate() | ||
----------- | ||
### `.toJSON()` | ||
For convenience in using `JSON.serialize(cookie)`. Returns a plain-old `Object` that can be JSON-serialized. | ||
Any `Date` properties (i.e., `.expires`, `.creation`, and `.lastAccessed`) are exported in ISO format (`.toISOString()`). | ||
**NOTE**: Custom `Cookie` properties will be discarded. In tough-cookie 1.x, since there was no `.toJSON` method explicitly defined, all enumerable properties were captured. If you want a property to be serialized, add the property name to the `Cookie.serializableProperties` Array. | ||
### `Cookie.fromJSON(strOrObj)` | ||
Does the reverse of `cookie.toJSON()`. If passed a string, will `JSON.parse()` that first. | ||
Any `Date` properties (i.e., `.expires`, `.creation`, and `.lastAccessed`) are parsed via `Date.parse()`, not the tough-cookie `parseDate`, since it's JavaScript/JSON-y timestamps being handled at this layer. | ||
Returns `null` upon JSON parsing error. | ||
### `.clone()` | ||
Does a deep clone of this cookie, exactly implemented as `Cookie.fromJSON(cookie.toJSON())`. | ||
### `.validate()` | ||
Status: *IN PROGRESS*. Works for a few things, but is by no means comprehensive. | ||
@@ -242,13 +242,14 @@ | ||
CookieJar | ||
========= | ||
Construction([store = new MemoryCookieStore()][, rejectPublicSuffixes]) | ||
------------ | ||
## CookieJar | ||
Exported via `tough.CookieJar`. | ||
### `CookieJar([store],[rejectPublicSuffixes])` | ||
Simply use `new CookieJar()`. If you'd like to use a custom store, pass that to the constructor otherwise a `MemoryCookieStore` will be created and used. | ||
### Properties | ||
Attributes | ||
---------- | ||
CookieJar object properties: | ||
@@ -259,6 +260,5 @@ * _rejectPublicSuffixes_ - boolean - reject cookies with domains like "com" and "co.uk" (default: `true`) | ||
.setCookie(cookieOrString, currentUrl, [{options},] cb(err,cookie)) | ||
------------------------------------------------------------------- | ||
### `.setCookie(cookieOrString, currentUrl, [{options},] cb(err,cookie))` | ||
Attempt to set the cookie in the cookie jar. If the operation fails, an error will be given to the callback `cb`, otherwise the cookie is passed through. The cookie will have updated `.created`, `.lastAccessed` and `.hostOnly` properties. | ||
Attempt to set the cookie in the cookie jar. If the operation fails, an error will be given to the callback `cb`, otherwise the cookie is passed through. The cookie will have updated `.creation`, `.lastAccessed` and `.hostOnly` properties. | ||
@@ -270,19 +270,12 @@ The `options` object can be omitted and can have the following properties: | ||
* _now_ - Date - default `new Date()` - what to use for the creation/access time of cookies | ||
* _ignoreError_ - boolean - default `false` - silently ignore things like parse errors and invalid domains. CookieStore errors aren't ignored by this option. | ||
* _ignoreError_ - boolean - default `false` - silently ignore things like parse errors and invalid domains. `Store` errors aren't ignored by this option. | ||
As per the RFC, the `.hostOnly` property is set if there was no "Domain=" parameter in the cookie string (or `.domain` was null on the Cookie object). The `.domain` property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a `domainMatch` as per usual). | ||
.setCookieSync(cookieOrString, currentUrl, [{options}]) | ||
------------------------------------------------------- | ||
### `.setCookieSync(cookieOrString, currentUrl, [{options}])` | ||
Synchronous version of `setCookie`; only works with synchronous stores (e.g. the default `MemoryCookieStore`). | ||
.storeCookie(cookie, [{options},] cb(err,cookie)) | ||
------------------------------------------------- | ||
### `.getCookies(currentUrl, [{options},] cb(err,cookies))` | ||
__REMOVED__ removed in lieu of the CookieStore API below | ||
.getCookies(currentUrl, [{options},] cb(err,cookies)) | ||
----------------------------------------------------- | ||
Retrieve the list of cookies that can be sent in a Cookie header for the current url. | ||
@@ -298,42 +291,84 @@ | ||
* _expire_ - boolean - default `true` - perform expiry-time checking of cookies and asynchronously remove expired cookies from the store. Using `false` will return expired cookies and **not** remove them from the store (which is useful for replaying Set-Cookie headers, potentially). | ||
* _allPaths_ - boolean - default `false` - if `true`, do not scope cookies by path. The default uses RFC-compliant path scoping. **Note**: may not be supported by the CookieStore `fetchCookies` function (the default MemoryCookieStore supports it). | ||
* _allPaths_ - boolean - default `false` - if `true`, do not scope cookies by path. The default uses RFC-compliant path scoping. **Note**: may not be supported by the underlying store (the default `MemoryCookieStore` supports it). | ||
The `.lastAccessed` property of the returned cookies will have been updated. | ||
.getCookiesSync(currentUrl, [{options}]) | ||
---------------------------------------- | ||
### `.getCookiesSync(currentUrl, [{options}])` | ||
Synchronous version of `getCookies`; only works with synchronous stores (e.g. the default `MemoryCookieStore`). | ||
.getCookieString(...) | ||
--------------------- | ||
### `.getCookieString(...)` | ||
Accepts the same options as `.getCookies()` but passes a string suitable for a Cookie header rather than an array to the callback. Simply maps the `Cookie` array via `.cookieString()`. | ||
.getCookieStringSync(...) | ||
------------------------- | ||
### `.getCookieStringSync(...)` | ||
Synchronous version of `getCookieString`; only works with synchronous stores (e.g. the default `MemoryCookieStore`). | ||
.getSetCookieStrings(...) | ||
------------------------- | ||
### `.getSetCookieStrings(...)` | ||
Returns an array of strings suitable for **Set-Cookie** headers. Accepts the same options as `.getCookies()`. Simply maps the cookie array via `.toString()`. | ||
.getSetCookieStringsSync(...) | ||
----------------------------- | ||
### `.getSetCookieStringsSync(...)` | ||
Synchronous version of `getSetCookieStrings`; only works with synchronous stores (e.g. the default `MemoryCookieStore`). | ||
Store | ||
===== | ||
### `.serialize(cb(err,serializedObject))` | ||
Base class for CookieJar stores. | ||
Serialize the Jar if the underlying store supports `.getAllCookies`. | ||
# CookieStore API | ||
**NOTE**: Custom `Cookie` properties will be discarded. If you want a property to be serialized, add the property name to the `Cookie.serializableProperties` Array. | ||
See [Serialization Format]. | ||
### `.serializeSync()` | ||
Sync version of .serialize | ||
### `.toJSON()` | ||
Alias of .serializeSync() for the convenience of `JSON.stringify(cookiejar)`. | ||
### `CookieJar.deserialize(serialized, [store], cb(err,object))` | ||
A new Jar is created and the serialized Cookies are added to the underlying store. Each `Cookie` is added via `store.putCookie` in the order in which they appear in the serialization. | ||
The `store` argument is optional, but should be an instance of `Store`. By default, a new instance of `MemoryCookieStore` is created. | ||
As a convenience, if `serialized` is a string, it is passed through `JSON.parse` first. If that throws an error, this is passed to the callback. | ||
### `CookieJar.deserializeSync(serialized, [store])` | ||
Sync version of `.deserialize`. _Note_ that the `store` must be synchronous for this to work. | ||
### `CookieJar.fromJSON(string)` | ||
Alias of `.deserializeSync` to provide consistency with `Cookie.fromJSON()`. | ||
### `.clone([store,]cb(err,newJar))` | ||
Produces a deep clone of this jar. Modifications to the original won't affect the clone, and vice versa. | ||
The `store` argument is optional, but should be an instance of `Store`. By default, a new instance of `MemoryCookieStore` is created. Transferring between store types is supported so long as the source implements `.getAllCookies()` and the destination implements `.putCookie()`. | ||
### `.cloneSync([store])` | ||
Synchronous version of `.clone`, returning a new `CookieJar` instance. | ||
The `store` argument is optional, but must be a _synchronous_ `Store` instance if specified. If not passed, a new instance of `MemoryCookieStore` is used. | ||
The _source_ and _destination_ must both be synchronous `Store`s. If one or both stores are asynchronous, use `.clone` instead. Recall that `MemoryCookieStore` supports both synchronous and asynchronous API calls. | ||
## Store | ||
Base class for CookieJar stores. Available as `tough.Store`. | ||
## Store API | ||
The storage model for each `CookieJar` instance can be replaced with a custom implementation. The default is `MemoryCookieStore` which can be found in the `lib/memstore.js` file. The API uses continuation-passing-style to allow for asynchronous stores. | ||
Stores should inherit from the base `Store` class, which is available as `require('tough-cookie').Store`. Stores are asynchronous by default, but if `store.synchronous` is set, then the `*Sync` methods on the CookieJar can be used. | ||
Stores should inherit from the base `Store` class, which is available as `require('tough-cookie').Store`. | ||
Stores are asynchronous by default, but if `store.synchronous` is set to `true`, then the `*Sync` methods on the of the containing `CookieJar` can be used (however, the continuation-passing style | ||
All `domain` parameters will have been normalized before calling. | ||
@@ -343,4 +378,3 @@ | ||
store.findCookie(domain, path, key, cb(err,cookie)) | ||
--------------------------------------------------- | ||
### `store.findCookie(domain, path, key, cb(err,cookie))` | ||
@@ -351,4 +385,3 @@ Retrieve a cookie with the given domain, path and key (a.k.a. name). The RFC maintains that exactly one of these cookies should exist in a store. If the store is using versioning, this means that the latest/newest such cookie should be returned. | ||
store.findCookies(domain, path, cb(err,cookies)) | ||
------------------------------------------------ | ||
### `store.findCookies(domain, path, cb(err,cookies))` | ||
@@ -363,4 +396,3 @@ Locates cookies matching the given domain and path. This is most often called in the context of `cookiejar.getCookies()` above. | ||
store.putCookie(cookie, cb(err)) | ||
-------------------------------- | ||
### `store.putCookie(cookie, cb(err))` | ||
@@ -373,8 +405,7 @@ Adds a new cookie to the store. The implementation SHOULD replace any existing cookie with the same `.domain`, `.path`, and `.key` properties -- depending on the nature of the implementation, it's possible that between the call to `fetchCookie` and `putCookie` that a duplicate `putCookie` can occur. | ||
store.updateCookie(oldCookie, newCookie, cb(err)) | ||
------------------------------------------------- | ||
### `store.updateCookie(oldCookie, newCookie, cb(err))` | ||
Update an existing cookie. The implementation MUST update the `.value` for a cookie with the same `domain`, `.path` and `.key`. The implementation SHOULD check that the old value in the store is equivalent to `oldCookie` - how the conflict is resolved is up to the store. | ||
The `.lastAccessed` property will always be different between the two objects and `.created` will always be the same. Stores MAY ignore or defer the `.lastAccessed` change at the cost of affecting how cookies are sorted (or selected for deletion). | ||
The `.lastAccessed` property will always be different between the two objects (to the precision possible via JavaScript's clock). Both `.creation` and `.creationIndex` are guaranteed to be the same. Stores MAY ignore or defer the `.lastAccessed` change at the cost of affecting how cookies are selected for automatic deletion (e.g., least-recently-used, which is up to the store to implement). | ||
@@ -387,4 +418,3 @@ Stores may wish to optimize changing the `.value` of the cookie in the store versus storing a new cookie. If the implementation doesn't define this method a stub that calls `putCookie(newCookie,cb)` will be added to the store object. | ||
store.removeCookie(domain, path, key, cb(err)) | ||
---------------------------------------------- | ||
### `store.removeCookie(domain, path, key, cb(err))` | ||
@@ -395,4 +425,3 @@ Remove a cookie from the store (see notes on `findCookie` about the uniqueness constraint). | ||
store.removeCookies(domain, path, cb(err)) | ||
------------------------------------------ | ||
### `store.removeCookies(domain, path, cb(err))` | ||
@@ -403,9 +432,44 @@ Removes matching cookies from the store. The `path` parameter is optional, and if missing means all paths in a domain should be removed. | ||
MemoryCookieStore | ||
================= | ||
### `store.getAllCookies(cb(err, cookies))` | ||
Inherits from Store. | ||
Produces an `Array` of all cookies during `jar.serialize()`. The items in the array can be true `Cookie` objects or generic `Object`s with the [Serialization Format] data structure. | ||
Just-in-memory CookieJar synchronous store implementation, used by default. | ||
Cookies SHOULD be returned in creation order to preserve sorting via `compareCookies()`. For reference, `MemoryCookieStore` will sort by `.creationIndex` since it uses true `Cookie` objects internally. If you don't return the cookies in creation order, they'll still be sorted by creation time, but this only has a precision of 1ms. See `compareCookies` for more detail. | ||
Pass an error if retrieval fails. | ||
## MemoryCookieStore | ||
Inherits from `Store`. | ||
A just-in-memory CookieJar synchronous store implementation, used by default. Despite being a synchronous implementation, it's usable with both the synchronous and asynchronous forms of the `CookieJar` API. | ||
# Serialization Format | ||
**NOTE**: if you want to have custom `Cookie` properties serialized, add the property name to `Cookie.serializableProperties`. | ||
```js | ||
{ | ||
// The version of tough-cookie that serialized this jar. | ||
version: 'tough-cookie@1.x.y', | ||
// add the store type, to make humans happy: | ||
storeType: 'MemoryCookieStore', | ||
// CookieJar configuration: | ||
rejectPublicSuffixes: true, | ||
// ... future items go here | ||
// Gets filled from jar.store.getAllCookies(): | ||
cookies: [ | ||
{ | ||
key: 'string', | ||
value: 'string', | ||
// ... | ||
/* other Cookie.serializableProperties go here */ | ||
} | ||
] | ||
} | ||
``` | ||
# Copyright and License | ||
@@ -412,0 +476,0 @@ |
@@ -36,2 +36,3 @@ /*! | ||
var Cookie = tough.Cookie; | ||
var CookieJar = tough.CookieJar; | ||
@@ -47,2 +48,67 @@ function toKeyArray(cookies) { | ||
.addBatch({ | ||
"Assumptions:": { | ||
".creationIndex is set during construction": function() { | ||
var now = new Date(); | ||
var c1 = new Cookie(); | ||
var c2 = new Cookie(); | ||
assert.isNumber(c1.creationIndex); | ||
assert.isNumber(c2.creationIndex); | ||
assert(c1.creationIndex < c2.creationIndex, | ||
'creationIndex should increase with each construction'); | ||
}, | ||
".creationIndex is set during construction (forced ctime)": function() { | ||
var now = new Date(); | ||
var c1 = new Cookie({creation: now}); | ||
var c2 = new Cookie({creation: now}); | ||
assert.strictEqual(c1.creation, c2.creation); | ||
assert.isNumber(c1.creationIndex); | ||
assert.isNumber(c2.creationIndex); | ||
assert(c1.creationIndex < c2.creationIndex, | ||
'creationIndex should increase with each construction'); | ||
}, | ||
".creationIndex is left alone during new setCookie": function() { | ||
var jar = new CookieJar(); | ||
var c = new Cookie({key:'k', value:'v', domain:'example.com'}); | ||
var now = new Date(); | ||
var beforeDate = c.creation; | ||
assert.instanceOf(beforeDate, Date); | ||
assert.notStrictEqual(now, beforeDate); | ||
var beforeIndex = c.creationIndex; | ||
assert.isNumber(c.creationIndex); | ||
jar.setCookieSync(c, 'http://example.com/', {now: now}); | ||
assert.strictEqual(c.creation, now); | ||
assert.strictEqual(c.creationIndex, beforeIndex); | ||
}, | ||
".creationIndex is preserved during update setCookie": function() { | ||
var jar = new CookieJar(); | ||
var thisMs = Date.now(); | ||
var t1 = new Date(thisMs); | ||
var t2 = new Date(thisMs); | ||
assert.notStrictEqual(t1, t2); // Date objects are distinct | ||
var c = new Cookie({key:'k', value:'v1', domain:'example.com'}); | ||
jar.setCookieSync(c, 'http://example.com/', {now: t1}); | ||
var originalIndex = c.creationIndex; | ||
assert.strictEqual(c.creation, t1); | ||
assert.strictEqual(c.lastAccessed, t1); | ||
c = new Cookie({key:'k', value:'v2', domain:'example.com'}); | ||
assert.notStrictEqual(c.creation, t1); // new timestamp assigned | ||
jar.setCookieSync(c, 'http://example.com/', {now: t2}); | ||
assert.strictEqual(c.creation, t1); // retained | ||
assert.strictEqual(c.lastAccessed, t2); // updated | ||
assert.strictEqual(c.creationIndex, originalIndex); // retained | ||
}, | ||
} | ||
}) | ||
.addBatch({ | ||
"Cookie Sorting": { | ||
@@ -49,0 +115,0 @@ topic: function () { |
@@ -126,4 +126,4 @@ /*! | ||
topic: function(f) { return f(null) }, | ||
"looks good": function(str) { | ||
assert.match(str, /"maxAge":null/); | ||
"absent": function(str) { | ||
assert.match(str, /(?!"maxAge":null)/); // NB: negative RegExp | ||
} | ||
@@ -130,0 +130,0 @@ } |
@@ -64,4 +64,3 @@ /*! | ||
var actual = jar.getCookiesSync(sentTo); | ||
actual = actual.sort(tough.cookieCompare); | ||
var actual = jar.getCookiesSync(sentTo,{sort:true}); | ||
@@ -68,0 +67,0 @@ assert.strictEqual(actual.length, expected.length); |
Sorry, the diff of this file is too big to display
544192
30
7055
487