You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 15:36:17 +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,
|
* Append-only audit trail for privileged housekeeping/admin actions (rank grants,
|
||||||
* currency grants, etc.). There was previously no record of which operator did
|
* currency grants, etc.). Writes are dispatched off the calling thread; the
|
||||||
* what to whom. Writes are dispatched off the calling thread; the backing table
|
* backing table is created on first use so no manual migration is required.
|
||||||
* is created on first use so no manual migration is required.
|
|
||||||
*/
|
*/
|
||||||
public final class HousekeepingAuditLog {
|
public final class HousekeepingAuditLog {
|
||||||
|
|
||||||
@@ -43,24 +42,26 @@ public final class HousekeepingAuditLog {
|
|||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
PreparedStatement statement = connection.prepareStatement(
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
"INSERT INTO housekeeping_log (operator_id, operator_name, action, target_user_id, detail, ip, timestamp) " +
|
"INSERT INTO housekeeping_log (timestamp, actor_id, actor_name, target_type, target_id, target_label, action, detail, success) " +
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?)")) {
|
"VALUES (?, ?, ?, 'user', ?, '', ?, ?, 1)")) {
|
||||||
statement.setInt(1, operatorId);
|
statement.setInt(1, Emulator.getIntUnixTimestamp());
|
||||||
statement.setString(2, operatorName != null ? operatorName : "");
|
statement.setInt(2, operatorId);
|
||||||
statement.setString(3, action != null ? action : "");
|
statement.setString(3, operatorName != null ? operatorName : "");
|
||||||
statement.setInt(4, targetUserId);
|
statement.setInt(4, targetUserId);
|
||||||
statement.setString(5, truncate(detail));
|
statement.setString(5, action != null ? action : "");
|
||||||
statement.setString(6, ip != null ? ip : "");
|
statement.setString(6, truncate(detail, ip));
|
||||||
statement.setInt(7, Emulator.getIntUnixTimestamp());
|
|
||||||
statement.execute();
|
statement.execute();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Failed to write housekeeping audit log entry", e);
|
LOGGER.error("Failed to write housekeeping audit log entry", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String truncate(String detail) {
|
private static String truncate(String detail, String ip) {
|
||||||
if (detail == null) return "";
|
String value = detail == null ? "" : detail;
|
||||||
return detail.length() > 512 ? detail.substring(0, 512) : 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() {
|
private static void ensureTable() {
|
||||||
@@ -75,19 +76,19 @@ public final class HousekeepingAuditLog {
|
|||||||
Statement statement = connection.createStatement()) {
|
Statement statement = connection.createStatement()) {
|
||||||
statement.execute(
|
statement.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS housekeeping_log (" +
|
"CREATE TABLE IF NOT EXISTS housekeeping_log (" +
|
||||||
"id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
|
"id INT NOT NULL AUTO_INCREMENT, " +
|
||||||
"operator_id INT NOT NULL, " +
|
|
||||||
"operator_name VARCHAR(64) NOT NULL DEFAULT '', " +
|
|
||||||
"action VARCHAR(64) NOT NULL, " +
|
|
||||||
"target_user_id INT NOT NULL DEFAULT 0, " +
|
|
||||||
"detail VARCHAR(512) NOT NULL DEFAULT '', " +
|
|
||||||
"ip VARCHAR(64) NOT NULL DEFAULT '', " +
|
|
||||||
"timestamp INT NOT NULL, " +
|
"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), " +
|
"PRIMARY KEY (id), " +
|
||||||
"KEY idx_operator (operator_id), " +
|
"KEY timestamp (timestamp)" +
|
||||||
"KEY idx_target (target_user_id), " +
|
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||||
"KEY idx_timestamp (timestamp)" +
|
|
||||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
|
||||||
tableReady = true;
|
tableReady = true;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Failed to create housekeeping_log table", 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 {
|
public class HousekeepingBanUserEvent extends MessageHandler {
|
||||||
private static final String ACTION_KEY = "user.ban";
|
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
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
@@ -33,19 +30,23 @@ public class HousekeepingBanUserEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int userId = this.packet.readInt();
|
int userId = this.packet.readInt();
|
||||||
String reason = this.packet.readString();
|
String reason = HousekeepingInputGuard.normalize(this.packet.readString());
|
||||||
int hours = this.packet.readInt();
|
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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long durationLong = (long) hours * SECONDS_IN_HOUR;
|
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
|
||||||
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
|
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()
|
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()) {
|
if (bans == null || bans.isEmpty()) {
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed"));
|
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
|
// object, so we return the target user id as the actionId — it's
|
||||||
// the only stable handle the client can use until a dedicated
|
// the only stable handle the client can use until a dedicated
|
||||||
// housekeeping_log row id supersedes it.
|
// 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, ""));
|
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);
|
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||||
|
|
||||||
if (room != null) {
|
if (room == null) {
|
||||||
room.ejectAll();
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||||
room.preventUnloading = false;
|
return;
|
||||||
room.dispose();
|
}
|
||||||
Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room);
|
|
||||||
|
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();
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
@@ -63,6 +66,16 @@ public class HousekeepingDeleteRoomEvent extends MessageHandler {
|
|||||||
return;
|
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, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -20,9 +20,9 @@ public class HousekeepingFindUserByNameEvent extends MessageHandler {
|
|||||||
return;
|
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));
|
this.client.sendResponse(new HousekeepingUserDetailComposer(null));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -26,9 +26,9 @@ public class HousekeepingForceDisconnectUserEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int userId = this.packet.readInt();
|
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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -40,13 +40,23 @@ public class HousekeepingForceDisconnectUserEvent extends MessageHandler {
|
|||||||
return;
|
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);
|
target.alert(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACK first so the action result lands before the target's socket
|
// ACK first so the action result lands before the target's socket
|
||||||
// closes (otherwise an alerted user on the same emulator thread may
|
// closes (otherwise an alerted user on the same emulator thread may
|
||||||
// already be torn down when we try to write).
|
// 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, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||||
|
|
||||||
target.disconnect();
|
target.disconnect();
|
||||||
|
|||||||
+6
-2
@@ -12,7 +12,6 @@ import java.sql.SQLException;
|
|||||||
|
|
||||||
public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||||
private static final String ACTION_KEY = "user.give_credits";
|
private static final String ACTION_KEY = "user.give_credits";
|
||||||
private static final int MAX_GRANT = 1_000_000_000;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
@@ -28,11 +27,16 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
|||||||
int userId = this.packet.readInt();
|
int userId = this.packet.readInt();
|
||||||
int amount = 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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
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);
|
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||||
|
|
||||||
if (online != null) {
|
if (online != null) {
|
||||||
|
|||||||
+11
-2
@@ -18,7 +18,6 @@ import java.sql.SQLException;
|
|||||||
*/
|
*/
|
||||||
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||||
private static final int CURRENCY_DUCKETS = 0;
|
private static final int CURRENCY_DUCKETS = 0;
|
||||||
private static final int MAX_GRANT = 1_000_000_000;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
@@ -37,11 +36,21 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
|||||||
|
|
||||||
String actionKey = "user.give_currency_" + currencyType;
|
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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
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);
|
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||||
|
|
||||||
if (online != null) {
|
if (online != null) {
|
||||||
|
|||||||
+20
@@ -40,6 +40,21 @@ public class HousekeepingGrantItemEvent extends MessageHandler {
|
|||||||
return;
|
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) {
|
if (quantity > MAX_QUANTITY_PER_CALL) {
|
||||||
quantity = MAX_QUANTITY_PER_CALL;
|
quantity = MAX_QUANTITY_PER_CALL;
|
||||||
}
|
}
|
||||||
@@ -57,6 +72,11 @@ public class HousekeepingGrantItemEvent extends MessageHandler {
|
|||||||
return;
|
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, ""));
|
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;
|
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();
|
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, ""));
|
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();
|
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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,11 @@ public class HousekeepingKickUserEvent extends MessageHandler {
|
|||||||
return;
|
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)) {
|
if (target.hasPermission(Permission.ACC_UNKICKABLE)) {
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.target_unkickable"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.target_unkickable"));
|
||||||
return;
|
return;
|
||||||
@@ -51,10 +56,15 @@ public class HousekeepingKickUserEvent extends MessageHandler {
|
|||||||
Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom());
|
Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reason != null && !reason.isEmpty()) {
|
if (!reason.isEmpty()) {
|
||||||
target.alert(reason);
|
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, ""));
|
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 roomId = this.packet.readInt();
|
||||||
int minutes = 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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,18 @@ public class HousekeepingMuteRoomEvent extends MessageHandler {
|
|||||||
return;
|
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);
|
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, ""));
|
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 {
|
public class HousekeepingMuteUserEvent extends MessageHandler {
|
||||||
private static final String ACTION_KEY = "user.mute";
|
private static final String ACTION_KEY = "user.mute";
|
||||||
private static final int SECONDS_IN_MINUTE = 60;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
@@ -29,10 +28,10 @@ public class HousekeepingMuteUserEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int userId = this.packet.readInt();
|
int userId = this.packet.readInt();
|
||||||
String reason = this.packet.readString();
|
String reason = HousekeepingInputGuard.normalize(this.packet.readString());
|
||||||
int minutes = this.packet.readInt();
|
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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -44,12 +43,22 @@ public class HousekeepingMuteUserEvent extends MessageHandler {
|
|||||||
return;
|
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);
|
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, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -46,6 +46,11 @@ public class HousekeepingResetUserPasswordEvent extends MessageHandler {
|
|||||||
return;
|
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 plain = randomPassword();
|
||||||
String hash;
|
String hash;
|
||||||
|
|
||||||
@@ -74,6 +79,11 @@ public class HousekeepingResetUserPasswordEvent extends MessageHandler {
|
|||||||
// Plaintext flows through `message` — the client surfaces it via the
|
// Plaintext flows through `message` — the client surfaces it via the
|
||||||
// status banner so the operator can read it once. SSL is on the
|
// status banner so the operator can read it once. SSL is on the
|
||||||
// operator: the only secure transport for the WS is wss://.
|
// 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));
|
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;
|
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.setState(open ? RoomState.OPEN : RoomState.LOCKED);
|
||||||
room.save();
|
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, ""));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String query = this.packet.readString();
|
String query = HousekeepingInputGuard.normalize(this.packet.readString());
|
||||||
boolean exactMatch = this.packet.readBoolean();
|
boolean exactMatch = this.packet.readBoolean();
|
||||||
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
|
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
|
||||||
|
|
||||||
if (query == null) query = "";
|
if (query.isEmpty() || !HousekeepingInputGuard.isWithinLimit(query, HousekeepingInputGuard.MAX_LOOKUP_LENGTH)) {
|
||||||
query = query.trim();
|
|
||||||
|
|
||||||
if (query.isEmpty()) {
|
|
||||||
this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>()));
|
this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -31,13 +31,18 @@ public class HousekeepingSendHotelAlertEvent extends MessageHandler {
|
|||||||
return;
|
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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.alert_empty"));
|
||||||
return;
|
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();
|
String body = message + "\r\n-" + this.client.getHabbo().getHabboInfo().getUsername();
|
||||||
ServerMessage broadcast = new StaffAlertWithLinkComposer(body, "").compose();
|
ServerMessage broadcast = new StaffAlertWithLinkComposer(body, "").compose();
|
||||||
|
|
||||||
@@ -53,6 +58,11 @@ public class HousekeepingSendHotelAlertEvent extends MessageHandler {
|
|||||||
reached++;
|
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, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, reached, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -38,6 +38,11 @@ public class HousekeepingSetHcSubscriptionEvent extends MessageHandler {
|
|||||||
return;
|
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 now = Emulator.getIntUnixTimestamp();
|
||||||
int newExpire;
|
int newExpire;
|
||||||
|
|
||||||
@@ -71,6 +76,11 @@ public class HousekeepingSetHcSubscriptionEvent extends MessageHandler {
|
|||||||
return;
|
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, ""));
|
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 rank = permissions.getRank(rankId);
|
||||||
|
|
||||||
// Rank-ceiling guard: an operator must never be able to grant a rank
|
if (!HousekeepingTargetRankGuard.canAssignRank(this.client.getHabbo(), rank.getId())) {
|
||||||
// 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) {
|
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
|
||||||
return;
|
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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
|
||||||
return;
|
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 {
|
public class HousekeepingTradeLockUserEvent extends MessageHandler {
|
||||||
private static final String ACTION_KEY = "user.trade_lock";
|
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
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
@@ -36,16 +34,20 @@ public class HousekeepingTradeLockUserEvent extends MessageHandler {
|
|||||||
|
|
||||||
int userId = this.packet.readInt();
|
int userId = this.packet.readInt();
|
||||||
int hours = 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"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long durationLong = (long) hours * SECONDS_IN_HOUR;
|
if (!HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)) {
|
||||||
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
|
||||||
int lockedUntil = Emulator.getIntUnixTimestamp() + duration;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int duration = HousekeepingSanctionDuration.secondsFromHours(hours);
|
||||||
|
int lockedUntil = HousekeepingSanctionDuration.unixUntil(Emulator.getIntUnixTimestamp(), duration);
|
||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET trade_locked_until = ? WHERE user_id = ? LIMIT 1")) {
|
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) {
|
if (online != null) {
|
||||||
online.getHabboStats().setAllowTrade(false);
|
online.getHabboStats().setAllowTrade(false);
|
||||||
|
|
||||||
if (reason != null && !reason.isEmpty()) {
|
if (!reason.isEmpty()) {
|
||||||
online.alert(reason);
|
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, ""));
|
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.Emulator;
|
||||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
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.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||||
@@ -39,6 +40,19 @@ public class HousekeepingTransferRoomOwnershipEvent extends MessageHandler {
|
|||||||
return;
|
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);
|
HabboInfo newOwner = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(newOwnerId);
|
||||||
|
|
||||||
if (newOwner == null) {
|
if (newOwner == null) {
|
||||||
@@ -62,6 +76,14 @@ public class HousekeepingTransferRoomOwnershipEvent extends MessageHandler {
|
|||||||
return;
|
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, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -34,11 +34,23 @@ public class HousekeepingUnbanUserEvent extends MessageHandler {
|
|||||||
return;
|
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
|
// ModToolManager.unban only takes a username; the SQL UPDATE
|
||||||
// happens against active bans (ban_expire > now), so calling it
|
// happens against active bans (ban_expire > now), so calling it
|
||||||
// on a never-banned user is a benign no-op that returns false.
|
// on a never-banned user is a benign no-op that returns false.
|
||||||
boolean cleared = Emulator.getGameEnvironment().getModToolManager().unban(info.getUsername());
|
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"));
|
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