diff --git a/controllers/pages.js b/controllers/pages.js index 0b75f092..01ca4bb1 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -21,7 +21,7 @@ const express = require('express') manageBoard, manageThread, manageLogs, manageCatalog, manageCustomPages, manageStaff, editStaff } = require(__dirname+'/../models/pages/manage/') , { globalManageSettings, globalManageReports, globalManageBans, globalManageBoards, editNews, editAccount, editRole, globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs, globalManageRoles } = require(__dirname+'/../models/pages/globalmanage/') - , { changePassword, blockBypass, home, register, login, create, myPermissions, + , { changePassword, blockBypass, home, register, login, create, myPermissions, sessions, board, catalog, banners, randombanner, news, captchaPage, overboard, overboardCatalog, captcha, thread, modlog, modloglist, account, boardlist, customPage, csrfPage } = require(__dirname+'/../models/pages/') , threadParamConverter = paramConverter({ processThreadIdParam: true }) @@ -118,6 +118,7 @@ router.get('/bypass_minimal.html', setMinimal, blockBypass); //block bypass page //accounts router.get('/account.html', useSession, sessionRefresh, isLoggedIn, calcPerms, csrf, account); //page showing boards you are mod/owner of, links to password rese, logout, etc +router.get('/sessions.html', useSession, sessionRefresh, isLoggedIn, calcPerms, csrf, sessions); router.get('/mypermissions.html', useSession, sessionRefresh, isLoggedIn, calcPerms, csrf, myPermissions); router.get('/login.html', login); router.get('/register.html', register); diff --git a/helpers/usesession.js b/helpers/usesession.js index dd6324f1..99505c31 100644 --- a/helpers/usesession.js +++ b/helpers/usesession.js @@ -1,6 +1,7 @@ 'use strict'; const session = require('express-session') + , uid = require('uid-safe').sync , redisStore = require('connect-redis')(session) , { cookieSecret } = require(__dirname+'/../configs/secrets.js') , config = require(__dirname+'/../config.js') @@ -14,19 +15,28 @@ module.exports = (req, res, next) => { const { secureCookies } = config.get; const proto = req.headers['x-forwarded-proto']; const sessionMiddleware = sessionMiddlewareCache[proto] || (sessionMiddlewareCache[proto] = session({ - secret: cookieSecret, - store: new redisStore({ - client: redisClient, - }), - resave: false, - saveUninitialized: false, - rolling: true, - cookie: { - httpOnly: true, - secure: secureCookies && production && (proto === 'https'), - sameSite: 'strict', - maxAge: DAY, - } + secret: cookieSecret, + store: new redisStore({ + client: redisClient, + }), + resave: false, + saveUninitialized: false, + rolling: true, + cookie: { + httpOnly: true, + secure: secureCookies && production && (proto === 'https'), + sameSite: 'strict', + maxAge: DAY, + }, + genid: (req) => { + //add user identifier to session id + //https://github.com/expressjs/session/blob/master/index.js#L518 + let id = uid(24); + if (req.path === '/login' && req.body.username) { + id += `:${req.body.username}`; + }; + return id; + }, })); return sessionMiddleware(req, res, next); diff --git a/models/pages/index.js b/models/pages/index.js index 6fd2289d..108c48d6 100644 --- a/models/pages/index.js +++ b/models/pages/index.js @@ -5,6 +5,7 @@ module.exports = { blockBypass: require(__dirname+'/blockbypass.js'), register: require(__dirname+'/register.js'), account: require(__dirname+'/account.js'), + sessions: require(__dirname+'/sessions.js'), myPermissions: require(__dirname+'/mypermissions.js'), home: require(__dirname+'/home.js'), login: require(__dirname+'/login.js'), diff --git a/models/pages/sessions.js b/models/pages/sessions.js new file mode 100644 index 00000000..a8561303 --- /dev/null +++ b/models/pages/sessions.js @@ -0,0 +1,18 @@ +'use strict'; + +const redis = require(__dirname+'/../../redis.js'); + +module.exports = async (req, res, next) => { + + const sessions = await redis.getPattern(`sess:*:${res.locals.user.username}`); + + res + .set('Cache-Control', 'private, max-age=5') + .render('sessions', { + user: res.locals.user, + permissions: res.locals.permissions, + currentSessionKey: `sess:${req.sessionID}`, + sessions, + }); + +} diff --git a/package-lock.json b/package-lock.json index bad4d940..a6bb4187 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "socket.io": "^4.3.0", "socket.io-redis": "^6.1.1", "socks-proxy-agent": "^6.1.0", + "uid-safe": "^2.1.5", "unix-crypt-td-js": "^1.1.4" } }, diff --git a/package.json b/package.json index a2f2e773..eaec5fc1 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dependencies": { "@fatchan/express-fileupload": "^1.3.1", "bcrypt": "^5.0.1", + "big-bitfield": "git+https://gitgud.io/fatchan/big-bitfield.git", "bull": "^3.29.3", "cache-pug-templates": "^2.0.3", "connect-redis": "^6.0.0", @@ -22,7 +23,6 @@ "fs": "0.0.1-security", "fs-extra": "^10.0.0", "gm": "git+https://gitgud.io/fatchan/gm.git", - "big-bitfield": "git+https://gitgud.io/fatchan/big-bitfield.git", "gulp": "^4.0.2", "gulp-clean-css": "^4.3.0", "gulp-concat": "^2.6.1", @@ -49,6 +49,7 @@ "socket.io": "^4.3.0", "socket.io-redis": "^6.1.1", "socks-proxy-agent": "^6.1.0", + "uid-safe": "^2.1.5", "unix-crypt-td-js": "^1.1.4" }, "scripts": { diff --git a/redis.js b/redis.js index 1a5f4e49..2113ee88 100644 --- a/redis.js +++ b/redis.js @@ -96,6 +96,41 @@ module.exports = { } }, + getPattern: (pattern) => { + return new Promise((resolve, reject) => { + const stream = sharedClient.scanStream({ + match: pattern + }); + const dataMap = {}; + stream.on('data', async (keys) => { + if (keys.length > 0) { + stream.pause(); //dont want end() called during this, its async + const pipeline = sharedClient.pipeline(); + for (let i = 0; i < keys.length; i++) { + pipeline.get(keys[i]); + } + let results; + try { + results = await pipeline.exec(); + } catch (e) { + stream.destroy(); + reject(e); + } + for (let i = 0; i < results.length; i++) { + dataMap[keys[i]] = JSON.parse(results[i][1]); + } + stream.resume(); + } + }); + stream.on('end', () => { + resolve(dataMap); + }); + stream.on('error', (err) => { + reject(err); + }); + }); + }, + deletePattern: (pattern) => { return new Promise((resolve, reject) => { const stream = sharedClient.scanStream({ diff --git a/views/pages/account.pug b/views/pages/account.pug index 3f531f9c..e630ce57 100644 --- a/views/pages/account.pug +++ b/views/pages/account.pug @@ -16,6 +16,7 @@ block content li: a(href='/register.html') Register an account li: a(href='/changepassword.html') Change password li: a(href='/mypermissions.html') My Permissions + li: a(href='/sessions.html') Login sessions form(action='/forms/logout' method='post') input(type='submit' value='Log out') diff --git a/views/pages/sessions.pug b/views/pages/sessions.pug new file mode 100644 index 00000000..11d77bd3 --- /dev/null +++ b/views/pages/sessions.pug @@ -0,0 +1,27 @@ +extends ../layout.pug + +block head + title Login Sessions + +block content + .board-header + h1.board-title Login Sessions + br + hr(size=1) + h4.mv-5 Login sessions: + form.form-post.nogrow(action=`/forms/deletesessions` method='POST' enctype='application/x-www-form-urlencoded') + input(type='hidden' name='_csrf' value=csrf) + .table-container.flex-left.text-center + table + tr + th + th ID + th Expires + each session, sessionId in sessions + tr(class=(sessionId === currentSessionKey ? 'bold' : '')) + td: input(type='checkbox', name='checkedsessionids' value=sessionId) + td #{sessionId} #{sessionId === currentSessionKey ? '(current)' : ''} + - const expiryDate = new Date(session.cookie.expires) + td: time.reltime(datetime=expiryDate.toISOString()) #{expiryDate.toLocaleString(undefined, {hourCycle:'h23'})} + h4.mv-5 Delete Selected: + input(type='submit', value='delete')