You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
feat: housekeeping audit log + shared Gson instances
Security: - HousekeepingAuditLog: append-only audit trail of privileged actions. There was no record of which operator granted ranks/currency to whom. SetUserRank, GiveCredits and GiveCurrency now log operator id+name, action, target, detail and IP. Writes are async; the housekeeping_log table is created on first use (CREATE TABLE IF NOT EXISTS) so no manual migration is needed. Speed (minor): - RCONServerHandler / PluginManager: reuse a shared Gson instead of allocating a new parser per request/plugin-config load (Gson is thread-safe). The wired Gson builders were already cached singletons.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package com.eu.habbo.habbohotel.modtool;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* Append-only audit trail for privileged housekeeping/admin actions (rank grants,
|
||||
* currency grants, etc.). There was previously no record of which operator did
|
||||
* what to whom. Writes are dispatched off the calling thread; the backing table
|
||||
* is created on first use so no manual migration is required.
|
||||
*/
|
||||
public final class HousekeepingAuditLog {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(HousekeepingAuditLog.class);
|
||||
|
||||
private static volatile boolean tableReady = false;
|
||||
|
||||
private HousekeepingAuditLog() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a privileged action asynchronously.
|
||||
*
|
||||
* @param operatorId the acting staff member's user id
|
||||
* @param operatorName the acting staff member's username
|
||||
* @param action a short action key, e.g. {@code "user.set_rank"}
|
||||
* @param targetUserId the affected user's id (0 if not applicable)
|
||||
* @param detail free-form detail, e.g. {@code "rankId=6"} (capped to 512 chars)
|
||||
* @param ip the operator's IP, for correlation
|
||||
*/
|
||||
public static void log(int operatorId, String operatorName, String action, int targetUserId, String detail, String ip) {
|
||||
Emulator.getThreading().run(() -> writeEntry(operatorId, operatorName, action, targetUserId, detail, ip));
|
||||
}
|
||||
|
||||
private static void writeEntry(int operatorId, String operatorName, String action, int targetUserId, String detail, String ip) {
|
||||
ensureTable();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO housekeeping_log (operator_id, operator_name, action, target_user_id, detail, ip, timestamp) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)")) {
|
||||
statement.setInt(1, operatorId);
|
||||
statement.setString(2, operatorName != null ? operatorName : "");
|
||||
statement.setString(3, action != null ? action : "");
|
||||
statement.setInt(4, targetUserId);
|
||||
statement.setString(5, truncate(detail));
|
||||
statement.setString(6, ip != null ? ip : "");
|
||||
statement.setInt(7, Emulator.getIntUnixTimestamp());
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to write housekeeping audit log entry", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String truncate(String detail) {
|
||||
if (detail == null) return "";
|
||||
return detail.length() > 512 ? detail.substring(0, 512) : detail;
|
||||
}
|
||||
|
||||
private static void ensureTable() {
|
||||
if (tableReady) {
|
||||
return;
|
||||
}
|
||||
synchronized (HousekeepingAuditLog.class) {
|
||||
if (tableReady) {
|
||||
return;
|
||||
}
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute(
|
||||
"CREATE TABLE IF NOT EXISTS housekeeping_log (" +
|
||||
"id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
|
||||
"operator_id INT NOT NULL, " +
|
||||
"operator_name VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"action VARCHAR(64) NOT NULL, " +
|
||||
"target_user_id INT NOT NULL DEFAULT 0, " +
|
||||
"detail VARCHAR(512) NOT NULL DEFAULT '', " +
|
||||
"ip VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"timestamp INT NOT NULL, " +
|
||||
"PRIMARY KEY (id), " +
|
||||
"KEY idx_operator (operator_id), " +
|
||||
"KEY idx_target (target_user_id), " +
|
||||
"KEY idx_timestamp (timestamp)" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
tableReady = true;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to create housekeeping_log table", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -39,6 +39,7 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||
// giveCredits already pushes UserCreditsComposer and persists via the
|
||||
// standard HabboInfo write path; nothing extra needed for the online branch.
|
||||
online.giveCredits(amount);
|
||||
this.audit(userId, amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
return;
|
||||
}
|
||||
@@ -58,6 +59,15 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
this.audit(userId, amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
|
||||
private void audit(int userId, int amount) {
|
||||
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||
this.client.getHabbo().getHabboInfo().getId(),
|
||||
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||
ACTION_KEY, userId, "amount=" + amount,
|
||||
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -53,6 +53,7 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||
online.givePoints(currencyType, amount);
|
||||
}
|
||||
|
||||
this.audit(actionKey, userId, currencyType, amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||
return;
|
||||
}
|
||||
@@ -70,6 +71,15 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
this.audit(actionKey, userId, currencyType, amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||
}
|
||||
|
||||
private void audit(String actionKey, int userId, int currencyType, int amount) {
|
||||
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||
this.client.getHabbo().getHabboInfo().getId(),
|
||||
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||
actionKey, userId, "type=" + currencyType + " amount=" + amount,
|
||||
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -102,6 +102,12 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
|
||||
online.getClient().sendResponse(new UserPermissionsComposer(online));
|
||||
}
|
||||
|
||||
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||
this.client.getHabbo().getHabboInfo().getId(),
|
||||
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||
ACTION_KEY, userId, "rankId=" + rankId,
|
||||
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RCONServerHandler.class);
|
||||
|
||||
// Gson is thread-safe and immutable once built — share one instance instead
|
||||
// of allocating a parser per RCON request.
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
@Override
|
||||
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
||||
String adress = ctx.channel().remoteAddress().toString().split(":")[0].replace("/", "");
|
||||
@@ -38,7 +42,7 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
||||
byte[] d = new byte[data.readableBytes()];
|
||||
data.getBytes(0, d);
|
||||
String message = new String(d);
|
||||
Gson gson = new Gson();
|
||||
Gson gson = GSON;
|
||||
String response = "ERROR";
|
||||
String key = "";
|
||||
try {
|
||||
|
||||
@@ -67,6 +67,10 @@ public class PluginManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PluginManager.class);
|
||||
|
||||
// Gson is thread-safe and immutable once built — reuse one instance instead
|
||||
// of building a parser per plugin-config load.
|
||||
private static final Gson PLUGIN_GSON = new GsonBuilder().create();
|
||||
|
||||
private final THashSet<HabboPlugin> plugins = new THashSet<>();
|
||||
private final THashSet<Method> methods = new THashSet<>();
|
||||
|
||||
@@ -275,8 +279,7 @@ public class PluginManager {
|
||||
if (stream.read(content) > 0) {
|
||||
String body = new String(content);
|
||||
|
||||
Gson gson = new GsonBuilder().create();
|
||||
HabboPluginConfiguration pluginConfigurtion = gson.fromJson(body, HabboPluginConfiguration.class);
|
||||
HabboPluginConfiguration pluginConfigurtion = PLUGIN_GSON.fromJson(body, HabboPluginConfiguration.class);
|
||||
|
||||
try {
|
||||
Class<?> clazz = urlClassLoader.loadClass(pluginConfigurtion.main);
|
||||
|
||||
Reference in New Issue
Block a user