Add action log

This commit is contained in:
calzoneman 2013-06-21 21:28:21 -04:00
parent 9d90f6c660
commit 6943845d7a
7 changed files with 163 additions and 103 deletions

14
acp.js
View File

@ -1,24 +1,30 @@
var Server = require("./server"); var Server = require("./server");
var Auth = require("./auth"); var Auth = require("./auth");
var Database = require("./database"); var Database = require("./database");
var ActionLog = require("./actionlog");
module.exports = { module.exports = {
init: function(user) { init: function(user) {
ActionLog.record(user.ip, user.name, "acp-init");
user.socket.on("acp-announce", function(data) { user.socket.on("acp-announce", function(data) {
ActionLog.record(user.ip, user.name, ["acp-announce", data]);
Server.announcement = data; Server.announcement = data;
Server.io.sockets.emit("announcement", data); Server.io.sockets.emit("announcement", data);
}); });
user.socket.on("acp-announce-clear", function() { user.socket.on("acp-announce-clear", function() {
ActionLog.record(user.ip, user.name, "acp-announce-clear");
Server.announcement = null; Server.announcement = null;
}); });
user.socket.on("acp-global-ban", function(data) { 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); Database.globalBanIP(data.ip, data.note);
user.socket.emit("acp-global-banlist", Database.refreshGlobalBans()); user.socket.emit("acp-global-banlist", Database.refreshGlobalBans());
}); });
user.socket.on("acp-global-unban", function(ip) { user.socket.on("acp-global-unban", function(ip) {
ActionLog.record(user.ip, user.name, ["acp-global-unban", data.ip]);
Database.globalUnbanIP(ip); Database.globalUnbanIP(ip);
user.socket.emit("acp-global-banlist", Database.refreshGlobalBans()); user.socket.emit("acp-global-banlist", Database.refreshGlobalBans());
}); });
@ -49,6 +55,7 @@ module.exports = {
return; return;
try { try {
var hash = Database.generatePasswordReset(user.ip, data.name, data.email); var hash = Database.generatePasswordReset(user.ip, data.name, data.email);
ActionLog.record(user.ip, user.name, ["acp-reset-password", data.name]);
} }
catch(e) { catch(e) {
user.socket.emit("acp-reset-password", { user.socket.emit("acp-reset-password", {
@ -83,6 +90,7 @@ module.exports = {
if(!db) if(!db)
return; return;
ActionLog.record(user.ip, user.name, ["acp-set-rank", data]);
var query = Database.createQuery( var query = Database.createQuery(
"UPDATE registrations SET global_rank=? WHERE uname=?", "UPDATE registrations SET global_rank=? WHERE uname=?",
[data.name, data.rank] [data.name, data.rank]
@ -120,6 +128,7 @@ module.exports = {
var c = Server.channels[data.name]; var c = Server.channels[data.name];
if(!c) if(!c)
return; return;
ActionLog.record(user.ip, user.name, "acp-channel-unload");
c.initialized = data.save; c.initialized = data.save;
c.users.forEach(function(u) { c.users.forEach(function(u) {
c.kick(u, "Channel shutting down"); c.kick(u, "Channel shutting down");
@ -127,5 +136,10 @@ module.exports = {
Server.unload(c); Server.unload(c);
} }
}); });
user.socket.on("acp-actionlog-clear", function() {
ActionLog.clear();
ActionLog.record(user.ip, user.name, "acp-actionlog-clear");
});
} }
} }

34
actionlog.js Normal file
View File

@ -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);

126
api.js
View File

@ -15,6 +15,7 @@ var Logger = require("./logger.js");
var apilog = new Logger.Logger("api.log"); var apilog = new Logger.Logger("api.log");
var Database = require("./database.js"); var Database = require("./database.js");
var Config = require("./config.js"); var Config = require("./config.js");
var ActionLog = require("./actionlog.js");
var fs = require("fs"); var fs = require("fs");
var plainHandlers = { var plainHandlers = {
@ -32,11 +33,21 @@ var jsonHandlers = {
"setprofile" : handleProfileChange, "setprofile" : handleProfileChange,
"getprofile" : handleProfileGet, "getprofile" : handleProfileGet,
"setemail" : handleEmailChange, "setemail" : handleEmailChange,
"globalbans" : handleGlobalBans,
"admreports" : handleAdmReports, "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) { function handle(path, req, res) {
var parts = path.split("/"); var parts = path.split("/");
var last = parts[parts.length - 1]; var last = parts[parts.length - 1];
@ -193,12 +204,14 @@ function handleLogin(params, req, res) {
var row = Auth.login(name, pw, session); var row = Auth.login(name, pw, session);
if(row) { if(row) {
ActionLog.record(getClientIP(req), name, "login-success");
sendJSON(res, { sendJSON(res, {
success: true, success: true,
session: row.session_hash session: row.session_hash
}); });
} }
else { else {
ActionLog.record(getClientIP(req), name, "login-failure");
sendJSON(res, { sendJSON(res, {
error: "Invalid username/password", error: "Invalid username/password",
success: false success: false
@ -219,6 +232,7 @@ function handlePasswordChange(params, req, res) {
} }
var row = Auth.login(name, oldpw); var row = Auth.login(name, oldpw);
if(row) { if(row) {
ActionLog.record(getClientIP(req), name, "password-change");
var success = Auth.setUserPassword(name, newpw); var success = Auth.setUserPassword(name, newpw);
sendJSON(res, { sendJSON(res, {
success: success, success: success,
@ -237,11 +251,12 @@ function handlePasswordChange(params, req, res) {
function handlePasswordReset(params, req, res) { function handlePasswordReset(params, req, res) {
var name = params.name || ""; var name = params.name || "";
var email = unescape(params.email || ""); var email = unescape(params.email || "");
var ip = req.socket.address().address; var ip = getClientIP(req);
var hash = false; var hash = false;
try { try {
hash = Database.generatePasswordReset(ip, name, email); hash = Database.generatePasswordReset(ip, name, email);
ActionLog.record(ip, name, "password-reset-generate");
} }
catch(e) { catch(e) {
sendJSON(res, { sendJSON(res, {
@ -301,7 +316,7 @@ function handlePasswordReset(params, req, res) {
function handlePasswordRecover(params, req, res) { function handlePasswordRecover(params, req, res) {
var hash = params.hash || ""; var hash = params.hash || "";
var ip = req.socket.address().address; var ip = getClientIP(req);
try { try {
var info = Database.recoverPassword(hash); var info = Database.recoverPassword(hash);
@ -310,10 +325,12 @@ function handlePasswordRecover(params, req, res) {
name: info[0], name: info[0],
pw: info[1] pw: info[1]
}); });
ActionLog.record(ip, name, "password-recover-success");
Logger.syslog.log(ip + " recovered password for " + name); Logger.syslog.log(ip + " recovered password for " + name);
return; return;
} }
catch(e) { catch(e) {
ActionLog.record(ip, name, "password-recover-failure");
sendJSON(res, { sendJSON(res, {
success: false, success: false,
error: e error: e
@ -412,6 +429,7 @@ function handleEmailChange(params, req, res) {
var row = Auth.login(name, pw); var row = Auth.login(name, pw);
if(row) { if(row) {
var success = Database.setUserEmail(name, email); var success = Database.setUserEmail(name, email);
ActionLog.record(getClientIP(req), name, ["email-update", email]);
sendJSON(res, { sendJSON(res, {
success: success, success: success,
error: success ? "" : "Email update failed", error: success ? "" : "Email update failed",
@ -438,6 +456,7 @@ function handleRegister(params, req, res) {
return; return;
} }
else if(Auth.isRegistered(name)) { else if(Auth.isRegistered(name)) {
ActionLog.record(getClientIP(req), name, "register-failure");
sendJSON(res, { sendJSON(res, {
success: false, success: false,
error: "That username is already taken" error: "That username is already taken"
@ -445,6 +464,7 @@ function handleRegister(params, req, res) {
return false; return false;
} }
else if(!Auth.validateName(name)) { else if(!Auth.validateName(name)) {
ActionLog.record(getClientIP(req), name, "register-failure");
sendJSON(res, { sendJSON(res, {
success: false, success: false,
error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores" 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 { else {
var session = Auth.register(name, pw); var session = Auth.register(name, pw);
if(session) { 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, { sendJSON(res, {
success: true, success: true,
session: session 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) { function handleAdmReports(params, req, res) {
sendJSON(res, { sendJSON(res, {
error: "Not implemented" 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 // Helper function
function pipeLast(res, file, len) { function pipeLast(res, file, len) {
fs.stat(file, function(err, data) { fs.stat(file, function(err, data) {
@ -599,6 +529,10 @@ function handleReadLog(params, req, res) {
else if(type == "err") { else if(type == "err") {
pipeLast(res, "error.log", 1024*1024); pipeLast(res, "error.log", 1024*1024);
} }
else if(type == "action") {
ActionLog.flush();
pipeLast(res, "action.log", 1024*1024*100);
}
else if(type == "channel") { else if(type == "channel") {
var chan = params.channel || ""; var chan = params.channel || "";
fs.exists("chanlogs/" + chan + ".log", function(exists) { fs.exists("chanlogs/" + chan + ".log", function(exists) {

View File

@ -268,12 +268,14 @@ function incrementalDump(chan) {
Channel.prototype.tryRegister = function(user) { Channel.prototype.tryRegister = function(user) {
if(this.registered) { if(this.registered) {
ActionLog.record(user.ip, user.name, ["channel-register-failure", this.name]);
user.socket.emit("registerChannel", { user.socket.emit("registerChannel", {
success: false, success: false,
error: "This channel is already registered" error: "This channel is already registered"
}); });
} }
else if(!user.loggedIn) { else if(!user.loggedIn) {
ActionLog.record(user.ip, user.name, ["channel-register-failure", this.name]);
user.socket.emit("registerChannel", { user.socket.emit("registerChannel", {
success: false, success: false,
error: "You must log in to register a channel" error: "You must log in to register a channel"
@ -281,6 +283,7 @@ Channel.prototype.tryRegister = function(user) {
} }
else if(!Rank.hasPermission(user, "registerChannel")) { else if(!Rank.hasPermission(user, "registerChannel")) {
ActionLog.record(user.ip, user.name, ["channel-register-failure", this.name]);
user.socket.emit("registerChannel", { user.socket.emit("registerChannel", {
success: false, success: false,
error: "You don't have permission to register this channel" error: "You don't have permission to register this channel"
@ -288,6 +291,7 @@ Channel.prototype.tryRegister = function(user) {
} }
else { else {
if(Database.registerChannel(this.name)) { if(Database.registerChannel(this.name)) {
ActionLog.record(user.ip, user.name, ["channel-register-success", this.name]);
this.registered = true; this.registered = true;
this.initialized = true; this.initialized = true;
this.saveDump(); this.saveDump();

17
user.js
View File

@ -18,6 +18,7 @@ var Database = require("./database.js");
var Logger = require("./logger.js"); var Logger = require("./logger.js");
var Config = require("./config.js"); var Config = require("./config.js");
var ACP = require("./acp"); var ACP = require("./acp");
var ActionLog = require("./actionlog");
// Represents a client connected via socket.io // Represents a client connected via socket.io
var User = function(socket, ip) { var User = function(socket, ip) {
@ -526,13 +527,6 @@ User.prototype.initCallbacks = function() {
var lastguestlogin = {}; var lastguestlogin = {};
// Attempt to login // Attempt to login
User.prototype.login = function(name, pw, session) { 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 // No password => try guest login
if(pw == "" && session == "") { if(pw == "" && session == "") {
if(this.ip in lastguestlogin) { if(this.ip in lastguestlogin) {
@ -593,6 +587,14 @@ User.prototype.login = function(name, pw, session) {
try { try {
var row; var row;
if((row = Auth.login(name, pw, session))) { 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.loggedIn = true;
this.socket.emit("login", { this.socket.emit("login", {
success: true, success: true,
@ -620,6 +622,7 @@ User.prototype.login = function(name, pw, session) {
} }
// Wrong password // Wrong password
else { else {
ActionLog.record(this.ip, this.name, "login-failure");
this.socket.emit("login", { this.socket.emit("login", {
success: false, success: false,
error: "Invalid session" error: "Invalid session"

View File

@ -59,6 +59,7 @@
<li id="li_gbans"><a href="javascript:void(0)" id="show_gbans">Global Bans</a></li> <li id="li_gbans"><a href="javascript:void(0)" id="show_gbans">Global Bans</a></li>
<li id="li_userlookup"><a href="javascript:void(0)" id="show_userlookup">Users</a></li> <li id="li_userlookup"><a href="javascript:void(0)" id="show_userlookup">Users</a></li>
<li id="li_chanloaded"><a href="javascript:void(0)" id="show_chanloaded">Loaded Channels</a></li> <li id="li_chanloaded"><a href="javascript:void(0)" id="show_chanloaded">Loaded Channels</a></li>
<li id="li_actionlog"><a href="javascript:void(0)" id="show_actionlog">Action Log</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -174,6 +175,22 @@
</thead> </thead>
</table> </table>
</div> </div>
<div class="span12" id="actionlog">
<h3>Action Log</h3>
<select multiple="multiple" id="actionlog_filter">
</select>
<button class="btn btn-danger" id="actionlog_clear">Clear</button>
<table class="table table-bordered table-striped table-compact">
<thead>
<tr>
<th>IP Address</th>
<th>Name</th>
<th>Action</th>
<th>Time</th>
</tr>
</thead>
</table>
</div>
</div> </div>
</div> <!-- /container --> </div> <!-- /container -->
<div class="push"></div> <div class="push"></div>

View File

@ -48,6 +48,27 @@ $("#show_chanloaded").click(function() {
$("#listloaded_refresh").click(function() { $("#listloaded_refresh").click(function() {
socket.emit("acp-list-loaded"); 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 = $("<tr/>").appendTo($("#actionlog table"));
$("<td/>").text(e.ip).appendTo(tr);
$("<td/>").text(e.name).appendTo(tr);
$("<td/>").text(e.action).appendTo(tr);
$("<td/>").text(new Date(e.time).toTimeString()).appendTo(tr);
});
});
$("#actionlog_clear").click(function() {
socket.emit("acp-actionlog-clear");
});
function getSyslog() { function getSyslog() {
$.ajax(WEB_URL+"/api/plain/readlog?type=sys&"+AUTH).done(function(data) { $.ajax(WEB_URL+"/api/plain/readlog?type=sys&"+AUTH).done(function(data) {
@ -61,6 +82,39 @@ function getErrlog() {
}); });
} }
$("#errlog").click(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) {
$("<option/>").text(a).val(a).appendTo($("#actionlog_filter"));
});
});
}
function getChanlog() { function getChanlog() {
var chan = $("#channame").val(); var chan = $("#channame").val();
$.ajax(WEB_URL+"/api/plain/readlog?type=channel&channel="+chan+"&"+AUTH).done(function(data) { $.ajax(WEB_URL+"/api/plain/readlog?type=channel&channel="+chan+"&"+AUTH).done(function(data) {