diff --git a/package.json b/package.json index 4ecfded3..589028e2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.43.3", + "version": "3.44.0", "repository": { "url": "http://github.com/calzoneman/sync" }, diff --git a/src/session.js b/src/session.js index 72c03915..b6dd2328 100644 --- a/src/session.js +++ b/src/session.js @@ -17,36 +17,34 @@ exports.genSession = function (account, expiration, cb) { var hashInput = [account.name, account.password, expiration, salt].join(":"); var hash = sha256(hashInput); - cb(null, [account.name, expiration, salt, hash].join(":")); + cb(null, [account.name, expiration, salt, hash, account.global_rank].join(":")); }; exports.verifySession = function (input, cb) { if (typeof input !== "string") { - return cb("Invalid auth string"); + return cb(new Error("Invalid auth string")); } var parts = input.split(":"); - if (parts.length !== 4) { - return cb("Invalid auth string"); + if (parts.length !== 4 && parts.length !== 5) { + return cb(new Error("Invalid auth string")); } - var name = parts[0]; - var expiration = parts[1]; - var salt = parts[2]; - var hash = parts[3]; + const [name, expiration, salt, hash, global_rank] = parts; - if (Date.now() > parseInt(expiration)) { - return cb("Session expired"); + if (Date.now() > parseInt(expiration, 10)) { + return cb(new Error("Session expired")); } dbAccounts.getUser(name, function (err, account) { if (err) { + if (!(err instanceof Error)) err = new Error(err); return cb(err); } var hashInput = [account.name, account.password, expiration, salt].join(":"); if (sha256(hashInput) !== hash) { - return cb("Invalid auth string"); + return cb(new Error("Invalid auth string")); } cb(null, account); diff --git a/src/web/account.js b/src/web/account.js index a0bc485a..09fe3458 100644 --- a/src/web/account.js +++ b/src/web/account.js @@ -51,7 +51,7 @@ function handleAccountEdit(req, res) { /** * Handles a request to change the user"s password */ -function handleChangePassword(req, res) { +async function handleChangePassword(req, res) { var name = req.body.name; var oldpassword = req.body.oldpassword; var newpassword = req.body.newpassword; @@ -70,7 +70,8 @@ function handleChangePassword(req, res) { return; } - if (!req.user) { + const reqUser = await webserver.authorize(req); + if (!reqUser) { sendPug(res, "account-edit", { errorMessage: "You must be logged in to change your password" }); @@ -105,7 +106,6 @@ function handleChangePassword(req, res) { }); } - res.user = user; var expiration = new Date(parseInt(req.signedCookies.auth.split(":")[1])); session.genSession(user, expiration, function (err, auth) { if (err) { @@ -114,20 +114,7 @@ function handleChangePassword(req, res) { }); } - if (req.hostname.indexOf(Config.get("http.root-domain")) >= 0) { - res.cookie("auth", auth, { - domain: Config.get("http.root-domain-dotted"), - expires: expiration, - httpOnly: true, - signed: true - }); - } else { - res.cookie("auth", auth, { - expires: expiration, - httpOnly: true, - signed: true - }); - } + webserver.setAuthCookie(req, res, expiration, auth); sendPug(res, "account-edit", { successMessage: "Password changed." @@ -188,18 +175,20 @@ function handleChangeEmail(req, res) { /** * Handles a GET request for /account/channels */ -function handleAccountChannelPage(req, res) { +async function handleAccountChannelPage(req, res) { if (webserver.redirectHttps(req, res)) { return; } - if (!req.user) { + const user = await webserver.authorize(req); + // TODO: error message + if (!user) { return sendPug(res, "account-channels", { channels: [] }); } - db.channels.listUserChannels(req.user.name, function (err, channels) { + db.channels.listUserChannels(user.name, function (err, channels) { sendPug(res, "account-channels", { channels: channels }); @@ -229,7 +218,7 @@ function handleAccountChannel(req, res) { /** * Handles a request to register a new channel */ -function handleNewChannel(req, res) { +async function handleNewChannel(req, res) { var name = req.body.name; if (typeof name !== "string") { @@ -237,13 +226,15 @@ function handleNewChannel(req, res) { return; } - if (!req.user) { + const user = await webserver.authorize(req); + // TODO: error message + if (!user) { return sendPug(res, "account-channels", { channels: [] }); } - db.channels.listUserChannels(req.user.name, function (err, channels) { + db.channels.listUserChannels(user.name, function (err, channels) { if (err) { sendPug(res, "account-channels", { channels: [], @@ -260,8 +251,8 @@ function handleNewChannel(req, res) { return; } - if (channels.length >= Config.get("max-channels-per-user") && - req.user.global_rank < 255) { + if (channels.length >= Config.get("max-channels-per-user") + && user.global_rank < 255) { sendPug(res, "account-channels", { channels: channels, newChannelError: "You are not allowed to register more than " + @@ -270,9 +261,9 @@ function handleNewChannel(req, res) { return; } - db.channels.register(name, req.user.name, function (err, channel) { + db.channels.register(name, user.name, function (err, channel) { if (!err) { - Logger.eventlog.log("[channel] " + req.user.name + "@" + + Logger.eventlog.log("[channel] " + user.name + "@" + req.realIP + " registered channel " + name); var sv = Server.getServer(); @@ -304,14 +295,16 @@ function handleNewChannel(req, res) { /** * Handles a request to delete a new channel */ -function handleDeleteChannel(req, res) { +async function handleDeleteChannel(req, res) { var name = req.body.name; if (typeof name !== "string") { res.send(400); return; } - if (!req.user) { + const user = await webserver.authorize(req); + // TODO: error + if (!user) { return sendPug(res, "account-channels", { channels: [], }); @@ -327,8 +320,8 @@ function handleDeleteChannel(req, res) { return; } - if ((!channel.owner || channel.owner.toLowerCase() !== req.user.name.toLowerCase()) && req.user.global_rank < 255) { - db.channels.listUserChannels(req.user.name, function (err2, channels) { + if ((!channel.owner || channel.owner.toLowerCase() !== user.name.toLowerCase()) && user.global_rank < 255) { + db.channels.listUserChannels(user.name, function (err2, channels) { sendPug(res, "account-channels", { channels: err2 ? [] : channels, deleteChannelError: "You do not have permission to delete this channel" @@ -339,7 +332,7 @@ function handleDeleteChannel(req, res) { db.channels.drop(name, function (err) { if (!err) { - Logger.eventlog.log("[channel] " + req.user.name + "@" + + Logger.eventlog.log("[channel] " + user.name + "@" + req.realIP + " deleted channel " + name); } @@ -356,7 +349,7 @@ function handleDeleteChannel(req, res) { chan.emit("empty"); } } - db.channels.listUserChannels(req.user.name, function (err2, channels) { + db.channels.listUserChannels(user.name, function (err2, channels) { sendPug(res, "account-channels", { channels: err2 ? [] : channels, deleteChannelError: err ? err : undefined @@ -369,19 +362,21 @@ function handleDeleteChannel(req, res) { /** * Handles a GET request for /account/profile */ -function handleAccountProfilePage(req, res) { +async function handleAccountProfilePage(req, res) { if (webserver.redirectHttps(req, res)) { return; } - if (!req.user) { + const user = await webserver.authorize(req); + // TODO: error message + if (!user) { return sendPug(res, "account-profile", { profileImage: "", profileText: "" }); } - db.users.getProfile(req.user.name, function (err, profile) { + db.users.getProfile(user.name, function (err, profile) { if (err) { sendPug(res, "account-profile", { profileError: err, @@ -421,10 +416,12 @@ function validateProfileImage(image, callback) { /** * Handles a POST request to edit a profile */ -function handleAccountProfile(req, res) { +async function handleAccountProfile(req, res) { csrf.verify(req); - if (!req.user) { + const user = await webserver.authorize(req); + // TODO: error message + if (!user) { return sendPug(res, "account-profile", { profileImage: "", profileText: "", @@ -437,7 +434,7 @@ function handleAccountProfile(req, res) { validateProfileImage(rawImage, (error, image) => { if (error) { - db.users.getProfile(req.user.name, function (err, profile) { + db.users.getProfile(user.name, function (err, profile) { var errorMessage = err || error.message; sendPug(res, "account-profile", { profileImage: profile ? profile.image : "", @@ -448,7 +445,7 @@ function handleAccountProfile(req, res) { return; } - db.users.setProfile(req.user.name, { image: image, text: text }, function (err) { + db.users.setProfile(user.name, { image: image, text: text }, function (err) { if (err) { sendPug(res, "account-profile", { profileImage: "", diff --git a/src/web/acp.js b/src/web/acp.js index 588e9695..ac47ea51 100644 --- a/src/web/acp.js +++ b/src/web/acp.js @@ -7,19 +7,20 @@ var db = require("../database"); var Config = require("../config"); function checkAdmin(cb) { - return function (req, res) { - if (!req.user) { + return async function (req, res) { + const user = await webserver.authorize(req); + if (!user) { return res.send(403); } - if (req.user.global_rank < 255) { + if (user.global_rank < 255) { res.send(403); Logger.eventlog.log("[acp] Attempted GET "+req.path+" from non-admin " + user.name + "@" + req.realIP); return; } - cb(req, res, req.user); + cb(req, res, user); }; } diff --git a/src/web/auth.js b/src/web/auth.js index 434c23e1..ef4bd84c 100644 --- a/src/web/auth.js +++ b/src/web/auth.js @@ -73,28 +73,16 @@ function handleLogin(req, res) { return; } - if (req.hostname.indexOf(Config.get("http.root-domain")) >= 0) { - // Prevent non-root cookie from screwing things up - res.clearCookie("auth"); - res.cookie("auth", auth, { - domain: Config.get("http.root-domain-dotted"), - expires: expiration, - httpOnly: true, - signed: true - }); - } else { - res.cookie("auth", auth, { - expires: expiration, - httpOnly: true, - signed: true - }); - } + webserver.setAuthCookie(req, res, expiration, auth); if (dest) { res.redirect(dest); } else { - res.user = user; - sendPug(res, "login", {}); + sendPug(res, "login", { + loggedIn: true, + loginName: user.name, + superadmin: user.global_rank >= 255 + }); } }); }); @@ -108,7 +96,7 @@ function handleLoginPage(req, res) { return; } - if (req.user) { + if (res.locals.loggedIn) { return sendPug(res, "login", { wasAlreadyLoggedIn: true }); @@ -130,7 +118,7 @@ function handleLogout(req, res) { csrf.verify(req); res.clearCookie("auth"); - req.user = res.user = null; + res.locals.loggedIn = res.locals.loginName = res.locals.superadmin = false; // Try to find an appropriate redirect var dest = req.body.dest || req.header("referer"); dest = dest && dest.match(/login|logout|account/) ? null : dest; @@ -155,7 +143,7 @@ function handleRegisterPage(req, res) { return; } - if (req.user) { + if (res.locals.loggedIn) { sendPug(res, "register", {}); return; } diff --git a/src/web/middleware/authorize.js b/src/web/middleware/authorize.js index 52d004dc..4715f552 100644 --- a/src/web/middleware/authorize.js +++ b/src/web/middleware/authorize.js @@ -1,19 +1,62 @@ +import { setAuthCookie } from '../webserver'; const STATIC_RESOURCE = /\..+$/; export default function initialize(app, session) { - app.use((req, res, next) => { + app.use(async (req, res, next) => { if (STATIC_RESOURCE.test(req.path)) { return next(); } else if (!req.signedCookies || !req.signedCookies.auth) { return next(); } else { - session.verifySession(req.signedCookies.auth, (err, account) => { - if (!err) { - req.user = res.user = account; - } + const [ + name, expiration, salt, hash, global_rank + ] = req.signedCookies.auth.split(':'); - next(); - }); + if (!name || !expiration || !salt || !hash) { + // Invalid auth cookie + return next(); + } + + let rank; + if (!global_rank) { + rank = await backfillRankIntoAuthCookie( + session, + new Date(parseInt(expiration, 10)), + req, + res + ); + } else { + rank = parseInt(global_rank, 10); + } + + res.locals.loggedIn = true; + res.locals.loginName = name; + res.locals.superadmin = rank >= 255; + next(); } }); } + +async function backfillRankIntoAuthCookie(session, expiration, req, res) { + return new Promise((resolve, reject) => { + session.verifySession(req.signedCookies.auth, (err, account) => { + if (err) { + reject(err); + return; + } + + session.genSession(account, expiration, (err2, auth) => { + if (err2) { + // genSession never returns an error, but it still + // has a callback parameter for one, so just in case... + reject(new Error('This should never happen: ' + err2)); + return; + } + + setAuthCookie(req, res, expiration, auth); + + resolve(parseInt(auth.split(':')[4], 10)); + }); + }); + }); +} diff --git a/src/web/pug.js b/src/web/pug.js index 204513e5..9c2dc452 100644 --- a/src/web/pug.js +++ b/src/web/pug.js @@ -40,9 +40,10 @@ function sendPug(res, view, locals) { if (!locals) { locals = {}; } - locals.loggedIn = locals.loggedIn || !!res.user; - locals.loginName = locals.loginName || res.user ? res.user.name : false; - locals.superadmin = locals.superadmin || res.user ? res.user.global_rank >= 255 : false; + locals.loggedIn = nvl(locals.loggedIn, res.locals.loggedIn); + locals.loginName = nvl(locals.loginName, res.locals.loginName); + locals.superadmin = nvl(locals.superadmin, res.locals.superadmin); + if (!(view in cache) || Config.get("debug")) { var file = path.join(templates, view + ".pug"); var fn = pug.compile(fs.readFileSync(file), { @@ -55,6 +56,11 @@ function sendPug(res, view, locals) { res.send(html); } +function nvl(a, b) { + if (typeof a === 'undefined') return b; + return a; +} + module.exports = { sendPug: sendPug }; diff --git a/src/web/webserver.js b/src/web/webserver.js index 8e09d498..7bb36d99 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -12,6 +12,8 @@ import * as HTTPStatus from './httpstatus'; import { CSRFError, HTTPError } from '../errors'; import counters from '../counters'; import { Summary, Counter } from 'prom-client'; +import session from '../session'; +const verifySessionAsync = require('bluebird').promisify(session.verifySession); const LOGGER = require('@calzoneman/jsli')('webserver'); @@ -227,5 +229,36 @@ module.exports = { initializeErrorHandlers(app); }, - redirectHttps: redirectHttps + redirectHttps: redirectHttps, + + authorize: async function authorize(req) { + if (!req.signedCookies || !req.signedCookies.auth) { + return false; + } + + try { + return await verifySessionAsync(req.signedCookies.auth); + } catch (error) { + return false; + } + }, + + setAuthCookie: function setAuthCookie(req, res, expiration, auth) { + if (req.hostname.indexOf(Config.get("http.root-domain")) >= 0) { + // Prevent non-root cookie from screwing things up + res.clearCookie("auth"); + res.cookie("auth", auth, { + domain: Config.get("http.root-domain-dotted"), + expires: expiration, + httpOnly: true, + signed: true + }); + } else { + res.cookie("auth", auth, { + expires: expiration, + httpOnly: true, + signed: true + }); + } + } };