Эх сурвалжийг харах

Работа с токенами сессий вынесена в отдельный класс

Book Pauk 2 жил өмнө
parent
commit
bf9ae0b9e1

+ 22 - 53
server/controllers/WebSocketController.js

@@ -6,18 +6,15 @@ const WebWorker = require('../core/WebWorker');//singleton
 const log = new (require('../core/AppLogger'))().log;//singleton
 const utils = require('../core/utils');
 
-const cleanPeriod = 1*60*1000;//1 минута, не менять!
-const cleanUnusedTokenTimeout = 5*60*1000;//5 минут
+const cleanPeriod = 1*60*1000;//1 минута
 const closeSocketOnIdle = 5*60*1000;//5 минут
 
 class WebSocketController {
-    constructor(wss, config) {
+    constructor(wss, webAccess, config) {
         this.config = config;
         this.isDevelopment = (config.branch == 'development');
 
-        this.freeAccess = (config.accessPassword === '');
-        this.accessTimeout = config.accessTimeout*60*1000;
-        this.accessMap = new Map();
+        this.webAccess = webAccess;
 
         this.workerState = new WorkerState();
         this.webWorker = new WebWorker(config);
@@ -34,51 +31,26 @@ class WebSocketController {
             });
         });
 
-        setTimeout(() => { this.periodicClean(); }, cleanPeriod);
+        this.periodicClean();//no await
     }
 
-    periodicClean() {
-        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)
-                        ) {
-                        this.accessMap.delete(accessToken);
-                    }
-                }
-            }
-
-            //почистим ws-клиентов
-            this.wss.clients.forEach((ws) => {
-                if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
-                    ws.terminate();
-                }
-            });
-        } finally {
-            setTimeout(() => { this.periodicClean(); }, cleanPeriod);
-        }
-    }
+    async periodicClean() {
+        while (1) {//eslint-disable-line no-constant-condition
+            try {
+                const now = Date.now();
 
-    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;
-                return true;
+                //почистим ws-клиентов
+                this.wss.clients.forEach((ws) => {
+                    if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
+                        ws.terminate();
+                    }
+                });
+            } catch(e) {
+                log(LM_ERR, `WebSocketController.periodicClean error: ${e.message}`);
             }
+            
+            await utils.sleep(cleanPeriod);
         }
-
-        return false;
     }
 
     async onMessage(ws, message) {
@@ -96,12 +68,9 @@ class WebSocketController {
             this.send({_rok: 1}, req, ws);
 
             //access
-            if (!this.hasAccess(req.accessToken)) {
+            if (!this.webAccess.hasAccess(req.accessToken)) {
                 await utils.sleep(500);
-                const salt = utils.randomHexString(32);
-                const accessToken = utils.getBufHash(this.config.accessPassword + salt, 'sha256', 'hex');
-                this.accessMap.set(accessToken, {time: Date.now(), used: 0});
-
+                const salt = this.webAccess.newToken();
                 this.send({error: 'need_access_token', salt}, req, ws);
                 return;
             }
@@ -163,14 +132,14 @@ class WebSocketController {
     }
 
     async logout(req, ws) {
-        this.accessMap.delete(req.accessToken);
+        await this.webAccess.deleteAccess(req.accessToken);
         this.send({success: true}, req, ws);
     }
 
     async getConfig(req, ws) {
         const config = _.pick(this.config, this.config.webConfigParams);
         config.dbConfig = await this.webWorker.dbConfig();
-        config.freeAccess = this.freeAccess;
+        config.freeAccess = this.webAccess.freeAccess;
 
         this.send(config, req, ws);
     }

+ 148 - 0
server/core/WebAccess.js

@@ -0,0 +1,148 @@
+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;

+ 5 - 1
server/index.js

@@ -158,8 +158,12 @@ async function main() {
     opds(app, config);
     initStatic(app, config);
     
+    const WebAccess = require('./core/WebAccess');
+    const webAccess = new WebAccess(config);
+    await webAccess.init();
+
     const { WebSocketController } = require('./controllers');
-    new WebSocketController(wss, config);
+    new WebSocketController(wss, webAccess, config);
 
     if (devModule) {
         devModule.logErrors(app);