jshinted
This commit is contained in:
Родитель
cedbdc4edf
Коммит
03c4bbad7e
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"bitwise": false,
|
||||
"boss": true,
|
||||
"browser": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"esnext": true,
|
||||
"eqeqeq": true,
|
||||
"eqnull": true,
|
||||
"expr": true,
|
||||
"forin": false,
|
||||
"indent": 2,
|
||||
"latedef": true,
|
||||
"laxbreak": true,
|
||||
"laxcomma": true,
|
||||
"maxcomplexity": 10,
|
||||
"maxlen": 80,
|
||||
"maxerr": 100,
|
||||
"node": true,
|
||||
"noarg": true,
|
||||
"passfail": false,
|
||||
"shadow": true,
|
||||
"strict": false,
|
||||
"supernew": false,
|
||||
"trailing": true,
|
||||
"undef": true,
|
||||
"unused": true
|
||||
}
|
|
@ -24,12 +24,17 @@ function base64urldecode(arg) {
|
|||
var s = arg;
|
||||
s = s.replace(/-/g, '+'); // 62nd char of encoding
|
||||
s = s.replace(/_/g, '/'); // 63rd char of encoding
|
||||
switch (s.length % 4) // Pad with trailing '='s
|
||||
{
|
||||
case 0: break; // No pad chars in this case
|
||||
case 2: s += "=="; break; // Two pad chars
|
||||
case 3: s += "="; break; // One pad char
|
||||
default: throw new Error("Illegal base64url string!");
|
||||
switch (s.length % 4) { // Pad with trailing '='s
|
||||
case 0:
|
||||
break; // No pad chars in this case
|
||||
case 2:
|
||||
s += "==";
|
||||
break; // Two pad chars
|
||||
case 3:
|
||||
s += "=";
|
||||
break; // One pad char
|
||||
default:
|
||||
throw new Error("Illegal base64url string!");
|
||||
}
|
||||
return new Buffer(s, 'base64'); // Standard base64 decoder
|
||||
}
|
||||
|
@ -44,132 +49,144 @@ function deriveKey(master, type) {
|
|||
function constantTimeEquals(a, b) {
|
||||
// Ideally this would be a native function, so it's less sensitive to how the
|
||||
// JS engine might optimize.
|
||||
if (a.length != b.length)
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
var ret = 0;
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
ret |= a.readUInt8(i) ^ b.readUInt8(i);
|
||||
}
|
||||
return ret == 0;
|
||||
return ret === 0;
|
||||
}
|
||||
|
||||
function encode(opts, content, duration, createdAt){
|
||||
// format will be:
|
||||
// iv.ciphertext.createdAt.duration.hmac
|
||||
// format will be:
|
||||
// iv.ciphertext.createdAt.duration.hmac
|
||||
|
||||
if (!opts.cookieName) {
|
||||
throw new Error('cookieName option required');
|
||||
} else if (String(opts.cookieName).indexOf(COOKIE_NAME_SEP) != -1) {
|
||||
throw new Error('cookieName cannot include "="');
|
||||
}
|
||||
if (!opts.cookieName) {
|
||||
throw new Error('cookieName option required');
|
||||
} else if (String(opts.cookieName).indexOf(COOKIE_NAME_SEP) !== -1) {
|
||||
throw new Error('cookieName cannot include "="');
|
||||
}
|
||||
|
||||
if (!opts.encryptionKey) {
|
||||
opts['encryptionKey'] = deriveKey(opts.secret, 'cookiesession-encryption');
|
||||
}
|
||||
if (!opts.encryptionKey) {
|
||||
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
|
||||
}
|
||||
|
||||
if (!opts.signatureKey) {
|
||||
opts['signatureKey'] = deriveKey(opts.secret, 'cookiesession-signature');
|
||||
}
|
||||
if (!opts.signatureKey) {
|
||||
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
|
||||
}
|
||||
|
||||
duration = duration || 24*60*60*1000;
|
||||
createdAt = createdAt || new Date().getTime();
|
||||
duration = duration || 24*60*60*1000;
|
||||
createdAt = createdAt || new Date().getTime();
|
||||
|
||||
// generate iv
|
||||
var iv = crypto.randomBytes(16);
|
||||
// generate iv
|
||||
var iv = crypto.randomBytes(16);
|
||||
|
||||
// encrypt with encryption key
|
||||
var plaintext = opts.cookieName + COOKIE_NAME_SEP + JSON.stringify(content);
|
||||
var cipher = crypto.createCipheriv('aes256', opts.encryptionKey, iv);
|
||||
var ciphertext = cipher.update(plaintext, 'utf8', 'binary');
|
||||
ciphertext += cipher.final('binary');
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
ciphertext = new Buffer(ciphertext, 'binary');
|
||||
// encrypt with encryption key
|
||||
var plaintext = opts.cookieName + COOKIE_NAME_SEP + JSON.stringify(content);
|
||||
var cipher = crypto.createCipheriv('aes256', opts.encryptionKey, iv);
|
||||
var ciphertext = cipher.update(plaintext, 'utf8', 'binary');
|
||||
ciphertext += cipher.final('binary');
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
ciphertext = new Buffer(ciphertext, 'binary');
|
||||
|
||||
// hmac it
|
||||
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
|
||||
hmacAlg.update(iv);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(ciphertext);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(createdAt.toString());
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(duration.toString());
|
||||
// hmac it
|
||||
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
|
||||
hmacAlg.update(iv);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(ciphertext);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(createdAt.toString());
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(duration.toString());
|
||||
|
||||
var hmac = hmacAlg.digest();
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
hmac = new Buffer(hmac, 'binary');
|
||||
var hmac = hmacAlg.digest();
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
hmac = new Buffer(hmac, 'binary');
|
||||
|
||||
return base64urlencode(iv) + "." + base64urlencode(ciphertext) + "." + createdAt + "." + duration + "." + base64urlencode(hmac);
|
||||
return [
|
||||
base64urlencode(iv),
|
||||
base64urlencode(ciphertext),
|
||||
createdAt,
|
||||
duration,
|
||||
base64urlencode(hmac)
|
||||
].join('.');
|
||||
}
|
||||
|
||||
function decode(opts, content) {
|
||||
|
||||
// stop at any time if there's an issue
|
||||
var components = content.split(".");
|
||||
if (components.length != 5)
|
||||
return;
|
||||
// stop at any time if there's an issue
|
||||
var components = content.split(".");
|
||||
if (components.length !== 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.cookieName) {
|
||||
throw new Error("cookieName option required");
|
||||
}
|
||||
if (!opts.cookieName) {
|
||||
throw new Error("cookieName option required");
|
||||
}
|
||||
|
||||
if (!opts.encryptionKey) {
|
||||
opts['encryptionKey'] = deriveKey(opts.secret, 'cookiesession-encryption');
|
||||
}
|
||||
if (!opts.encryptionKey) {
|
||||
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
|
||||
}
|
||||
|
||||
if (!opts.signatureKey) {
|
||||
opts['signatureKey'] = deriveKey(opts.secret, 'cookiesession-signature');
|
||||
}
|
||||
if (!opts.signatureKey) {
|
||||
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
|
||||
}
|
||||
|
||||
var iv = base64urldecode(components[0]);
|
||||
var ciphertext = base64urldecode(components[1]);
|
||||
var createdAt = parseInt(components[2], 10);
|
||||
var duration = parseInt(components[3], 10);
|
||||
var hmac = base64urldecode(components[4]);
|
||||
var iv = base64urldecode(components[0]);
|
||||
var ciphertext = base64urldecode(components[1]);
|
||||
var createdAt = parseInt(components[2], 10);
|
||||
var duration = parseInt(components[3], 10);
|
||||
var hmac = base64urldecode(components[4]);
|
||||
|
||||
// make sure IV is right length
|
||||
if (iv.length != 16)
|
||||
return;
|
||||
// make sure IV is right length
|
||||
if (iv.length !== 16) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check hmac
|
||||
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
|
||||
hmacAlg.update(iv);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(ciphertext);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(createdAt.toString());
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(duration.toString());
|
||||
// check hmac
|
||||
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
|
||||
hmacAlg.update(iv);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(ciphertext);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(createdAt.toString());
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(duration.toString());
|
||||
|
||||
var expected_hmac = hmacAlg.digest();
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
expected_hmac = new Buffer(expected_hmac, 'binary');
|
||||
var expectedHmac = hmacAlg.digest();
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
expectedHmac = new Buffer(expectedHmac, 'binary');
|
||||
|
||||
if (!constantTimeEquals(hmac, expected_hmac))
|
||||
return;
|
||||
if (!constantTimeEquals(hmac, expectedHmac)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// decrypt
|
||||
var cipher = crypto.createDecipheriv('aes256', opts.encryptionKey, iv);
|
||||
var plaintext = cipher.update(ciphertext, 'binary', 'utf8');
|
||||
plaintext += cipher.final('utf8');
|
||||
// decrypt
|
||||
var cipher = crypto.createDecipheriv('aes256', opts.encryptionKey, iv);
|
||||
var plaintext = cipher.update(ciphertext, 'binary', 'utf8');
|
||||
plaintext += cipher.final('utf8');
|
||||
|
||||
var cookieName = plaintext.substring(0, plaintext.indexOf(COOKIE_NAME_SEP));
|
||||
if (cookieName !== opts.cookieName) {
|
||||
return;
|
||||
}
|
||||
var cookieName = plaintext.substring(0, plaintext.indexOf(COOKIE_NAME_SEP));
|
||||
if (cookieName !== opts.cookieName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
content: JSON.parse(plaintext.substring(plaintext.indexOf(COOKIE_NAME_SEP) + 1)),
|
||||
createdAt: createdAt,
|
||||
duration: duration
|
||||
};
|
||||
} catch (x) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return {
|
||||
content: JSON.parse(
|
||||
plaintext.substring(plaintext.indexOf(COOKIE_NAME_SEP) + 1)
|
||||
),
|
||||
createdAt: createdAt,
|
||||
duration: duration
|
||||
};
|
||||
} catch (x) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -205,21 +222,27 @@ function Session(req, res, cookies, opts) {
|
|||
}
|
||||
|
||||
// here, we check that the security bits are set correctly
|
||||
var secure = (res.socket && res.socket.encrypted) || (req.connection && req.connection.proxySecure);
|
||||
if (opts.cookie.secure && !secure)
|
||||
throw new Error("you cannot have a secure cookie unless the socket is secure or you declare req.connection.proxySecure to be true.");
|
||||
var secure = (res.socket && res.socket.encrypted) ||
|
||||
(req.connection && req.connection.proxySecure);
|
||||
if (opts.cookie.secure && !secure) {
|
||||
throw new Error("you cannot have a secure cookie unless the socket is " +
|
||||
" secure or you declare req.connection.proxySecure to be true.");
|
||||
}
|
||||
}
|
||||
|
||||
Session.prototype = {
|
||||
updateDefaultExpires: function() {
|
||||
if (this.opts.cookie.maxAge) return;
|
||||
if (this.opts.cookie.maxAge) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.opts.cookie.ephemeral) {
|
||||
this.expires = null;
|
||||
} else {
|
||||
var time = this.createdAt || new Date().getTime();
|
||||
// the cookie should expire when it becomes invalid
|
||||
// we add an extra second because the conversion to a date truncates the milliseconds
|
||||
// we add an extra second because the conversion to a date
|
||||
// truncates the milliseconds
|
||||
this.expires = new Date(time + this.duration + 1000);
|
||||
}
|
||||
},
|
||||
|
@ -228,8 +251,9 @@ Session.prototype = {
|
|||
var self = this;
|
||||
Object.keys(this._content).forEach(function(k) {
|
||||
// exclude this key if it's meant to be preserved
|
||||
if (keysToPreserve && (keysToPreserve.indexOf(k) > -1))
|
||||
if (keysToPreserve && (keysToPreserve.indexOf(k) > -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete self._content[k];
|
||||
});
|
||||
|
@ -248,8 +272,9 @@ Session.prototype = {
|
|||
if (ephemeral && this.opts.cookie.maxAge) {
|
||||
throw new Error("you cannot have an ephemeral cookie with a maxAge.");
|
||||
}
|
||||
if (!this.loaded)
|
||||
if (!this.loaded) {
|
||||
this.loadFromCookie(true);
|
||||
}
|
||||
this.dirty = true;
|
||||
this.duration = newDuration;
|
||||
this.createdAt = new Date().getTime();
|
||||
|
@ -267,7 +292,9 @@ Session.prototype = {
|
|||
this.clearContent();
|
||||
|
||||
var unboxed = decode(this.opts, content);
|
||||
if (!unboxed) return;
|
||||
if (!unboxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
|
@ -295,7 +322,7 @@ Session.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
loadFromCookie: function(force_reset) {
|
||||
loadFromCookie: function(forceReset) {
|
||||
var cookie = this.cookies.get(this.opts.cookieName);
|
||||
if (cookie) {
|
||||
this.unbox(cookie);
|
||||
|
@ -303,16 +330,16 @@ Session.prototype = {
|
|||
var expiresAt = this.createdAt + this.duration;
|
||||
var now = Date.now();
|
||||
// should we reset this session?
|
||||
if (expiresAt < now)
|
||||
if (expiresAt < now) {
|
||||
this.reset();
|
||||
// if expiration is soon, push back a few minutes to not interrupt user
|
||||
else if (expiresAt - now < this.activeDuration) {
|
||||
} else if (expiresAt - now < this.activeDuration) {
|
||||
this.createdAt += this.activeDuration;
|
||||
this.dirty = true;
|
||||
this.updateDefaultExpires();
|
||||
}
|
||||
} else {
|
||||
if (force_reset) {
|
||||
if (forceReset) {
|
||||
this.reset();
|
||||
} else {
|
||||
return false; // didn't actually load the cookie
|
||||
|
@ -338,11 +365,11 @@ Object.defineProperty(Session.prototype, 'content', {
|
|||
return this._content;
|
||||
},
|
||||
set: function setContent(value) {
|
||||
Object.defineProperty(value, 'reset', {
|
||||
Object.defineProperty(value, 'reset', {
|
||||
enumerable: false,
|
||||
value: this.reset.bind(this)
|
||||
});
|
||||
Object.defineProperty(value, 'setDuration', {
|
||||
Object.defineProperty(value, 'setDuration', {
|
||||
enumerable: false,
|
||||
value: this.setDuration.bind(this)
|
||||
});
|
||||
|
@ -352,21 +379,25 @@ Object.defineProperty(Session.prototype, 'content', {
|
|||
|
||||
|
||||
function clientSessionFactory(opts) {
|
||||
if (!opts)
|
||||
throw "no options provided, some are required"; // XXX rename opts?
|
||||
if (!opts) {
|
||||
throw new Error("no options provided, some are required");
|
||||
}
|
||||
|
||||
if (!opts.secret)
|
||||
throw "cannot set up sessions without a secret";
|
||||
if (!opts.secret) {
|
||||
throw new Error("cannot set up sessions without a secret");
|
||||
}
|
||||
|
||||
// defaults
|
||||
opts.cookieName = opts.cookieName || "session_state";
|
||||
opts.duration = opts.duration || 24*60*60*1000;
|
||||
opts.activeDuration = 'activeDuration' in opts ? opts.activeDuration : ACTIVE_DURATION;
|
||||
opts.activeDuration = 'activeDuration' in opts ?
|
||||
opts.activeDuration : ACTIVE_DURATION;
|
||||
|
||||
// set up cookie defaults
|
||||
opts.cookie = opts.cookie || {};
|
||||
if (typeof(opts.cookie.httpOnly) == 'undefined')
|
||||
if (typeof opts.cookie.httpOnly === 'undefined') {
|
||||
opts.cookie.httpOnly = true;
|
||||
}
|
||||
|
||||
// let's not default to secure just yet,
|
||||
// as this depends on the socket being secure,
|
||||
|
@ -393,7 +424,9 @@ function clientSessionFactory(opts) {
|
|||
rawSession = new Session(req, res, cookies, opts);
|
||||
} catch (x) {
|
||||
// this happens only if there's a big problem
|
||||
process.nextTick(function() {next("client-sessions error: " + x.toString());});
|
||||
process.nextTick(function() {
|
||||
next("client-sessions error: " + x.toString());
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -415,11 +448,11 @@ function clientSessionFactory(opts) {
|
|||
res.writeHead = function () {
|
||||
rawSession.updateCookie();
|
||||
return writeHead.apply(res, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = clientSessionFactory;
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче