const { JembaDbThread } = require('jembadb'); const utils = require('../core/utils'); const log = new (require('../core/AppLogger'))().log;//singleton const cleanPeriod = 1*60*1000;//1 минута const cleanUnusedTokenTimeout = 5*60*1000;//5 минут class WebAccess { constructor(config) { this.config = config; this.freeAccess = (config.accessPassword === ''); this.accessTimeout = config.accessTimeout*60*1000; this.accessMap = new Map(); setTimeout(() => { this.periodicClean(); }, cleanPeriod); } async init() { const config = this.config; const dbPath = `${config.dataDir}/web-access`; const db = new JembaDbThread();//в отдельном потоке await db.lock({ dbPath, create: true, softLock: true, tableDefaults: { cacheSize: config.dbCacheSize, }, }); try { //открываем таблицы await db.openAll(); } catch(e) { if ( e.message.indexOf('corrupted') >= 0 || e.message.indexOf('Unexpected token') >= 0 || e.message.indexOf('invalid stored block lengths') >= 0 ) { log(LM_ERR, `DB ${dbPath} corrupted`); log(`Open "${dbPath}" with auto repair`); await db.openAll({autoRepair: true}); } else { throw e; } } //проверим, можно ли загружать токены из таблицы access const pass = utils.getBufHash(this.config.accessPassword, 'sha256', 'hex'); await db.create({table: 'config', quietIfExists: true}); let rows = await db.select({table: 'config', where: `@@id('pass')`}); let loadMap = false; if (rows.length && rows[0].value === pass) { //пароль не сменился в конфиге, можно загружать токены loadMap = true; } else { await db.insert({table: 'config', replace: true, rows: [{id: 'pass', value: pass}]}); } await db.create({table: 'access', quietIfExists: true}); if (loadMap) { //загрузим токены сессий rows = await db.select({table: 'access'}); for (const row of rows) this.accessMap.set(row.id, row.value); } this.db = db; } async periodicClean() { while (1) {//eslint-disable-line no-constant-condition try { const now = Date.now(); //почистим accessMap if (!this.freeAccess) { for (const [accessToken, accessRec] of this.accessMap) { if ( !(accessRec.used > 0 || now - accessRec.time < cleanUnusedTokenTimeout) || !(this.accessTimeout === 0 || now - accessRec.time < this.accessTimeout) ) { await this.deleteAccess(accessToken); } else if (!accessRec.saved) { await this.saveAccess(accessToken); } } } } catch(e) { log(LM_ERR, `WebAccess.periodicClean error: ${e.message}`); } await utils.sleep(cleanPeriod); } } hasAccess(accessToken) { if (this.freeAccess) return true; const accessRec = this.accessMap.get(accessToken); if (accessRec) { const now = Date.now(); if (this.accessTimeout === 0 || now - accessRec.time < this.accessTimeout) { accessRec.used++; accessRec.time = now; accessRec.saved = false; return true; } } return false; } async deleteAccess(accessToken) { await this.db.delete({table: 'access', where: `@@id(${this.db.esc(accessToken)})`}); this.accessMap.delete(accessToken); } async saveAccess(accessToken) { const value = this.accessMap.get(accessToken); if (!value || value.saved) return; value.saved = true; await this.db.insert({ table: 'access', replace: true, rows: [{id: accessToken, value}] }); } newToken() { const salt = utils.randomHexString(32); const accessToken = utils.getBufHash(this.config.accessPassword + salt, 'sha256', 'hex'); this.accessMap.set(accessToken, {time: Date.now(), used: 0}); return salt; } } module.exports = WebAccess;