total4
Version:
Total.js framework v4
829 lines (669 loc) • 19.1 kB
JavaScript
require('./index');
const COOKIEOPTIONS = { httponly: true, security: 'lax' };
const Fs = require('fs');
const filename = 'sessions{0}.txt';
function Session(name, ondata) {
if (typeof(name) === 'function') {
ondata = name;
name = null;
}
var t = this;
var timeoutsave = null;
t.name = name || '';
t.items = new Map();
t.$sync = true;
t.$savecallback = ERROR('session.save');
t.ondata = ondata;
t.pending = {};
t.ddos = {};
t.ddosis = false;
// t.onremove = function(item)
// t.onrelease = function(item)
// t.ondata = function(item, next(err, data))
t.$save = function() {
timeoutsave && clearTimeout(timeoutsave);
timeoutsave = setTimeout(t.$saveforce, 1000 * 10); // 10 seconds
};
t.$saveforce = function() {
var storage = [];
for (var m of t.items.values()) {
if (m.expire > NOW)
storage.push(encodeURIComponent(m.sessionid) + ';' + (m.id ? encodeURIComponent(m.id) : '') + ';' + m.expire.getTime() + ';' + (m.used ? m.used.getTime() : '') + ';' + (m.created ? m.created.getTime() : '') + ';' + (m.note ? encodeURIComponent(m.note) : '') + ';' + (m.settings ? encodeURIComponent(m.settings) : ''));
else {
t.onremove && t.onremove(m);
t.items.delete(m.sessionid);
}
}
Fs.writeFile(PATH.databases(filename.format((t.name && t.name !== 'default' ? ('_' + t.name) : ''))), storage.join('\n'), t.$savecallback);
timeoutsave = null;
};
}
const SessionProto = Session.prototype;
SessionProto.listlive = function(callback) {
var self = this;
var arr = [];
for (var m of self.items.values()) {
if (m && !m.released && m.data && m.expire >= NOW)
arr.push(m);
}
callback(null, arr);
};
SessionProto.list = function(id, callback) {
var self = this;
var arr = [];
for (var m of self.items.values()) {
if (m && m.expire >= NOW) {
if (m.id === id)
arr.push(m);
} else {
self.onremove && self.onremove(m);
self.items.delete(m.sessionid);
if (F.isCluster && self.$sync)
cluster_send({ method: 'remove', NAME: self.name, sessionid: m.sessionid });
if (!F.id || F.id === '0')
self.$save();
}
}
callback(null, arr);
};
SessionProto.has = function(sessionid, callback) {
callback(null, this.items.has(sessionid));
};
SessionProto.has2 = function(id, callback) {
for (var m of this.items.values()) {
if (m && m.expire >= NOW && m.id === id) {
callback(null, true);
return;
}
}
callback(null, false);
};
SessionProto.contains = function(sessionid, callback) {
var self = this;
var item = self.items.get(sessionid);
if (item && item.expire >= NOW && item.data && !item.released)
callback(null, item.data, item);
else
callback();
};
SessionProto.contains2 = function(id, callback) {
for (var m of this.items.values()) {
if (m && m.expire >= NOW && m.id === id && m.data && !m.released) {
callback(null, m.data, m);
return;
}
}
callback(null);
};
SessionProto.getcookie = function(req, opt, callback, param) {
// opt.name {String} A cookie name
// opt.expire {String} Expiration
// opt.key {String} Encrypt key
// opt.extendcookie {Boolean} Extends cookie expiration (default: true)
// opt.removecookie {Boolean} Removes cookie if isn't valid (default: true)
// opt.options {Object} A cookie options (default: undefined)
// opt.ddos {Number} Enable DDOS attempts
if (req.controller)
req = req.controller.req;
else if (req.req)
req = req.req;
var self = this;
// DDOS Protection
if (opt.ddos && self.ddos[req.ip] > opt.ddos) {
callback(null, null, null, null, param);
return;
}
var token = req.cookie(opt.name);
if (!token || token.length < 20) {
// remove cookies
if (token && opt.removecookie !== false)
req.res.cookie(opt.name, '', '-1 day');
callback(null, null, null, null, param);
return;
}
// IMPORTANT: "req.res" can be null cause of WebSocket
var value = DECRYPTREQ(req, token, opt.key || opt.secret);
if (value && typeof(value) === 'string') {
value = value.split(';');
if (req.res && opt.expire && opt.extendcookie !== false)
req.res.cookie(opt.name, token, opt.expire, opt.options || COOKIEOPTIONS);
self.get(value[0], opt.expire, function(err, data, meta, init) {
if ((err || !data)) {
if (opt.ddos) {
if (self.ddos[req.ip])
self.ddos[req.ip]++;
else {
self.ddos[req.ip] = 1;
self.ddosis = true;
}
}
if (req.res && opt.removecookie !== false)
req.res.cookie(opt.name, '', '-1 day');
} else
req.sessionid = meta.sessionid;
callback(err, data, meta, init, param);
});
} else {
// remove cookies
if (req.res && opt.removecookie !== false)
req.res.cookie(opt.name, '', '-1 day');
if (opt.ddos) {
if (self.ddos[req.ip])
self.ddos[req.ip]++;
else {
self.ddos[req.ip] = 1;
self.ddosis = true;
}
}
callback(null, null, null, null, param);
}
};
SessionProto.gettoken = function(req, opt, callback, param) {
// opt.token {String} a token
// opt.expire {String} Expiration
// opt.key {String} Encrypt key
// opt.ddos {Number} Enable DDOS attempts
var self = this;
if (req.req)
req = req.req;
// DDOS Protection
if (opt.ddos && self.ddos[req.ip] > opt.ddos) {
callback(null, null, null, null, param);
return;
}
var token = opt.token;
if (!token || token.length < 20) {
callback(null, null, null, null, param);
return;
}
// IMPORTANT: "req.res" can be null cause of WebSocket
var value = DECRYPTREQ(req, token, opt.key || opt.secret);
if (value && typeof(value) === 'string') {
value = value.split(';');
self.get(value[0], opt.expire, function(err, data, meta, init) {
if (!err && data)
req.sessionid = meta.sessionid;
else if (opt.ddos) {
if (self.ddos[req.ip])
self.ddos[req.ip]++;
else {
self.ddos[req.ip] = 1;
self.ddosis = true;
}
}
callback(err, data, meta, init, param);
});
} else {
if (opt.ddos) {
if (self.ddos[req.ip])
self.ddos[req.ip]++;
else {
self.ddos[req.ip] = 1;
self.ddosis = true;
}
}
callback(null, null, null, null, param);
}
};
SessionProto.usage = function() {
var o = {};
o.used = 0;
o.free = 0;
for (var m of this.items.values()) {
if (m.data)
o.used++;
else
o.free++;
}
o.count = o.used + o.free;
return o;
};
SessionProto.release = function(sessionid, expire, callback) {
if (sessionid && sessionid.sessionid)
sessionid = sessionid.sessionid;
if (typeof(expire) === 'function') {
callback = expire;
expire = null;
}
var self = this;
// We can't release data when the session doesn't have ".ondata" delegate implemented
if (!self.ondata)
return;
if (F.isCluster && F.id !== '0' && self.$sync)
cluster_send({ NAME: self.name, type: 'release', sessionid: sessionid, expire: expire });
// refreshes all
if (sessionid == null) {
var count = 0;
for (var m of self.items.values()) {
if (m.data) {
m.released = true;
self.onrelease && self.onrelease(m);
m.data = null;
count++;
}
}
callback && callback(null, count);
return;
}
var item = self.items.get(sessionid);
if (item) {
item.released = true;
self.onrelease && self.onrelease(item);
item.data = null;
}
callback && callback(null, item ? 1 : 0);
};
SessionProto.release2 = function(id, expire, callback) {
if (typeof(expire) === 'function') {
callback = expire;
expire = null;
}
var self = this;
// We can't release data when the session doesn't have ".ondata" delegate implemented
if (!self.ondata)
return;
var count = 0;
var exiration = expire ? NOW.add(expire) : null;
for (var m of self.items.values()) {
if (m && m.id === id && m.data) {
m.released = true;
self.onrelease && self.onrelease(m);
m.data = null;
count++;
if (exiration)
m.expire = exiration;
}
}
if (F.isCluster && F.id !== '0' && self.$sync)
cluster_send({ NAME: self.name, type: 'release2', id: id, expire: expire });
callback && callback(null, count);
};
SessionProto.releaseunused = function(lastusage, callback) {
var self = this;
var count = 0;
var lu = NOW.add(lastusage[0] === '-' ? lastusage : ('-' + lastusage));
for (var m of self.items.values()) {
if (m.data && (!m.used || m.used <= lu)) {
m.released = true;
self.onrelease && self.onrelease(m);
m.data = null;
count++;
}
}
if (F.isCluster && F.id !== '0')
self.$sync && cluster_send({ NAME: self.name, type: 'releaseunused', lastusage: lastusage });
callback && callback(null, count);
};
SessionProto.setcookie = function(res, opt, callback) {
// opt.name {String} A cookie name
// opt.sessionid {String} A unique session ID
// opt.id {String} Optional, custom ID
// opt.expire {String} Expiration
// opt.strict {Boolean} Strict comparing of cookie according to IP (default: false)
// opt.key {String} Encrypt key
// opt.data {Object} A session data
// opt.note {String} A simple note for this session
// opt.settings {String} Settings data for the session
// opt.options {Object} A cookie options (default: undefined)
if (res.controller)
res = res.controller.res;
else if (res.res)
res = res.res;
if (!opt.sessionid)
opt.sessionid = UID();
this.set(opt.sessionid, opt.id, opt.data, opt.expire, opt.note, opt.settings, function(err, item, meta) {
if (err) {
callback && callback(err);
} else {
var data = opt.sessionid + ';' + (opt.id || '');
var token = ENCRYPTREQ(res.req, data, opt.key || opt.secret, opt.strict);
res.cookie(opt.name, token, opt.expire, opt.options || COOKIEOPTIONS);
res.req.sessionid = opt.sessionid;
callback && callback(null, item, meta);
}
});
};
SessionProto.settoken = function(res, opt, callback) {
// opt.name {String} A cookie name
// opt.sessionid {String} A unique session ID
// opt.id {String} Optional, custom ID
// opt.expire {String} Expiration
// opt.strict {Boolean} Strict comparing of cookie according to IP (default: false)
// opt.key {String} Encrypt key
// opt.data {Object} A session data
// opt.note {String} A simple note for this session
// opt.settings {String} Settings data for the session
if (res.res)
res = res.res;
if (!opt.sessionid)
opt.sessionid = UID();
this.set(opt.sessionid, opt.id, opt.data, opt.expire, opt.note, opt.settings, function(err, item, meta) {
if (err) {
callback && callback(err);
} else {
var data = opt.sessionid + ';' + (opt.id || '');
var token = ENCRYPTREQ(res.req, data, opt.key || opt.secret, opt.strict);
res.req.sessionid = opt.sessionid;
callback && callback(null, token, item, meta);
}
});
};
SessionProto.set2 = function(id, data, expire, note, settings, callback) {
if (typeof(expire) === 'function') {
callback = expire;
expire = '';
} else if (typeof(note) === 'function') {
callback = note;
note = null;
} else if (typeof(settings) === 'function') {
callback = settings;
settings = null;
}
var self = this;
var updated = 0;
for (var m of self.items.values()) {
if (m && m.id === id && m.data) {
m.data = data;
if (expire)
m.expire = NOW.add(expire);
if (note != null)
m.note = note;
if (settings != null)
m.settings = settings;
updated++;
}
}
callback && callback(null, updated);
if (F.isCluster && self.$sync)
cluster_send({ method: 'set2', NAME: self.name, id: id, data: data, expire: expire, note: note, settings: settings });
if (updated && (!F.id || F.id === '0'))
self.$save();
};
SessionProto.set = function(sessionid, id, data, expire, note, settings, callback) {
if (typeof(id) === 'object') {
callback = settings;
settings = note;
note = expire;
expire = data;
data = id;
id = '';
}
if (typeof(note) === 'function') {
callback = note;
note = null;
} else if (typeof(settings) === 'function') {
callback = settings;
settings = null;
}
var self = this;
var obj = {};
obj.sessionid = sessionid;
obj.id = id == null ? '' : (id + '');
obj.expire = NOW.add(expire);
obj.data = data;
obj.note = note || '';
obj.created = NOW;
obj.settings = settings || '';
self.items.set(sessionid, obj);
if (F.isCluster && self.$sync)
cluster_send({ method: 'set', NAME: self.name, sessionid: sessionid, id: obj.id, data: data, expire: expire, note: note, settings: settings });
if (!F.id || F.id === '0')
self.$save();
callback && callback(null, data, obj);
};
SessionProto.get2 = function(id, callback) {
var self = this;
var output = [];
for (var m of self.items.values()) {
if (m && m.id === id && m.expire >= NOW) {
m.used = NOW;
output.push(m);
}
}
callback && callback(null, output);
};
SessionProto.get = function(sessionid, expire, callback) {
if (typeof(expire) === 'function') {
callback = expire;
expire = null;
}
var self = this;
var item = self.items.get(sessionid);
if (item) {
if (item.expire < NOW) {
self.onremove && self.onremove(item);
self.items.delete(sessionid);
item = null;
if (F.isCluster && self.$sync)
cluster_send({ method: 'remove', NAME: self.name, sessionid: sessionid });
if (!F.id || F.id === '0')
self.$save();
} else if (expire)
item.expire = NOW.add(expire);
}
// we need to load data
if (item) {
if (item.data == null && self.ondata) {
if (self.pending[item.id]) {
self.pending[item.id].push(callback);
return;
}
self.pending[item.id] = [];
self.ondata(item, function(err, data) {
if (item.released)
item.released = false;
item.data = data;
callback(err, data, item, true);
item.used = NOW;
var pending = self.pending[item.id];
for (var i = 0; i < pending.length; i++)
pending[i](err, data, item);
delete self.pending[item.id];
});
return;
}
}
callback(null, item ? item.data : null, item);
if (item)
item.used = NOW;
};
SessionProto.update2 = function(id, data, expire, note, settings, callback) {
if (typeof(expire) === 'function') {
callback = expire;
expire = null;
} else if (typeof(note) === 'function') {
callback = note;
note = null;
} else if (typeof(settings) === 'function') {
callback = settings;
settings = null;
}
var self = this;
var updated = 0;
if (expire)
expire = NOW.add(expire);
for (var m of self.items.values()) {
if (m && m.id === id) {
if (m.data)
m.data = data;
if (note != null)
m.note = note;
if (settings != null)
m.settings = settings;
if (expire)
m.expire = expire;
if (m.data || expire)
updated++;
}
}
if (F.isCluster && self.$sync)
cluster_send({ method: 'update2', NAME: self.name, id: id, data: data, expire: expire, note: note, settings: settings });
if (updated && (!F.id || F.id === '0'))
self.$save();
callback && callback(null, updated);
};
SessionProto.update = function(sessionid, data, expire, note, settings, callback) {
if (typeof(expire) === 'function') {
callback = expire;
expire = null;
} else if (typeof(note) === 'function') {
callback = note;
note = null;
} else if (typeof(settings) === 'function') {
callback = settings;
settings = null;
}
var self = this;
var item = self.items.get(sessionid);
if (item) {
if (item.data)
item.data = data;
if (note != null)
item.note = note;
if (settings != null)
item.settings = settings;
if (expire)
item.expire = NOW.add(expire);
if (F.isCluster && self.$sync)
cluster_send({ method: 'update', NAME: self.name, sessionid: sessionid, data: data, expire: expire, note: note, settings: settings });
if ((item.data || expire) && (!F.id || F.id === '0'))
self.$save();
if (callback) {
if (item.data)
callback(null, data, item);
else
callback();
}
} else if (callback)
callback();
};
SessionProto.count = function(id, callback) {
if (!callback) {
callback = id;
id = null;
}
var o = {};
o.used = 0;
o.free = 0;
for (var m of this.items.values()) {
if (id && m.id !== id)
continue;
if (m.data)
o.used++;
else
o.free++;
}
o.count = o.used + o.free;
callback(null, o);
};
SessionProto.remove2 = function(id, callback) {
var self = this;
var count = 0;
for (var m of self.items.values()) {
if (m && m.id === id) {
self.onremove && self.onremove(m);
self.items.delete(m.sessionid);
count++;
}
}
if (F.isCluster && self.$sync)
cluster_send({ method: 'remove2', NAME: self.name, id: id });
if (!F.id || F.id === '0')
self.$save();
callback && callback(null, count);
};
SessionProto.remove = function(sessionid, callback) {
if (sessionid && sessionid.sessionid)
sessionid = sessionid.sessionid;
var self = this;
var item = self.items.get(sessionid);
if (item) {
self.items.delete(sessionid);
if (F.isCluster && self.$sync)
cluster_send({ method: 'remove', NAME: self.name, sessionid: sessionid });
if (!F.id || F.id === '0')
self.$save();
}
callback && callback(null, item);
self.onremove && self.onremove(item);
};
SessionProto.clear = function(lastusage, callback) {
if (typeof(lastusage) === 'function') {
callback = lastusage;
lastusage = null;
}
var self = this;
var count = 0;
if (lastusage) {
var lu = NOW.add(lastusage[0] === '-' ? lastusage : ('-' + lastusage));
for (var m of self.items.values()) {
if (!m.used || m.used <= lu) {
self.onremove && self.onremove(m);
self.items.delete(m.sessionid);
count++;
}
}
} else {
count = self.items.length;
if (self.onremove) {
for (var m of self.items.values())
self.onremove(m);
}
self.items.clear();
}
if (F.isCluster && self.$sync)
cluster_send({ method: 'clear', NAME: self.name, lastusage: lastusage });
if (!F.id || F.id === '0')
self.$save();
callback && callback(null, count);
};
SessionProto.clean = function() {
var self = this;
var is = false;
for (var m of self.items.values()) {
if (m.expire < NOW) {
self.onremove && self.onremove(m);
self.items.delete(m.sessionid);
is = true;
}
}
if (is) {
if (F.isCluster && self.$sync)
cluster_send({ method: 'clean', NAME: self.name });
if (!F.id || F.id === '0')
self.$save();
}
};
SessionProto.load = function(callback) {
var self = this;
var removed = 0;
var data = [];
try {
data = Fs.readFileSync(PATH.databases(filename.format((self.name && self.name !== 'default' ? ('_' + self.name) : '')))).toString('utf8').split('\n');
} catch (e) {}
for (var i = 0; i < data.length; i++) {
var item = data[i].split(';');
var obj = {};
obj.sessionid = decodeURIComponent(item[0]);
obj.id = item[1] ? decodeURIComponent(item[1]) : '';
obj.expire = new Date(+item[2]);
obj.used = item[3] ? new Date(+item[3]) : null;
obj.created = item[4] ? new Date(+item[4]) : null;
obj.note = item[5] ? decodeURIComponent(item[5]) : '';
obj.settings = item[6] ? decodeURIComponent(item[6]) : '';
obj.data = null;
if (obj.expire > NOW)
self.items.set(obj.sessionid, obj);
else
removed++;
}
if (removed && (!F.id || F.id === '0'))
self.$save();
callback && callback();
};
function cluster_send(obj) {
obj.TYPE = 'session';
obj.ID = F.id;
process.send(obj);
}
exports.Session = Session;