From 525c124fa570551f376e392db4cfb7ff710346da Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:17:42 +0200 Subject: [PATCH] feat(housekeeping): set-rank + trade-lock + reset-password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes out the users-domain HK actions. * Incoming 9107 HousekeepingSetUserRankEvent — (userId, rankId). Validates the rank exists in `permission_ranks`, UPDATEs users.rank, and if the target is online rebinds their HabboInfo to the fresh Rank object and ships a UserPermissionsComposer so server-side hasPermission() and the client's useHasPermission(key) consumers re-render against the new permissions without a relog. * Incoming 9108 HousekeepingTradeLockUserEvent — (userId, hours, reason). Writes `users_settings.trade_locked_until = now + hours*3600` so the lock survives logout/login. Online targets also get their in-memory HabboStats.allowTrade cleared and an optional alert. * Incoming 9109 HousekeepingResetUserPasswordEvent — (userId). Generates a 12-char alphanumeric (SecureRandom over a curated ambiguity-free alphabet), writes its SHA-256 hex to users.password (the column is varchar(64) — already sized for SHA-256 hex) and blanks auth_ticket so any live SSO ticket can't bypass the reset. Plaintext is returned to the operator in the action-result message — they relay it out-of-band. If your CMS uses a hash other than SHA-256, swap the MessageDigest.getInstance constant. `mvn compile` clean. --- .../com/eu/habbo/messages/PacketManager.java | 3 + .../eu/habbo/messages/incoming/Incoming.java | 3 + .../HousekeepingResetUserPasswordEvent.java | 104 ++++++++++++++++++ .../HousekeepingSetUserRankEvent.java | 71 ++++++++++++ .../HousekeepingTradeLockUserEvent.java | 77 +++++++++++++ 5 files changed, 258 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index bc45b669..00f69eb4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -725,5 +725,8 @@ public class PacketManager { this.registerHandler(Incoming.HousekeepingMuteUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteUserEvent.class); this.registerHandler(Incoming.HousekeepingKickUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickUserEvent.class); this.registerHandler(Incoming.HousekeepingForceDisconnectUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingForceDisconnectUserEvent.class); + this.registerHandler(Incoming.HousekeepingSetUserRankEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetUserRankEvent.class); + this.registerHandler(Incoming.HousekeepingTradeLockUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTradeLockUserEvent.class); + this.registerHandler(Incoming.HousekeepingResetUserPasswordEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingResetUserPasswordEvent.class); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index a43ba1ab..5f4a1a58 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -469,4 +469,7 @@ public class Incoming { public static final int HousekeepingMuteUserEvent = 9104; public static final int HousekeepingKickUserEvent = 9105; public static final int HousekeepingForceDisconnectUserEvent = 9106; + public static final int HousekeepingSetUserRankEvent = 9107; + public static final int HousekeepingTradeLockUserEvent = 9108; + public static final int HousekeepingResetUserPasswordEvent = 9109; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java new file mode 100644 index 00000000..1f1a57ba --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java @@ -0,0 +1,104 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Reset a user's password to a fresh random 12-character alphanumeric + * string. Persists the SHA-256 hex of the new password into + * `users.password` (varchar(64) — sized to hold SHA-256 hex), clears + * `auth_ticket` so any active session can't be re-used to bypass the + * reset, and ships the PLAINTEXT new password back to the operator in + * the action-result `message` so they can communicate it out-of-band. + * + * If your CMS uses a hash other than SHA-256 (bcrypt / argon2 / SHA-1), + * swap the MessageDigest constant — the rest of the flow is hash-agnostic. + */ +public class HousekeepingResetUserPasswordEvent extends MessageHandler { + private static final String ACTION_KEY = "user.reset_password"; + private static final String PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; + private static final int PASSWORD_LENGTH = 12; + + private static final SecureRandom RNG = new SecureRandom(); + + @Override + public int getRatelimit() { + return 2000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + + if (userId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + String plain = randomPassword(); + String hash; + + try { + hash = sha256Hex(plain); + } catch (NoSuchAlgorithmException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.hash_failed")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users SET password = ?, auth_ticket = '' WHERE id = ? LIMIT 1")) { + statement.setString(1, hash); + statement.setInt(2, userId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + // 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://. + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, plain)); + } + + private static String randomPassword() { + StringBuilder sb = new StringBuilder(PASSWORD_LENGTH); + + for (int i = 0; i < PASSWORD_LENGTH; i++) { + sb.append(PASSWORD_ALPHABET.charAt(RNG.nextInt(PASSWORD_ALPHABET.length()))); + } + + return sb.toString(); + } + + private static String sha256Hex(String plain) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(plain.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(digest.length * 2); + + for (byte b : digest) { + hex.append(String.format("%02x", b)); + } + + return hex.toString(); + } +} 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 new file mode 100644 index 00000000..67f35d8a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java @@ -0,0 +1,71 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.permissions.PermissionsManager; +import com.eu.habbo.habbohotel.permissions.Rank; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; +import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class HousekeepingSetUserRankEvent extends MessageHandler { + private static final String ACTION_KEY = "user.set_rank"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int rankId = this.packet.readInt(); + + if (userId <= 0 || rankId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager(); + + if (!permissions.rankExists(rankId)) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_not_found")); + return; + } + + Rank rank = permissions.getRank(rankId); + + // Persist for the offline path. Online users get their in-memory + // HabboInfo.rank rebound below so server-side hasPermission() + // checks land on the new permission set without a relogin. + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users SET rank = ? WHERE id = ? LIMIT 1")) { + statement.setInt(1, rankId); + statement.setInt(2, userId); + statement.execute(); + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (online != null) { + online.getHabboInfo().setRank(rank); + // Ship the refreshed permissions snapshot — same payload the + // :update_permissions command emits when a rank is rebound. + online.getClient().sendResponse(new UserPermissionsComposer(online)); + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java new file mode 100644 index 00000000..cf961c15 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java @@ -0,0 +1,77 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Apply an arbitrary-duration trade lock. Writes + * `users_settings.trade_locked_until = now + hours*3600` so the lock + * survives logout/login — that column is the canonical timestamp the + * mod-tool user-info composer queries on. Online users also get their + * in-memory HabboStats.allowTrade flag cleared so the lock takes + * effect on the active session without waiting for a relog. + */ +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() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int hours = this.packet.readInt(); + String reason = this.packet.readString(); + + if (userId <= 0 || hours <= 0) { + 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; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET trade_locked_until = ? WHERE user_id = ? LIMIT 1")) { + statement.setInt(1, lockedUntil); + statement.setInt(2, userId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (online != null) { + online.getHabboStats().setAllowTrade(false); + + if (reason != null && !reason.isEmpty()) { + online.alert(reason); + } + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +}