From dcc23ba744641f88ba62df4ee82ded9f0fead6b0 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 9 Jun 2026 15:56:49 +0000 Subject: [PATCH] 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. --- .../modtool/HousekeepingAuditLog.java | 97 +++++++++++++++++++ .../HousekeepingGiveCreditsEvent.java | 10 ++ .../HousekeepingGiveCurrencyEvent.java | 10 ++ .../HousekeepingSetUserRankEvent.java | 6 ++ .../rconserver/RCONServerHandler.java | 6 +- .../com/eu/habbo/plugin/PluginManager.java | 7 +- 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java new file mode 100644 index 00000000..0507115e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java @@ -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); + } + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java index 50a04b24..c47bad24 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java @@ -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()); + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java index afe2df66..5e5053a1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java @@ -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()); + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java index dbdba31c..db100eb8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java @@ -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, "")); } } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java index 0563fe0e..6d55c443 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java @@ -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 { diff --git a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java index a8fe1e44..59ff591a 100644 --- a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java +++ b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java @@ -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 plugins = new THashSet<>(); private final THashSet 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);