package: build with babel for ES2015 support
* Rename lib/ -> src/ * Add `postinstall` npm target for compiling src files to lib * Add `build-watch` npm target for development with babel --watch * Add `lib/` to .gitignore * Add `source-map-support` module for babel-generated sourcemaps
This commit is contained in:
parent
d042619b21
commit
0109a87e55
55 changed files with 9 additions and 3 deletions
644
src/web/account.js
Normal file
644
src/web/account.js
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
/**
|
||||
* web/account.js - Webserver details for account management
|
||||
*
|
||||
* @author Calvin Montgomery <cyzon@cyzon.us>
|
||||
*/
|
||||
|
||||
var webserver = require("./webserver");
|
||||
var sendJade = require("./jade").sendJade;
|
||||
var Logger = require("../logger");
|
||||
var db = require("../database");
|
||||
var $util = require("../utilities");
|
||||
var Config = require("../config");
|
||||
var Server = require("../server");
|
||||
var session = require("../session");
|
||||
var csrf = require("./csrf");
|
||||
|
||||
/**
|
||||
* Handles a GET request for /account/edit
|
||||
*/
|
||||
function handleAccountEditPage(req, res) {
|
||||
if (webserver.redirectHttps(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendJade(res, "account-edit", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a POST request to edit a user"s account
|
||||
*/
|
||||
function handleAccountEdit(req, res) {
|
||||
csrf.verify(req);
|
||||
|
||||
var action = req.body.action;
|
||||
switch(action) {
|
||||
case "change_password":
|
||||
handleChangePassword(req, res);
|
||||
break;
|
||||
case "change_email":
|
||||
handleChangeEmail(req, res);
|
||||
break;
|
||||
default:
|
||||
res.send(400);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to change the user"s password
|
||||
*/
|
||||
function handleChangePassword(req, res) {
|
||||
var name = req.body.name;
|
||||
var oldpassword = req.body.oldpassword;
|
||||
var newpassword = req.body.newpassword;
|
||||
|
||||
if (typeof name !== "string" ||
|
||||
typeof oldpassword !== "string" ||
|
||||
typeof newpassword !== "string") {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newpassword.length === 0) {
|
||||
sendJade(res, "account-edit", {
|
||||
errorMessage: "New password must not be empty"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
sendJade(res, "account-edit", {
|
||||
errorMessage: "You must be logged in to change your password"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
newpassword = newpassword.substring(0, 100);
|
||||
|
||||
db.users.verifyLogin(name, oldpassword, function (err, user) {
|
||||
if (err) {
|
||||
sendJade(res, "account-edit", {
|
||||
errorMessage: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.setPassword(name, newpassword, function (err, dbres) {
|
||||
if (err) {
|
||||
sendJade(res, "account-edit", {
|
||||
errorMessage: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.eventlog.log("[account] " + webserver.ipForRequest(req) +
|
||||
" changed password for " + name);
|
||||
|
||||
db.users.getUser(name, function (err, user) {
|
||||
if (err) {
|
||||
return sendJade(res, "account-edit", {
|
||||
errorMessage: err
|
||||
});
|
||||
}
|
||||
|
||||
res.user = user;
|
||||
var expiration = new Date(parseInt(req.signedCookies.auth.split(":")[1]));
|
||||
session.genSession(user, expiration, function (err, auth) {
|
||||
if (err) {
|
||||
return sendJade(res, "account-edit", {
|
||||
errorMessage: err
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
sendJade(res, "account-edit", {
|
||||
successMessage: "Password changed."
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to change the user"s email
|
||||
*/
|
||||
function handleChangeEmail(req, res) {
|
||||
var name = req.body.name;
|
||||
var password = req.body.password;
|
||||
var email = req.body.email;
|
||||
|
||||
if (typeof name !== "string" ||
|
||||
typeof password !== "string" ||
|
||||
typeof email !== "string") {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$util.isValidEmail(email) && email !== "") {
|
||||
sendJade(res, "account-edit", {
|
||||
errorMessage: "Invalid email address"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.verifyLogin(name, password, function (err, user) {
|
||||
if (err) {
|
||||
sendJade(res, "account-edit", {
|
||||
errorMessage: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.setEmail(name, email, function (err, dbres) {
|
||||
if (err) {
|
||||
sendJade(res, "account-edit", {
|
||||
errorMessage: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
Logger.eventlog.log("[account] " + webserver.ipForRequest(req) +
|
||||
" changed email for " + name +
|
||||
" to " + email);
|
||||
sendJade(res, "account-edit", {
|
||||
successMessage: "Email address changed."
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a GET request for /account/channels
|
||||
*/
|
||||
function handleAccountChannelPage(req, res) {
|
||||
if (webserver.redirectHttps(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
return sendJade(res, "account-channels", {
|
||||
channels: []
|
||||
});
|
||||
}
|
||||
|
||||
db.channels.listUserChannels(req.user.name, function (err, channels) {
|
||||
sendJade(res, "account-channels", {
|
||||
channels: channels
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a POST request to modify a user"s channels
|
||||
*/
|
||||
function handleAccountChannel(req, res) {
|
||||
csrf.verify(req);
|
||||
|
||||
var action = req.body.action;
|
||||
switch(action) {
|
||||
case "new_channel":
|
||||
handleNewChannel(req, res);
|
||||
break;
|
||||
case "delete_channel":
|
||||
handleDeleteChannel(req, res);
|
||||
break;
|
||||
default:
|
||||
res.send(400);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to register a new channel
|
||||
*/
|
||||
function handleNewChannel(req, res) {
|
||||
|
||||
var name = req.body.name;
|
||||
if (typeof name !== "string") {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
return sendJade(res, "account-channels", {
|
||||
channels: []
|
||||
});
|
||||
}
|
||||
|
||||
db.channels.listUserChannels(req.user.name, function (err, channels) {
|
||||
if (err) {
|
||||
sendJade(res, "account-channels", {
|
||||
channels: [],
|
||||
newChannelError: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.match(Config.get("reserved-names.channels"))) {
|
||||
sendJade(res, "account-channels", {
|
||||
channels: channels,
|
||||
newChannelError: "That channel name is reserved"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (channels.length >= Config.get("max-channels-per-user")) {
|
||||
sendJade(res, "account-channels", {
|
||||
channels: channels,
|
||||
newChannelError: "You are not allowed to register more than " +
|
||||
Config.get("max-channels-per-user") + " channels."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.channels.register(name, req.user.name, function (err, channel) {
|
||||
if (!err) {
|
||||
Logger.eventlog.log("[channel] " + req.user.name + "@" +
|
||||
webserver.ipForRequest(req) +
|
||||
" registered channel " + name);
|
||||
var sv = Server.getServer();
|
||||
if (sv.isChannelLoaded(name)) {
|
||||
var chan = sv.getChannel(name);
|
||||
var users = Array.prototype.slice.call(chan.users);
|
||||
users.forEach(function (u) {
|
||||
u.kick("Channel reloading");
|
||||
});
|
||||
|
||||
if (!chan.dead) {
|
||||
chan.emit("empty");
|
||||
}
|
||||
}
|
||||
channels.push({
|
||||
name: name
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
sendJade(res, "account-channels", {
|
||||
channels: channels,
|
||||
newChannelError: err ? err : undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to delete a new channel
|
||||
*/
|
||||
function handleDeleteChannel(req, res) {
|
||||
var name = req.body.name;
|
||||
if (typeof name !== "string") {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
return sendJade(res, "account-channels", {
|
||||
channels: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
db.channels.lookup(name, function (err, channel) {
|
||||
if (err) {
|
||||
sendJade(res, "account-channels", {
|
||||
channels: [],
|
||||
deleteChannelError: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel.owner !== req.user.name && req.user.global_rank < 255) {
|
||||
db.channels.listUserChannels(req.user.name, function (err2, channels) {
|
||||
sendJade(res, "account-channels", {
|
||||
channels: err2 ? [] : channels,
|
||||
deleteChannelError: "You do not have permission to delete this channel"
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.channels.drop(name, function (err) {
|
||||
if (!err) {
|
||||
Logger.eventlog.log("[channel] " + req.user.name + "@" +
|
||||
webserver.ipForRequest(req) + " deleted channel " +
|
||||
name);
|
||||
}
|
||||
var sv = Server.getServer();
|
||||
if (sv.isChannelLoaded(name)) {
|
||||
var chan = sv.getChannel(name);
|
||||
chan.clearFlag(require("../flags").C_REGISTERED);
|
||||
var users = Array.prototype.slice.call(chan.users);
|
||||
users.forEach(function (u) {
|
||||
u.kick("Channel reloading");
|
||||
});
|
||||
|
||||
if (!chan.dead) {
|
||||
chan.emit("empty");
|
||||
}
|
||||
}
|
||||
db.channels.listUserChannels(req.user.name, function (err2, channels) {
|
||||
sendJade(res, "account-channels", {
|
||||
channels: err2 ? [] : channels,
|
||||
deleteChannelError: err ? err : undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a GET request for /account/profile
|
||||
*/
|
||||
function handleAccountProfilePage(req, res) {
|
||||
if (webserver.redirectHttps(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
return sendJade(res, "account-profile", {
|
||||
profileImage: "",
|
||||
profileText: ""
|
||||
});
|
||||
}
|
||||
|
||||
db.users.getProfile(req.user.name, function (err, profile) {
|
||||
if (err) {
|
||||
sendJade(res, "account-profile", {
|
||||
profileError: err,
|
||||
profileImage: "",
|
||||
profileText: ""
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendJade(res, "account-profile", {
|
||||
profileImage: profile.image,
|
||||
profileText: profile.text,
|
||||
profileError: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a POST request to edit a profile
|
||||
*/
|
||||
function handleAccountProfile(req, res) {
|
||||
csrf.verify(req);
|
||||
|
||||
if (!req.user) {
|
||||
return sendJade(res, "account-profile", {
|
||||
profileImage: "",
|
||||
profileText: "",
|
||||
profileError: "You must be logged in to edit your profile",
|
||||
});
|
||||
}
|
||||
|
||||
var image = req.body.image;
|
||||
var text = req.body.text;
|
||||
|
||||
db.users.setProfile(req.user.name, { image: image, text: text }, function (err) {
|
||||
if (err) {
|
||||
sendJade(res, "account-profile", {
|
||||
profileImage: "",
|
||||
profileText: "",
|
||||
profileError: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendJade(res, "account-profile", {
|
||||
profileImage: image,
|
||||
profileText: text,
|
||||
profileError: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a GET request for /account/passwordreset
|
||||
*/
|
||||
function handlePasswordResetPage(req, res) {
|
||||
if (webserver.redirectHttps(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendJade(res, "account-passwordreset", {
|
||||
reset: false,
|
||||
resetEmail: "",
|
||||
resetErr: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a POST request to reset a user's password
|
||||
*/
|
||||
function handlePasswordReset(req, res) {
|
||||
csrf.verify(req);
|
||||
|
||||
var name = req.body.name,
|
||||
email = req.body.email;
|
||||
|
||||
if (typeof name !== "string" || typeof email !== "string") {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$util.isValidUserName(name)) {
|
||||
sendJade(res, "account-passwordreset", {
|
||||
reset: false,
|
||||
resetEmail: "",
|
||||
resetErr: "Invalid username '" + name + "'"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.getEmail(name, function (err, actualEmail) {
|
||||
if (err) {
|
||||
sendJade(res, "account-passwordreset", {
|
||||
reset: false,
|
||||
resetEmail: "",
|
||||
resetErr: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (actualEmail !== email.trim()) {
|
||||
sendJade(res, "account-passwordreset", {
|
||||
reset: false,
|
||||
resetEmail: "",
|
||||
resetErr: "Provided email does not match the email address on record for " + name
|
||||
});
|
||||
return;
|
||||
} else if (actualEmail === "") {
|
||||
sendJade(res, "account-passwordreset", {
|
||||
reset: false,
|
||||
resetEmail: "",
|
||||
resetErr: name + " doesn't have an email address on record. Please contact an " +
|
||||
"administrator to manually reset your password."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Logger.eventlog.log("[account] " + ip + " requested password recovery for " +
|
||||
name + " <" + email + ">");
|
||||
|
||||
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") + "/account/passwordrecover/"+hash;
|
||||
|
||||
var mail = {
|
||||
from: Config.get("mail.from-name") + " <" + Config.get("mail.from-address") + ">",
|
||||
to: email,
|
||||
subject: "Password reset request",
|
||||
text: msg
|
||||
};
|
||||
|
||||
Config.get("mail.nodemailer").sendMail(mail, function (err, response) {
|
||||
if (err) {
|
||||
Logger.errlog.log("mail fail: " + err);
|
||||
sendJade(res, "account-passwordreset", {
|
||||
reset: false,
|
||||
resetEmail: email,
|
||||
resetErr: "Sending reset email failed. Please contact an " +
|
||||
"administrator for assistance."
|
||||
});
|
||||
} else {
|
||||
sendJade(res, "account-passwordreset", {
|
||||
reset: true,
|
||||
resetEmail: email,
|
||||
resetErr: false
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request for /account/passwordrecover/<hash>
|
||||
*/
|
||||
function handlePasswordRecover(req, res) {
|
||||
var hash = req.params.hash;
|
||||
if (typeof hash !== "string") {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
var ip = webserver.ipForRequest(req);
|
||||
|
||||
db.lookupPasswordReset(hash, function (err, row) {
|
||||
if (err) {
|
||||
sendJade(res, "account-passwordrecover", {
|
||||
recovered: false,
|
||||
recoverErr: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() >= row.expire) {
|
||||
sendJade(res, "account-passwordrecover", {
|
||||
recovered: false,
|
||||
recoverErr: "This password recovery link has expired. Password " +
|
||||
"recovery links are valid only for 24 hours after " +
|
||||
"submission."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var newpw = "";
|
||||
const avail = "abcdefgihkmnpqrstuvwxyz0123456789";
|
||||
for (var i = 0; i < 10; i++) {
|
||||
newpw += avail[Math.floor(Math.random() * avail.length)];
|
||||
}
|
||||
db.users.setPassword(row.name, newpw, function (err) {
|
||||
if (err) {
|
||||
sendJade(res, "account-passwordrecover", {
|
||||
recovered: false,
|
||||
recoverErr: "Database error. Please contact an administrator if " +
|
||||
"this persists."
|
||||
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.deletePasswordReset(hash);
|
||||
Logger.eventlog.log("[account] " + ip + " recovered password for " + row.name);
|
||||
|
||||
sendJade(res, "account-passwordrecover", {
|
||||
recovered: true,
|
||||
recoverPw: newpw
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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/passwordreset", handlePasswordResetPage);
|
||||
app.post("/account/passwordreset", handlePasswordReset);
|
||||
app.get("/account/passwordrecover/:hash", handlePasswordRecover);
|
||||
app.get("/account", function (req, res) {
|
||||
res.redirect("/login");
|
||||
});
|
||||
}
|
||||
};
|
||||
114
src/web/acp.js
Normal file
114
src/web/acp.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
var path = require("path");
|
||||
var fs = require("fs");
|
||||
var webserver = require("./webserver");
|
||||
var sendJade = require("./jade").sendJade;
|
||||
var Logger = require("../logger");
|
||||
var db = require("../database");
|
||||
var Config = require("../config");
|
||||
|
||||
function checkAdmin(cb) {
|
||||
return function (req, res) {
|
||||
if (!req.user) {
|
||||
return res.send(403);
|
||||
}
|
||||
|
||||
if (req.user.global_rank < 255) {
|
||||
res.send(403);
|
||||
Logger.eventlog.log("[acp] Attempted GET "+req.path+" from non-admin " +
|
||||
user.name + "@" + webserver.ipForRequest(req));
|
||||
return;
|
||||
}
|
||||
|
||||
cb(req, res, req.user);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request for the ACP
|
||||
*/
|
||||
function handleAcp(req, res, user) {
|
||||
var sio;
|
||||
if (req.secure || req.header("x-forwarded-proto") === "https") {
|
||||
sio = Config.get("https.domain") + ":" + Config.get("https.default-port");
|
||||
} else {
|
||||
sio = Config.get("io.domain") + ":" + Config.get("io.default-port");
|
||||
}
|
||||
sio += "/socket.io/socket.io.js";
|
||||
|
||||
sendJade(res, "acp", {
|
||||
sioSource: sio
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams the last length bytes of file to the given HTTP response
|
||||
*/
|
||||
function readLog(res, file, length) {
|
||||
fs.stat(file, function (err, data) {
|
||||
if (err) {
|
||||
res.send(500);
|
||||
return;
|
||||
}
|
||||
|
||||
var start = Math.max(0, data.size - length);
|
||||
if (isNaN(start)) {
|
||||
res.send(500);
|
||||
}
|
||||
var end = Math.max(0, data.size - 1);
|
||||
if (isNaN(end)) {
|
||||
res.send(500);
|
||||
}
|
||||
fs.createReadStream(file, { start: start, end: end })
|
||||
.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to read the syslog
|
||||
*/
|
||||
function handleReadSyslog(req, res) {
|
||||
readLog(res, path.join(__dirname, "..", "..", "sys.log"), 1048576);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to read the error log
|
||||
*/
|
||||
function handleReadErrlog(req, res) {
|
||||
readLog(res, path.join(__dirname, "..", "..", "error.log"), 1048576);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to read the http log
|
||||
*/
|
||||
function handleReadHttplog(req, res) {
|
||||
readLog(res, path.join(__dirname, "..", "..", "http.log"), 1048576);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to read the event log
|
||||
*/
|
||||
function handleReadEventlog(req, res) {
|
||||
readLog(res, path.join(__dirname, "..", "..", "events.log"), 1048576);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request to read a channel log
|
||||
*/
|
||||
function handleReadChanlog(req, res) {
|
||||
if (!req.params.name.match(/^[\w-]{1,30}$/)) {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
readLog(res, path.join(__dirname, "..", "..", "chanlogs", req.params.name + ".log"), 1048576);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function (app) {
|
||||
app.get("/acp", checkAdmin(handleAcp));
|
||||
app.get("/acp/syslog", checkAdmin(handleReadSyslog));
|
||||
app.get("/acp/errlog", checkAdmin(handleReadErrlog));
|
||||
app.get("/acp/httplog", checkAdmin(handleReadHttplog));
|
||||
app.get("/acp/eventlog", checkAdmin(handleReadEventlog));
|
||||
app.get("/acp/chanlog/:name", checkAdmin(handleReadChanlog));
|
||||
}
|
||||
};
|
||||
241
src/web/auth.js
Normal file
241
src/web/auth.js
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/**
|
||||
* web/auth.js - Webserver functions for user authentication and registration
|
||||
*
|
||||
* @author Calvin Montgomery <cyzon@cyzon.us>
|
||||
*/
|
||||
|
||||
var jade = require("jade");
|
||||
var path = require("path");
|
||||
var webserver = require("./webserver");
|
||||
var cookieall = webserver.cookieall;
|
||||
var sendJade = require("./jade").sendJade;
|
||||
var Logger = require("../logger");
|
||||
var $util = require("../utilities");
|
||||
var db = require("../database");
|
||||
var Config = require("../config");
|
||||
var url = require("url");
|
||||
var session = require("../session");
|
||||
var csrf = require("./csrf");
|
||||
|
||||
/**
|
||||
* Processes a login request. Sets a cookie upon successful authentication
|
||||
*/
|
||||
function handleLogin(req, res) {
|
||||
csrf.verify(req);
|
||||
|
||||
var name = req.body.name;
|
||||
var password = req.body.password;
|
||||
var rememberMe = req.body.remember;
|
||||
var dest = req.body.dest || req.header("referer") || null;
|
||||
dest = dest && dest.match(/login|logout/) ? null : dest;
|
||||
|
||||
if (typeof name !== "string" || typeof password !== "string") {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
var host = req.hostname;
|
||||
if (host.indexOf(Config.get("http.root-domain")) === -1 &&
|
||||
Config.get("http.alt-domains").indexOf(host) === -1) {
|
||||
Logger.syslog.log("WARNING: Attempted login from non-approved domain " + host);
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
var expiration;
|
||||
if (rememberMe) {
|
||||
expiration = new Date("Fri, 31 Dec 9999 23:59:59 GMT");
|
||||
} else {
|
||||
expiration = new Date(Date.now() + 7*24*60*60*1000);
|
||||
}
|
||||
|
||||
password = password.substring(0, 100);
|
||||
|
||||
db.users.verifyLogin(name, password, function (err, user) {
|
||||
if (err) {
|
||||
if (err === "Invalid username/password combination") {
|
||||
Logger.eventlog.log("[loginfail] Login failed (bad password): " + name
|
||||
+ "@" + webserver.ipForRequest(req));
|
||||
}
|
||||
sendJade(res, "login", {
|
||||
loggedIn: false,
|
||||
loginError: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
session.genSession(user, expiration, function (err, auth) {
|
||||
if (err) {
|
||||
sendJade(res, "login", {
|
||||
loggedIn: false,
|
||||
loginError: err
|
||||
});
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
if (dest) {
|
||||
res.redirect(dest);
|
||||
} else {
|
||||
res.user = user;
|
||||
sendJade(res, "login", {});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a GET request for /login
|
||||
*/
|
||||
function handleLoginPage(req, res) {
|
||||
if (webserver.redirectHttps(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user) {
|
||||
return sendJade(res, "login", {
|
||||
wasAlreadyLoggedIn: true
|
||||
});
|
||||
}
|
||||
|
||||
sendJade(res, "login", {
|
||||
redirect: req.query.dest || req.header("referer")
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request for /logout. Clears auth cookie
|
||||
*/
|
||||
function handleLogout(req, res) {
|
||||
csrf.verify(req);
|
||||
|
||||
res.clearCookie("auth");
|
||||
req.user = res.user = null;
|
||||
// Try to find an appropriate redirect
|
||||
var dest = req.query.dest || req.header("referer");
|
||||
dest = dest && dest.match(/login|logout|account/) ? null : dest;
|
||||
|
||||
var host = req.hostname;
|
||||
if (host.indexOf(Config.get("http.root-domain")) !== -1) {
|
||||
res.clearCookie("auth", { domain: Config.get("http.root-domain-dotted") });
|
||||
}
|
||||
|
||||
if (dest) {
|
||||
res.redirect(dest);
|
||||
} else {
|
||||
sendJade(res, "logout", {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a GET request for /register
|
||||
*/
|
||||
function handleRegisterPage(req, res) {
|
||||
if (webserver.redirectHttps(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user) {
|
||||
sendJade(res, "register", {});
|
||||
return;
|
||||
}
|
||||
|
||||
sendJade(res, "register", {
|
||||
registered: false,
|
||||
registerError: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a registration request.
|
||||
*/
|
||||
function handleRegister(req, res) {
|
||||
csrf.verify(req);
|
||||
|
||||
var name = req.body.name;
|
||||
var password = req.body.password;
|
||||
var email = req.body.email;
|
||||
if (typeof email !== "string") {
|
||||
email = "";
|
||||
}
|
||||
var ip = webserver.ipForRequest(req);
|
||||
|
||||
if (typeof name !== "string" || typeof password !== "string") {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.length === 0) {
|
||||
sendJade(res, "register", {
|
||||
registerError: "Username must not be empty"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.match(Config.get("reserved-names.usernames"))) {
|
||||
sendJade(res, "register", {
|
||||
registerError: "That username is reserved"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length === 0) {
|
||||
sendJade(res, "register", {
|
||||
registerError: "Password must not be empty"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
password = password.substring(0, 100);
|
||||
|
||||
if (email.length > 0 && !$util.isValidEmail(email)) {
|
||||
sendJade(res, "register", {
|
||||
registerError: "Invalid email address"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.register(name, password, email, ip, function (err) {
|
||||
if (err) {
|
||||
sendJade(res, "register", {
|
||||
registerError: err
|
||||
});
|
||||
} else {
|
||||
Logger.eventlog.log("[register] " + ip + " registered account: " + name +
|
||||
(email.length > 0 ? " <" + email + ">" : ""));
|
||||
sendJade(res, "register", {
|
||||
registered: true,
|
||||
registerName: name,
|
||||
redirect: req.body.redirect
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Initializes auth callbacks
|
||||
*/
|
||||
init: function (app) {
|
||||
app.get("/login", handleLoginPage);
|
||||
app.post("/login", handleLogin);
|
||||
app.get("/logout", handleLogout);
|
||||
app.get("/register", handleRegisterPage);
|
||||
app.post("/register", handleRegister);
|
||||
}
|
||||
};
|
||||
46
src/web/csrf.js
Normal file
46
src/web/csrf.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Adapted from https://github.com/expressjs/csurf
|
||||
*/
|
||||
|
||||
var csrf = require("csrf");
|
||||
var createError = require("http-errors");
|
||||
|
||||
var tokens = csrf();
|
||||
|
||||
exports.init = function csrfInit (domain) {
|
||||
return function (req, res, next) {
|
||||
var secret = req.signedCookies._csrf;
|
||||
if (!secret) {
|
||||
secret = tokens.secretSync();
|
||||
res.cookie("_csrf", secret, {
|
||||
domain: domain,
|
||||
signed: true,
|
||||
httpOnly: true
|
||||
});
|
||||
}
|
||||
|
||||
var token;
|
||||
|
||||
req.csrfToken = function csrfToken() {
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
|
||||
token = tokens.create(secret);
|
||||
return token;
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
exports.verify = function csrfVerify(req) {
|
||||
var secret = req.signedCookies._csrf;
|
||||
var token = req.body._csrf || req.query._csrf;
|
||||
|
||||
if (!tokens.verify(secret, token)) {
|
||||
throw createError(403, 'invalid csrf token', {
|
||||
code: 'EBADCSRFTOKEN'
|
||||
});
|
||||
}
|
||||
};
|
||||
62
src/web/jade.js
Normal file
62
src/web/jade.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
var jade = require("jade");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var Config = require("../config");
|
||||
var templates = path.join(__dirname, "..", "..", "templates");
|
||||
var cache = {};
|
||||
|
||||
/**
|
||||
* Merges locals with globals for jade rendering
|
||||
*/
|
||||
function merge(locals, res) {
|
||||
var _locals = {
|
||||
siteTitle: Config.get("html-template.title"),
|
||||
siteDescription: Config.get("html-template.description"),
|
||||
siteAuthor: "Calvin 'calzoneman' 'cyzon' Montgomery",
|
||||
loginDomain: Config.get("https.enabled") ? Config.get("https.full-address")
|
||||
: Config.get("http.full-address"),
|
||||
csrfToken: res.req.csrfToken(),
|
||||
baseUrl: getBaseUrl(res)
|
||||
};
|
||||
if (typeof locals !== "object") {
|
||||
return _locals;
|
||||
}
|
||||
for (var key in locals) {
|
||||
_locals[key] = locals[key];
|
||||
}
|
||||
return _locals;
|
||||
}
|
||||
|
||||
function getBaseUrl(res) {
|
||||
var req = res.req;
|
||||
var proto;
|
||||
if (["http", "https"].indexOf(req.header("x-forwarded-proto")) >= 0) {
|
||||
proto = req.header("x-forwarded-proto");
|
||||
} else {
|
||||
proto = req.protocol;
|
||||
}
|
||||
|
||||
return proto + "://" + req.header("host");
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders and serves a jade template
|
||||
*/
|
||||
function sendJade(res, view, locals) {
|
||||
locals.loggedIn = locals.loggedIn || !!res.user;
|
||||
locals.loginName = locals.loginName || res.user ? res.user.name : false;
|
||||
if (!(view in cache) || Config.get("debug")) {
|
||||
var file = path.join(templates, view + ".jade");
|
||||
var fn = jade.compile(fs.readFileSync(file), {
|
||||
filename: file,
|
||||
pretty: !Config.get("http.minify")
|
||||
});
|
||||
cache[view] = fn;
|
||||
}
|
||||
var html = cache[view](merge(locals, res));
|
||||
res.send(html);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendJade: sendJade
|
||||
};
|
||||
278
src/web/webserver.js
Normal file
278
src/web/webserver.js
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
var path = require("path");
|
||||
var fs = require("fs");
|
||||
var net = require("net");
|
||||
var express = require("express");
|
||||
var webroot = path.join(__dirname, "..", "www");
|
||||
var sendJade = require("./jade").sendJade;
|
||||
var Server = require("../server");
|
||||
var $util = require("../utilities");
|
||||
var Logger = require("../logger");
|
||||
var Config = require("../config");
|
||||
var db = require("../database");
|
||||
var bodyParser = require("body-parser");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var serveStatic = require("serve-static");
|
||||
var morgan = require("morgan");
|
||||
var session = require("../session");
|
||||
var csrf = require("./csrf");
|
||||
var XSS = require("../xss");
|
||||
|
||||
const LOG_FORMAT = ':real-address - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"';
|
||||
morgan.token('real-address', function (req) { return req._ip; });
|
||||
|
||||
/**
|
||||
* Extracts an IP address from a request. Uses X-Forwarded-For if the IP is localhost
|
||||
*/
|
||||
function ipForRequest(req) {
|
||||
var ip = req.ip;
|
||||
if (ip === "127.0.0.1" || ip === "::1") {
|
||||
var xforward = req.header("x-forwarded-for");
|
||||
if (typeof xforward !== "string") {
|
||||
xforward = [];
|
||||
} else {
|
||||
xforward = xforward.split(",");
|
||||
}
|
||||
|
||||
for (var i = 0; i < xforward.length; i++) {
|
||||
if (net.isIP(xforward[i])) {
|
||||
return xforward[i];
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects a request to HTTPS if the server supports it
|
||||
*/
|
||||
function redirectHttps(req, res) {
|
||||
if (!req.secure && Config.get("https.enabled") && Config.get("https.redirect")) {
|
||||
var ssldomain = Config.get("https.full-address");
|
||||
if (ssldomain.indexOf(req.hostname) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
res.redirect(ssldomain + req.path);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects a request to HTTP if the server supports it
|
||||
*/
|
||||
function redirectHttp(req, res) {
|
||||
if (req.secure) {
|
||||
var domain = Config.get("http.full-address");
|
||||
res.redirect(domain + req.path);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a GET request for /r/:channel - serves channel.html
|
||||
*/
|
||||
function handleChannel(req, res) {
|
||||
if (!$util.isValidChannelName(req.params.channel)) {
|
||||
res.status(404);
|
||||
res.send("Invalid channel name '" + XSS.sanitizeText(req.params.channel) + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
var sio;
|
||||
if (net.isIPv6(ipForRequest(req))) {
|
||||
sio = Config.get("io.ipv6-default");
|
||||
}
|
||||
|
||||
if (!sio) {
|
||||
sio = Config.get("io.ipv4-default");
|
||||
}
|
||||
|
||||
sio += "/socket.io/socket.io.js";
|
||||
|
||||
sendJade(res, "channel", {
|
||||
channelName: req.params.channel,
|
||||
sioSource: sio
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request for the index page
|
||||
*/
|
||||
function handleIndex(req, res) {
|
||||
var channels = Server.getServer().packChannelList(true);
|
||||
channels.sort(function (a, b) {
|
||||
if (a.usercount === b.usercount) {
|
||||
return a.uniqueName > b.uniqueName ? -1 : 1;
|
||||
}
|
||||
|
||||
return b.usercount - a.usercount;
|
||||
});
|
||||
|
||||
sendJade(res, "index", {
|
||||
channels: channels
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a request for the socket.io information
|
||||
*/
|
||||
function handleSocketConfig(req, res) {
|
||||
res.type("application/javascript");
|
||||
|
||||
var sioconfig = Config.get("sioconfig");
|
||||
var iourl;
|
||||
var ip = ipForRequest(req);
|
||||
var ipv6 = false;
|
||||
|
||||
if (net.isIPv6(ip)) {
|
||||
iourl = Config.get("io.ipv6-default");
|
||||
ipv6 = true;
|
||||
}
|
||||
|
||||
if (!iourl) {
|
||||
iourl = Config.get("io.ipv4-default");
|
||||
}
|
||||
|
||||
sioconfig += "var IO_URL='" + iourl + "';";
|
||||
sioconfig += "var IO_V6=" + ipv6 + ";";
|
||||
res.send(sioconfig);
|
||||
}
|
||||
|
||||
function handleUserAgreement(req, res) {
|
||||
sendJade(res, "tos", {
|
||||
domain: Config.get("http.domain")
|
||||
});
|
||||
}
|
||||
|
||||
function handleContactPage(req, res) {
|
||||
// Make a copy to prevent messing with the original
|
||||
var contacts = Config.get("contacts").map(function (c) {
|
||||
return {
|
||||
name: c.name,
|
||||
email: c.email,
|
||||
title: c.title
|
||||
};
|
||||
});
|
||||
|
||||
// Rudimentary hiding of email addresses to prevent spambots
|
||||
contacts.forEach(function (c) {
|
||||
c.emkey = $util.randomSalt(16)
|
||||
var email = new Array(c.email.length);
|
||||
for (var i = 0; i < c.email.length; i++) {
|
||||
email[i] = String.fromCharCode(
|
||||
c.email.charCodeAt(i) ^ c.emkey.charCodeAt(i % c.emkey.length)
|
||||
);
|
||||
}
|
||||
c.email = escape(email.join(""));
|
||||
c.emkey = escape(c.emkey);
|
||||
});
|
||||
|
||||
sendJade(res, "contact", {
|
||||
contacts: contacts
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Initializes webserver callbacks
|
||||
*/
|
||||
init: function (app) {
|
||||
app.use(function (req, res, next) {
|
||||
req._ip = ipForRequest(req);
|
||||
next();
|
||||
});
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: false,
|
||||
limit: '1kb' // No POST data should ever exceed this size under normal usage
|
||||
}));
|
||||
if (Config.get("http.cookie-secret") === "change-me") {
|
||||
Logger.errlog.log("YOU SHOULD CHANGE THE VALUE OF cookie-secret IN config.yaml");
|
||||
}
|
||||
app.use(cookieParser(Config.get("http.cookie-secret")));
|
||||
app.use(csrf.init(Config.get("http.root-domain-dotted")));
|
||||
app.use(morgan(LOG_FORMAT, {
|
||||
stream: require("fs").createWriteStream(path.join(__dirname, "..", "..",
|
||||
"http.log"), {
|
||||
flags: "a",
|
||||
encoding: "utf-8"
|
||||
})
|
||||
}));
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
if (req.path.match(/^\/(css|js|img|boop).*$/)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!req.signedCookies || !req.signedCookies.auth) {
|
||||
return next();
|
||||
}
|
||||
|
||||
session.verifySession(req.signedCookies.auth, function (err, account) {
|
||||
if (!err) {
|
||||
req.user = res.user = account;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
if (Config.get("http.gzip")) {
|
||||
app.use(require("compression")({ threshold: Config.get("http.gzip-threshold") }));
|
||||
Logger.syslog.log("Enabled gzip compression");
|
||||
}
|
||||
|
||||
if (Config.get("http.minify")) {
|
||||
var cache = path.join(__dirname, "..", "..", "www", "cache")
|
||||
if (!fs.existsSync(cache)) {
|
||||
fs.mkdirSync(cache);
|
||||
}
|
||||
app.use(require("express-minify")({
|
||||
cache: cache
|
||||
}));
|
||||
Logger.syslog.log("Enabled express-minify for CSS and JS");
|
||||
}
|
||||
|
||||
app.get("/r/:channel", handleChannel);
|
||||
app.get("/", handleIndex);
|
||||
app.get("/sioconfig", handleSocketConfig);
|
||||
app.get("/useragreement", handleUserAgreement);
|
||||
app.get("/contact", handleContactPage);
|
||||
require("./auth").init(app);
|
||||
require("./account").init(app);
|
||||
require("./acp").init(app);
|
||||
require("../google2vtt").attach(app);
|
||||
app.use(serveStatic(path.join(__dirname, "..", "..", "www"), {
|
||||
maxAge: Config.get("http.max-age") || Config.get("http.cache-ttl")
|
||||
}));
|
||||
app.use(function (err, req, res, next) {
|
||||
if (err) {
|
||||
if (err.message && err.message.match(/failed to decode param/i)) {
|
||||
return res.status(400).send("Malformed path: " + req.path);
|
||||
} else if (err.message && err.message.match(/requested range not/i)) {
|
||||
return res.status(416).end();
|
||||
} else if (err.message && err.message.match(/request entity too large/i)) {
|
||||
return res.status(413).end();
|
||||
} else if (err.message && err.message.match(/bad request/i)) {
|
||||
return res.status(400).end("Bad Request");
|
||||
} else if (err.message && err.message.match(/invalid csrf token/i)) {
|
||||
res.status(403);
|
||||
sendJade(res, 'csrferror', { path: req.path });
|
||||
return;
|
||||
}
|
||||
Logger.errlog.log(err.stack);
|
||||
res.status(500).end();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
ipForRequest: ipForRequest,
|
||||
|
||||
redirectHttps: redirectHttps,
|
||||
|
||||
redirectHttp: redirectHttp
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue