Merge pull request #203 from simoleo89/fix/housekeeping-core-peer-rank

fix(housekeeping): harden privileged staff actions
This commit is contained in:
DuckieTM
2026-06-15 07:24:55 +02:00
committed by GitHub
36 changed files with 836 additions and 79 deletions
@@ -11,9 +11,8 @@ 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.
* currency grants, etc.). 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 {
@@ -43,24 +42,26 @@ public final class HousekeepingAuditLog {
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 : "");
"INSERT INTO housekeeping_log (timestamp, actor_id, actor_name, target_type, target_id, target_label, action, detail, success) " +
"VALUES (?, ?, ?, 'user', ?, '', ?, ?, 1)")) {
statement.setInt(1, Emulator.getIntUnixTimestamp());
statement.setInt(2, operatorId);
statement.setString(3, operatorName != null ? operatorName : "");
statement.setInt(4, targetUserId);
statement.setString(5, truncate(detail));
statement.setString(6, ip != null ? ip : "");
statement.setInt(7, Emulator.getIntUnixTimestamp());
statement.setString(5, action != null ? action : "");
statement.setString(6, truncate(detail, ip));
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 String truncate(String detail, String ip) {
String value = detail == null ? "" : detail;
if (ip != null && !ip.isEmpty()) {
value = value.isEmpty() ? "ip=" + ip : value + " ip=" + ip;
}
return value.length() > 500 ? value.substring(0, 500) : value;
}
private static void ensureTable() {
@@ -75,19 +76,19 @@ public final class HousekeepingAuditLog {
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 '', " +
"id INT NOT NULL AUTO_INCREMENT, " +
"timestamp INT NOT NULL, " +
"actor_id INT NOT NULL, " +
"actor_name VARCHAR(64) NOT NULL DEFAULT '', " +
"target_type VARCHAR(16) NOT NULL DEFAULT 'user', " +
"target_id INT NOT NULL DEFAULT 0, " +
"target_label VARCHAR(128) NOT NULL DEFAULT '', " +
"action VARCHAR(64) NOT NULL DEFAULT '', " +
"detail VARCHAR(500) NOT NULL DEFAULT '', " +
"success TINYINT NOT NULL DEFAULT 1, " +
"PRIMARY KEY (id), " +
"KEY idx_operator (operator_id), " +
"KEY idx_target (target_user_id), " +
"KEY idx_timestamp (timestamp)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
"KEY timestamp (timestamp)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
tableReady = true;
} catch (SQLException e) {
LOGGER.error("Failed to create housekeeping_log table", e);
@@ -17,9 +17,6 @@ import java.util.List;
*/
public class HousekeepingBanUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.ban";
private static final int SECONDS_IN_HOUR = 3600;
// 100-year ceiling, matches ModToolSanctionBanEvent's permanent ban.
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
@Override
public int getRatelimit() {
@@ -33,19 +30,23 @@ public class HousekeepingBanUserEvent extends MessageHandler {
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
String reason = HousekeepingInputGuard.normalize(this.packet.readString());
int hours = this.packet.readInt();
if (userId <= 0 || hours <= 0) {
if (userId <= 0 || hours <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
long durationLong = (long) hours * SECONDS_IN_HOUR;
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
int duration = HousekeepingSanctionDuration.secondsFromHours(hours);
List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager()
.ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0);
.ban(userId, this.client.getHabbo(), reason, duration, ModToolBanType.ACCOUNT, 0);
if (bans == null || bans.isEmpty()) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed"));
@@ -56,6 +57,11 @@ public class HousekeepingBanUserEvent extends MessageHandler {
// object, so we return the target user id as the actionId — it's
// the only stable handle the client can use until a dedicated
// housekeeping_log row id supersedes it.
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "hours=" + hours + " reason=" + HousekeepingInputGuard.auditValue(reason),
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -42,11 +42,14 @@ public class HousekeepingDeleteRoomEvent extends MessageHandler {
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room != null) {
room.ejectAll();
room.preventUnloading = false;
room.dispose();
Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room);
if (room == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
if (!HousekeepingRoomGuard.canManageRoom(this.client.getHabbo(), room)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
@@ -63,6 +66,16 @@ public class HousekeepingDeleteRoomEvent extends MessageHandler {
return;
}
room.ejectAll();
room.preventUnloading = false;
room.dispose();
Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room);
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, 0, "roomId=" + roomId,
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -20,9 +20,9 @@ public class HousekeepingFindUserByNameEvent extends MessageHandler {
return;
}
String username = this.packet.readString();
String username = HousekeepingInputGuard.normalize(this.packet.readString());
if (username == null || username.isEmpty()) {
if (username.isEmpty() || !HousekeepingInputGuard.isWithinLimit(username, HousekeepingInputGuard.MAX_LOOKUP_LENGTH)) {
this.client.sendResponse(new HousekeepingUserDetailComposer(null));
return;
}
@@ -26,9 +26,9 @@ public class HousekeepingForceDisconnectUserEvent extends MessageHandler {
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
String reason = HousekeepingInputGuard.normalize(this.packet.readString());
if (userId <= 0) {
if (userId <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
@@ -40,13 +40,23 @@ public class HousekeepingForceDisconnectUserEvent extends MessageHandler {
return;
}
if (reason != null && !reason.isEmpty()) {
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
if (!reason.isEmpty()) {
target.alert(reason);
}
// ACK first so the action result lands before the target's socket
// closes (otherwise an alerted user on the same emulator thread may
// already be torn down when we try to write).
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "reason=" + HousekeepingInputGuard.auditValue(reason),
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
target.disconnect();
@@ -12,7 +12,6 @@ import java.sql.SQLException;
public class HousekeepingGiveCreditsEvent extends MessageHandler {
private static final String ACTION_KEY = "user.give_credits";
private static final int MAX_GRANT = 1_000_000_000;
@Override
public int getRatelimit() {
@@ -28,11 +27,16 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
int userId = this.packet.readInt();
int amount = this.packet.readInt();
if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) {
if (userId <= 0 || !HousekeepingMutationGuard.isPositiveGrantAmount(amount)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (online != null) {
@@ -18,7 +18,6 @@ import java.sql.SQLException;
*/
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
private static final int CURRENCY_DUCKETS = 0;
private static final int MAX_GRANT = 1_000_000_000;
@Override
public int getRatelimit() {
@@ -37,11 +36,21 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
String actionKey = "user.give_currency_" + currencyType;
if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) {
if (userId <= 0 || !HousekeepingMutationGuard.isCurrencyType(currencyType) || !HousekeepingMutationGuard.isPositiveGrantAmount(amount)) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
return;
}
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.rank_too_high"));
return;
}
if (!HousekeepingMutationGuard.userExists(userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.user_not_found"));
return;
}
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (online != null) {
@@ -40,6 +40,21 @@ public class HousekeepingGrantItemEvent extends MessageHandler {
return;
}
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
if (!HousekeepingMutationGuard.userExists(userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
return;
}
if (!HousekeepingMutationGuard.itemExists(itemId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.item_not_found"));
return;
}
if (quantity > MAX_QUANTITY_PER_CALL) {
quantity = MAX_QUANTITY_PER_CALL;
}
@@ -57,6 +72,11 @@ public class HousekeepingGrantItemEvent extends MessageHandler {
return;
}
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "itemId=" + itemId + " quantity=" + quantity,
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.incoming.housekeeping;
final class HousekeepingInputGuard {
static final int MAX_LOOKUP_LENGTH = 64;
static final int MAX_REASON_LENGTH = 500;
static final int MAX_ALERT_LENGTH = 1000;
private HousekeepingInputGuard() {
}
static String normalize(String value) {
return value == null ? "" : value.trim();
}
static boolean isWithinLimit(String value, int maxLength) {
return value != null && value.length() <= maxLength;
}
static String auditValue(String value) {
String normalized = normalize(value)
.replace('\r', ' ')
.replace('\n', ' ')
.replace('\t', ' ');
return normalized.length() > MAX_REASON_LENGTH ? normalized.substring(0, MAX_REASON_LENGTH) : normalized;
}
}
@@ -34,8 +34,18 @@ public class HousekeepingKickAllFromRoomEvent extends MessageHandler {
return;
}
if (!HousekeepingRoomGuard.canManageRoom(this.client.getHabbo(), room)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
room.ejectAll();
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, 0, "roomId=" + roomId,
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -28,9 +28,9 @@ public class HousekeepingKickUserEvent extends MessageHandler {
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
String reason = HousekeepingInputGuard.normalize(this.packet.readString());
if (userId <= 0) {
if (userId <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
@@ -42,6 +42,11 @@ public class HousekeepingKickUserEvent extends MessageHandler {
return;
}
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
if (target.hasPermission(Permission.ACC_UNKICKABLE)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.target_unkickable"));
return;
@@ -51,10 +56,15 @@ public class HousekeepingKickUserEvent extends MessageHandler {
Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom());
}
if (reason != null && !reason.isEmpty()) {
if (!reason.isEmpty()) {
target.alert(reason);
}
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "reason=" + HousekeepingInputGuard.auditValue(reason),
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,46 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
final class HousekeepingMutationGuard {
static final int MAX_GRANT = 1_000_000_000;
private HousekeepingMutationGuard() {
}
static boolean isPositiveGrantAmount(int amount) {
return amount > 0 && amount <= MAX_GRANT;
}
static boolean isCurrencyType(int currencyType) {
return currencyType >= 0;
}
static boolean userExists(int userId) {
if (userId <= 0) {
return false;
}
if (Emulator.getGameEnvironment().getHabboManager().getHabbo(userId) != null) {
return true;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE id = ? LIMIT 1")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
return set.next();
}
} catch (Exception e) {
return false;
}
}
static boolean itemExists(int itemId) {
return itemId > 0 && Emulator.getGameEnvironment().getItemManager().getItem(itemId) != null;
}
}
@@ -31,7 +31,7 @@ public class HousekeepingMuteRoomEvent extends MessageHandler {
int roomId = this.packet.readInt();
int minutes = this.packet.readInt();
if (roomId <= 0) {
if (roomId <= 0 || minutes < 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
@@ -43,8 +43,18 @@ public class HousekeepingMuteRoomEvent extends MessageHandler {
return;
}
if (!HousekeepingRoomGuard.canManageRoom(this.client.getHabbo(), room)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
room.setMuted(minutes > 0);
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, 0, "roomId=" + roomId + " minutes=" + minutes + " muted=" + (minutes > 0),
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -15,7 +15,6 @@ import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultCompo
*/
public class HousekeepingMuteUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.mute";
private static final int SECONDS_IN_MINUTE = 60;
@Override
public int getRatelimit() {
@@ -29,10 +28,10 @@ public class HousekeepingMuteUserEvent extends MessageHandler {
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
String reason = HousekeepingInputGuard.normalize(this.packet.readString());
int minutes = this.packet.readInt();
if (userId <= 0 || minutes <= 0) {
if (userId <= 0 || minutes <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
@@ -44,12 +43,22 @@ public class HousekeepingMuteUserEvent extends MessageHandler {
return;
}
target.mute(minutes * SECONDS_IN_MINUTE, false);
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
if (reason != null && !reason.isEmpty()) {
target.mute(HousekeepingSanctionDuration.secondsFromMinutes(minutes), false);
if (!reason.isEmpty()) {
target.alert(reason);
}
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "minutes=" + minutes + " reason=" + HousekeepingInputGuard.auditValue(reason),
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -46,6 +46,11 @@ public class HousekeepingResetUserPasswordEvent extends MessageHandler {
return;
}
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
String plain = randomPassword();
String hash;
@@ -74,6 +79,11 @@ public class HousekeepingResetUserPasswordEvent extends MessageHandler {
// Plaintext flows through `message` — the client surfaces it via the
// status banner so the operator can read it once. SSL is on the
// operator: the only secure transport for the WS is wss://.
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "password_reset=1",
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, plain));
}
@@ -0,0 +1,13 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
final class HousekeepingRoomGuard {
private HousekeepingRoomGuard() {
}
static boolean canManageRoom(Habbo operator, Room room) {
return room != null && HousekeepingTargetRankGuard.canTargetUser(operator, room.getOwnerId());
}
}
@@ -41,9 +41,19 @@ public class HousekeepingRoomStateEvent extends MessageHandler {
return;
}
if (!HousekeepingRoomGuard.canManageRoom(this.client.getHabbo(), room)) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.rank_too_high"));
return;
}
room.setState(open ? RoomState.OPEN : RoomState.LOCKED);
room.save();
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
actionKey, 0, "roomId=" + roomId + " open=" + open,
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, roomId, ""));
}
}
@@ -0,0 +1,37 @@
package com.eu.habbo.messages.incoming.housekeeping;
final class HousekeepingSanctionDuration {
static final int SECONDS_IN_MINUTE = 60;
static final int SECONDS_IN_HOUR = 3600;
static final int MAX_SECONDS = Integer.MAX_VALUE;
private HousekeepingSanctionDuration() {
}
static int secondsFromHours(int hours) {
if (hours <= 0) {
return 0;
}
long seconds = (long) hours * SECONDS_IN_HOUR;
return seconds > MAX_SECONDS ? MAX_SECONDS : (int) seconds;
}
static int secondsFromMinutes(int minutes) {
if (minutes <= 0) {
return 0;
}
long seconds = (long) minutes * SECONDS_IN_MINUTE;
return seconds > MAX_SECONDS ? MAX_SECONDS : (int) seconds;
}
static int unixUntil(int now, int durationSeconds) {
if (durationSeconds <= 0) {
return now;
}
long until = (long) now + durationSeconds;
return until > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) until;
}
}
@@ -36,14 +36,11 @@ public class HousekeepingSearchRoomsEvent extends MessageHandler {
return;
}
String query = this.packet.readString();
String query = HousekeepingInputGuard.normalize(this.packet.readString());
boolean exactMatch = this.packet.readBoolean();
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
if (query == null) query = "";
query = query.trim();
if (query.isEmpty()) {
if (query.isEmpty() || !HousekeepingInputGuard.isWithinLimit(query, HousekeepingInputGuard.MAX_LOOKUP_LENGTH)) {
this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>()));
return;
}
@@ -31,13 +31,18 @@ public class HousekeepingSendHotelAlertEvent extends MessageHandler {
return;
}
String message = this.packet.readString();
String message = HousekeepingInputGuard.normalize(this.packet.readString());
if (message == null || message.trim().isEmpty()) {
if (message.isEmpty()) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.alert_empty"));
return;
}
if (!HousekeepingInputGuard.isWithinLimit(message, HousekeepingInputGuard.MAX_ALERT_LENGTH)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.input_too_long"));
return;
}
String body = message + "\r\n-" + this.client.getHabbo().getHabboInfo().getUsername();
ServerMessage broadcast = new StaffAlertWithLinkComposer(body, "").compose();
@@ -53,6 +58,11 @@ public class HousekeepingSendHotelAlertEvent extends MessageHandler {
reached++;
}
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, 0, "reached=" + reached + " message=" + HousekeepingInputGuard.auditValue(message),
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, reached, ""));
}
}
@@ -38,6 +38,11 @@ public class HousekeepingSetHcSubscriptionEvent extends MessageHandler {
return;
}
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
int now = Emulator.getIntUnixTimestamp();
int newExpire;
@@ -71,6 +76,11 @@ public class HousekeepingSetHcSubscriptionEvent extends MessageHandler {
return;
}
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "days=" + days + " expire=" + newExpire,
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -45,13 +45,7 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
Rank rank = permissions.getRank(rankId);
// Rank-ceiling guard: an operator must never be able to grant a rank
// above their own, nor modify a user who already outranks them. This
// mirrors GiveRankCommand and prevents privilege escalation through
// the housekeeping path (including self-promotion).
int operatorRankId = this.client.getHabbo().getHabboInfo().getRank().getId();
if (rank.getId() > operatorRankId) {
if (!HousekeepingTargetRankGuard.canAssignRank(this.client.getHabbo(), rank.getId())) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
@@ -77,7 +71,12 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
}
}
if (targetRankId > operatorRankId) {
if (targetRankId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
return;
}
if (!HousekeepingTargetRankGuard.canTargetRank(this.client.getHabbo(), targetRankId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
@@ -0,0 +1,47 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Rank;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
final class HousekeepingTargetRankGuard {
private HousekeepingTargetRankGuard() {
}
static boolean canTargetUser(Habbo operator, int targetUserId) {
if (operator == null || targetUserId <= 0) {
return false;
}
HabboInfo targetInfo = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(targetUserId);
if (targetInfo == null) {
return true;
}
return canTargetRank(operator, targetInfo.getRank().getId());
}
static boolean canTargetRank(Habbo operator, int targetRankId) {
if (operator == null || targetRankId <= 0) {
return false;
}
int operatorRankId = operator.getHabboInfo().getRank().getId();
return targetRankId < operatorRankId || isCoreRank(operatorRankId) && targetRankId <= operatorRankId;
}
static boolean canAssignRank(Habbo operator, int rankId) {
return canTargetRank(operator, rankId);
}
private static boolean isCoreRank(int rankId) {
int highestRankId = 0;
for (Rank rank : Emulator.getGameEnvironment().getPermissionsManager().getAllRanks()) {
highestRankId = Math.max(highestRankId, rank.getId());
}
return highestRankId > 0 && rankId >= highestRankId;
}
}
@@ -20,8 +20,6 @@ import java.sql.SQLException;
*/
public class HousekeepingTradeLockUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.trade_lock";
private static final int SECONDS_IN_HOUR = 3600;
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
@Override
public int getRatelimit() {
@@ -36,16 +34,20 @@ public class HousekeepingTradeLockUserEvent extends MessageHandler {
int userId = this.packet.readInt();
int hours = this.packet.readInt();
String reason = this.packet.readString();
String reason = HousekeepingInputGuard.normalize(this.packet.readString());
if (userId <= 0 || hours <= 0) {
if (userId <= 0 || hours <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
long durationLong = (long) hours * SECONDS_IN_HOUR;
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
int lockedUntil = Emulator.getIntUnixTimestamp() + duration;
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
int duration = HousekeepingSanctionDuration.secondsFromHours(hours);
int lockedUntil = HousekeepingSanctionDuration.unixUntil(Emulator.getIntUnixTimestamp(), duration);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET trade_locked_until = ? WHERE user_id = ? LIMIT 1")) {
@@ -67,11 +69,16 @@ public class HousekeepingTradeLockUserEvent extends MessageHandler {
if (online != null) {
online.getHabboStats().setAllowTrade(false);
if (reason != null && !reason.isEmpty()) {
if (!reason.isEmpty()) {
online.alert(reason);
}
}
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "hours=" + hours + " lockedUntil=" + lockedUntil + " reason=" + HousekeepingInputGuard.auditValue(reason),
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
@@ -39,6 +40,19 @@ public class HousekeepingTransferRoomOwnershipEvent extends MessageHandler {
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
if (!HousekeepingRoomGuard.canManageRoom(this.client.getHabbo(), room) ||
!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), newOwnerId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
HabboInfo newOwner = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(newOwnerId);
if (newOwner == null) {
@@ -62,6 +76,14 @@ public class HousekeepingTransferRoomOwnershipEvent extends MessageHandler {
return;
}
room.setOwnerId(newOwnerId);
room.setOwnerName(newOwner.getUsername());
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, newOwnerId, "roomId=" + roomId + " newOwner=" + newOwner.getUsername(),
this.client.getHabbo().getHabboInfo().getIpLogin());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -34,11 +34,23 @@ public class HousekeepingUnbanUserEvent extends MessageHandler {
return;
}
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
return;
}
// ModToolManager.unban only takes a username; the SQL UPDATE
// happens against active bans (ban_expire > now), so calling it
// on a never-banned user is a benign no-op that returns false.
boolean cleared = Emulator.getGameEnvironment().getModToolManager().unban(info.getUsername());
if (cleared) {
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
this.client.getHabbo().getHabboInfo().getId(),
this.client.getHabbo().getHabboInfo().getUsername(),
ACTION_KEY, userId, "username=" + info.getUsername(),
this.client.getHabbo().getHabboInfo().getIpLogin());
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, cleared, cleared ? userId : 0, cleared ? "" : "housekeeping.error.no_active_ban"));
}
}