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 #198 from simoleo89/chore/deps-resilience-validation
fix(rcon): harden privileged commands and payloads
This commit is contained in:
@@ -117,6 +117,37 @@
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Caffeine cache - high-performance local caching for hot emulator lookups -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Resilience4j - rate limits and circuit breakers for RCON/HTTP/external integrations -->
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-ratelimiter</artifactId>
|
||||
<version>2.4.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-circuitbreaker</artifactId>
|
||||
<version>2.4.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Hibernate Validator - Jakarta Bean Validation for packet/RCON/admin DTO guards -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
<version>9.1.0.Final</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Lang -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class AlertUser extends RCONMessage<AlertUser.JSONAlertUser> {
|
||||
|
||||
@@ -16,6 +19,7 @@ public class AlertUser extends RCONMessage<AlertUser.JSONAlertUser> {
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.alert(object.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
@@ -23,9 +27,12 @@ public class AlertUser extends RCONMessage<AlertUser.JSONAlertUser> {
|
||||
|
||||
static class JSONAlertUser {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
int user_id;
|
||||
|
||||
|
||||
@NotBlank(message = "invalid message")
|
||||
@Size(max = 4096, message = "invalid message")
|
||||
String message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.rcon;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
public class ChangeRoomOwner extends RCONMessage<ChangeRoomOwner.JSON> {
|
||||
@@ -11,15 +12,33 @@ public class ChangeRoomOwner extends RCONMessage<ChangeRoomOwner.JSON> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSON json) {
|
||||
if (json.room_id <= 0 || json.user_id <= 0) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "invalid room or user";
|
||||
return;
|
||||
}
|
||||
|
||||
HabboInfo owner = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(json.user_id);
|
||||
if (owner == null) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(json.room_id);
|
||||
|
||||
if (room != null) {
|
||||
room.setOwnerId(json.user_id);
|
||||
room.setOwnerName(json.username);
|
||||
room.setNeedsUpdate(true);
|
||||
room.save();
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
if (room == null) {
|
||||
this.status = RCONMessage.ROOM_NOT_FOUND;
|
||||
this.message = "room not found";
|
||||
return;
|
||||
}
|
||||
|
||||
room.setOwnerId(owner.getId());
|
||||
room.setOwnerName(owner.getUsername());
|
||||
room.setNeedsUpdate(true);
|
||||
room.save();
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
this.message = "updated room owner";
|
||||
}
|
||||
|
||||
static class JSON {
|
||||
|
||||
@@ -1,14 +1,48 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.commands.Command;
|
||||
import com.eu.habbo.habbohotel.commands.CommandHandler;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExecuteCommand extends RCONMessage<ExecuteCommand.JSONExecuteCommand> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ExecuteCommand.class);
|
||||
static final int DEFAULT_MAX_COMMAND_LENGTH = 256;
|
||||
private static final String DEFAULT_DENIED_PERMISSIONS = String.join(";",
|
||||
"cmd_shutdown",
|
||||
"cmd_update_config",
|
||||
"cmd_update_permissions",
|
||||
"cmd_give_rank",
|
||||
"cmd_badge",
|
||||
"cmd_gift",
|
||||
"cmd_credits",
|
||||
"cmd_points",
|
||||
"cmd_pixels",
|
||||
"cmd_massbadge",
|
||||
"cmd_masscredits",
|
||||
"cmd_massgift",
|
||||
"cmd_massduckets",
|
||||
"cmd_masspoints",
|
||||
"cmd_empty",
|
||||
"cmd_empty_bots",
|
||||
"cmd_empty_pets",
|
||||
"cmd_unload",
|
||||
"cmd_ban",
|
||||
"cmd_superban",
|
||||
"cmd_ip_ban",
|
||||
"cmd_machine_ban",
|
||||
"cmd_disconnect");
|
||||
|
||||
|
||||
public ExecuteCommand() {
|
||||
@@ -18,6 +52,33 @@ public class ExecuteCommand extends RCONMessage<ExecuteCommand.JSONExecuteComman
|
||||
@Override
|
||||
public void handle(Gson gson, JSONExecuteCommand json) {
|
||||
try {
|
||||
String commandLine = json.command.trim();
|
||||
int maxLength = parseMaxCommandLength(Emulator.getConfig().getValue("rcon.execute_command.max_length", String.valueOf(DEFAULT_MAX_COMMAND_LENGTH)));
|
||||
|
||||
if (!commandLine.startsWith(":") || commandLine.length() > maxLength) {
|
||||
this.status = STATUS_ERROR;
|
||||
this.message = "invalid command";
|
||||
return;
|
||||
}
|
||||
|
||||
String commandKey = commandKey(commandLine);
|
||||
if (commandKey.isEmpty()) {
|
||||
this.status = STATUS_ERROR;
|
||||
this.message = "invalid command";
|
||||
return;
|
||||
}
|
||||
|
||||
Command command = CommandHandler.getCommand(commandKey);
|
||||
String commandPermission = command != null && command.permission != null ? command.permission : commandKey;
|
||||
|
||||
if (!isAllowed(commandPermission,
|
||||
Emulator.getConfig().getValue("rcon.execute_command.denied_permissions", DEFAULT_DENIED_PERMISSIONS),
|
||||
Emulator.getConfig().getValue("rcon.execute_command.allowed_permissions", ""))) {
|
||||
this.status = STATUS_ERROR;
|
||||
this.message = "command not allowed";
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(json.user_id);
|
||||
|
||||
if (habbo == null) {
|
||||
@@ -26,18 +87,78 @@ public class ExecuteCommand extends RCONMessage<ExecuteCommand.JSONExecuteComman
|
||||
}
|
||||
|
||||
|
||||
CommandHandler.handleCommand(habbo.getClient(), json.command);
|
||||
if (!CommandHandler.handleCommand(habbo.getClient(), commandLine)) {
|
||||
this.status = STATUS_ERROR;
|
||||
this.message = "command failed";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
this.status = STATUS_ERROR;
|
||||
LOGGER.error("Caught exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isAllowed(String commandPermission, String deniedPermissions, String allowedPermissions) {
|
||||
String normalized = normalize(commandPermission);
|
||||
Set<String> allowed = permissionSet(allowedPermissions);
|
||||
if (!allowed.isEmpty()) {
|
||||
return allowed.contains(normalized);
|
||||
}
|
||||
|
||||
return !permissionSet(deniedPermissions).contains(normalized);
|
||||
}
|
||||
|
||||
static String commandKey(String commandLine) {
|
||||
if (commandLine == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String trimmed = commandLine.trim();
|
||||
if (!trimmed.startsWith(":")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String withoutPrefix = trimmed.substring(1).trim();
|
||||
if (withoutPrefix.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return withoutPrefix.split("\\s+", 2)[0].toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
static int parseMaxCommandLength(String configured) {
|
||||
try {
|
||||
int parsed = Integer.parseInt(configured);
|
||||
if (parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
return DEFAULT_MAX_COMMAND_LENGTH;
|
||||
}
|
||||
|
||||
private static Set<String> permissionSet(String permissions) {
|
||||
if (permissions == null || permissions.isBlank()) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
return Arrays.stream(permissions.split("[;,]"))
|
||||
.map(ExecuteCommand::normalize)
|
||||
.filter(value -> !value.isEmpty())
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static String normalize(String permission) {
|
||||
return permission == null ? "" : permission.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
static class JSONExecuteCommand {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@NotBlank(message = "invalid command")
|
||||
@Size(max = 512, message = "invalid command")
|
||||
public String command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
public class ForwardUser extends RCONMessage<ForwardUser.ForwardUserJSON> {
|
||||
|
||||
@@ -26,8 +27,10 @@ public class ForwardUser extends RCONMessage<ForwardUser.ForwardUserJSON> {
|
||||
|
||||
habbo.getClient().sendResponse(new ForwardToRoomComposer(object.room_id));
|
||||
Emulator.getGameEnvironment().getRoomManager().enterRoom(habbo, object.room_id, "", true);
|
||||
return;
|
||||
} else {
|
||||
this.status = RCONMessage.ROOM_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +39,11 @@ public class ForwardUser extends RCONMessage<ForwardUser.ForwardUserJSON> {
|
||||
|
||||
static class ForwardUserJSON {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid room")
|
||||
public int room_id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
import com.eu.habbo.messages.outgoing.friends.FriendRequestComposer;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
public class FriendRequest extends RCONMessage<FriendRequest.JSON> {
|
||||
public FriendRequest() {
|
||||
@@ -18,6 +19,18 @@ public class FriendRequest extends RCONMessage<FriendRequest.JSON> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSON json) {
|
||||
if (json.user_id == json.target_id) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "cannot friend self";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RconUserLookup.userExists(json.user_id) || !RconUserLookup.userExists(json.target_id)) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Messenger.friendRequested(json.user_id, json.target_id)) {
|
||||
Messenger.makeFriendRequest(json.user_id, json.target_id);
|
||||
|
||||
@@ -49,9 +62,11 @@ public class FriendRequest extends RCONMessage<FriendRequest.JSON> {
|
||||
|
||||
static class JSON {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid target")
|
||||
public int target_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,14 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboBadge;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import com.eu.habbo.messages.outgoing.users.AddUserBadgeComposer;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -53,11 +59,18 @@ public class GiveBadge extends RCONMessage<GiveBadge.GiveBadgeJSON> {
|
||||
this.message = Emulator.getTexts().getValue("commands.succes.cmd_badge.given").replace("%user%", username).replace("%badge%", badgeCode);
|
||||
}
|
||||
} else {
|
||||
HabboInfo habboInfo = HabboManager.getOfflineHabboInfo(json.user_id);
|
||||
if (habboInfo == null) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
|
||||
username = habboInfo.getUsername();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
for (String badgeCode : json.badge.split(";")) {
|
||||
int numberOfRows = 0;
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(slot_id) FROM users_badges INNER JOIN users ON users.id = user_id WHERE users.id = ? AND badge_code = ? LIMIT 1")) {
|
||||
statement.setInt(1, json.user_id);
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(slot_id) FROM users_badges WHERE user_id = ? AND badge_code = ? LIMIT 1")) {
|
||||
statement.setInt(1, habboInfo.getId());
|
||||
statement.setString(2, badgeCode);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()){
|
||||
@@ -70,8 +83,8 @@ public class GiveBadge extends RCONMessage<GiveBadge.GiveBadgeJSON> {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message += Emulator.getTexts().getValue("commands.error.cmd_badge.already_owns").replace("%user%", username).replace("%badge%", badgeCode) + "\r";
|
||||
} else {
|
||||
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO users_badges VALUES (null, (SELECT id FROM users WHERE users.id = ? LIMIT 1), 0, ?)", Statement.RETURN_GENERATED_KEYS)) {
|
||||
statement.setInt(1, json.user_id);
|
||||
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO users_badges (`id`, `user_id`, `slot_id`, `badge_code`) VALUES (null, ?, 0, ?)", Statement.RETURN_GENERATED_KEYS)) {
|
||||
statement.setInt(1, habboInfo.getId());
|
||||
statement.setString(2, badgeCode);
|
||||
statement.execute();
|
||||
}
|
||||
@@ -89,9 +102,13 @@ public class GiveBadge extends RCONMessage<GiveBadge.GiveBadgeJSON> {
|
||||
|
||||
static class GiveBadgeJSON {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id = -1;
|
||||
|
||||
|
||||
@NotBlank(message = "invalid badge")
|
||||
@Size(max = 512, message = "invalid badge")
|
||||
@Pattern(regexp = "[A-Za-z0-9_\\-;]+", message = "invalid badge")
|
||||
public String badge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -20,6 +21,18 @@ public class GiveCredits extends RCONMessage<GiveCredits.JSONGiveCredits> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSONGiveCredits object) {
|
||||
int maxAmount = RconGrantGuard.parseMaxAmount(
|
||||
Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT)));
|
||||
String validationError = RconGrantGuard.validateUserId(object.user_id);
|
||||
if (validationError == null) {
|
||||
validationError = RconGrantGuard.validatePositiveAmount(object.credits, maxAmount, "credits");
|
||||
}
|
||||
if (validationError != null) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
|
||||
|
||||
if (habbo != null) {
|
||||
@@ -28,10 +41,14 @@ public class GiveCredits extends RCONMessage<GiveCredits.JSONGiveCredits> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users SET credits = credits + ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, object.credits);
|
||||
statement.setInt(2, object.user_id);
|
||||
statement.execute();
|
||||
if (statement.executeUpdate() == 0) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.status = RCONMessage.SYSTEM_ERROR;
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.message = "offline";
|
||||
@@ -40,9 +57,11 @@ public class GiveCredits extends RCONMessage<GiveCredits.JSONGiveCredits> {
|
||||
|
||||
static class JSONGiveCredits {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid credits")
|
||||
public int credits;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -20,15 +21,34 @@ public class GivePixels extends RCONMessage<GivePixels.JSONGivePixels> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSONGivePixels object) {
|
||||
int maxAmount = RconGrantGuard.parseMaxAmount(
|
||||
Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT)));
|
||||
String validationError = RconGrantGuard.validateUserId(object.user_id);
|
||||
if (validationError == null) {
|
||||
validationError = RconGrantGuard.validatePositiveAmount(object.pixels, maxAmount, "pixels");
|
||||
}
|
||||
if (validationError != null) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.givePixels(object.pixels);
|
||||
} else {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_currency SET users_currency.amount = users_currency.amount + ? WHERE users_currency.user_id = ? AND users_currency.type = 0")) {
|
||||
statement.setInt(1, object.pixels);
|
||||
statement.setInt(2, object.user_id);
|
||||
statement.execute();
|
||||
if (!RconUserLookup.userExists(object.user_id)) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_currency (`user_id`, `type`, `amount`) VALUES (?, 0, ?) ON DUPLICATE KEY UPDATE amount = amount + ?")) {
|
||||
statement.setInt(1, object.user_id);
|
||||
statement.setInt(2, object.pixels);
|
||||
statement.setInt(3, object.pixels);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
this.status = RCONMessage.SYSTEM_ERROR;
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
@@ -40,9 +60,11 @@ public class GivePixels extends RCONMessage<GivePixels.JSONGivePixels> {
|
||||
|
||||
static class JSONGivePixels {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid pixels")
|
||||
public int pixels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -20,11 +22,32 @@ public class GivePoints extends RCONMessage<GivePoints.JSONGivePoints> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSONGivePoints object) {
|
||||
int maxAmount = RconGrantGuard.parseMaxAmount(
|
||||
Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT)));
|
||||
String validationError = RconGrantGuard.validateUserId(object.user_id);
|
||||
if (validationError == null) {
|
||||
validationError = RconGrantGuard.validateCurrencyType(object.type);
|
||||
}
|
||||
if (validationError == null) {
|
||||
validationError = RconGrantGuard.validatePositiveAmount(object.points, maxAmount, "points");
|
||||
}
|
||||
if (validationError != null) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.givePoints(object.type, object.points);
|
||||
} else {
|
||||
if (!RconUserLookup.userExists(object.user_id)) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_currency (`user_id`, `type`, `amount`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE amount = amount + ?")) {
|
||||
statement.setInt(1, object.user_id);
|
||||
statement.setInt(2, object.type);
|
||||
@@ -42,12 +65,15 @@ public class GivePoints extends RCONMessage<GivePoints.JSONGivePoints> {
|
||||
|
||||
static class JSONGivePoints {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid points")
|
||||
public int points;
|
||||
|
||||
|
||||
@Min(value = 0, message = "invalid currency type")
|
||||
public int type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,27 @@ public class GiveRespect extends RCONMessage<GiveRespect.JSONGiveRespect> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSONGiveRespect object) {
|
||||
int maxAmount = RconGrantGuard.parseMaxAmount(
|
||||
Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT)));
|
||||
String validationError = RconGrantGuard.validateUserId(object.user_id);
|
||||
if (validationError == null) {
|
||||
validationError = RconGrantGuard.validateNonNegativeAmount(object.respect_given, maxAmount, "respect_given");
|
||||
}
|
||||
if (validationError == null) {
|
||||
validationError = RconGrantGuard.validateNonNegativeAmount(object.respect_received, maxAmount, "respect_received");
|
||||
}
|
||||
if (validationError == null) {
|
||||
validationError = RconGrantGuard.validateNonNegativeAmount(object.daily_respects, maxAmount, "daily_respects");
|
||||
}
|
||||
if (validationError == null && object.respect_given == 0 && object.respect_received == 0 && object.daily_respects == 0) {
|
||||
validationError = "no respect grant provided";
|
||||
}
|
||||
if (validationError != null) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
|
||||
|
||||
if (habbo != null) {
|
||||
@@ -29,15 +50,26 @@ public class GiveRespect extends RCONMessage<GiveRespect.JSONGiveRespect> {
|
||||
habbo.getHabboStats().respectPointsToGive += object.daily_respects;
|
||||
habbo.getClient().sendResponse(new UserDataComposer(habbo));
|
||||
} else {
|
||||
if (!RconUserLookup.userExists(object.user_id)) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET respects_given = respects_given + ?, respects_received = respects_received + ?, daily_respect_points = daily_respect_points + ? WHERE user_id = ? LIMIT 1")) {
|
||||
statement.setInt(1, object.respect_received);
|
||||
statement.setInt(2, object.respect_given);
|
||||
statement.setInt(1, object.respect_given);
|
||||
statement.setInt(2, object.respect_received);
|
||||
statement.setInt(3, object.daily_respects);
|
||||
statement.setInt(4, object.user_id);
|
||||
statement.execute();
|
||||
if (statement.executeUpdate() == 0) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.status = RCONMessage.SYSTEM_ERROR;
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.message = "offline";
|
||||
@@ -50,4 +82,4 @@ public class GiveRespect extends RCONMessage<GiveRespect.JSONGiveRespect> {
|
||||
public int respect_received = 0;
|
||||
public int daily_respects = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class GiveUserClothing extends RCONMessage<GiveUserClothing.JSONGiveUserClothing> {
|
||||
@@ -23,14 +24,35 @@ public class GiveUserClothing extends RCONMessage<GiveUserClothing.JSONGiveUserC
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, GiveUserClothing.JSONGiveUserClothing object) {
|
||||
if (object.user_id <= 0 || object.clothing_id <= 0) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "invalid user or clothing";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userExists(object.user_id)) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clothingExists(object.clothing_id)) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "clothing not found";
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_clothing (user_id, clothing_id) VALUES (?, ?)")) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT IGNORE INTO users_clothing (user_id, clothing_id) VALUES (?, ?)")) {
|
||||
statement.setInt(1, object.user_id);
|
||||
statement.setInt(2, object.clothing_id);
|
||||
statement.execute();
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
this.status = RCONMessage.SYSTEM_ERROR;
|
||||
this.message = "failed to grant clothing";
|
||||
return;
|
||||
}
|
||||
|
||||
if (habbo != null) {
|
||||
@@ -42,6 +64,39 @@ public class GiveUserClothing extends RCONMessage<GiveUserClothing.JSONGiveUserC
|
||||
client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FIGURESET_REDEEMED.key));
|
||||
}
|
||||
}
|
||||
|
||||
this.message = "granted clothing";
|
||||
}
|
||||
|
||||
private static boolean userExists(int userId) {
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
if (habbo != 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 (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean clothingExists(int clothingId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT id FROM catalog_clothing WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, clothingId);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
return set.next();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class JSONGiveUserClothing {
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.StaffAlertWithLinkComposer;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -37,9 +40,13 @@ public class HotelAlert extends RCONMessage<HotelAlert.JSONHotelAlert> {
|
||||
|
||||
static class JSONHotelAlert {
|
||||
|
||||
@NotBlank(message = "invalid message")
|
||||
@Size(max = 4096, message = "invalid message")
|
||||
public String message;
|
||||
|
||||
|
||||
@Size(max = 2048, message = "invalid url")
|
||||
@Pattern(regexp = "^$|https?://.+", message = "invalid url")
|
||||
public String url = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -19,13 +20,25 @@ public class IgnoreUser extends RCONMessage<IgnoreUser.JSONIgnoreUser> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSONIgnoreUser object) {
|
||||
if (object.user_id == object.target_id) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "cannot ignore self";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RconUserLookup.userExists(object.user_id) || !RconUserLookup.userExists(object.target_id)) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.getHabboStats().ignoreUser(habbo.getClient(), object.target_id);
|
||||
} else {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO users_ignored (user_id, target_id) VALUES (?, ?)")) {
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT IGNORE INTO users_ignored (user_id, target_id) VALUES (?, ?)")) {
|
||||
statement.setInt(1, object.user_id);
|
||||
statement.setInt(2, object.target_id);
|
||||
statement.execute();
|
||||
@@ -39,8 +52,10 @@ public class IgnoreUser extends RCONMessage<IgnoreUser.JSONIgnoreUser> {
|
||||
|
||||
static class JSONIgnoreUser {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
@Positive(message = "invalid target")
|
||||
public int target_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
|
||||
import com.google.gson.Gson;
|
||||
import gnu.trove.map.hash.THashMap;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class ImageAlertUser extends RCONMessage<ImageAlertUser.JSON> {
|
||||
public ImageAlertUser() {
|
||||
@@ -51,27 +55,39 @@ public class ImageAlertUser extends RCONMessage<ImageAlertUser.JSON> {
|
||||
|
||||
static class JSON {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@NotBlank(message = "invalid bubble")
|
||||
@Size(max = 64, message = "invalid bubble")
|
||||
@Pattern(regexp = "[A-Za-z0-9_.-]+", message = "invalid bubble")
|
||||
public String bubble_key = "";
|
||||
|
||||
|
||||
@Size(max = 4096, message = "invalid message")
|
||||
public String message = "";
|
||||
|
||||
|
||||
@Size(max = 2048, message = "invalid url")
|
||||
@Pattern(regexp = "^$|https?://.+", message = "invalid url")
|
||||
public String url = "";
|
||||
|
||||
|
||||
@Size(max = 256, message = "invalid url title")
|
||||
public String url_message = "";
|
||||
|
||||
|
||||
@Size(max = 256, message = "invalid title")
|
||||
public String title = "";
|
||||
|
||||
|
||||
@Size(max = 32, message = "invalid display")
|
||||
@Pattern(regexp = "^$|[A-Za-z0-9_.-]+", message = "invalid display")
|
||||
public String display_type = "";
|
||||
|
||||
|
||||
@Size(max = 2048, message = "invalid image")
|
||||
public String image = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
|
||||
import com.google.gson.Gson;
|
||||
import gnu.trove.map.hash.THashMap;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -55,24 +58,35 @@ public class ImageHotelAlert extends RCONMessage<ImageHotelAlert.JSON> {
|
||||
|
||||
static class JSON {
|
||||
|
||||
@NotBlank(message = "invalid bubble")
|
||||
@Size(max = 64, message = "invalid bubble")
|
||||
@Pattern(regexp = "[A-Za-z0-9_.-]+", message = "invalid bubble")
|
||||
public String bubble_key = "";
|
||||
|
||||
|
||||
@Size(max = 4096, message = "invalid message")
|
||||
public String message = "";
|
||||
|
||||
|
||||
@Size(max = 2048, message = "invalid url")
|
||||
@Pattern(regexp = "^$|https?://.+", message = "invalid url")
|
||||
public String url = "";
|
||||
|
||||
|
||||
@Size(max = 256, message = "invalid url title")
|
||||
public String url_message = "";
|
||||
|
||||
|
||||
@Size(max = 256, message = "invalid title")
|
||||
public String title = "";
|
||||
|
||||
|
||||
@Size(max = 32, message = "invalid display")
|
||||
@Pattern(regexp = "^$|[A-Za-z0-9_.-]+", message = "invalid display")
|
||||
public String display_type = "";
|
||||
|
||||
|
||||
@Size(max = 2048, message = "invalid image")
|
||||
public String image = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ModifyUserSubscription extends RCONMessage<ModifyUserSubscription.JSON> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModifyUserSubscription.class);
|
||||
static final int DEFAULT_MAX_DURATION_SECONDS = 31_536_000;
|
||||
|
||||
public ModifyUserSubscription() {
|
||||
super(ModifyUserSubscription.JSON.class);
|
||||
@@ -38,10 +39,11 @@ public class ModifyUserSubscription extends RCONMessage<ModifyUserSubscription.J
|
||||
return;
|
||||
}
|
||||
|
||||
int maxDuration = parseMaxDuration(Emulator.getConfig().getValue("rcon.subscription.max_duration_seconds", String.valueOf(DEFAULT_MAX_DURATION_SECONDS)));
|
||||
if (json.action.equalsIgnoreCase("add") || json.action.equalsIgnoreCase("+") || json.action.equalsIgnoreCase("a")) {
|
||||
if (json.duration < 1) {
|
||||
if (!isValidDuration(json.duration, maxDuration)) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "duration must be > 0";
|
||||
this.message = "duration must be between 1 and " + maxDuration + " seconds";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,13 +60,13 @@ public class ModifyUserSubscription extends RCONMessage<ModifyUserSubscription.J
|
||||
}
|
||||
|
||||
if (json.duration != -1) {
|
||||
if (json.duration < 1) {
|
||||
if (!isValidDuration(json.duration, maxDuration)) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "duration must be > 0 or -1 to remove all time";
|
||||
this.message = "duration must be between 1 and " + maxDuration + " seconds, or -1 to remove all time";
|
||||
return;
|
||||
}
|
||||
|
||||
s.addDuration(-json.duration);
|
||||
s.addDuration(-Math.min(json.duration, s.getRemaining()));
|
||||
this.status = RCONMessage.STATUS_OK;
|
||||
this.message = "Successfully removed %time% seconds from %subscription% on %user%".replace("%time%", json.duration + "").replace("%user%", habbo.getUsername()).replace("%subscription%", json.type);
|
||||
} else {
|
||||
@@ -85,6 +87,22 @@ public class ModifyUserSubscription extends RCONMessage<ModifyUserSubscription.J
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isValidDuration(int duration, int maxDuration) {
|
||||
return duration >= 1 && duration <= maxDuration;
|
||||
}
|
||||
|
||||
static int parseMaxDuration(String configured) {
|
||||
try {
|
||||
int parsed = Integer.parseInt(configured);
|
||||
if (parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
return DEFAULT_MAX_DURATION_SECONDS;
|
||||
}
|
||||
|
||||
static class JSON {
|
||||
|
||||
public int user_id;
|
||||
@@ -96,4 +114,4 @@ public class ModifyUserSubscription extends RCONMessage<ModifyUserSubscription.J
|
||||
public int duration = -1; // Time to add/remove in seconds. -1 means remove subscription entirely
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -12,6 +14,7 @@ import java.sql.SQLException;
|
||||
|
||||
public class MuteUser extends RCONMessage<MuteUser.JSON> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MuteUser.class);
|
||||
static final int DEFAULT_MAX_DURATION_SECONDS = 604_800;
|
||||
|
||||
public MuteUser() {
|
||||
super(MuteUser.JSON.class);
|
||||
@@ -19,6 +22,13 @@ public class MuteUser extends RCONMessage<MuteUser.JSON> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSON json) {
|
||||
int maxDuration = parseMaxDuration(Emulator.getConfig().getValue("rcon.mute.max_duration_seconds", String.valueOf(DEFAULT_MAX_DURATION_SECONDS)));
|
||||
if (json.duration < 0 || json.duration > maxDuration) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "duration must be between 0 and " + maxDuration + " seconds";
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(json.user_id);
|
||||
|
||||
if (habbo != null) {
|
||||
@@ -29,7 +39,7 @@ public class MuteUser extends RCONMessage<MuteUser.JSON> {
|
||||
}
|
||||
} else {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET mute_end_timestamp = ? WHERE user_id = ? LIMIT 1")) {
|
||||
statement.setInt(1, Emulator.getIntUnixTimestamp() + json.duration);
|
||||
statement.setInt(1, json.duration == 0 ? 0 : Emulator.getIntUnixTimestamp() + json.duration);
|
||||
statement.setInt(2, json.user_id);
|
||||
if (statement.executeUpdate() == 0) {
|
||||
this.status = HABBO_NOT_FOUND;
|
||||
@@ -40,11 +50,24 @@ public class MuteUser extends RCONMessage<MuteUser.JSON> {
|
||||
}
|
||||
}
|
||||
|
||||
static int parseMaxDuration(String configured) {
|
||||
try {
|
||||
int parsed = Integer.parseInt(configured);
|
||||
if (parsed >= 0) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
return DEFAULT_MAX_DURATION_SECONDS;
|
||||
}
|
||||
|
||||
static class JSON {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Min(value = 0, message = "invalid duration")
|
||||
public int duration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import com.eu.habbo.habbohotel.achievements.Achievement;
|
||||
import com.eu.habbo.habbohotel.achievements.AchievementManager;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
public class ProgressAchievement extends RCONMessage<ProgressAchievement.ProgressAchievementJSON> {
|
||||
static final int DEFAULT_MAX_PROGRESS = 10_000;
|
||||
|
||||
public ProgressAchievement() {
|
||||
super(ProgressAchievementJSON.class);
|
||||
@@ -14,6 +16,13 @@ public class ProgressAchievement extends RCONMessage<ProgressAchievement.Progres
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, ProgressAchievementJSON json) {
|
||||
int maxProgress = parseMaxProgress(Emulator.getConfig().getValue("rcon.achievement.max_progress", String.valueOf(DEFAULT_MAX_PROGRESS)));
|
||||
if (json.progress > maxProgress) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "progress must be between 1 and " + maxProgress;
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(json.user_id);
|
||||
|
||||
if (habbo != null) {
|
||||
@@ -28,14 +37,27 @@ public class ProgressAchievement extends RCONMessage<ProgressAchievement.Progres
|
||||
}
|
||||
}
|
||||
|
||||
static int parseMaxProgress(String configured) {
|
||||
try {
|
||||
int parsed = Integer.parseInt(configured);
|
||||
if (parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
return DEFAULT_MAX_PROGRESS;
|
||||
}
|
||||
|
||||
static class ProgressAchievementJSON {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid achievement")
|
||||
public int achievement_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid progress")
|
||||
public int progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,17 @@ public abstract class RCONMessage<T> {
|
||||
|
||||
public abstract void handle(Gson gson, T json);
|
||||
|
||||
public boolean validate(T json) {
|
||||
String validationError = RconPayloadValidator.validate(json);
|
||||
if (validationError == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.status = STATUS_ERROR;
|
||||
this.message = validationError;
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static class RCONMessageSerializer implements JsonSerializer<RCONMessage> {
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
public class RconGrantGuard {
|
||||
public static final int DEFAULT_MAX_AMOUNT = 1_000_000;
|
||||
|
||||
private RconGrantGuard() {
|
||||
}
|
||||
|
||||
public static String validateUserId(int userId) {
|
||||
return userId > 0 ? null : "invalid user";
|
||||
}
|
||||
|
||||
public static String validatePositiveAmount(int amount, int maxAmount, String fieldName) {
|
||||
if (amount <= 0) {
|
||||
return "invalid " + fieldName;
|
||||
}
|
||||
|
||||
if (maxAmount > 0 && amount > maxAmount) {
|
||||
return fieldName + " exceeds rcon grant ceiling";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String validateNonNegativeAmount(int amount, int maxAmount, String fieldName) {
|
||||
if (amount < 0) {
|
||||
return "invalid " + fieldName;
|
||||
}
|
||||
|
||||
if (maxAmount > 0 && amount > maxAmount) {
|
||||
return fieldName + " exceeds rcon grant ceiling";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String validateCurrencyType(int type) {
|
||||
return type >= 0 ? null : "invalid currency type";
|
||||
}
|
||||
|
||||
public static int parseMaxAmount(String rawValue) {
|
||||
try {
|
||||
int value = Integer.parseInt(rawValue);
|
||||
return value > 0 ? value : DEFAULT_MAX_AMOUNT;
|
||||
} catch (Exception e) {
|
||||
return DEFAULT_MAX_AMOUNT;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Set;
|
||||
|
||||
final class RconPayloadValidator {
|
||||
private static final Validator VALIDATOR;
|
||||
|
||||
static {
|
||||
ValidatorFactory factory = Validation.byDefaultProvider()
|
||||
.configure()
|
||||
.messageInterpolator(new ParameterMessageInterpolator())
|
||||
.buildValidatorFactory();
|
||||
VALIDATOR = factory.getValidator();
|
||||
}
|
||||
|
||||
private RconPayloadValidator() {
|
||||
}
|
||||
|
||||
static String validate(Object payload) {
|
||||
if (payload == null) {
|
||||
return "invalid payload";
|
||||
}
|
||||
|
||||
Set<ConstraintViolation<Object>> violations = VALIDATOR.validate(payload);
|
||||
return violations.stream()
|
||||
.min(Comparator.comparing(violation -> violation.getPropertyPath().toString()))
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
public class RconUserLookup {
|
||||
private RconUserLookup() {
|
||||
}
|
||||
|
||||
public static boolean userExists(int userId) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,14 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.items.Item;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.habbohotel.users.HabboItem;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
|
||||
import com.google.gson.Gson;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class SendGift extends RCONMessage<SendGift.SendGiftJSON> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SendGift.class);
|
||||
private static final int DEFAULT_MAX_MESSAGE_LENGTH = 300;
|
||||
|
||||
public SendGift() {
|
||||
super(SendGiftJSON.class);
|
||||
@@ -23,13 +18,13 @@ public class SendGift extends RCONMessage<SendGift.SendGiftJSON> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, SendGiftJSON json) {
|
||||
if (json.user_id < 0) {
|
||||
if (json.user_id <= 0) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = Emulator.getTexts().getValue("commands.error.cmd_gift.user_not_found").replace("%username%", json.user_id + "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (json.itemid < 0) {
|
||||
if (json.itemid <= 0) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = Emulator.getTexts().getValue("commands.error.cmd_gift.not_a_number");
|
||||
return;
|
||||
@@ -42,50 +37,74 @@ public class SendGift extends RCONMessage<SendGift.SendGiftJSON> {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean userFound;
|
||||
Habbo habbo;
|
||||
|
||||
habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(json.user_id);
|
||||
|
||||
userFound = habbo != null;
|
||||
String username = "";
|
||||
if (!userFound) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, json.user_id);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) {
|
||||
username = set.getString("username");
|
||||
userFound = true;
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
} else {
|
||||
username = habbo.getHabboInfo().getUsername();
|
||||
if (!baseItem.allowGift()) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = Emulator.getTexts().getValue("commands.error.cmd_gift.not_found").replace("%itemid%", json.itemid + "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userFound) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = Emulator.getTexts().getValue("commands.error.cmd_gift.user_not_found").replace("%username%", username);
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(json.user_id);
|
||||
HabboInfo habboInfo = habbo != null ? habbo.getHabboInfo() : HabboManager.getOfflineHabboInfo(json.user_id);
|
||||
if (habboInfo == null) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = Emulator.getTexts().getValue("commands.error.cmd_gift.user_not_found").replace("%username%", json.user_id + "");
|
||||
return;
|
||||
}
|
||||
|
||||
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, 0, 0, "");
|
||||
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem((Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())]);
|
||||
Item giftItem = this.randomGiftItem();
|
||||
if (item == null || giftItem == null) {
|
||||
this.status = RCONMessage.SYSTEM_ERROR;
|
||||
this.message = "gift configuration unavailable";
|
||||
return;
|
||||
}
|
||||
|
||||
String extraData = "1\t" + item.getId();
|
||||
extraData += "\t0\t0\t0\t" + json.message + "\t0\t0";
|
||||
extraData += "\t0\t0\t0\t" + sanitizeGiftMessage(json.message) + "\t0\t0";
|
||||
|
||||
Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, extraData, 0, 0);
|
||||
if (Emulator.getGameEnvironment().getItemManager().createGift(habboInfo.getUsername(), giftItem, extraData, 0, 0) == null) {
|
||||
this.status = RCONMessage.SYSTEM_ERROR;
|
||||
this.message = "failed to create gift";
|
||||
return;
|
||||
}
|
||||
|
||||
this.message = Emulator.getTexts().getValue("commands.succes.cmd_gift").replace("%username%", username).replace("%itemname%", item.getBaseItem().getName());
|
||||
this.message = Emulator.getTexts().getValue("commands.succes.cmd_gift").replace("%username%", habboInfo.getUsername()).replace("%itemname%", item.getBaseItem().getName());
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.getClient().sendResponse(new InventoryRefreshComposer());
|
||||
}
|
||||
}
|
||||
|
||||
private Item randomGiftItem() {
|
||||
synchronized (Emulator.getGameEnvironment().getCatalogManager().giftFurnis) {
|
||||
int size = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size();
|
||||
if (size == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object[] giftIds = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray();
|
||||
return Emulator.getGameEnvironment().getItemManager().getItem((Integer) giftIds[Emulator.getRandom().nextInt(size)]);
|
||||
}
|
||||
}
|
||||
|
||||
static String sanitizeGiftMessage(String message) {
|
||||
int maxLength = Emulator.getConfig().getInt("hotel.gifts.length.max", DEFAULT_MAX_MESSAGE_LENGTH);
|
||||
if (maxLength <= 0) {
|
||||
maxLength = DEFAULT_MAX_MESSAGE_LENGTH;
|
||||
}
|
||||
|
||||
if (message == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String sanitized = message.replace('\t', ' ').replace('\r', ' ').replace('\n', ' ');
|
||||
if (sanitized.length() > maxLength) {
|
||||
return sanitized.substring(0, maxLength);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
static class SendGiftJSON {
|
||||
|
||||
public int user_id = -1;
|
||||
@@ -96,4 +115,4 @@ public class SendGift extends RCONMessage<SendGift.SendGiftJSON> {
|
||||
|
||||
public String message = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ public class SetMotto extends RCONMessage<SetMotto.SetMottoJSON> {
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.getHabboInfo().setMotto(json.motto);
|
||||
habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
|
||||
if (habbo.getHabboInfo().getCurrentRoom() != null) {
|
||||
habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
|
||||
}
|
||||
} else {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("UPDATE users SET motto = ? WHERE id = ? LIMIT 1")) {
|
||||
@@ -45,4 +47,4 @@ public class SetMotto extends RCONMessage<SetMotto.SetMottoJSON> {
|
||||
|
||||
public String motto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ package com.eu.habbo.messages.rcon;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
public class SetRank extends RCONMessage<SetRank.JSONSetRank> {
|
||||
|
||||
@@ -12,6 +17,25 @@ public class SetRank extends RCONMessage<SetRank.JSONSetRank> {
|
||||
|
||||
@Override
|
||||
public void handle(Gson gson, JSONSetRank object) {
|
||||
int maxRank = SetRankRequestGuard.parseMaxRank(
|
||||
Emulator.getConfig().getValue("rcon.setrank.max_rank", String.valueOf(SetRankRequestGuard.DEFAULT_MAX_RANK)));
|
||||
String validationError = SetRankRequestGuard.validate(
|
||||
object.user_id,
|
||||
object.rank,
|
||||
maxRank,
|
||||
rankId -> Emulator.getGameEnvironment().getPermissionsManager().rankExists(rankId));
|
||||
if (validationError != null) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userExists(object.user_id)) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Emulator.getGameEnvironment().getHabboManager().setRank(object.user_id, object.rank);
|
||||
} catch (Exception e) {
|
||||
@@ -29,11 +53,30 @@ public class SetRank extends RCONMessage<SetRank.JSONSetRank> {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean userExists(int userId) {
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
if (habbo != 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 class JSONSetRank {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid rank")
|
||||
public int rank;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import java.util.function.IntPredicate;
|
||||
|
||||
public class SetRankRequestGuard {
|
||||
public static final int DEFAULT_MAX_RANK = 12;
|
||||
|
||||
private SetRankRequestGuard() {
|
||||
}
|
||||
|
||||
public static String validate(int userId, int rankId, int maxRank, IntPredicate rankExists) {
|
||||
if (userId <= 0) {
|
||||
return "invalid user";
|
||||
}
|
||||
|
||||
if (rankId <= 0) {
|
||||
return "invalid rank";
|
||||
}
|
||||
|
||||
if (maxRank > 0 && rankId > maxRank) {
|
||||
return "rank exceeds rcon ceiling";
|
||||
}
|
||||
|
||||
if (!rankExists.test(rankId)) {
|
||||
return "invalid rank";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int parseMaxRank(String rawValue) {
|
||||
try {
|
||||
int value = Integer.parseInt(rawValue);
|
||||
return value > 0 ? value : DEFAULT_MAX_RANK;
|
||||
} catch (Exception e) {
|
||||
return DEFAULT_MAX_RANK;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
public class StalkUser extends RCONMessage<StalkUser.StalkUserJSON> {
|
||||
public StalkUser() {
|
||||
@@ -44,14 +45,19 @@ public class StalkUser extends RCONMessage<StalkUser.StalkUserJSON> {
|
||||
if (this.status == 0) {
|
||||
habbo.getClient().sendResponse(new ForwardToRoomComposer(target.getHabboInfo().getCurrentRoom().getId()));
|
||||
}
|
||||
} else {
|
||||
this.status = HABBO_NOT_FOUND;
|
||||
this.message = "offline";
|
||||
}
|
||||
}
|
||||
|
||||
static class StalkUserJSON {
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
@Positive(message = "invalid target")
|
||||
public int follow_id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class TalkUser extends RCONMessage<TalkUser.JSON> {
|
||||
public TalkUser() {
|
||||
@@ -38,15 +41,20 @@ public class TalkUser extends RCONMessage<TalkUser.JSON> {
|
||||
|
||||
static class JSON {
|
||||
|
||||
@NotBlank(message = "invalid type")
|
||||
@Size(max = 16, message = "invalid type")
|
||||
public String type;
|
||||
|
||||
|
||||
@Positive(message = "invalid user")
|
||||
public int user_id;
|
||||
|
||||
|
||||
public int bubble_id = -1;
|
||||
|
||||
|
||||
@NotBlank(message = "invalid message")
|
||||
@Size(max = 512, message = "invalid message")
|
||||
public String message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,13 @@ import org.slf4j.LoggerFactory;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class UpdateUser extends RCONMessage<UpdateUser.JSON> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(UpdateUser.class);
|
||||
static final int DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA = 10_000;
|
||||
static final int MAX_LOOK_LENGTH = 256;
|
||||
private static final Pattern LOOK_PATTERN = Pattern.compile("^[A-Za-z0-9.-]{1,256}$");
|
||||
|
||||
public UpdateUser() {
|
||||
super(UpdateUser.JSON.class);
|
||||
@@ -23,6 +27,19 @@ public class UpdateUser extends RCONMessage<UpdateUser.JSON> {
|
||||
@Override
|
||||
public void handle(Gson gson, JSON json) {
|
||||
if (json.user_id > 0) {
|
||||
int maxAchievementScoreDelta = parseMaxAchievementScoreDelta(Emulator.getConfig().getValue("rcon.updateuser.max_achievement_score_delta", String.valueOf(DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA)));
|
||||
if (!isValidAchievementScoreDelta(json.achievement_score, maxAchievementScoreDelta)) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "invalid achievement score";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidLook(json.look)) {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "invalid look";
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(json.user_id);
|
||||
|
||||
if (habbo != null) {
|
||||
@@ -97,23 +114,56 @@ public class UpdateUser extends RCONMessage<UpdateUser.JSON> {
|
||||
index++;
|
||||
}
|
||||
statement.setInt(index, json.user_id);
|
||||
statement.execute();
|
||||
if (statement.executeUpdate() == 0) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!json.look.isEmpty()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("UPDATE users SET look = ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setString(1, json.look);
|
||||
statement.setInt(2, json.user_id);
|
||||
statement.execute();
|
||||
if (statement.executeUpdate() == 0) {
|
||||
this.status = RCONMessage.HABBO_NOT_FOUND;
|
||||
this.message = "user not found";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
this.status = RCONMessage.SYSTEM_ERROR;
|
||||
this.message = "failed to update user";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.status = RCONMessage.STATUS_ERROR;
|
||||
this.message = "invalid user";
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isValidAchievementScoreDelta(int achievementScoreDelta, int maxAchievementScoreDelta) {
|
||||
return achievementScoreDelta >= 0 && achievementScoreDelta <= maxAchievementScoreDelta;
|
||||
}
|
||||
|
||||
static int parseMaxAchievementScoreDelta(String configured) {
|
||||
try {
|
||||
int parsed = Integer.parseInt(configured);
|
||||
if (parsed >= 0) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
return DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA;
|
||||
}
|
||||
|
||||
static boolean isValidLook(String look) {
|
||||
return look == null || look.isEmpty() || (look.length() <= MAX_LOOK_LENGTH && LOOK_PATTERN.matcher(look).matches());
|
||||
}
|
||||
|
||||
static class JSON {
|
||||
|
||||
public int user_id;
|
||||
@@ -142,4 +192,4 @@ public class UpdateUser extends RCONMessage<UpdateUser.JSON> {
|
||||
public boolean strip_unredeemed_clothing = false;
|
||||
//More could be added in the future.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,21 @@ package com.eu.habbo.networking.rconserver;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.messages.rcon.*;
|
||||
import com.eu.habbo.networking.Server;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import gnu.trove.map.hash.THashMap;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiter;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -23,6 +29,8 @@ public class RCONServer extends Server {
|
||||
|
||||
private final THashMap<String, Class<? extends RCONMessage>> messages;
|
||||
private final GsonBuilder gsonBuilder;
|
||||
private final boolean rateLimitEnabled;
|
||||
private final LoadingCache<String, RateLimiter> rateLimiters;
|
||||
List<String> allowedAdresses = new ArrayList<>();
|
||||
|
||||
public RCONServer(String host, int port) throws Exception {
|
||||
@@ -32,6 +40,16 @@ public class RCONServer extends Server {
|
||||
|
||||
this.gsonBuilder = new GsonBuilder();
|
||||
this.gsonBuilder.registerTypeAdapter(RCONMessage.class, new RCONMessage.RCONMessageSerializer());
|
||||
this.rateLimitEnabled = Emulator.getConfig().getBoolean("rcon.rate_limit.enabled", true);
|
||||
RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
|
||||
.limitForPeriod(Math.max(1, Emulator.getConfig().getInt("rcon.rate_limit.limit_for_period", 60)))
|
||||
.limitRefreshPeriod(Duration.ofMillis(Math.max(100, Emulator.getConfig().getInt("rcon.rate_limit.refresh_period_ms", 1000))))
|
||||
.timeoutDuration(Duration.ofMillis(Math.max(0, Emulator.getConfig().getInt("rcon.rate_limit.timeout_ms", 0))))
|
||||
.build();
|
||||
this.rateLimiters = Caffeine.newBuilder()
|
||||
.maximumSize(512)
|
||||
.expireAfterAccess(Duration.ofMinutes(10))
|
||||
.build(address -> RateLimiter.of("rcon-" + address, rateLimiterConfig));
|
||||
|
||||
this.addRCONMessage("alertuser", AlertUser.class);
|
||||
this.addRCONMessage("disconnect", DisconnectUser.class);
|
||||
@@ -89,6 +107,11 @@ public class RCONServer extends Server {
|
||||
}
|
||||
|
||||
public String handle(ChannelHandlerContext ctx, String key, String body) throws Exception {
|
||||
if (!this.acquirePermit(ctx)) {
|
||||
LOGGER.warn("RCON rate limit exceeded for {}", remoteAddress(ctx));
|
||||
return "RATE_LIMITED";
|
||||
}
|
||||
|
||||
Class<? extends RCONMessage> message = this.messages.get(key.replace("_", "").toLowerCase());
|
||||
|
||||
String result;
|
||||
@@ -96,7 +119,10 @@ public class RCONServer extends Server {
|
||||
try {
|
||||
RCONMessage rcon = message.getDeclaredConstructor().newInstance();
|
||||
Gson gson = this.gsonBuilder.create();
|
||||
rcon.handle(gson, gson.fromJson(body, rcon.type));
|
||||
Object payload = gson.fromJson(body, rcon.type);
|
||||
if (rcon.validate(payload)) {
|
||||
rcon.handle(gson, payload);
|
||||
}
|
||||
LOGGER.info("Handled RCON Message: {}", message.getSimpleName());
|
||||
result = gson.toJson(rcon, RCONMessage.class);
|
||||
|
||||
@@ -118,4 +144,17 @@ public class RCONServer extends Server {
|
||||
public List<String> getCommands() {
|
||||
return new ArrayList<>(this.messages.keySet());
|
||||
}
|
||||
|
||||
private boolean acquirePermit(ChannelHandlerContext ctx) {
|
||||
return !this.rateLimitEnabled || this.rateLimiters.get(remoteAddress(ctx)).acquirePermission();
|
||||
}
|
||||
|
||||
private static String remoteAddress(ChannelHandlerContext ctx) {
|
||||
if (ctx == null || ctx.channel() == null) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
SocketAddress address = ctx.channel().remoteAddress();
|
||||
return address == null ? "unknown" : address.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,29 +37,35 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
ByteBuf data = (ByteBuf) msg;
|
||||
|
||||
byte[] d = new byte[data.readableBytes()];
|
||||
data.getBytes(0, d);
|
||||
String message = new String(d, java.nio.charset.StandardCharsets.UTF_8);
|
||||
Gson gson = GSON;
|
||||
String response = "ERROR";
|
||||
String key = "";
|
||||
try {
|
||||
JsonObject object = gson.fromJson(message, JsonObject.class);
|
||||
key = object.get("key").getAsString();
|
||||
response = Emulator.getRconServer().handle(ctx, key, object.get("data").toString());
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
LOGGER.error("Unknown RCON Message: {}", key);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Invalid RCON Message: {}", message);
|
||||
e.printStackTrace();
|
||||
if (!(msg instanceof ByteBuf data)) {
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise());
|
||||
ctx.channel().flush();
|
||||
ctx.flush();
|
||||
f.channel().close();
|
||||
data.release();
|
||||
try {
|
||||
byte[] d = new byte[data.readableBytes()];
|
||||
data.getBytes(0, d);
|
||||
String message = new String(d, java.nio.charset.StandardCharsets.UTF_8);
|
||||
Gson gson = GSON;
|
||||
String response = "ERROR";
|
||||
String key = "";
|
||||
try {
|
||||
JsonObject object = gson.fromJson(message, JsonObject.class);
|
||||
key = object.get("key").getAsString();
|
||||
response = Emulator.getRconServer().handle(ctx, key, object.get("data").toString());
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
LOGGER.error("Unknown RCON Message: {}", key);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Invalid RCON Message: {}", message);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise());
|
||||
ctx.channel().flush();
|
||||
ctx.flush();
|
||||
f.channel().close();
|
||||
} finally {
|
||||
data.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AlertPayloadGuardTest {
|
||||
@Test
|
||||
void hotelAlertPayloadIsBoundedAndUrlValidated() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/HotelAlert.java"));
|
||||
|
||||
assertTrue(source.contains("@NotBlank(message = \"invalid message\")"),
|
||||
"HotelAlert must reject blank global alerts");
|
||||
assertTrue(source.contains("@Size(max = 4096"),
|
||||
"HotelAlert must bound global alert text");
|
||||
assertTrue(source.contains("@Pattern(regexp = \"^$|https?://.+\""),
|
||||
"HotelAlert must reject non-http alert links");
|
||||
}
|
||||
|
||||
@Test
|
||||
void imageHotelAlertPayloadIsBounded() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/ImageHotelAlert.java"));
|
||||
|
||||
assertTrue(source.contains("@NotBlank(message = \"invalid bubble\")"),
|
||||
"ImageHotelAlert must require a bubble key");
|
||||
assertTrue(source.contains("@Pattern(regexp = \"[A-Za-z0-9_.-]+\""),
|
||||
"ImageHotelAlert bubble keys must be constrained to safe token characters");
|
||||
assertTrue(source.contains("@Size(max = 2048"),
|
||||
"ImageHotelAlert URL/image fields must be bounded");
|
||||
assertTrue(source.contains("@Pattern(regexp = \"^$|https?://.+\""),
|
||||
"ImageHotelAlert must reject non-http links");
|
||||
}
|
||||
|
||||
@Test
|
||||
void imageUserAlertPayloadIsBoundedAndTargetsValidUsers() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/ImageAlertUser.java"));
|
||||
|
||||
assertTrue(source.contains("@Positive(message = \"invalid user\")"),
|
||||
"ImageAlertUser must reject invalid target users before execution");
|
||||
assertTrue(source.contains("@NotBlank(message = \"invalid bubble\")"),
|
||||
"ImageAlertUser must require a bubble key");
|
||||
assertTrue(source.contains("@Size(max = 4096"),
|
||||
"ImageAlertUser must bound alert text");
|
||||
assertTrue(source.contains("@Pattern(regexp = \"^$|https?://.+\""),
|
||||
"ImageAlertUser must reject non-http links");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
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 ChangeRoomOwnerContractTest {
|
||||
private static String source() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/ChangeRoomOwner.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesRoomAndUserBeforeChangingOwnership() throws Exception {
|
||||
String source = source();
|
||||
|
||||
assertTrue(source.contains("json.room_id <= 0 || json.user_id <= 0"),
|
||||
"Room owner changes must reject invalid identifiers");
|
||||
assertTrue(source.contains("getHabboInfo(json.user_id)"),
|
||||
"Room owner changes must resolve the canonical user from storage");
|
||||
assertTrue(source.contains("HABBO_NOT_FOUND"),
|
||||
"Room owner changes must report missing target users");
|
||||
assertTrue(source.contains("ROOM_NOT_FOUND"),
|
||||
"Room owner changes must report missing rooms");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotTrustClientSuppliedOwnerNames() throws Exception {
|
||||
String source = source();
|
||||
|
||||
assertTrue(source.contains("setOwnerName(owner.getUsername())"),
|
||||
"Room owner changes must use the stored username");
|
||||
assertFalse(source.contains("setOwnerName(json.username)"),
|
||||
"Room owner changes must not trust JSON usernames");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
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 ExecuteCommandGuardTest {
|
||||
@Test
|
||||
void extractsCommandKeyOnlyFromColonCommands() {
|
||||
assertEquals("dance", ExecuteCommand.commandKey(":dance"));
|
||||
assertEquals("dance", ExecuteCommand.commandKey(" :DaNcE 1 2 "));
|
||||
assertEquals("", ExecuteCommand.commandKey("dance"));
|
||||
assertEquals("", ExecuteCommand.commandKey(": "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deniedPermissionsBlockDangerousDefaultsUnlessExplicitlyAllowed() {
|
||||
assertFalse(ExecuteCommand.isAllowed("cmd_shutdown", "cmd_shutdown;cmd_give_rank", ""));
|
||||
assertFalse(ExecuteCommand.isAllowed("CMD_GIVE_RANK", "cmd_shutdown,cmd_give_rank", ""));
|
||||
assertTrue(ExecuteCommand.isAllowed("cmd_dance", "cmd_shutdown;cmd_give_rank", ""));
|
||||
assertTrue(ExecuteCommand.isAllowed("cmd_shutdown", "cmd_shutdown", "cmd_shutdown"));
|
||||
assertFalse(ExecuteCommand.isAllowed("cmd_dance", "cmd_shutdown", "cmd_about"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesInvalidCommandLengthAsDefault() {
|
||||
assertEquals(ExecuteCommand.DEFAULT_MAX_COMMAND_LENGTH, ExecuteCommand.parseMaxCommandLength(null));
|
||||
assertEquals(ExecuteCommand.DEFAULT_MAX_COMMAND_LENGTH, ExecuteCommand.parseMaxCommandLength("0"));
|
||||
assertEquals(64, ExecuteCommand.parseMaxCommandLength("64"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeCommandHasConfigurableGuardRails() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/ExecuteCommand.java"));
|
||||
String emulator = Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
|
||||
|
||||
assertTrue(source.contains("CommandHandler.getCommand(commandKey)"),
|
||||
"RCON executecommand must resolve aliases to the registered command permission");
|
||||
assertTrue(source.contains("rcon.execute_command.denied_permissions"),
|
||||
"RCON executecommand must support a configurable denied-permission list");
|
||||
assertTrue(source.contains("rcon.execute_command.allowed_permissions"),
|
||||
"RCON executecommand must support a stricter configurable allowlist");
|
||||
assertTrue(source.contains("!commandLine.startsWith(\":\") || commandLine.length() > maxLength"),
|
||||
"RCON executecommand must reject non-command payloads and oversized command lines");
|
||||
assertTrue(emulator.contains("rcon.execute_command.denied_permissions"),
|
||||
"RCON executecommand guard defaults must be registered before the RCON server starts");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
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 GiveBadgeContractTest {
|
||||
private static String giveBadgeSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GiveBadge.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offlineBadgeGrantRequiresExistingUserBeforeInsert() throws Exception {
|
||||
String source = giveBadgeSource();
|
||||
|
||||
assertTrue(source.contains("HabboManager.getOfflineHabboInfo(json.user_id)"),
|
||||
"Offline RCON badge grants must verify the target user exists");
|
||||
assertTrue(source.contains("RCONMessage.HABBO_NOT_FOUND"),
|
||||
"Offline RCON badge grants must report missing users");
|
||||
assertFalse(source.contains("(SELECT id FROM users WHERE users.id = ? LIMIT 1)"),
|
||||
"Offline RCON badge grants must not insert through a nullable user subquery");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GiveCreditsContractTest {
|
||||
private static String giveCreditsSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GiveCredits.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offlineCreditGrantReportsMissingUsersWhenNoRowsChange() throws Exception {
|
||||
String source = giveCreditsSource();
|
||||
|
||||
assertTrue(source.contains("executeUpdate()"),
|
||||
"Offline RCON credit grants must inspect the affected row count");
|
||||
assertTrue(source.contains("HABBO_NOT_FOUND"),
|
||||
"Offline RCON credit grants must report missing users when the UPDATE changes no rows");
|
||||
assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"),
|
||||
"RCON credit grants must reject zero, negative, and oversized grants");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GivePixelsContractTest {
|
||||
private static String givePixelsSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GivePixels.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offlinePixelGrantCreatesMissingCurrencyRow() throws Exception {
|
||||
String source = givePixelsSource();
|
||||
|
||||
assertTrue(source.contains("INSERT INTO users_currency"),
|
||||
"Offline RCON pixel grants must create the users_currency type 0 row when it is missing");
|
||||
assertTrue(source.contains("ON DUPLICATE KEY UPDATE"),
|
||||
"Offline RCON pixel grants should increment existing rows with an upsert");
|
||||
assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"),
|
||||
"RCON pixel grants must reject zero, negative, and oversized grants");
|
||||
assertTrue(source.contains("RconUserLookup.userExists"),
|
||||
"Offline RCON pixel grants must not create orphan currency rows for missing users");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GivePointsContractTest {
|
||||
private static String givePointsSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GivePoints.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pointGrantsValidateAmountTypeAndOfflineUserExistence() throws Exception {
|
||||
String source = givePointsSource();
|
||||
|
||||
assertTrue(source.contains("RconGrantGuard.validateCurrencyType"),
|
||||
"RCON point grants must reject invalid currency types");
|
||||
assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"),
|
||||
"RCON point grants must reject zero, negative, and oversized grants");
|
||||
assertTrue(source.contains("RconUserLookup.userExists"),
|
||||
"Offline RCON point grants must not create orphan currency rows for missing users");
|
||||
assertTrue(source.contains("ON DUPLICATE KEY UPDATE"),
|
||||
"Offline RCON point grants should increment existing rows with an upsert");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GiveRespectContractTest {
|
||||
private static String giveRespectSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GiveRespect.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offlineRespectGrantBindsGivenAndReceivedToMatchingColumns() throws Exception {
|
||||
String source = giveRespectSource();
|
||||
|
||||
assertTrue(source.contains("statement.setInt(1, object.respect_given);"),
|
||||
"respects_given must be incremented with respect_given");
|
||||
assertTrue(source.contains("statement.setInt(2, object.respect_received);"),
|
||||
"respects_received must be incremented with respect_received");
|
||||
assertTrue(source.contains("RconGrantGuard.validateNonNegativeAmount"),
|
||||
"RCON respect grants must reject negative values");
|
||||
assertTrue(source.contains("statement.executeUpdate() == 0"),
|
||||
"Offline RCON respect grants must report missing users when the UPDATE changes no rows");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GiveUserClothingContractTest {
|
||||
private static String source() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GiveUserClothing.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesUsersAndClothingBeforeWritingInventoryRows() throws Exception {
|
||||
String source = source();
|
||||
|
||||
assertTrue(source.contains("object.user_id <= 0 || object.clothing_id <= 0"),
|
||||
"Clothing grants must reject invalid identifiers");
|
||||
assertTrue(source.contains("userExists(object.user_id)"),
|
||||
"Clothing grants must reject missing users before inserting rows");
|
||||
assertTrue(source.contains("clothingExists(object.clothing_id)"),
|
||||
"Clothing grants must reject catalog clothing ids that do not exist");
|
||||
assertTrue(source.contains("catalog_clothing"),
|
||||
"Clothing grants must validate against catalog_clothing");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesDuplicateGrantsWithoutSurfacingSqlErrors() throws Exception {
|
||||
String source = source();
|
||||
|
||||
assertTrue(source.contains("INSERT IGNORE INTO users_clothing"),
|
||||
"Duplicate clothing grants should be idempotent");
|
||||
assertTrue(source.contains("SYSTEM_ERROR"),
|
||||
"Unexpected SQL failures must be surfaced to the RCON caller");
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
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 ModifyUserSubscriptionGuardTest {
|
||||
@Test
|
||||
void validatesDurationAgainstConfiguredCeiling() {
|
||||
assertTrue(ModifyUserSubscription.isValidDuration(1, 10));
|
||||
assertTrue(ModifyUserSubscription.isValidDuration(10, 10));
|
||||
assertFalse(ModifyUserSubscription.isValidDuration(0, 10));
|
||||
assertFalse(ModifyUserSubscription.isValidDuration(-1, 10));
|
||||
assertFalse(ModifyUserSubscription.isValidDuration(11, 10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesInvalidDurationCeilingsAsDefault() {
|
||||
assertEquals(ModifyUserSubscription.DEFAULT_MAX_DURATION_SECONDS, ModifyUserSubscription.parseMaxDuration(null));
|
||||
assertEquals(ModifyUserSubscription.DEFAULT_MAX_DURATION_SECONDS, ModifyUserSubscription.parseMaxDuration("0"));
|
||||
assertEquals(60, ModifyUserSubscription.parseMaxDuration("60"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void clampsPartialRemovalToRemainingSubscriptionTime() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/ModifyUserSubscription.java"));
|
||||
|
||||
assertTrue(source.contains("Math.min(json.duration, s.getRemaining())"),
|
||||
"Partial subscription removal must not drive duration below the remaining time");
|
||||
assertTrue(source.contains("rcon.subscription.max_duration_seconds"),
|
||||
"RCON subscription duration ceiling must be configurable");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MuteUserGuardTest {
|
||||
@Test
|
||||
void parsesInvalidDurationCeilingsAsDefault() {
|
||||
assertEquals(MuteUser.DEFAULT_MAX_DURATION_SECONDS, MuteUser.parseMaxDuration(null));
|
||||
assertEquals(MuteUser.DEFAULT_MAX_DURATION_SECONDS, MuteUser.parseMaxDuration("-1"));
|
||||
assertEquals(0, MuteUser.parseMaxDuration("0"));
|
||||
assertEquals(60, MuteUser.parseMaxDuration("60"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNegativeAndOversizedMuteDurations() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/MuteUser.java"));
|
||||
|
||||
assertTrue(source.contains("json.duration < 0 || json.duration > maxDuration"),
|
||||
"RCON mute must reject negative durations and configured-duration overflows");
|
||||
assertTrue(source.contains("json.duration == 0 ? 0 : Emulator.getIntUnixTimestamp() + json.duration"),
|
||||
"Offline unmute must clear mute_end_timestamp instead of writing the current timestamp");
|
||||
assertTrue(source.contains("rcon.mute.max_duration_seconds"),
|
||||
"RCON mute duration ceiling must be configurable");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ProgressAchievementGuardTest {
|
||||
@Test
|
||||
void parsesInvalidProgressCeilingsAsDefault() {
|
||||
assertEquals(ProgressAchievement.DEFAULT_MAX_PROGRESS, ProgressAchievement.parseMaxProgress(null));
|
||||
assertEquals(ProgressAchievement.DEFAULT_MAX_PROGRESS, ProgressAchievement.parseMaxProgress("0"));
|
||||
assertEquals(50, ProgressAchievement.parseMaxProgress("50"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesAchievementProgressPayload() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/ProgressAchievement.java"));
|
||||
|
||||
assertTrue(source.contains("@Positive(message = \"invalid user\")"),
|
||||
"RCON achievement progress must reject invalid target users before execution");
|
||||
assertTrue(source.contains("@Positive(message = \"invalid achievement\")"),
|
||||
"RCON achievement progress must reject invalid achievement ids before execution");
|
||||
assertTrue(source.contains("@Positive(message = \"invalid progress\")"),
|
||||
"RCON achievement progress must reject zero or negative progress before execution");
|
||||
assertTrue(source.contains("json.progress > maxProgress"),
|
||||
"RCON achievement progress must reject configured-progress overflows");
|
||||
assertTrue(source.contains("rcon.achievement.max_progress"),
|
||||
"RCON achievement progress ceiling must be configurable");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class RconGrantGuardTest {
|
||||
@Test
|
||||
void validatesPositiveGrantAmounts() {
|
||||
assertNull(RconGrantGuard.validatePositiveAmount(1, 100, "credits"));
|
||||
assertEquals("invalid credits", RconGrantGuard.validatePositiveAmount(0, 100, "credits"));
|
||||
assertEquals("invalid credits", RconGrantGuard.validatePositiveAmount(-1, 100, "credits"));
|
||||
assertEquals("credits exceeds rcon grant ceiling", RconGrantGuard.validatePositiveAmount(101, 100, "credits"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesNonNegativeGrantAmounts() {
|
||||
assertNull(RconGrantGuard.validateNonNegativeAmount(0, 100, "respect_given"));
|
||||
assertEquals("invalid respect_given", RconGrantGuard.validateNonNegativeAmount(-1, 100, "respect_given"));
|
||||
assertEquals("respect_given exceeds rcon grant ceiling", RconGrantGuard.validateNonNegativeAmount(101, 100, "respect_given"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesUserAndCurrencyIdentifiers() {
|
||||
assertNull(RconGrantGuard.validateUserId(1));
|
||||
assertEquals("invalid user", RconGrantGuard.validateUserId(0));
|
||||
assertNull(RconGrantGuard.validateCurrencyType(0));
|
||||
assertEquals("invalid currency type", RconGrantGuard.validateCurrencyType(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesInvalidGrantCeilingsAsDefault() {
|
||||
assertEquals(RconGrantGuard.DEFAULT_MAX_AMOUNT, RconGrantGuard.parseMaxAmount(null));
|
||||
assertEquals(RconGrantGuard.DEFAULT_MAX_AMOUNT, RconGrantGuard.parseMaxAmount("0"));
|
||||
assertEquals(500, RconGrantGuard.parseMaxAmount("500"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class RconPayloadValidatorTest {
|
||||
@Test
|
||||
void acceptsValidAnnotatedPayloads() {
|
||||
SetRank.JSONSetRank payload = new SetRank.JSONSetRank();
|
||||
payload.user_id = 1;
|
||||
payload.rank = 2;
|
||||
|
||||
assertNull(RconPayloadValidator.validate(payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidSetRankPayloadsBeforeDispatch() {
|
||||
SetRank.JSONSetRank payload = new SetRank.JSONSetRank();
|
||||
payload.user_id = 0;
|
||||
payload.rank = 2;
|
||||
|
||||
assertEquals("invalid user", RconPayloadValidator.validate(payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidGrantPayloadsBeforeDispatch() {
|
||||
GiveCredits.JSONGiveCredits payload = new GiveCredits.JSONGiveCredits();
|
||||
payload.user_id = 1;
|
||||
payload.credits = 0;
|
||||
|
||||
assertEquals("invalid credits", RconPayloadValidator.validate(payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBlankBadgePayloadsBeforeDispatch() {
|
||||
GiveBadge.GiveBadgeJSON payload = new GiveBadge.GiveBadgeJSON();
|
||||
payload.user_id = 1;
|
||||
payload.badge = " ";
|
||||
|
||||
assertEquals("invalid badge", RconPayloadValidator.validate(payload));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SendGiftContractTest {
|
||||
private static String source() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/SendGift.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesGiftTargetsAndItemsBeforeCreatingInventoryRows() throws Exception {
|
||||
String source = source();
|
||||
|
||||
assertTrue(source.contains("json.user_id <= 0"),
|
||||
"RCON gifts must reject invalid target users");
|
||||
assertTrue(source.contains("json.itemid <= 0"),
|
||||
"RCON gifts must reject invalid item ids");
|
||||
assertTrue(source.contains("baseItem.allowGift()"),
|
||||
"RCON gifts must respect the item giftability flag");
|
||||
assertTrue(source.contains("HabboManager.getOfflineHabboInfo(json.user_id)"),
|
||||
"RCON gifts must resolve offline users through HabboManager");
|
||||
assertTrue(source.contains("HABBO_NOT_FOUND"),
|
||||
"RCON gifts must report missing users with the RCON missing-user status");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sanitizesGiftMessageAndHandlesMissingGiftConfiguration() throws Exception {
|
||||
String source = source();
|
||||
|
||||
assertTrue(source.contains("sanitizeGiftMessage(json.message)"),
|
||||
"RCON gift extraData must use a sanitized message");
|
||||
assertTrue(source.contains("replace('\\t', ' ').replace('\\r', ' ').replace('\\n', ' ')"),
|
||||
"RCON gift messages must not inject gift extraData delimiters");
|
||||
assertTrue(source.contains("hotel.gifts.length.max"),
|
||||
"RCON gift messages must respect the configured gift length limit");
|
||||
assertTrue(source.contains("giftFurnis.size()"),
|
||||
"RCON gift creation must guard against empty gift wrapper configuration");
|
||||
assertTrue(source.contains("createGift(habboInfo.getUsername()"),
|
||||
"RCON gifts must create the wrapper for the canonical target username");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SetMottoContractTest {
|
||||
private static String setMottoSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/SetMotto.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlineMottoUpdateDoesNotRequireCurrentRoom() throws Exception {
|
||||
String source = setMottoSource();
|
||||
|
||||
assertTrue(source.contains("getCurrentRoom() != null"),
|
||||
"RCON SetMotto must not fail for online users outside a room");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class SetRankRequestGuardTest {
|
||||
@Test
|
||||
void acceptsKnownRanksWithinTheRconCeiling() {
|
||||
assertNull(SetRankRequestGuard.validate(1, 5, 12, rankId -> rankId == 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidUsersRanksAndUnknownRanks() {
|
||||
assertEquals("invalid user", SetRankRequestGuard.validate(0, 5, 12, rankId -> true));
|
||||
assertEquals("invalid rank", SetRankRequestGuard.validate(1, 0, 12, rankId -> true));
|
||||
assertEquals("invalid rank", SetRankRequestGuard.validate(1, 5, 12, rankId -> false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsRanksAboveConfiguredCeiling() {
|
||||
assertEquals("rank exceeds rcon ceiling", SetRankRequestGuard.validate(1, 13, 12, rankId -> true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesInvalidMaxRankAsDefaultCeiling() {
|
||||
assertEquals(SetRankRequestGuard.DEFAULT_MAX_RANK, SetRankRequestGuard.parseMaxRank(null));
|
||||
assertEquals(SetRankRequestGuard.DEFAULT_MAX_RANK, SetRankRequestGuard.parseMaxRank("0"));
|
||||
assertEquals(7, SetRankRequestGuard.parseMaxRank("7"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SocialRoomCommandGuardTest {
|
||||
@Test
|
||||
void forwardUserDoesNotOverwriteSuccessfulStatusWithNotFound() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/ForwardUser.java"));
|
||||
|
||||
assertTrue(source.contains("enterRoom(habbo, object.room_id, \"\", true);") && source.contains("return;"),
|
||||
"ForwardUser must return after a successful room forward instead of falling through to HABBO_NOT_FOUND");
|
||||
assertTrue(source.contains("@Positive(message = \"invalid user\")"),
|
||||
"ForwardUser must reject invalid user ids before execution");
|
||||
assertTrue(source.contains("@Positive(message = \"invalid room\")"),
|
||||
"ForwardUser must reject invalid room ids before execution");
|
||||
}
|
||||
|
||||
@Test
|
||||
void alertUserOnlyReportsNotFoundWhenTargetIsMissing() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/AlertUser.java"));
|
||||
|
||||
assertTrue(source.contains("habbo.alert(object.message);") && source.contains("return;"),
|
||||
"AlertUser must return after delivering the alert");
|
||||
assertTrue(source.contains("@NotBlank(message = \"invalid message\")"),
|
||||
"AlertUser must reject blank alerts before execution");
|
||||
assertTrue(source.contains("@Size(max = 4096"),
|
||||
"AlertUser must bound alert payload size");
|
||||
}
|
||||
|
||||
@Test
|
||||
void friendAndIgnoreRequestsValidateBothUsers() throws Exception {
|
||||
String friend = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/FriendRequest.java"));
|
||||
String ignore = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/IgnoreUser.java"));
|
||||
|
||||
assertTrue(friend.contains("json.user_id == json.target_id"),
|
||||
"FriendRequest must reject self-friend requests");
|
||||
assertTrue(friend.contains("RconUserLookup.userExists(json.user_id)") && friend.contains("RconUserLookup.userExists(json.target_id)"),
|
||||
"FriendRequest must reject missing source or target users");
|
||||
assertTrue(ignore.contains("object.user_id == object.target_id"),
|
||||
"IgnoreUser must reject self-ignore requests");
|
||||
assertTrue(ignore.contains("RconUserLookup.userExists(object.user_id)") && ignore.contains("RconUserLookup.userExists(object.target_id)"),
|
||||
"IgnoreUser must reject missing source or target users");
|
||||
assertTrue(ignore.contains("INSERT IGNORE INTO users_ignored"),
|
||||
"IgnoreUser offline writes must avoid duplicate rows");
|
||||
}
|
||||
|
||||
@Test
|
||||
void talkAndStalkPayloadsAreValidated() throws Exception {
|
||||
String talk = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/TalkUser.java"));
|
||||
String stalk = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/StalkUser.java"));
|
||||
|
||||
assertTrue(talk.contains("@NotBlank(message = \"invalid type\")"),
|
||||
"TalkUser must reject blank talk types before execution");
|
||||
assertTrue(talk.contains("@Size(max = 512"),
|
||||
"TalkUser must bound impersonated chat message size");
|
||||
assertTrue(stalk.contains("@Positive(message = \"invalid target\")"),
|
||||
"StalkUser must reject invalid target ids before execution");
|
||||
assertTrue(stalk.contains("this.status = HABBO_NOT_FOUND"),
|
||||
"StalkUser must report missing source users");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.rcon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
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 UpdateUserGuardTest {
|
||||
@Test
|
||||
void validatesAchievementScoreDelta() {
|
||||
assertTrue(UpdateUser.isValidAchievementScoreDelta(0, 100));
|
||||
assertTrue(UpdateUser.isValidAchievementScoreDelta(100, 100));
|
||||
assertFalse(UpdateUser.isValidAchievementScoreDelta(-1, 100));
|
||||
assertFalse(UpdateUser.isValidAchievementScoreDelta(101, 100));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesInvalidAchievementScoreCeilingsAsDefault() {
|
||||
assertEquals(UpdateUser.DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA, UpdateUser.parseMaxAchievementScoreDelta(null));
|
||||
assertEquals(UpdateUser.DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA, UpdateUser.parseMaxAchievementScoreDelta("-1"));
|
||||
assertEquals(0, UpdateUser.parseMaxAchievementScoreDelta("0"));
|
||||
assertEquals(50, UpdateUser.parseMaxAchievementScoreDelta("50"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesLookShapeAndLength() {
|
||||
assertTrue(UpdateUser.isValidLook(null));
|
||||
assertTrue(UpdateUser.isValidLook(""));
|
||||
assertTrue(UpdateUser.isValidLook("hr-115-42.hd-195-19.ch-3030-82"));
|
||||
assertFalse(UpdateUser.isValidLook("hd-1\nch-1"));
|
||||
assertFalse(UpdateUser.isValidLook("hd_1"));
|
||||
assertFalse(UpdateUser.isValidLook("a".repeat(UpdateUser.MAX_LOOK_LENGTH + 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offlineUpdatesReportMissingUsersAndUseAffectedRows() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/UpdateUser.java"));
|
||||
|
||||
assertTrue(source.contains("executeUpdate() == 0"),
|
||||
"Offline UpdateUser mutations must inspect affected row counts");
|
||||
assertTrue(source.contains("HABBO_NOT_FOUND"),
|
||||
"Offline UpdateUser mutations must report missing users");
|
||||
assertTrue(source.contains("rcon.updateuser.max_achievement_score_delta"),
|
||||
"Achievement score deltas must have a configurable RCON ceiling");
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.eu.habbo.networking.rconserver;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RCONServerHandlerContractTest {
|
||||
private static String serverSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/rconserver/RCONServer.java"));
|
||||
}
|
||||
|
||||
private static String handlerSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java"));
|
||||
}
|
||||
|
||||
private static String emulatorSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rconRequestsAreRateLimitedPerRemoteAddress() throws Exception {
|
||||
String source = serverSource();
|
||||
|
||||
assertTrue(source.contains("LoadingCache<String, RateLimiter>"),
|
||||
"RCON server must keep per-remote-address rate limiters");
|
||||
assertTrue(source.contains("Caffeine.newBuilder()"),
|
||||
"RCON rate limiters must expire instead of growing forever");
|
||||
assertTrue(source.contains(".acquirePermission()"),
|
||||
"RCON handler must consume a Resilience4j permit before dispatching commands");
|
||||
assertTrue(source.contains("RATE_LIMITED"),
|
||||
"RCON callers need a deterministic response when the rate limit rejects a request");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rconRateLimitDefaultsAreRegisteredBeforeServerStarts() throws Exception {
|
||||
String source = emulatorSource();
|
||||
int registerIndex = source.indexOf("register(\"rcon.rate_limit.enabled\", \"1\")");
|
||||
int serverIndex = source.indexOf("new RCONServer");
|
||||
|
||||
assertTrue(registerIndex >= 0, "RCON rate limiting must have a registered default toggle");
|
||||
assertTrue(source.contains("register(\"rcon.rate_limit.limit_for_period\", \"60\")"),
|
||||
"RCON rate limit must have a registered default limit");
|
||||
assertTrue(source.contains("register(\"rcon.rate_limit.refresh_period_ms\", \"1000\")"),
|
||||
"RCON rate limit must have a registered default refresh period");
|
||||
assertTrue(source.contains("register(\"rcon.rate_limit.timeout_ms\", \"0\")"),
|
||||
"RCON rate limit must reject immediately by default instead of blocking event loops");
|
||||
assertTrue(registerIndex < serverIndex, "RCON rate limit defaults must be registered before RCONServer is constructed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inboundByteBufIsReleasedFromFinallyBlock() throws Exception {
|
||||
String source = handlerSource();
|
||||
int finallyIndex = source.indexOf("finally");
|
||||
int releaseIndex = source.indexOf("data.release()");
|
||||
|
||||
assertTrue(finallyIndex >= 0, "RCON channelRead must release inbound ByteBufs from a finally block");
|
||||
assertTrue(releaseIndex > finallyIndex, "RCON channelRead must release the inbound ByteBuf after finally starts");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user