UNPKG

@nodefony/http-bundle

Version:

Nodefony Framework Bundle HTTP

618 lines (575 loc) 18.4 kB
/* * * CLASS SESSION */ // eslint-disable-next-line init-declarations let crypto; try { crypto = require("node:crypto"); } catch (err) { crypto = require("crypto"); // console.log('crypto support is disabled!', err); } const { createHash, createCipheriv, createDecipheriv, randomBytes } = crypto; // eslint-disable-next-line max-lines-per-function nodefony.register("Session", () => { const checkSecureReferer = function checkSecureReferer (context) { let host = null; switch (this.context.type) { case "HTTP": case "HTTPS": case "HTTP2": host = context.getHost(); break; case "WEBSOCKET": case "WEBSOCKET SECURE": // eslint-disable-next-line prefer-destructuring host = this.context.request.httpRequest.headers.host; break; default: } const meta = this.getMetaBag("host"); if (host === meta) { return host; } this.manager.log(`SESSION START WARNING REFERRER NOT SAME, HOST : ${host} ,META STORAGE :${meta}`, "WARNING"); // eslint-disable-next-line no-throw-literal throw { meta, host }; }; const setMetasSession = function setMetasSession (cookieSetting = {}) { // let time = new Date(); let ua = null; this.setMetaBag("lifetime", cookieSetting.maxAge || this.settings.cookie.maxAge); this.setMetaBag("context", this.contextSession || null); this.setMetaBag("request", this.context.type); // this.setMetaBag("created", time); try { this.setMetaBag("remoteAddress", this.context.getRemoteAddress()); this.setMetaBag("host", this.context.getHost()); ua = this.context.getUserAgent(); } catch (e) { this.log(e, "DEBUG"); } if (ua) { this.setMetaBag("user_agent", ua); } else { this.setMetaBag("user_agent", "Not Defined"); } }; const Session = class Session extends nodefony.Container { constructor (name, settings, storage, manager) { super(); if (!storage) { this.status = "disabled"; } else { this.status = "none"; // active | disabled } this.manager = manager; this.strategy = this.manager.sessionStrategy; this.strategyNone = false; this.log = manager.log.bind(manager); this.setName(name); this.id = null; this.settings = settings; this.storage = storage; this.context = null; this.contextSession = "default"; this.lifetime = this.settings.cookie.maxAge; this.saved = false; this.flashBag = {}; this.key = this.settings.encrypt.password; this.iv = this.settings.encrypt.iv; this.created = null; this.updated = null; this.username = null; } get (name) { if (!name) { return this.protoService.prototype; } return super.get(name); } encrypt (text) { const cipher = createCipheriv("aes-256-ctr", this.key, this.iv); let encrypted = cipher.update(text, "utf8", "hex"); encrypted += cipher.final("hex"); return encrypted; } decrypt (text) { const decipher = createDecipheriv("aes-256-ctr", this.key, this.iv); let decrypted = decipher.update(text, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } create (lifetime, id, settingsCookie = {}) { this.id = id || this.setId(); const defaultSetting = nodefony.extend({}, this.settings.cookie); settingsCookie = nodefony.extend(defaultSetting, settingsCookie); this.manager.log(`NEW SESSION CREATE : ${this.id}`, "DEBUG"); try { this.cookieSession = this.setCookieSession(lifetime, settingsCookie); setMetasSession.call(this, settingsCookie); this.status = "active"; return this; } catch (e) { this.log(e, "ERROR"); throw new Error("Request can't create cookieSession"); // this.log(`,"WARNING") } } start (context, contextSession) { this.context = context; if (!contextSession) { // eslint-disable-next-line prefer-destructuring contextSession = this.contextSession; } if (this.settings.use_only_cookies) { this.applyTranId = 0; } else { this.applyTranId = this.settings.use_trans_sid; } try { const ret = this.checkStatus(); switch (ret) { case false: return new Promise((resolve /* , reject*/) => resolve(this)); case "restart": return this.start(context, contextSession) .catch((e) => { throw e; }); default: return this.getSession(contextSession) .catch((e) => { throw e; }); } } catch (e) { this.log(e, "ERROR"); throw e; } } checkStatus () { switch (this.status) { case "active": this.manager.log(`SESSION ALLREADY STARTED ==> ${this.name} : ${this.id}`, "WARNING"); return false; case "disabled": try { this.storage = this.manager.initializeStorage(); if (this.storage) { this.status = "none"; return "restart"; } } catch (e) { this.manager.log("SESSION STORAGE HANDLER NOT FOUND ", "ERROR"); throw new Error("SESSION STORAGE HANDLER NOT FOUND "); } break; default: return true; } return true; } async getSession (contextSession) { if (this.settings.use_cookies) { if (this.context.cookieSession) { this.id = this.getId(this.context.cookieSession.value); this.cookieSession = this.context.cookieSession; } this.applyTranId = 0; } if (!this.settings.use_only_cookies && !this.id) { if (this.name in this.context.request.query) { this.id = this.getId(this.context.request.query[this.name]); } } if (this.id) { return this.checkChangeContext(contextSession) .catch((e) => { throw e; }); } try { this.clear(); return this.create(this.lifetime, null); } catch (e) { throw e; } } async checkChangeContext (contextSession) { // change context session if (contextSession && this.contextSession !== contextSession) { this.manager.log(`SESSION CONTEXT CHANGE : ${this.contextSession} ==> ${contextSession}`); switch (this.strategy) { case "migrate": return this.storage.start(this.id, this.contextSession) .then(async (result) => { this.deSerialize(result); if (!this.isValidSession(result, this.context)) { this.manager.log(`INVALID SESSION ==> ${this.name} : ${this.id}`, "WARNING"); await this.destroy(); this.contextSession = contextSession; return this.create(this.lifetime, null); } await this.removeSession(); this.manager.log(`STRATEGY MIGRATE SESSION ==> ${this.name} : ${this.id}`, "DEBUG"); this.migrated = true; this.contextSession = contextSession; return this.create(this.lifetime, null); }) .catch((error) => { throw error; }); case "invalidate": this.manager.log(`STRATEGY INVALIDATE SESSION ==> ${this.name} : ${this.id}`, "DEBUG"); await this.destroy(); this.contextSession = contextSession; return new Promise((resolve, reject) => { try { resolve(this.create(this.lifetime, null)); } catch (e) { reject(e); } }); case "none": this.strategyNone = true; break; default: } if (!this.strategyNone) { return new Promise((resolve /* , reject*/) => resolve(this)); } } return this.storage.start(this.id, this.contextSession) .then(async (result) => { try { if (result && Object.keys(result).length) { this.deSerialize(result); if (!this.isValidSession(result, this.context)) { this.manager.log(`SESSION ==> ${this.name} : ${this.id} session invalid `, "ERROR"); await this.invalidate(); } } else if (this.settings.use_strict_mode) { if (!this.strategyNone) { this.manager.log(`SESSION ==> ${this.name} : ${this.id} use_strict_mode `, "ERROR"); await this.invalidate(); } } this.status = "active"; return this; } catch (e) { throw e; } }) .catch(async (error) => { if (error) { try { this.manager.log(`SESSION ==> ${this.name} : ${this.id} ${error}`, "ERROR"); if (!this.strategyNone) { await this.invalidate() .catch((e) => { throw e; }); } throw error; } catch (e) { throw error; } } }); } isValidSession (data, context) { if (this.settings.referer_check) { try { checkSecureReferer.call(this, context); } catch (e) { this.manager.log(`SESSION REFERER ERROR SESSION ==> ${this.name} : ${this.id}`, "WARNING"); return false; } } // console.log( this.updated , new Date(this.updated) ) const lastUsed = new Date(this.updated).getTime(); // let lastUsed = new Date(this.getMetaBag("lastUsed")).getTime(); const now = new Date().getTime(); if (this.lifetime === 0) { // if ( lastUsed && lastUsed + ( this.settings.gc_maxlifetime * 1000 ) < now ){ // this.manager.log("SESSION INVALIDE gc_maxlifetime ==> " + this.name + " : "+ this.id, "WARNING"); // return false ; // } return true; } // eslint-disable-next-line no-mixed-operators if (lastUsed && lastUsed + this.lifetime * 1000 < now) { this.manager.log(`SESSION INVALIDE lifetime ==> ${this.name} : ${this.id}`, "WARNING"); return false; } return true; } attributes () { return this.protoService.prototype; } getAttributes () { return this.attributes(); } metaBag () { return this.protoParameters.prototype; } getMetas () { return this.metaBag(); } setMetaBag (key, value) { return this.setParameters(key, value); } getMetaBag (key) { return this.getParameters(key); } getFlashBag (key) { // this.log("GET FlashBag : " + key ,"WARNING") const res = this.flashBag[key]; if (res) { this.log(`Delete FlashBag : ${key}`, "DEBUG"); delete this.flashBag[key]; return res; } return null; } setFlashBag (key, value) { if (!key) { throw new Error(`FlashBag key must be define : ${key}`); } if (!value) { this.log(`ADD FlashBag : ${key} value not defined `, "WARNING"); } else { this.log(`ADD FlashBag : ${key}`, "DEBUG"); } this.flashBag[key] = value; return value; } flashBags () { return this.flashBag; } clearFlashBags () { delete this.flashBag; this.flashBag = {}; } clearFlashBag (key) { if (!key) { throw new Error(`clearFlashBag key must be define : ${key}`); } if (this.flashBag[key]) { delete this.flashBag[key]; } } deleteCookieSession (cookie = null) { if (this.context && this.context.response) { if (cookie) { cookie.expires = new Date(null); } else if (this.cookieSession) { this.cookieSession.expires = new Date(null); cookie = this.cookieSession; } else { // eslint-disable-next-line new-cap cookie = new nodefony.cookies.cookie(this.name, "", { expires: new Date(null) // path: "/" }); } this.context.response.setCookie(cookie); this.cookieSession = null; this.context.cookieSession = null; return cookie; } return this.cookieSession; } setCookieSession (leftTime, settings = {}) { if (this.context && this.context.response) { // let settings = null; const defaultsettings = nodefony.extend({}, this.settings.cookie); settings = nodefony.extend(defaultsettings, settings); if (leftTime) { settings.maxAge = leftTime; } // eslint-disable-next-line new-cap const cookie = new nodefony.cookies.cookie(this.name, this.id, settings); // this.context.response.setCookie(cookie); this.context.response.addCookie(cookie); this.cookieSession = cookie; this.context.cookieSession = cookie; return cookie; } return null; } serialize (user) { const obj = { Attributes: this.protoService.prototype, metaBag: this.protoParameters.prototype, flashBag: this.flashBag, username: user }; return obj; } deSerialize (obj) { // var obj = JSON.parse(data); for (const attr in obj.Attributes) { this.set(attr, obj.Attributes[attr]); } for (const meta in obj.metaBag) { // console.log(meta + " : " + obj.metaBag[meta]) this.setMetaBag(meta, obj.metaBag[meta]); } for (const flash in obj.flashBag) { this.setFlashBag(flash, obj.flashBag[flash]); } this.created = obj.created; this.updated = obj.updated; this.username = obj.user || obj.username; } removeSession (cookieDelete = null) { if (this.saved === true) { return this.storage.destroy(this.id, this.contextSession) .then(() => { if (cookieDelete) { this.deleteCookieSession(cookieDelete); } this.saved = true; return true; }) .catch((e) => { this.manager.log(e, "ERROR"); throw e; }); } if (cookieDelete) { this.deleteCookieSession(cookieDelete); } return Promise.resolve(true); } delete (cookieDelete) { return this.destroy(cookieDelete); } destroy (cookieDelete) { this.clear(); return this.removeSession(cookieDelete); } clear () { delete this.protoService; // eslint-disable-next-line no-empty-function this.protoService = function protoService () {}; delete this.protoParameters; // eslint-disable-next-line no-empty-function this.protoParameters = function protoParameters () {}; delete this.services; // eslint-disable-next-line new-cap this.services = new this.protoService(); delete this.parameters; // eslint-disable-next-line new-cap this.parameters = new this.protoParameters(); this.clearFlashBags(); } async invalidate (lifetime = this.lifetime, id = null, settingsCookie = null) { this.manager.log(`INVALIDATE SESSION ==>${this.name} : ${this.id}`, "DEBUG"); this.saved = true; return await this.destroy(this.cookieSession) .then(() => { this.saved = false; return this.create(lifetime, id, settingsCookie); }) .catch((e) => { throw e; }); } async migrate (destroy, lifetime = this.lifetime, id = null, settingsCookie = null) { this.manager.log(`MIGRATE SESSION ==>${this.name} : ${this.id}`, "DEBUG"); try { if (destroy) { this.saved = true; await this.removeSession(this.cookieSession) .then((ret) => { this.saved = false; return ret; }) .catch((e) => { throw e; }); } return this.create(lifetime, id, settingsCookie); } catch (e) { this.log(e, "WARNING"); return this; } } setId () { let ip = ""; try { ip = this.context.getRemoteAddress(); } catch (e) { this.log(e, "DEBUG"); } const date = new Date().getTime(); // eslint-disable-next-line no-mixed-operators const concat = ip + date + this.randomValueHex(16) + Math.random() * 10; let hash = null; switch (this.settings.hash_function) { case "md5": hash = createHash("md5"); break; case "sha1": hash = createHash("sha1"); break; default: hash = createHash("md5"); } const res = hash.update(concat).digest("hex"); return this.encrypt(`${res}:${this.contextSession}`); } getId (value) { const res = this.decrypt(value); // eslint-disable-next-line prefer-destructuring this.contextSession = res.split(":")[1]; return value; } save (user, sessionContext) { return this.storage.write(this.id, this.serialize(user), sessionContext) .then((session) => { this.created = session.createdAt; this.updated = session.updatedAt; if (!this.context) { throw new Error("SAVE SESSION ERROR context already deleted "); } else { this.saved = true; if (this.context) { this.context.fire("onSaveSession", this); } return this; } }) .catch((error) => { // console.trace(error); // this.log(error, "ERROR"); this.saved = false; throw error; }); } getName () { return this.name; } setName (name) { this.name = name || this.settings.name; } randomValueHex (len) { return randomBytes(Math.ceil(len / 2)) .toString("hex") // convert to hexadecimal format .slice(0, len); // return required number of characters } }; return Session; });