diff --git a/lib/database.js b/lib/database.js index 8ee76d55..204602b3 100644 --- a/lib/database.js +++ b/lib/database.js @@ -75,7 +75,7 @@ module.exports.query = function (query, sub, callback) { } } }); -}; +}; /** * Dummy function to be used as a callback when none is provided @@ -99,7 +99,7 @@ module.exports.initGlobalTables = function () { "PRIMARY KEY (`ip`)) " + "CHARACTER SET utf8", fail("global_bans")); - + query("CREATE TABLE IF NOT EXISTS `password_reset` (" + "`ip` VARCHAR(39) NOT NULL," + "`name` VARCHAR(20) NOT NULL," + @@ -235,6 +235,27 @@ module.exports.globalUnbanIP = function (ip, callback) { /* password recovery */ +module.exports.addPasswordReset = function (data, cb) { + if (typeof cb !== "function") { + cb = blackHole; + } + + var ip = data.ip || ""; + var name = data.name; + var email = data.email; + var hash = data.hash; + var expire = data.expire; + + if (!name || !hash) { + cb("Internal error: Must provide name and hash to insert a new password reset", null); + return; + } + + module.exports.query("INSERT INTO `password_reset` (`ip`, `name`, `email`, `hash`, `expire`) " + + "VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE ip=?, hash=?, email=?, expire=?", + [ip, name, email, hash, expire, ip, hash, email, expire], cb); +}; + /* module.exports.genPasswordReset = function (ip, name, email, callback) { if(typeof callback !== "function") @@ -506,7 +527,7 @@ module.exports.getIPs = function (name, callback) { if(!err) { ips = res.map(function (row) { return row.ip; }); } - + callback(err, ips); }); }; diff --git a/lib/utilities.js b/lib/utilities.js index ada297c8..e672f359 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -1,3 +1,5 @@ +var crypto = require("crypto"); + /* Set prototype- simple wrapper around JS objects to manipulate them like a set @@ -194,5 +196,11 @@ module.exports = { } }, - Set: Set + Set: Set, + + sha1: function (data) { + var shasum = crypto.createHash("sha1"); + shasum.update(data); + return shasum.digest("hex"); + } }; diff --git a/lib/web/account.js b/lib/web/account.js index f06de7ae..585c6c23 100644 --- a/lib/web/account.js +++ b/lib/web/account.js @@ -4,12 +4,13 @@ * @author Calvin Montgomery */ -var webserver = require('./webserver'); +var webserver = require("./webserver"); var logRequest = webserver.logRequest; -var sendJade = require('./jade').sendJade; -var Logger = require('../logger'); -var db = require('../database'); -var $util = require('../utilities'); +var sendJade = require("./jade").sendJade; +var Logger = require("../logger"); +var db = require("../database"); +var $util = require("../utilities"); +var Config = require("../config"); /** * Handles a GET request for /account/edit @@ -22,25 +23,25 @@ function handleAccountEditPage(req, res) { logRequest(req); var loginName = false; if (req.cookies.auth) { - loginName = req.cookies.auth.split(':')[0]; + loginName = req.cookies.auth.split(":")[0]; } - sendJade(res, 'account-edit', { + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName }); } /** - * Handles a POST request to edit a user's account + * Handles a POST request to edit a user"s account */ function handleAccountEdit(req, res) { logRequest(req); var action = req.body.action; switch(action) { - case 'change_password': + case "change_password": handleChangePassword(req, res); break; - case 'change_email': + case "change_email": handleChangeEmail(req, res); break; default: @@ -50,7 +51,7 @@ function handleAccountEdit(req, res) { } /** - * Handles a request to change the user's password + * Handles a request to change the user"s password */ function handleChangePassword(req, res) { var name = req.body.name; @@ -58,21 +59,21 @@ function handleChangePassword(req, res) { var newpassword = req.body.newpassword; var loginName = false; if (req.cookies.auth) { - loginName = req.cookies.auth.split(':')[0]; + loginName = req.cookies.auth.split(":")[0]; } - if (typeof name !== 'string' || - typeof oldpassword !== 'string' || - typeof newpassword !== 'string') { + if (typeof name !== "string" || + typeof oldpassword !== "string" || + typeof newpassword !== "string") { res.send(400); return; } if (newpassword.length === 0) { - sendJade(res, 'account-edit', { + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName, - errorMessage: 'New password must not be empty' + errorMessage: "New password must not be empty" }); return; } @@ -81,7 +82,7 @@ function handleChangePassword(req, res) { db.users.verifyLogin(name, oldpassword, function (err, user) { if (err) { - sendJade(res, 'account-edit', { + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName, errorMessage: err @@ -91,25 +92,25 @@ function handleChangePassword(req, res) { db.users.setPassword(name, newpassword, function (err, dbres) { if (err) { - sendJade(res, 'account-edit', { + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName, errorMessage: err }); return; } - Logger.eventlog(webserver.ipForRequest(req) + ' changed password for ' + name); - sendJade(res, 'account-edit', { + Logger.eventlog(webserver.ipForRequest(req) + " changed password for " + name); + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName, - successMessage: 'Password changed.' + successMessage: "Password changed." }); }); }); } /** - * Handles a request to change the user's email + * Handles a request to change the user"s email */ function handleChangeEmail(req, res) { var name = req.body.name; @@ -117,28 +118,28 @@ function handleChangeEmail(req, res) { var email = req.body.email; var loginName = false; if (req.cookies.auth) { - loginName = req.cookies.auth.split(':')[0]; + loginName = req.cookies.auth.split(":")[0]; } - if (typeof name !== 'string' || - typeof password !== 'string' || - typeof email !== 'string') { + if (typeof name !== "string" || + typeof password !== "string" || + typeof email !== "string") { res.send(400); return; } if (!$util.isValidEmail(email)) { - sendJade(res, 'account-edit', { + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName, - errorMessage: 'Invalid email address' + errorMessage: "Invalid email address" }); return; } db.users.verifyLogin(name, password, function (err, user) { if (err) { - sendJade(res, 'account-edit', { + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName, errorMessage: err @@ -148,19 +149,20 @@ function handleChangeEmail(req, res) { db.users.setEmail(name, email, function (err, dbres) { if (err) { - sendJade(res, 'account-edit', { + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName, errorMessage: err }); return; } - Logger.eventlog(webserver.ipForRequest(req) + ' changed email for ' + name + - ' to ' + email); - sendJade(res, 'account-edit', { + // TODO event log + Logger.syslog.log(webserver.ipForRequest(req) + " changed email for " + name + + " to " + email); + sendJade(res, "account-edit", { loggedIn: loginName !== false, loginName: loginName, - successMessage: 'Email address changed.' + successMessage: "Email address changed." }); }); }); @@ -177,19 +179,19 @@ function handleAccountChannelPage(req, res) { logRequest(req); var loginName = false; if (req.cookies.auth) { - loginName = req.cookies.auth.split(':')[0]; + loginName = req.cookies.auth.split(":")[0]; } if (loginName) { db.channels.listUserChannels(loginName, function (err, channels) { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: true, loginName: loginName, channels: channels }); }); } else { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: false, channels: [], }); @@ -197,16 +199,16 @@ function handleAccountChannelPage(req, res) { } /** - * Handles a POST request to modify a user's channels + * Handles a POST request to modify a user"s channels */ function handleAccountChannel(req, res) { logRequest(req); var action = req.body.action; switch(action) { - case 'new_channel': + case "new_channel": handleNewChannel(req, res); break; - case 'delete_channel': + case "delete_channel": handleDeleteChannel(req, res); break; default: @@ -222,16 +224,16 @@ function handleNewChannel(req, res) { logRequest(req); var name = req.body.name; - if (typeof name !== 'string') { + if (typeof name !== "string") { res.send(400); return; } var loginName = false; if (req.cookies.auth) { - loginName = req.cookies.auth.split(':')[0]; + loginName = req.cookies.auth.split(":")[0]; } else { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: false, channels: [] }); @@ -239,7 +241,7 @@ function handleNewChannel(req, res) { } db.users.verifyAuth(req.cookies.auth, function (err, user) { if (err) { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: false, channels: [], newChannelError: err @@ -249,7 +251,7 @@ function handleNewChannel(req, res) { db.channels.register(name, user.name, function (err, channel) { db.channels.listUserChannels(loginName, function (err2, channels) { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: true, loginName: loginName, channels: err2 ? [] : channels, @@ -267,16 +269,16 @@ function handleDeleteChannel(req, res) { logRequest(req); var name = req.body.name; - if (typeof name !== 'string') { + if (typeof name !== "string") { res.send(400); return; } var loginName = false; if (req.cookies.auth) { - loginName = req.cookies.auth.split(':')[0]; + loginName = req.cookies.auth.split(":")[0]; } else { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: false, channels: [], }); @@ -284,7 +286,7 @@ function handleDeleteChannel(req, res) { } db.users.verifyAuth(req.cookies.auth, function (err, user) { if (err) { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: false, channels: [], deleteChannelError: err @@ -295,18 +297,18 @@ function handleDeleteChannel(req, res) { db.channels.lookup(name, function (err, channel) { if (channel.owner !== user.name && user.global_rank < 255) { db.channels.listUserChannels(loginName, function (err2, channels) { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: true, loginName: loginName, channels: err2 ? [] : channels, - deleteChannelError: 'You do not have permission to delete this channel' + deleteChannelError: "You do not have permission to delete this channel" }); }); return; } db.channels.drop(name, function (err) { db.channels.listUserChannels(loginName, function (err2, channels) { - sendJade(res, 'account-channels', { + sendJade(res, "account-channels", { loggedIn: true, loginName: loginName, channels: err2 ? [] : channels, @@ -330,29 +332,29 @@ function handleAccountProfilePage(req, res) { var loginName = false; if (req.cookies.auth) { - loginName = req.cookies.auth.split(':')[0]; + loginName = req.cookies.auth.split(":")[0]; } else { - sendJade(res, 'account-profile', { + sendJade(res, "account-profile", { loggedIn: false, - profileImage: '', - profileText: '' + profileImage: "", + profileText: "" }); return; } db.users.getProfile(loginName, function (err, profile) { if (err) { - sendJade(res, 'account-profile', { + sendJade(res, "account-profile", { loggedIn: true, loginName: loginName, profileError: err, - profileImage: '', - profileText: '' + profileImage: "", + profileText: "" }); return; } - sendJade(res, 'account-profile', { + sendJade(res, "account-profile", { loggedIn: true, loginName: loginName, profileImage: profile.image, @@ -370,9 +372,9 @@ function handleAccountProfile(req, res) { var loginName = false; if (req.cookies.auth) { - loginName = req.cookies.auth.split(':')[0]; + loginName = req.cookies.auth.split(":")[0]; } else { - sendJade(res, 'account-profile', { + sendJade(res, "account-profile", { loggedIn: false, profileImage: "", profileText: "", @@ -386,7 +388,7 @@ function handleAccountProfile(req, res) { db.users.verifyAuth(req.cookies.auth, function (err, user) { if (err) { - sendJade(res, 'account-profile', { + sendJade(res, "account-profile", { loggedIn: false, profileImage: "", profileText: "", @@ -397,7 +399,7 @@ function handleAccountProfile(req, res) { db.users.setProfile(user.name, { image: image, text: text }, function (err) { if (err) { - sendJade(res, 'account-profile', { + sendJade(res, "account-profile", { loggedIn: true, loginName: user.name, profileImage: "", @@ -407,7 +409,7 @@ function handleAccountProfile(req, res) { return; } - sendJade(res, 'account-profile', { + sendJade(res, "account-profile", { loggedIn: true, loginName: user.name, profileImage: image, @@ -436,7 +438,7 @@ function handlePasswordResetPage(req, res) { } /** - * Handles a POST request to reset a user's password + * Handles a POST request to reset a user"s password */ function handlePasswordReset(req, res) { logRequest(req); @@ -485,25 +487,133 @@ function handlePasswordReset(req, res) { return; } - sendJade(res, "account-passwordreset", { - reset: true, - resetEmail: user.email, - resetErr: false + var hash = $util.sha1($util.randomSalt(64)); + // 24-hour expiration + var expire = Date.now() + 86400000; + var ip = webserver.ipForRequest(req); + + db.addPasswordReset({ + ip: ip, + name: name, + email: email, + hash: hash, + expire: expire + }, function (err, dbres) { + if (err) { + sendJade(res, "account-passwordreset", { + reset: false, + resetEmail: "", + resetErr: err + }); + return; + } + + if (!Config.get("mail.enabled")) { + sendJade(res, "account-passwordreset", { + reset: false, + resetEmail: email, + resetErr: "This server does not have mail support enabled. Please " + + "contact an administrator for assistance." + }); + return; + } + + var msg = "A password reset request was issued for your " + + "account `"+ name + "` on " + Config.get("http.domain") + + ". This request is valid for 24 hours. If you did "+ + "not initiate this, there is no need to take action."+ + " To reset your password, copy and paste the " + + "following link into your browser: " + + Config.get("http.domain") + "/passwordrecover/"+hash; + + var mail = { + from: "CyTube Services <" + Config.get("mail.from") + ">", + to: email, + subject: "Password reset request", + text: msg + }; + + Config.get("nodemailer").sendMail(mail, function (err, response) { + if (err) { + Logger.errlog.log("mail fail: " + err); + sendJade(res, "account-passwordreset", { + reset: false, + resetEmail: user.email, + resetErr: "Sending reset email failed. Please contact an " + + "administrator for assistance." + }); + } else { + sendJade(res, "account-passwordreset", { + reset: true, + resetEmail: user.email, + resetErr: false + }); + } + }); }); }); } +/** + * Handles a request for /passwordreceover/ + */ +function handlePasswordRecover(req, res) { + var hash = req.query.hash; + if (typeof hash !== "string") { + res.send(400); + return; + } + + var ip = webserver.ipForRequest(req); + + db.lookupPasswordReset(hash, function (err, row) { + if (err) { + sendJade(req, "account-passwordrecover", { + recovered: false, + recoverErr: err, + loginName: false + }); + return; + } + + if (row.ip && row.ip !== ip) { + sendJade(req, "account-passwordrecover", { + recovered: false, + recoverErr: "Your IP address does not match the address " + + "used to submit the reset request. For your " + + "security, only the IP which initiates the reset " + + "may reclaim an account.", + loginName: false + }); + return; + } + + if (Date.now() >= row.expire) { + sendJade(req, "account-passwordrecover", { + recovered: false, + recoverErr: "This password recovery link has expired. Password " + + "recovery links are valid only for 24 hours after " + + "submission.", + loginName: false + }); + return; + } + + // TODO actual reset + }); +} + module.exports = { /** * Initialize the module */ init: function (app) { - app.get('/account/edit', handleAccountEditPage); - app.post('/account/edit', handleAccountEdit); - app.get('/account/channels', handleAccountChannelPage); - app.post('/account/channels', handleAccountChannel); - app.get('/account/profile', handleAccountProfilePage); - app.post('/account/profile', handleAccountProfile); + app.get("/account/edit", handleAccountEditPage); + app.post("/account/edit", handleAccountEdit); + app.get("/account/channels", handleAccountChannelPage); + app.post("/account/channels", handleAccountChannel); + app.get("/account/profile", handleAccountProfilePage); + app.post("/account/profile", handleAccountProfile); app.get("/account/passwordreset", handlePasswordResetPage); app.post("/account/passwordreset", handlePasswordReset); }