diff --git a/acp.js b/acp.js index 6bbf0ea7..9a4bbcfb 100644 --- a/acp.js +++ b/acp.js @@ -1,24 +1,30 @@ var Server = require("./server"); var Auth = require("./auth"); var Database = require("./database"); +var ActionLog = require("./actionlog"); module.exports = { init: function(user) { + ActionLog.record(user.ip, user.name, "acp-init"); user.socket.on("acp-announce", function(data) { + ActionLog.record(user.ip, user.name, ["acp-announce", data]); Server.announcement = data; Server.io.sockets.emit("announcement", data); }); user.socket.on("acp-announce-clear", function() { + ActionLog.record(user.ip, user.name, "acp-announce-clear"); Server.announcement = null; }); user.socket.on("acp-global-ban", function(data) { + ActionLog.record(user.ip, user.name, ["acp-global-ban", data.ip]); Database.globalBanIP(data.ip, data.note); user.socket.emit("acp-global-banlist", Database.refreshGlobalBans()); }); user.socket.on("acp-global-unban", function(ip) { + ActionLog.record(user.ip, user.name, ["acp-global-unban", data.ip]); Database.globalUnbanIP(ip); user.socket.emit("acp-global-banlist", Database.refreshGlobalBans()); }); @@ -49,6 +55,7 @@ module.exports = { return; try { var hash = Database.generatePasswordReset(user.ip, data.name, data.email); + ActionLog.record(user.ip, user.name, ["acp-reset-password", data.name]); } catch(e) { user.socket.emit("acp-reset-password", { @@ -83,6 +90,7 @@ module.exports = { if(!db) return; + ActionLog.record(user.ip, user.name, ["acp-set-rank", data]); var query = Database.createQuery( "UPDATE registrations SET global_rank=? WHERE uname=?", [data.name, data.rank] @@ -120,6 +128,7 @@ module.exports = { var c = Server.channels[data.name]; if(!c) return; + ActionLog.record(user.ip, user.name, "acp-channel-unload"); c.initialized = data.save; c.users.forEach(function(u) { c.kick(u, "Channel shutting down"); @@ -127,5 +136,10 @@ module.exports = { Server.unload(c); } }); + + user.socket.on("acp-actionlog-clear", function() { + ActionLog.clear(); + ActionLog.record(user.ip, user.name, "acp-actionlog-clear"); + }); } } diff --git a/actionlog.js b/actionlog.js new file mode 100644 index 00000000..9f4701da --- /dev/null +++ b/actionlog.js @@ -0,0 +1,34 @@ +var fs = require("fs"); + +var buffer = []; + +exports.record = function(ip, name, action) { + buffer.push(JSON.stringify({ + ip: ip, + name: name, + action: action, + time: Date.now() + })); +} + +exports.flush = function() { + if(buffer.length == 0) + return; + var text = buffer.join("\n") + "\n"; + buffer = []; + fs.appendFile("action.log", text, function(err) { + if(err) { + errlog.log("Append to actionlog failed: "); + errlog.log(err); + } + }); +} + +exports.clear = function() { + try { + fs.renameSync("action.log", "action-until-"+Date.now()+".log"); + } + catch(e) { } +} + +setInterval(exports.flush, 15000); diff --git a/api.js b/api.js index 8726b0fd..20049d12 100644 --- a/api.js +++ b/api.js @@ -15,6 +15,7 @@ var Logger = require("./logger.js"); var apilog = new Logger.Logger("api.log"); var Database = require("./database.js"); var Config = require("./config.js"); +var ActionLog = require("./actionlog.js"); var fs = require("fs"); var plainHandlers = { @@ -32,11 +33,21 @@ var jsonHandlers = { "setprofile" : handleProfileChange, "getprofile" : handleProfileGet, "setemail" : handleEmailChange, - "globalbans" : handleGlobalBans, "admreports" : handleAdmReports, - "acppwreset" : handleAcpPasswordReset }; +function getClientIP(req) { + var ip; + var forward = req.header("x-forwarded-for"); + if(forward) { + ip = forward.split(",")[0]; + } + if(!ip) { + ip = req.connection.remoteAddress; + } + return ip; +} + function handle(path, req, res) { var parts = path.split("/"); var last = parts[parts.length - 1]; @@ -193,12 +204,14 @@ function handleLogin(params, req, res) { var row = Auth.login(name, pw, session); if(row) { + ActionLog.record(getClientIP(req), name, "login-success"); sendJSON(res, { success: true, session: row.session_hash }); } else { + ActionLog.record(getClientIP(req), name, "login-failure"); sendJSON(res, { error: "Invalid username/password", success: false @@ -219,6 +232,7 @@ function handlePasswordChange(params, req, res) { } var row = Auth.login(name, oldpw); if(row) { + ActionLog.record(getClientIP(req), name, "password-change"); var success = Auth.setUserPassword(name, newpw); sendJSON(res, { success: success, @@ -237,11 +251,12 @@ function handlePasswordChange(params, req, res) { function handlePasswordReset(params, req, res) { var name = params.name || ""; var email = unescape(params.email || ""); - var ip = req.socket.address().address; + var ip = getClientIP(req); var hash = false; try { hash = Database.generatePasswordReset(ip, name, email); + ActionLog.record(ip, name, "password-reset-generate"); } catch(e) { sendJSON(res, { @@ -301,7 +316,7 @@ function handlePasswordReset(params, req, res) { function handlePasswordRecover(params, req, res) { var hash = params.hash || ""; - var ip = req.socket.address().address; + var ip = getClientIP(req); try { var info = Database.recoverPassword(hash); @@ -310,10 +325,12 @@ function handlePasswordRecover(params, req, res) { name: info[0], pw: info[1] }); + ActionLog.record(ip, name, "password-recover-success"); Logger.syslog.log(ip + " recovered password for " + name); return; } catch(e) { + ActionLog.record(ip, name, "password-recover-failure"); sendJSON(res, { success: false, error: e @@ -412,6 +429,7 @@ function handleEmailChange(params, req, res) { var row = Auth.login(name, pw); if(row) { var success = Database.setUserEmail(name, email); + ActionLog.record(getClientIP(req), name, ["email-update", email]); sendJSON(res, { success: success, error: success ? "" : "Email update failed", @@ -438,6 +456,7 @@ function handleRegister(params, req, res) { return; } else if(Auth.isRegistered(name)) { + ActionLog.record(getClientIP(req), name, "register-failure"); sendJSON(res, { success: false, error: "That username is already taken" @@ -445,6 +464,7 @@ function handleRegister(params, req, res) { return false; } else if(!Auth.validateName(name)) { + ActionLog.record(getClientIP(req), name, "register-failure"); sendJSON(res, { success: false, error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores" @@ -453,7 +473,8 @@ function handleRegister(params, req, res) { else { var session = Auth.register(name, pw); if(session) { - Logger.syslog.log(this.ip + " registered " + name); + ActionLog.record(getClientIP(req), name, "register-success"); + Logger.syslog.log(getClientIP(req) + " registered " + name); sendJSON(res, { success: true, session: session @@ -468,103 +489,12 @@ function handleRegister(params, req, res) { } } -function handleGlobalBans(params, req, res) { - var name = params.name || ""; - var pw = params.pw || ""; - var session = params.session || ""; - var row = Auth.login(name, pw, session); - if(!row || row.global_rank < 255) { - res.send(403); - return; - } - - var action = params.action || "list"; - if(action == "list") { - var gbans = Database.refreshGlobalBans(); - sendJSON(res, gbans); - } - else if(action == "add") { - var ip = params.ip || ""; - var reason = params.reason || ""; - if(!ip.match(/\d+\.\d+\.(\d+\.(\d+)?)?/)) { - sendJSON(res, { - error: "Invalid IP address" - }); - return; - } - var result = Database.globalBanIP(ip, reason); - sendJSON(res, { - success: result, - ip: ip, - reason: reason - }); - } - else if(action == "remove") { - var ip = params.ip || ""; - if(!ip.match(/\d+\.\d+\.(\d+\.(\d+)?)?/)) { - sendJSON(res, { - error: "Invalid IP address" - }); - return; - } - var result = Database.globalUnbanIP(ip); - sendJSON(res, { - success: result, - ip: ip, - }); - } - else { - sendJSON(res, { - error: "Invalid action: " + action - }); - } -} - function handleAdmReports(params, req, res) { sendJSON(res, { error: "Not implemented" }); } -function handleAcpPasswordReset(params, req, res) { - var name = params.name || ""; - var pw = params.pw || ""; - var session = params.session || ""; - var row = Auth.login(name, pw, session); - if(!row || row.global_rank < 255) { - res.send(403); - return; - } - - var action = params.action || ""; - if(action == "reset") { - var uname = params.reset_name; - if(Auth.getGlobalRank(uname) > row.global_rank) { - sendJSON(res, { - success: false - }); - return; - } - var new_pw = Database.resetPassword(uname); - if(new_pw) { - sendJSON(res, { - success: true, - pw: new_pw - }); - } - else { - sendJSON(res, { - success: false - }); - } - } - else { - sendJSON(res, { - success: false - }); - } -} - // Helper function function pipeLast(res, file, len) { fs.stat(file, function(err, data) { @@ -599,6 +529,10 @@ function handleReadLog(params, req, res) { else if(type == "err") { pipeLast(res, "error.log", 1024*1024); } + else if(type == "action") { + ActionLog.flush(); + pipeLast(res, "action.log", 1024*1024*100); + } else if(type == "channel") { var chan = params.channel || ""; fs.exists("chanlogs/" + chan + ".log", function(exists) { diff --git a/channel.js b/channel.js index 10dd81f4..28396245 100644 --- a/channel.js +++ b/channel.js @@ -268,12 +268,14 @@ function incrementalDump(chan) { Channel.prototype.tryRegister = function(user) { if(this.registered) { + ActionLog.record(user.ip, user.name, ["channel-register-failure", this.name]); user.socket.emit("registerChannel", { success: false, error: "This channel is already registered" }); } else if(!user.loggedIn) { + ActionLog.record(user.ip, user.name, ["channel-register-failure", this.name]); user.socket.emit("registerChannel", { success: false, error: "You must log in to register a channel" @@ -281,6 +283,7 @@ Channel.prototype.tryRegister = function(user) { } else if(!Rank.hasPermission(user, "registerChannel")) { + ActionLog.record(user.ip, user.name, ["channel-register-failure", this.name]); user.socket.emit("registerChannel", { success: false, error: "You don't have permission to register this channel" @@ -288,6 +291,7 @@ Channel.prototype.tryRegister = function(user) { } else { if(Database.registerChannel(this.name)) { + ActionLog.record(user.ip, user.name, ["channel-register-success", this.name]); this.registered = true; this.initialized = true; this.saveDump(); diff --git a/user.js b/user.js index 6191d1d1..b3408fe2 100644 --- a/user.js +++ b/user.js @@ -18,6 +18,7 @@ var Database = require("./database.js"); var Logger = require("./logger.js"); var Config = require("./config.js"); var ACP = require("./acp"); +var ActionLog = require("./actionlog"); // Represents a client connected via socket.io var User = function(socket, ip) { @@ -526,13 +527,6 @@ User.prototype.initCallbacks = function() { var lastguestlogin = {}; // Attempt to login User.prototype.login = function(name, pw, session) { - if(this.channel != null && name != "") { - for(var i = 0; i < this.channel.users.length; i++) { - if(this.channel.users[i].name == name) { - this.channel.kick(this.channel.users[i], "Duplicate login"); - } - } - } // No password => try guest login if(pw == "" && session == "") { if(this.ip in lastguestlogin) { @@ -593,6 +587,14 @@ User.prototype.login = function(name, pw, session) { try { var row; if((row = Auth.login(name, pw, session))) { + if(this.channel != null) { + for(var i = 0; i < this.channel.users.length; i++) { + if(this.channel.users[i].name == name) { + this.channel.kick(this.channel.users[i], "Duplicate login"); + } + } + } + ActionLog.record(this.ip, name, "login-success"); this.loggedIn = true; this.socket.emit("login", { success: true, @@ -620,6 +622,7 @@ User.prototype.login = function(name, pw, session) { } // Wrong password else { + ActionLog.record(this.ip, this.name, "login-failure"); this.socket.emit("login", { success: false, error: "Invalid session" diff --git a/www/acp.html b/www/acp.html index 735451ad..3b3fa1fc 100644 --- a/www/acp.html +++ b/www/acp.html @@ -59,6 +59,7 @@
  • Global Bans
  • Users
  • Loaded Channels
  • +
  • Action Log
  • @@ -174,6 +175,22 @@ +
    +

    Action Log

    + + + + + + + + + + + +
    IP AddressNameActionTime
    +
    diff --git a/www/assets/js/acp.js b/www/assets/js/acp.js index 3ab28ddb..1d946aec 100644 --- a/www/assets/js/acp.js +++ b/www/assets/js/acp.js @@ -48,6 +48,27 @@ $("#show_chanloaded").click(function() { $("#listloaded_refresh").click(function() { socket.emit("acp-list-loaded"); }); +menuHandler("#show_actionlog", "#actionlog"); +$("#show_actionlog").click(getActionLog); +$("#actionlog_filter").click(function() { + var actions = $(this).val(); + $("#actionlog tbody").remove(); + $("#actionlog table").data("entries").forEach(function(e) { + if(typeof e.action == "string" && actions.indexOf(e.action) == -1) + return; + if(typeof e.action == "object" && "0" in e.action && actions.indexOf(e.action[0]) == -1) + return; + + var tr = $("").appendTo($("#actionlog table")); + $("").text(e.ip).appendTo(tr); + $("").text(e.name).appendTo(tr); + $("").text(e.action).appendTo(tr); + $("").text(new Date(e.time).toTimeString()).appendTo(tr); + }); +}); +$("#actionlog_clear").click(function() { + socket.emit("acp-actionlog-clear"); +}); function getSyslog() { $.ajax(WEB_URL+"/api/plain/readlog?type=sys&"+AUTH).done(function(data) { @@ -61,6 +82,39 @@ function getErrlog() { }); } $("#errlog").click(getErrlog); +function getActionLog() { + $.ajax(WEB_URL+"/api/plain/readlog?type=action&"+AUTH).done(function(data) { + var entries = []; + var actions = []; + data.split("\n").forEach(function(ln) { + var entry; + try { + entry = JSON.parse(ln); + if(typeof entry.action == "string") { + if(actions.indexOf(entry.action) == -1) + actions.push(entry.action); + } + else if(typeof entry.action == "object" && "0" in entry.action) { + if(actions.indexOf(entry.action[0]) == -1) + actions.push(entry.action[0]); + } + entries.push(entry); + } + catch(e) { } + }); + entries.sort(function(a, b) { + return a.time == b.time ? 0 : (a.time < b.time ? 1 : -1); + }); + $("#actionlog table").data("entries", entries); + $("#actionlog_filter").html(""); + actions.sort(function(a, b) { + return a == b ? 0 : (a < b ? -1 : 1); + }); + actions.forEach(function(a) { + $("