Move action log to DB, throttle registrations

This commit is contained in:
calzoneman 2013-07-13 12:05:58 -04:00
parent 7bc86037b7
commit 5df30cb8a9
6 changed files with 149 additions and 86 deletions

10
acp.js
View File

@ -18,7 +18,7 @@ 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]);
ActionLog.record(user.ip, user.name, "acp-announce", data);
Server.announcement = data;
Server.io.sockets.emit("announcement", data);
});
@ -29,13 +29,13 @@ module.exports = {
});
user.socket.on("acp-global-ban", function(data) {
ActionLog.record(user.ip, user.name, "acp-global-ban", [data.ip]);
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", [ip]);
ActionLog.record(user.ip, user.name, "acp-global-unban", ip);
Database.globalUnbanIP(ip);
user.socket.emit("acp-global-banlist", Database.refreshGlobalBans());
});
@ -66,7 +66,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]);
ActionLog.record(user.ip, user.name, "acp-reset-password", data.name);
}
catch(e) {
user.socket.emit("acp-reset-password", {
@ -101,7 +101,7 @@ module.exports = {
if(!db)
return;
ActionLog.record(user.ip, user.name, "acp-set-rank", [data]);
ActionLog.record(user.ip, user.name, "acp-set-rank", data);
var query = Database.createQuery(
"UPDATE registrations SET global_rank=? WHERE uname=?",
[data.rank, data.name]

View File

@ -9,68 +9,95 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var fs = require("fs");
var Database = require("./database");
var Logger = require("./logger");
var buffer = [];
exports.record = function(ip, name, action, args) {
buffer.push(JSON.stringify({
ip: ip,
name: name,
action: action,
args: args ? args : [],
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);
if(typeof args === "undefined" || args === null) {
args = "";
} else {
try {
args = JSON.stringify(args);
} catch(e) {
args = "";
}
});
}
var db = Database.getConnection();
if(!db)
return false;
var query = Database.createQuery(
"INSERT INTO actionlog (ip, name, action, args, time) "+
"VALUES (?, ?, ?, ?, ?)",
[ip, name, action, args, Date.now()]
);
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to record action");
}
return result;
}
exports.clear = function(actions) {
clearInterval(FLUSH_TMR);
var rs = fs.createReadStream("action.log");
var ws = fs.createWriteStream("action.log.tmp");
function handleLine(ln) {
try {
js = JSON.parse(ln);
if(actions.indexOf(js.action) == -1)
ws.write(ln + "\n");
}
catch(e) { }
var db = Database.getConnection();
if(!db)
return false;
var list = new Array(actions.length);
for(var i = 0; i < actions.length; i++)
list[i] = "?";
var query = Database.createQuery(
"DELETE FROM actionlog WHERE action IN ("+
list.join(",")+
")",
actions
);
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to clear action log");
}
var buffer = "";
rs.on("data", function(chunk) {
buffer += chunk;
if(buffer.indexOf("\n") != -1) {
var lines = buffer.split("\n");
buffer = lines[lines.length - 1];
lines.length = lines.length - 1;
lines.forEach(handleLine);
}
});
rs.on("end", function() {
handleLine(buffer);
ws.end();
});
try {
fs.renameSync("action.log.tmp", "action.log");
}
catch(e) {
Logger.errlog.log("Failed to move action.log.tmp => action.log");
Logger.errlog.log(e);
}
FLUSH_TMR = setInterval(exports.flush, 15000);
return result;
}
var FLUSH_TMR = setInterval(exports.flush, 15000);
exports.tooManyRegistrations = function (ip) {
var db = Database.getConnection();
if(!db)
return true;
var query = Database.createQuery(
"SELECT * FROM actionlog WHERE ip=? AND action='register-success'"+
"AND time > ?",
[ip, Date.now() - 48 * 3600 * 1000]
);
var results = db.querySync(query);
if(!results) {
Logger.errlog.log("! Failed to check tooManyRegistrations");
return true;
}
var rows = results.fetchAllSync();
// TODO Config value for this
return rows.length > 4;
}
exports.readLog = function () {
var db = Database.getConnection();
if(!db)
return false;
var query = "SELECT * FROM actionlog";
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to read action log");
return [];
}
return result.fetchAllSync();
}

42
api.js
View File

@ -34,12 +34,13 @@ var jsonHandlers = {
"getprofile" : handleProfileGet,
"setemail" : handleEmailChange,
"admreports" : handleAdmReports,
"readactionlog" : handleReadActionLog
};
function getClientIP(req) {
var ip;
var forward = req.header("x-forwarded-for");
if(forward) {
if(Config.REVERSE_PROXY && forward) {
ip = forward.split(",")[0];
}
if(!ip) {
@ -256,7 +257,7 @@ function handlePasswordReset(params, req, res) {
var hash = false;
try {
hash = Database.generatePasswordReset(ip, name, email);
ActionLog.record(ip, name, "password-reset-generate");
ActionLog.record(ip, name, "password-reset-generate", email);
}
catch(e) {
sendJSON(res, {
@ -429,7 +430,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]);
ActionLog.record(getClientIP(req), name, "email-update", email);
sendJSON(res, {
success: success,
error: success ? "" : "Email update failed",
@ -447,6 +448,17 @@ function handleEmailChange(params, req, res) {
function handleRegister(params, req, res) {
var name = params.name || "";
var pw = params.pw || "";
if(ActionLog.tooManyRegistrations(getClientIP(req))) {
ActionLog.record(getClientIP(req), name, "register-failure",
"Too many recent registrations from this IP");
sendJSON(res, {
success: false,
error: "Your IP address has registered several accounts in "+
"the past 48 hours. Please wait a while or ask an "+
"administrator for assistance."
});
return;
}
if(pw == "") {
sendJSON(res, {
@ -456,7 +468,8 @@ function handleRegister(params, req, res) {
return;
}
else if(Auth.isRegistered(name)) {
ActionLog.record(getClientIP(req), name, "register-failure");
ActionLog.record(getClientIP(req), name, "register-failure",
"Name taken");
sendJSON(res, {
success: false,
error: "That username is already taken"
@ -464,7 +477,8 @@ function handleRegister(params, req, res) {
return false;
}
else if(!Auth.validateName(name)) {
ActionLog.record(getClientIP(req), name, "register-failure");
ActionLog.record(getClientIP(req), name, "register-failure",
"Invalid name");
sendJSON(res, {
success: false,
error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores"
@ -495,6 +509,20 @@ function handleAdmReports(params, req, res) {
});
}
function handleReadActionLog(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 actions = ActionLog.readLog();
sendJSON(res, actions);
}
// Helper function
function pipeLast(res, file, len) {
fs.stat(file, function(err, data) {
@ -529,10 +557,6 @@ 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) {

View File

@ -278,14 +278,16 @@ function incrementalDump(chan) {
Channel.prototype.tryRegister = function(user) {
if(this.registered) {
ActionLog.record(user.ip, user.name, "channel-register-failure", [this.name]);
ActionLog.record(user.ip, user.name, "channel-register-failure", [
this.name, "Channel already registered"]);
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]);
ActionLog.record(user.ip, user.name, "channel-register-failure", [
this.name, "Not logged in"]);
user.socket.emit("registerChannel", {
success: false,
error: "You must log in to register a channel"
@ -293,7 +295,8 @@ Channel.prototype.tryRegister = function(user) {
}
else if(!Rank.hasPermission(user, "registerChannel")) {
ActionLog.record(user.ip, user.name, "channel-register-failure", [this.name]);
ActionLog.record(user.ip, user.name, "channel-register-failure", [
this.name, "Insufficient permissions"]);
user.socket.emit("registerChannel", {
success: false,
error: "You don't have permission to register this channel"
@ -301,7 +304,7 @@ Channel.prototype.tryRegister = function(user) {
}
else {
if(Database.registerChannel(this.name, user.name)) {
ActionLog.record(user.ip, user.name, "channel-register-success", [this.name]);
ActionLog.record(user.ip, user.name, "channel-register-success", this.name);
this.registered = true;
this.initialized = true;
this.saveDump();

View File

@ -187,6 +187,20 @@ function init() {
if(!results) {
Logger.errlog.log("! Failed to create aliases table");
}
// Create action log table
query = ["CREATE TABLE IF NOT EXISTS `actionlog` (",
"`ip` VARCHAR(15) NOT NULL,",
"`name` VARCHAR(20) NOT NULL,",
"`action` VARCHAR(255) NOT NULL,",
"`args` TEXT NOT NULL,",
"`time` BIGINT NOT NULL,",
"PRIMARY KEY (`ip`, `time`), INDEX (`action`))",
"ENGINE = MyISAM;"].join("");
results = db.querySync(query);
if(!results) {
Logger.errlog.log("! Failed to create actionlog table");
}
}
/* REGION Global Bans */

View File

@ -142,18 +142,13 @@ function getErrlog() {
}
$("#errlog").click(getErrlog);
function getActionLog() {
$.ajax(WEB_URL+"/api/plain/readlog?type=action&"+AUTH).done(function(data) {
var entries = [];
$.getJSON(WEB_URL+"/api/json/readactionlog?"+AUTH+"&callback=?").done(function(data) {
var entries = data;
var actions = [];
data.split("\n").forEach(function(ln) {
var entry;
try {
entry = JSON.parse(ln);
if(actions.indexOf(entry.action) == -1)
actions.push(entry.action);
entries.push(entry);
}
catch(e) { }
entries.forEach(function (e) {
if(actions.indexOf(e.action) == -1)
actions.push(e.action);
e.time = parseInt(e.time);
});
var tbl = $("#actionlog table");
tbl.data("sortby", "time");
@ -165,8 +160,8 @@ function getActionLog() {
$("<td/>").text(e.ip).appendTo(tr);
$("<td/>").text(e.name).appendTo(tr);
$("<td/>").text(e.action).appendTo(tr);
$("<td/>").text(e.args.join(", ")).appendTo(tr);
$("<td/>").text(new Date(e.time).toTimeString()).appendTo(tr);
$("<td/>").text(e.args).appendTo(tr);
$("<td/>").text(new Date(e.time).toString()).appendTo(tr);
});
$("#actionlog table").data("entries", entries);
$("#actionlog_filter").html("");