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): set-rank + trade-lock + reset-password
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.
This commit is contained in:
@@ -725,5 +725,8 @@ public class PacketManager {
|
|||||||
this.registerHandler(Incoming.HousekeepingMuteUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteUserEvent.class);
|
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.HousekeepingKickUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickUserEvent.class);
|
||||||
this.registerHandler(Incoming.HousekeepingForceDisconnectUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingForceDisconnectUserEvent.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,4 +469,7 @@ public class Incoming {
|
|||||||
public static final int HousekeepingMuteUserEvent = 9104;
|
public static final int HousekeepingMuteUserEvent = 9104;
|
||||||
public static final int HousekeepingKickUserEvent = 9105;
|
public static final int HousekeepingKickUserEvent = 9105;
|
||||||
public static final int HousekeepingForceDisconnectUserEvent = 9106;
|
public static final int HousekeepingForceDisconnectUserEvent = 9106;
|
||||||
|
public static final int HousekeepingSetUserRankEvent = 9107;
|
||||||
|
public static final int HousekeepingTradeLockUserEvent = 9108;
|
||||||
|
public static final int HousekeepingResetUserPasswordEvent = 9109;
|
||||||
}
|
}
|
||||||
|
|||||||
+104
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
@@ -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, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -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, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user