427 lines
14 KiB
JavaScript
427 lines
14 KiB
JavaScript
var _ = require('../util').lodash,
|
|
PropertyBase = require('./property-base').PropertyBase,
|
|
PropertyList = require('./property-list').PropertyList,
|
|
|
|
E = '',
|
|
EQ = '=',
|
|
PAIR_SPLIT_REGEX = /; */,
|
|
COOKIES_SEPARATOR = '; ',
|
|
|
|
/**
|
|
* Enum for all the Cookie attributes.
|
|
*
|
|
* @private
|
|
* @readonly
|
|
* @enum {string} CookieAttributes
|
|
*/
|
|
cookieAttributes = {
|
|
httponly: 'httpOnly',
|
|
secure: 'secure',
|
|
domain: 'domain',
|
|
path: 'path',
|
|
'max-age': 'maxAge',
|
|
session: 'session',
|
|
expires: 'expires'
|
|
},
|
|
|
|
Cookie;
|
|
|
|
/**
|
|
* The following is the object structure accepted as constructor parameter while calling `new Cookie(...)`. It is
|
|
* also the structure exported when {@link Property#toJSON} or {@link Property#toObjectResolved} is called on a
|
|
* Cookie instance.
|
|
*
|
|
* @typedef Cookie.definition
|
|
*
|
|
* @property {String=} [key] The name of the cookie. Some call it the "name".
|
|
* @property {String=} [value] The value stored in the Cookie
|
|
* @property {String=} [expires] Expires sets an expiry date for when a cookie gets deleted. It should either be a
|
|
* date object or timestamp string of date.
|
|
* @property {Number=} [maxAge] Max-age sets the time in seconds for when a cookie will be deleted.
|
|
* @property {String=} [domain] Indicates the domain(s) for which the cookie should be sent.
|
|
* @property {String=} [path] Limits the scope of the cookie to a specified path, e.g: "/accounts"
|
|
* @property {Boolean=} [secure] A secure cookie will only be sent to the server when a request is made using SSL and
|
|
* the HTTPS protocol.
|
|
* The idea that the contents of the cookie are of high value and could be potentially damaging to transmit
|
|
* as clear text.
|
|
* @property {Boolean=} [httpOnly] The idea behind HTTP-only cookies is to instruct a browser that a cookie should never
|
|
* be accessible via JavaScript through the document.cookie property. This feature was designed as a security measure
|
|
* to help prevent cross-site scripting (XSS) attacks perpetrated by stealing cookies via JavaScript.
|
|
* @property {Boolean=} [hostOnly] Indicates that this cookie is only valid for the given domain (and not its parent or
|
|
* child domains.)
|
|
* @property {Boolean=} [session] Indicates whether this is a Session Cookie. (A transient cookie, which is deleted at
|
|
* the end of an HTTP session.)
|
|
* @property {Array=} [extensions] Any extra attributes that are extensions to the original Cookie specification can be
|
|
* specified here.
|
|
* @property {String} [extensions[].key] Name of the extension.
|
|
* @property {String} [extensions[].value] Value of the extension
|
|
*
|
|
* @example <caption>JSON definition of an example cookie</caption>
|
|
* {
|
|
* "key": "my-cookie-name",
|
|
* "expires": "1464769543832",
|
|
* // UNIX timestamp, in *milliseconds*
|
|
* "maxAge": "300",
|
|
* // In seconds. In this case, the Cookie is valid for 5 minutes
|
|
* "domain": "something.example.com",
|
|
* "path": "/",
|
|
* "secure": false,
|
|
* "httpOnly": true,
|
|
* "session": false,
|
|
* "value": "my-cookie-value",
|
|
* "extensions": [{
|
|
* "key": "Priority",
|
|
* "value": "HIGH"
|
|
* }]
|
|
* }
|
|
*/
|
|
_.inherit((
|
|
|
|
/**
|
|
* A Postman Cookie definition that comprehensively represents an HTTP Cookie.
|
|
*
|
|
* @constructor
|
|
* @extends {PropertyBase}
|
|
*
|
|
* @param {Cookie.definition} [options] Pass the initial definition of the Cookie.
|
|
* @example <caption>Create a new Cookie</caption>
|
|
* var Cookie = require('postman-collection').Cookie,
|
|
* myCookie = new Cookie({
|
|
* name: 'my-cookie-name',
|
|
* expires: '1464769543832', // UNIX timestamp, in *milliseconds*
|
|
* maxAge: '300', // In seconds. In this case, the Cookie is valid for 5 minutes
|
|
* domain: 'something.example.com',
|
|
* path: '/',
|
|
* secure: false,
|
|
* httpOnly: true,
|
|
* session: false,
|
|
* value: 'my-cookie-value',
|
|
* extensions: [{
|
|
* key: 'Priority',
|
|
* value: 'HIGH'
|
|
* }]
|
|
* });
|
|
*
|
|
* @example <caption>Parse a Cookie Header</caption>
|
|
* var Cookie = require('postman-collection').Cookie,
|
|
* rawHeader = 'myCookie=myValue;Path=/;Expires=Sun, 04-Feb-2018 14:18:27 GMT;Secure;HttpOnly;Priority=HIGH'
|
|
* myCookie = new Cookie(rawHeader);
|
|
*
|
|
* console.log(myCookie.toJSON());
|
|
*/
|
|
Cookie = function PostmanCookie (options) {
|
|
// this constructor is intended to inherit and as such the super constructor is required to be executed
|
|
Cookie.super_.call(this, options);
|
|
|
|
_.isString(options) && (options = Cookie.parse(options));
|
|
|
|
options && this.update(options);
|
|
}), PropertyBase);
|
|
|
|
_.assign(Cookie.prototype, /** @lends Cookie.prototype */ {
|
|
update (options) {
|
|
_.mergeDefined(this, /** @lends Cookie.prototype */ {
|
|
/**
|
|
* The name of the cookie.
|
|
*
|
|
* @type {String}
|
|
*/
|
|
name: _.choose(options.name, options.key),
|
|
|
|
/**
|
|
* Expires sets an expiry date for when a cookie gets deleted. It should either be a date object or
|
|
* timestamp string of date.
|
|
*
|
|
* @type {Date|String}
|
|
*
|
|
* @note
|
|
* The value for this option is a date in the format Wdy, DD-Mon-YYYY HH:MM:SS GMT such as
|
|
* "Sat, 02 May 2009 23:38:25 GMT". Without the expires option, a cookie has a lifespan of a single session.
|
|
* A session is defined as finished when the browser is shut down, so session cookies exist only while the
|
|
* browser remains open. If the expires option is set to a date that appears in the past, then the cookie is
|
|
* immediately deleted in browser.
|
|
*
|
|
* @todo Accept date object and convert stringified date (timestamp only) to date object
|
|
* @todo Consider using Infinity as a default
|
|
*/
|
|
expires: _.isString(options.expires) ? new Date(options.expires) : options.expires,
|
|
|
|
/**
|
|
* Max-age sets the time in seconds for when a cookie will be deleted.
|
|
*
|
|
* @type {Number}
|
|
*/
|
|
maxAge: _.has(options, 'maxAge') ? Number(options.maxAge) : undefined,
|
|
|
|
/**
|
|
* Indicates the domain(s) for which the cookie should be sent.
|
|
*
|
|
* @type {String}
|
|
*
|
|
* @note
|
|
* By default, domain is set to the host name of the page setting the cookie, so the cookie value is sent
|
|
* whenever a request is made to the same host name. The value set for the domain option must be part of the
|
|
* host name that is sending the Set-Cookie header. The SDK does not perform this check, but the underlying
|
|
* client that actually sends the request could do it automatically.
|
|
*/
|
|
domain: options.domain,
|
|
|
|
/**
|
|
* @type {String}
|
|
*
|
|
* @note
|
|
* On server, the default value for the path option is the path of the URL that sent the Set-Cookie header.
|
|
*/
|
|
path: options.path,
|
|
|
|
/**
|
|
* A secure cookie will only be sent to the server when a request is made using SSL and the HTTPS protocol.
|
|
* The idea that the contents of the cookie are of high value and could be potentially damaging to transmit
|
|
* as clear text.
|
|
*
|
|
* @type {Boolean}
|
|
*/
|
|
secure: _.has(options, 'secure') ? Boolean(options.secure) : undefined,
|
|
|
|
/**
|
|
* The idea behind HTTP-only cookies is to instruct a browser that a cookie should never be accessible via
|
|
* JavaScript through the document.cookie property. This feature was designed as a security measure to help
|
|
* prevent cross-site scripting (XSS) attacks perpetrated by stealing cookies via JavaScript.
|
|
*
|
|
* @type {Boolean}
|
|
*/
|
|
httpOnly: _.has(options, 'httpOnly') ? Boolean(options.httpOnly) : undefined,
|
|
|
|
/**
|
|
* @type {Boolean}
|
|
*/
|
|
hostOnly: _.has(options, 'hostOnly') ? Boolean(options.hostOnly) : undefined,
|
|
|
|
/**
|
|
* Indicates whether this is a Session Cookie.
|
|
*
|
|
* @type {Boolean}
|
|
*/
|
|
session: _.has(options, 'session') ? Boolean(options.session) : undefined,
|
|
|
|
/**
|
|
* @note The commonly held belief is that cookie values must be URL-encoded, but this is a fallacy even
|
|
* though it is the de facto implementation. The original specification indicates that only three types of
|
|
* characters must be encoded: semicolon, comma, and white space. The specification indicates that URL
|
|
* encoding may be used but stops short of requiring it. The RFC makes no mention of encoding whatsoever.
|
|
* Still, almost all implementations perform some sort of URL encoding on cookie values.
|
|
* @type {String}
|
|
*/
|
|
value: options.value ? _.ensureEncoded(options.value) : undefined,
|
|
|
|
/**
|
|
* Any extra parameters that are not strictly a part of the Cookie spec go here.
|
|
*
|
|
* @type {Array}
|
|
*/
|
|
extensions: options.extensions || undefined
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Get the value of this cookie.
|
|
*
|
|
* @returns {String}
|
|
*/
|
|
valueOf () {
|
|
try {
|
|
return decodeURIComponent(this.value);
|
|
}
|
|
// handle malformed URI sequence
|
|
// refer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Malformed_URI
|
|
catch (error) {
|
|
/* istanbul ignore next */
|
|
return this.value;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Converts the Cookie to a single Set-Cookie header string.
|
|
*
|
|
* @returns {String}
|
|
*/
|
|
toString () {
|
|
var str = Cookie.unparseSingle(this);
|
|
|
|
if (this.expires && this.expires instanceof Date) {
|
|
// check for valid date
|
|
if (!Number.isNaN(this.expires.getTime())) {
|
|
str += '; Expires=' + this.expires.toUTCString();
|
|
}
|
|
}
|
|
else if (this.expires) {
|
|
str += '; Expires=' + this.expires;
|
|
}
|
|
|
|
if (this.maxAge && 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(({ key, value }) => {
|
|
str += `; ${key}`;
|
|
str += value === true ? '' : `=${value}`;
|
|
});
|
|
}
|
|
|
|
return str;
|
|
}
|
|
});
|
|
|
|
_.assign(Cookie, /** @lends Cookie */ {
|
|
|
|
/**
|
|
* Defines the name of this property for internal use.
|
|
*
|
|
* @private
|
|
* @readOnly
|
|
* @type {String}
|
|
*/
|
|
_postman_propertyName: 'Cookie',
|
|
|
|
// define behaviour of this object when put in list
|
|
_postman_propertyIndexKey: 'name',
|
|
_postman_propertyIndexCaseInsensitive: true,
|
|
_postman_propertyAllowsMultipleValues: true,
|
|
|
|
/**
|
|
* Check whether an object is an instance of PostmanCookie.
|
|
*
|
|
* @param {*} obj -
|
|
* @returns {Boolean}
|
|
*/
|
|
isCookie: function (obj) {
|
|
return Boolean(obj) && ((obj instanceof Cookie) ||
|
|
_.inSuperChain(obj.constructor, '_postman_propertyName', Cookie._postman_propertyName));
|
|
},
|
|
|
|
/**
|
|
* Stringifies an Array or {@link PropertyList} of Cookies into a single string.
|
|
*
|
|
* @param {Cookie[]} cookies - List of cookie definition object
|
|
* @returns {String}
|
|
*/
|
|
unparse: function (cookies) {
|
|
if (!_.isArray(cookies) && !PropertyList.isPropertyList(cookies)) {
|
|
return E;
|
|
}
|
|
|
|
return cookies.map(Cookie.unparseSingle).join(COOKIES_SEPARATOR);
|
|
},
|
|
|
|
/**
|
|
* Unparses a single Cookie.
|
|
*
|
|
* @param {Cookie} cookie - Cookie definition object
|
|
* @returns {String}
|
|
*/
|
|
unparseSingle: function (cookie) {
|
|
if (!_.isObject(cookie)) { return E; }
|
|
|
|
var value = _.isNil(cookie.value) ? E : cookie.value;
|
|
|
|
// for the empty name, return just the value to match the browser behavior
|
|
if (!cookie.name) {
|
|
return value;
|
|
}
|
|
|
|
return cookie.name + EQ + value;
|
|
},
|
|
|
|
/**
|
|
* Cookie header parser
|
|
*
|
|
* @param {String} str -
|
|
* @returns {*} A plain cookie options object, use it to create a new Cookie
|
|
*/
|
|
parse: function (str) {
|
|
if (!_.isString(str)) {
|
|
return str;
|
|
}
|
|
|
|
var obj = {},
|
|
pairs = str.split(PAIR_SPLIT_REGEX),
|
|
nameval;
|
|
|
|
nameval = Cookie.splitParam(pairs.shift()); // The first kvp is the name and value
|
|
obj.key = nameval.key;
|
|
obj.value = nameval.value;
|
|
|
|
pairs.forEach(function (pair) {
|
|
var keyval = Cookie.splitParam(pair),
|
|
value = keyval.value,
|
|
keyLower = keyval.key.toLowerCase();
|
|
|
|
if (cookieAttributes[keyLower]) {
|
|
obj[cookieAttributes[keyLower]] = value;
|
|
}
|
|
else {
|
|
obj.extensions = obj.extensions || [];
|
|
obj.extensions.push(keyval);
|
|
}
|
|
});
|
|
// Handle the hostOnly flag
|
|
if (!obj.domain) {
|
|
obj.hostOnly = true;
|
|
}
|
|
|
|
return obj;
|
|
},
|
|
|
|
/**
|
|
* Converts the Cookie to a single Set-Cookie header string.
|
|
*
|
|
* @param {Cookie} cookie - Cookie definition object
|
|
* @returns {String}
|
|
*/
|
|
stringify: function (cookie) {
|
|
return Cookie.prototype.toString.call(cookie);
|
|
},
|
|
|
|
/**
|
|
* Splits a Cookie parameter into a key and a value
|
|
*
|
|
* @private
|
|
* @param {String} param -
|
|
* @returns {{key: *, value: (Boolean|*)}}
|
|
*/
|
|
splitParam: function (param) {
|
|
var split = param.split('='),
|
|
key, value;
|
|
|
|
key = split[0].trim();
|
|
value = _.isString(split[1]) ? split[1].trim() : true;
|
|
|
|
if (_.isString(value) && value[0] === '"') {
|
|
value = value.slice(1, -1);
|
|
}
|
|
|
|
return { key, value };
|
|
}
|
|
});
|
|
|
|
module.exports = {
|
|
Cookie
|
|
};
|