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
Merge pull request #203 from simoleo89/fix/housekeeping-core-peer-rank
fix(housekeeping): harden privileged staff actions
This commit is contained in:
@@ -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);
|
||||
|
||||
+14
-8
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+18
-5
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
+13
-3
@@ -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();
|
||||
|
||||
+6
-2
@@ -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) {
|
||||
|
||||
+11
-2
@@ -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) {
|
||||
|
||||
+20
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
+10
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+13
-3
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+14
-5
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -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());
|
||||
}
|
||||
}
|
||||
+10
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -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;
|
||||
}
|
||||
|
||||
+12
-2
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-8
@@ -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;
|
||||
}
|
||||
|
||||
+47
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
-8
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -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, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.eu.habbo.habbohotel.modtool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingAuditLogContractTest {
|
||||
private static String auditLogSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writerUsesActionLogSchemaReadByHousekeepingClient() throws Exception {
|
||||
String source = auditLogSource();
|
||||
|
||||
assertTrue(source.contains("actor_id"), "housekeeping_log writer must persist actor_id");
|
||||
assertTrue(source.contains("actor_name"), "housekeeping_log writer must persist actor_name");
|
||||
assertTrue(source.contains("target_type"), "housekeeping_log writer must persist target_type");
|
||||
assertTrue(source.contains("target_id"), "housekeeping_log writer must persist target_id");
|
||||
assertTrue(source.contains("target_label"), "housekeeping_log writer must persist target_label");
|
||||
assertTrue(source.contains("success"), "housekeeping_log writer must persist success");
|
||||
assertFalse(source.contains("operator_id"), "housekeeping_log writer must not use the obsolete operator_id schema");
|
||||
assertFalse(source.contains("target_user_id"), "housekeeping_log writer must not use the obsolete target_user_id schema");
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingAuditCoverageContractTest {
|
||||
private static final List<String> SENSITIVE_HANDLERS = List.of(
|
||||
"HousekeepingBanUserEvent.java",
|
||||
"HousekeepingMuteUserEvent.java",
|
||||
"HousekeepingGiveCreditsEvent.java",
|
||||
"HousekeepingGiveCurrencyEvent.java",
|
||||
"HousekeepingResetUserPasswordEvent.java",
|
||||
"HousekeepingSetUserRankEvent.java",
|
||||
"HousekeepingSetHcSubscriptionEvent.java",
|
||||
"HousekeepingTradeLockUserEvent.java",
|
||||
"HousekeepingGrantItemEvent.java",
|
||||
"HousekeepingTransferRoomOwnershipEvent.java",
|
||||
"HousekeepingSendHotelAlertEvent.java",
|
||||
"HousekeepingDeleteRoomEvent.java",
|
||||
"HousekeepingForceDisconnectUserEvent.java",
|
||||
"HousekeepingKickAllFromRoomEvent.java",
|
||||
"HousekeepingKickUserEvent.java",
|
||||
"HousekeepingMuteRoomEvent.java",
|
||||
"HousekeepingRoomStateEvent.java",
|
||||
"HousekeepingUnbanUserEvent.java"
|
||||
);
|
||||
|
||||
@Test
|
||||
void sensitiveHousekeepingActionsWriteAuditEntries() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : SENSITIVE_HANDLERS) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
assertTrue(source.contains("HousekeepingAuditLog.log"),
|
||||
handler + " must append a housekeeping audit log entry after successful privileged actions");
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingGrantMutationContractTest {
|
||||
private static final Path CREDITS_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java");
|
||||
private static final Path CURRENCY_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java");
|
||||
private static final Path GRANT_ITEM_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java");
|
||||
|
||||
@Test
|
||||
void housekeepingGrantsRejectNegativeOrOversizedAmountsServerSide() throws IOException {
|
||||
String credits = Files.readString(CREDITS_SOURCE);
|
||||
String currency = Files.readString(CURRENCY_SOURCE);
|
||||
|
||||
assertTrue(credits.contains("HousekeepingMutationGuard.isPositiveGrantAmount(amount)"),
|
||||
"credit grants must only accept positive bounded amounts");
|
||||
assertTrue(currency.contains("HousekeepingMutationGuard.isPositiveGrantAmount(amount)"),
|
||||
"currency grants must only accept positive bounded amounts");
|
||||
}
|
||||
|
||||
@Test
|
||||
void housekeepingCurrencyGrantsRejectInvalidTypesAndMissingUsers() throws IOException {
|
||||
String currency = Files.readString(CURRENCY_SOURCE);
|
||||
|
||||
assertTrue(currency.contains("HousekeepingMutationGuard.isCurrencyType(currencyType)"),
|
||||
"currency grants must reject negative currency types");
|
||||
assertTrue(currency.contains("HousekeepingMutationGuard.userExists(userId)"),
|
||||
"offline currency grants must not create orphan users_currency rows");
|
||||
}
|
||||
|
||||
@Test
|
||||
void housekeepingItemGrantsRequireRealUsersAndItemsBeforeInsert() throws IOException {
|
||||
String grantItem = Files.readString(GRANT_ITEM_SOURCE);
|
||||
|
||||
int userCheck = grantItem.indexOf("HousekeepingMutationGuard.userExists(userId)");
|
||||
int itemCheck = grantItem.indexOf("HousekeepingMutationGuard.itemExists(itemId)");
|
||||
int insert = grantItem.indexOf("INSERT INTO items");
|
||||
|
||||
assertTrue(userCheck >= 0, "item grants must check the target user exists");
|
||||
assertTrue(itemCheck >= 0, "item grants must check the item base exists");
|
||||
assertTrue(userCheck < insert, "target user must be validated before item insert");
|
||||
assertTrue(itemCheck < insert, "item base must be validated before item insert");
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingInputGuardContractTest {
|
||||
@Test
|
||||
void stringDrivenHousekeepingHandlersUseSharedLimits() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : List.of(
|
||||
"HousekeepingBanUserEvent.java",
|
||||
"HousekeepingForceDisconnectUserEvent.java",
|
||||
"HousekeepingKickUserEvent.java",
|
||||
"HousekeepingMuteUserEvent.java",
|
||||
"HousekeepingTradeLockUserEvent.java",
|
||||
"HousekeepingSendHotelAlertEvent.java",
|
||||
"HousekeepingSearchRoomsEvent.java",
|
||||
"HousekeepingFindUserByNameEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("HousekeepingInputGuard.normalize"),
|
||||
handler + " must normalize client-provided strings before use");
|
||||
assertTrue(source.contains("HousekeepingInputGuard.isWithinLimit"),
|
||||
handler + " must bound client-provided strings before expensive work or broadcast");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditedFreeTextIsSanitizedBeforePersistence() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : List.of(
|
||||
"HousekeepingBanUserEvent.java",
|
||||
"HousekeepingForceDisconnectUserEvent.java",
|
||||
"HousekeepingKickUserEvent.java",
|
||||
"HousekeepingMuteUserEvent.java",
|
||||
"HousekeepingTradeLockUserEvent.java",
|
||||
"HousekeepingSendHotelAlertEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("HousekeepingInputGuard.auditValue"),
|
||||
handler + " must collapse control whitespace before writing free text to audit detail");
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingInputGuardTest {
|
||||
@Test
|
||||
void normalizesNullableText() {
|
||||
assertEquals("", HousekeepingInputGuard.normalize(null));
|
||||
assertEquals("hello", HousekeepingInputGuard.normalize(" hello "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void enforcesInclusiveLengthLimits() {
|
||||
assertTrue(HousekeepingInputGuard.isWithinLimit("abc", 3));
|
||||
assertFalse(HousekeepingInputGuard.isWithinLimit("abcd", 3));
|
||||
assertFalse(HousekeepingInputGuard.isWithinLimit(null, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditValuesCollapseControlWhitespaceAndCapLength() {
|
||||
String value = HousekeepingInputGuard.auditValue(" one\r\ntwo\tthree ");
|
||||
|
||||
assertEquals("one two three", value);
|
||||
|
||||
String oversized = "x".repeat(HousekeepingInputGuard.MAX_REASON_LENGTH + 1);
|
||||
assertEquals(HousekeepingInputGuard.MAX_REASON_LENGTH, HousekeepingInputGuard.auditValue(oversized).length());
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingMutationGuardTest {
|
||||
@Test
|
||||
void positiveGrantAmountsMustBeStrictlyPositiveAndBounded() {
|
||||
assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(-1));
|
||||
assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(0));
|
||||
assertTrue(HousekeepingMutationGuard.isPositiveGrantAmount(1));
|
||||
assertTrue(HousekeepingMutationGuard.isPositiveGrantAmount(HousekeepingMutationGuard.MAX_GRANT));
|
||||
assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(HousekeepingMutationGuard.MAX_GRANT + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void currencyTypesCannotBeNegative() {
|
||||
assertFalse(HousekeepingMutationGuard.isCurrencyType(-1));
|
||||
assertTrue(HousekeepingMutationGuard.isCurrencyType(0));
|
||||
assertTrue(HousekeepingMutationGuard.isCurrencyType(101));
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingRoomGuardContractTest {
|
||||
@Test
|
||||
void destructiveRoomActionsRespectOwnerRankCeiling() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : List.of(
|
||||
"HousekeepingDeleteRoomEvent.java",
|
||||
"HousekeepingKickAllFromRoomEvent.java",
|
||||
"HousekeepingMuteRoomEvent.java",
|
||||
"HousekeepingRoomStateEvent.java",
|
||||
"HousekeepingTransferRoomOwnershipEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("HousekeepingRoomGuard.canManageRoom(this.client.getHabbo(), room)"),
|
||||
handler + " must reject room mutations when the room owner is peer-or-higher ranked");
|
||||
assertTrue(source.contains("housekeeping.error.rank_too_high"),
|
||||
handler + " must surface a rank-ceiling error for protected room owners");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomGuardDelegatesToTargetRankGuard() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomGuard.java"));
|
||||
|
||||
assertTrue(source.contains("HousekeepingTargetRankGuard.canTargetUser(operator, room.getOwnerId())"),
|
||||
"room-owner checks must use the same core-rank peer override as user moderation");
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomMuteRejectsNegativeDurations() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteRoomEvent.java"));
|
||||
|
||||
assertTrue(source.contains("minutes < 0"),
|
||||
"room mute should reject negative duration values instead of treating them as unmute");
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingSanctionDurationContractTest {
|
||||
private static final Path BAN_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java");
|
||||
private static final Path MUTE_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java");
|
||||
private static final Path TRADE_LOCK_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java");
|
||||
|
||||
@Test
|
||||
void sanctionsUseSharedOverflowSafeDurationHelpers() throws IOException {
|
||||
String ban = Files.readString(BAN_SOURCE);
|
||||
String mute = Files.readString(MUTE_SOURCE);
|
||||
String tradeLock = Files.readString(TRADE_LOCK_SOURCE);
|
||||
|
||||
assertTrue(ban.contains("HousekeepingSanctionDuration.secondsFromHours(hours)"));
|
||||
assertTrue(mute.contains("HousekeepingSanctionDuration.secondsFromMinutes(minutes)"));
|
||||
assertTrue(tradeLock.contains("HousekeepingSanctionDuration.secondsFromHours(hours)"));
|
||||
assertTrue(tradeLock.contains("HousekeepingSanctionDuration.unixUntil("));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sanctionsDoNotUseOverflowProneIntDurationConstants() throws IOException {
|
||||
String ban = Files.readString(BAN_SOURCE);
|
||||
String tradeLock = Files.readString(TRADE_LOCK_SOURCE);
|
||||
|
||||
assertFalse(ban.contains("100 * 365 * 24 * 3600"));
|
||||
assertFalse(tradeLock.contains("100 * 365 * 24 * 3600"));
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class HousekeepingSanctionDurationTest {
|
||||
@Test
|
||||
void convertsHoursAndMinutesWithoutIntegerOverflow() {
|
||||
assertEquals(3600, HousekeepingSanctionDuration.secondsFromHours(1));
|
||||
assertEquals(60, HousekeepingSanctionDuration.secondsFromMinutes(1));
|
||||
assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.secondsFromHours(Integer.MAX_VALUE));
|
||||
assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.secondsFromMinutes(Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void capsUnixTimestampInsteadOfWrapping() {
|
||||
assertEquals(1_000_060, HousekeepingSanctionDuration.unixUntil(1_000_000, 60));
|
||||
assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.unixUntil(Integer.MAX_VALUE - 10, 60));
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingTargetRankGuardContractTest {
|
||||
private static final List<String> RANK_GUARDED_HANDLERS = List.of(
|
||||
"HousekeepingBanUserEvent.java",
|
||||
"HousekeepingForceDisconnectUserEvent.java",
|
||||
"HousekeepingGiveCreditsEvent.java",
|
||||
"HousekeepingGiveCurrencyEvent.java",
|
||||
"HousekeepingGrantItemEvent.java",
|
||||
"HousekeepingKickUserEvent.java",
|
||||
"HousekeepingMuteUserEvent.java",
|
||||
"HousekeepingResetUserPasswordEvent.java",
|
||||
"HousekeepingSetHcSubscriptionEvent.java",
|
||||
"HousekeepingTradeLockUserEvent.java",
|
||||
"HousekeepingUnbanUserEvent.java"
|
||||
);
|
||||
|
||||
@Test
|
||||
void privilegedUserActionsRejectPeerRanksUnlessOperatorIsCoreRank() throws Exception {
|
||||
String guard = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuard.java"));
|
||||
|
||||
assertTrue(guard.contains("static boolean canTargetRank(Habbo operator, int targetRankId)"),
|
||||
"rank comparison should be reusable for online and offline housekeeping targets");
|
||||
assertTrue(guard.contains("static boolean canAssignRank(Habbo operator, int rankId)"),
|
||||
"rank assignment should use the same peer/core ceiling as target moderation");
|
||||
assertTrue(guard.contains("targetRankId < operatorRankId"),
|
||||
"non-core housekeeping operators must only target lower-ranked users");
|
||||
assertTrue(guard.contains("isCoreRank(operatorRankId) && targetRankId <= operatorRankId"),
|
||||
"the highest/core rank should be allowed to target peer ranks");
|
||||
assertTrue(guard.contains("private static boolean isCoreRank(int rankId)"),
|
||||
"core-rank detection should be centralized in the target-rank guard");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sensitiveHousekeepingUserActionsUseRankGuard() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : RANK_GUARDED_HANDLERS) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
assertTrue(source.contains("HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)"),
|
||||
handler + " must reject equal or higher-ranked targets before applying privileged user actions");
|
||||
assertTrue(source.contains("housekeeping.error.rank_too_high"),
|
||||
handler + " must return a rank-ceiling error when the target cannot be managed");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void housekeepingRankChangesUseCentralRankCeilings() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java"));
|
||||
|
||||
assertTrue(source.contains("HousekeepingTargetRankGuard.canAssignRank(this.client.getHabbo(), rank.getId())"),
|
||||
"housekeeping rank assignment must not grant peer-or-higher ranks to non-core operators");
|
||||
assertTrue(source.contains("HousekeepingTargetRankGuard.canTargetRank(this.client.getHabbo(), targetRankId)"),
|
||||
"housekeeping rank assignment must not modify peer-or-higher ranked targets for non-core operators");
|
||||
assertTrue(source.contains("housekeeping.error.user_not_found"),
|
||||
"rank changes must reject missing offline users instead of reporting success for a zero-row update");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user