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 #163 from simoleo89/feat/security-concurrency-economy-hardening
Security, concurrency & economy hardening + dependency upgrades and modernization
This commit is contained in:
+12
-19
@@ -66,7 +66,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<version>3.2.5</version>
|
<version>3.5.2</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
@@ -83,21 +83,21 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.netty</groupId>
|
<groupId>io.netty</groupId>
|
||||||
<artifactId>netty-all</artifactId>
|
<artifactId>netty-all</artifactId>
|
||||||
<version>4.1.115.Final</version>
|
<version>4.2.15.Final</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- GSON -->
|
<!-- GSON -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.code.gson</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
<version>2.11.0</version>
|
<version>2.14.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- MariaDB Connector/J (native driver for MariaDB) -->
|
<!-- MariaDB Connector/J (native driver for MariaDB) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mariadb.jdbc</groupId>
|
<groupId>org.mariadb.jdbc</groupId>
|
||||||
<artifactId>mariadb-java-client</artifactId>
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
<version>3.5.1</version>
|
<version>3.5.8</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.zaxxer</groupId>
|
<groupId>com.zaxxer</groupId>
|
||||||
<artifactId>HikariCP</artifactId>
|
<artifactId>HikariCP</artifactId>
|
||||||
<version>6.2.1</version>
|
<version>7.0.2</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-lang3</artifactId>
|
<artifactId>commons-lang3</artifactId>
|
||||||
<version>3.17.0</version>
|
<version>3.20.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
<artifactId>jsoup</artifactId>
|
<artifactId>jsoup</artifactId>
|
||||||
<version>1.18.3</version>
|
<version>1.22.2</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -145,14 +145,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
<version>2.0.16</version>
|
<version>2.0.18</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Logback -->
|
<!-- Logback -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.qos.logback</groupId>
|
<groupId>ch.qos.logback</groupId>
|
||||||
<artifactId>logback-classic</artifactId>
|
<artifactId>logback-classic</artifactId>
|
||||||
<version>1.5.15</version>
|
<version>1.5.34</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -160,14 +160,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.fusesource.jansi</groupId>
|
<groupId>org.fusesource.jansi</groupId>
|
||||||
<artifactId>jansi</artifactId>
|
<artifactId>jansi</artifactId>
|
||||||
<version>2.4.1</version>
|
<version>2.4.3</version>
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Joda Time -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>joda-time</groupId>
|
|
||||||
<artifactId>joda-time</artifactId>
|
|
||||||
<version>2.13.0</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- jBCrypt � used by the built-in /api/auth/* HTTP login handler
|
<!-- jBCrypt � used by the built-in /api/auth/* HTTP login handler
|
||||||
@@ -183,14 +176,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.angus</groupId>
|
<groupId>org.eclipse.angus</groupId>
|
||||||
<artifactId>jakarta.mail</artifactId>
|
<artifactId>jakarta.mail</artifactId>
|
||||||
<version>2.0.3</version>
|
<version>2.0.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- JUnit Jupiter -->
|
<!-- JUnit Jupiter -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<version>5.10.2</version>
|
<version>6.1.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ class DatabasePool {
|
|||||||
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
|
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true");
|
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true");
|
databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true");
|
||||||
|
|
||||||
|
// Fail fast instead of pinning a pooled connection (and its worker
|
||||||
|
// thread) indefinitely on a stalled/slow MariaDB. HikariCP's
|
||||||
|
// connectionTimeout only bounds the pool *borrow*; these bound the
|
||||||
|
// actual socket/connect round-trip. Overridable via db.params.
|
||||||
|
databaseConfiguration.addDataSourceProperty("socketTimeout", "30000");
|
||||||
|
databaseConfiguration.addDataSourceProperty("connectTimeout", "10000");
|
||||||
|
databaseConfiguration.addDataSourceProperty("tcpKeepAlive", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false");
|
databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false");
|
||||||
|
|
||||||
databaseConfiguration.setPoolName("HabboHikariPool");
|
databaseConfiguration.setPoolName("HabboHikariPool");
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ public class AchievementManager {
|
|||||||
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
|
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
|
||||||
return;
|
return;
|
||||||
|
|
||||||
habbo.getHabboStats().setProgress(achievement, currentProgress + amount);
|
int newProgress = habbo.getHabboStats().incrementProgress(achievement, amount);
|
||||||
|
|
||||||
AchievementLevel newLevel = achievement.getLevelForProgress(currentProgress + amount);
|
AchievementLevel newLevel = achievement.getLevelForProgress(newProgress);
|
||||||
|
|
||||||
if (AchievementManager.TALENTTRACK_ENABLED) {
|
if (AchievementManager.TALENTTRACK_ENABLED) {
|
||||||
for (TalentTrackType type : TalentTrackType.values()) {
|
for (TalentTrackType type : TalentTrackType.values()) {
|
||||||
|
|||||||
@@ -188,7 +188,11 @@ public class BotManager {
|
|||||||
if (pickedUpEvent.isCancelled())
|
if (pickedUpEvent.isCancelled())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (habbo == null || (bot.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_ANYROOMOWNER))) {
|
Room currentRoom = habbo != null ? habbo.getHabboInfo().getCurrentRoom() : null;
|
||||||
|
if (habbo == null
|
||||||
|
|| bot.getOwnerId() == habbo.getHabboInfo().getId()
|
||||||
|
|| habbo.hasPermission(Permission.ACC_ANYROOMOWNER)
|
||||||
|
|| (currentRoom != null && (currentRoom.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_PLACEFURNI)))) {
|
||||||
if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
||||||
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
||||||
return;
|
return;
|
||||||
|
|||||||
+15
-4
@@ -171,8 +171,9 @@ public class MarketPlace {
|
|||||||
statement.setInt(paramIndex++, maxPrice);
|
statement.setInt(paramIndex++, maxPrice);
|
||||||
}
|
}
|
||||||
if (!search.isEmpty()) {
|
if (!search.isEmpty()) {
|
||||||
statement.setString(paramIndex++, "%" + search + "%");
|
String likeSearch = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(search) + "%";
|
||||||
statement.setString(paramIndex++, "%" + search + "%");
|
statement.setString(paramIndex++, likeSearch);
|
||||||
|
statement.setString(paramIndex++, likeSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
@@ -278,8 +279,9 @@ public class MarketPlace {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int soldTimestamp = Emulator.getIntUnixTimestamp();
|
||||||
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
|
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
|
||||||
updateOffer.setInt(1, Emulator.getIntUnixTimestamp());
|
updateOffer.setInt(1, soldTimestamp);
|
||||||
updateOffer.setInt(2, offerId);
|
updateOffer.setInt(2, offerId);
|
||||||
int updated = updateOffer.executeUpdate();
|
int updated = updateOffer.executeUpdate();
|
||||||
if (updated == 0) {
|
if (updated == 0) {
|
||||||
@@ -306,7 +308,11 @@ public class MarketPlace {
|
|||||||
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
|
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
|
||||||
|
|
||||||
if (habbo != null) {
|
if (habbo != null) {
|
||||||
habbo.getInventory().getOffer(offerId).setState(MarketPlaceState.SOLD);
|
MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
|
||||||
|
if (offer != null) {
|
||||||
|
offer.setState(MarketPlaceState.SOLD);
|
||||||
|
offer.setSoldTimestamp(soldTimestamp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +374,11 @@ public class MarketPlace {
|
|||||||
event.item.setFromGift(false);
|
event.item.setFromGift(false);
|
||||||
|
|
||||||
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
|
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
|
||||||
|
if (!offer.isPersisted()) {
|
||||||
|
LOGGER.warn("Marketplace offer insert failed for user {} item {}", client.getHabbo().getHabboInfo().getId(), event.item.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
client.getHabbo().getInventory().addMarketplaceOffer(offer);
|
client.getHabbo().getInventory().addMarketplaceOffer(offer);
|
||||||
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
|
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
|
||||||
item.setUserId(-1);
|
item.setUserId(-1);
|
||||||
|
|||||||
+4
@@ -98,6 +98,10 @@ public class MarketPlaceOffer implements Runnable {
|
|||||||
return this.offerId;
|
return this.offerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPersisted() {
|
||||||
|
return this.offerId > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public void setOfferId(int offerId) {
|
public void setOfferId(int offerId) {
|
||||||
this.offerId = offerId;
|
this.offerId = offerId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class BanCommand extends Command {
|
public class BanCommand extends Command {
|
||||||
public BanCommand() {
|
public BanCommand() {
|
||||||
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
|
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
|
||||||
@@ -72,7 +74,13 @@ public class BanCommand extends Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ModToolBan ban = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1).get(0);
|
List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1);
|
||||||
|
if (bans == null || bans.isEmpty()) {
|
||||||
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.user_offline"), RoomChatMessageBubbles.ALERT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModToolBan ban = bans.get(0);
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class IPBanCommand extends Command {
|
public class IPBanCommand extends Command {
|
||||||
public final static int TEN_YEARS = 315569260;
|
public final static int TEN_YEARS = 315569260;
|
||||||
|
|
||||||
@@ -50,12 +52,12 @@ public class IPBanCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||||
count++;
|
count += bans != null ? bans.size() : 0;
|
||||||
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
|
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
|
||||||
if (h != null) {
|
if (h != null) {
|
||||||
count++;
|
bans = Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||||
Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
count += bans != null ? bans.size() : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class MachineBanCommand extends Command {
|
public class MachineBanCommand extends Command {
|
||||||
public MachineBanCommand() {
|
public MachineBanCommand() {
|
||||||
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
|
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
|
||||||
@@ -46,7 +48,8 @@ public class MachineBanCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
count = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1).size();
|
List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1);
|
||||||
|
count = bans != null ? bans.size() : 0;
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ public class GameClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
|
this.dispose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose(boolean allowSessionResume) {
|
||||||
try {
|
try {
|
||||||
this.channel.close();
|
this.channel.close();
|
||||||
|
|
||||||
@@ -161,7 +165,7 @@ public class GameClient {
|
|||||||
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
|
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
|
||||||
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
|
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
|
||||||
// Try to park the habbo in the grace period instead of immediate disconnect
|
// Try to park the habbo in the grace period instead of immediate disconnect
|
||||||
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
|
boolean parked = allowSessionResume && SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
|
||||||
|
|
||||||
if (!parked) {
|
if (!parked) {
|
||||||
// No grace period configured — immediate disconnect as before
|
// No grace period configured — immediate disconnect as before
|
||||||
|
|||||||
@@ -43,14 +43,34 @@ public class GameClientManager {
|
|||||||
|
|
||||||
|
|
||||||
public void disposeClient(GameClient client) {
|
public void disposeClient(GameClient client) {
|
||||||
this.disposeClient(client.getChannel());
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disposeClient(client.getChannel(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceDisposeClient(GameClient client) {
|
||||||
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disposeClient(client.getChannel(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void disposeClient(Channel channel) {
|
private void disposeClient(Channel channel) {
|
||||||
|
this.disposeClient(channel, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disposeClient(Channel channel, boolean allowSessionResume) {
|
||||||
|
if (channel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
|
GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
|
||||||
|
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
client.dispose();
|
client.dispose(allowSessionResume);
|
||||||
}
|
}
|
||||||
channel.deregister();
|
channel.deregister();
|
||||||
channel.attr(GameServerAttributes.CLIENT).set(null);
|
channel.attr(GameServerAttributes.CLIENT).set(null);
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ public class SessionResumeManager {
|
|||||||
}
|
}
|
||||||
}, graceSeconds * 1000);
|
}, graceSeconds * 1000);
|
||||||
|
|
||||||
|
if (future == null) {
|
||||||
|
// The scheduler refused the grace-expiry task (pool saturated or
|
||||||
|
// shutting down). Parking now would leave a GhostSession that nothing
|
||||||
|
// can ever reap (the Habbo + room refs pinned for the JVM lifetime),
|
||||||
|
// so disconnect immediately instead.
|
||||||
|
performFullDisconnect(habbo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
||||||
|
|
||||||
applyPausedEffect(habbo);
|
applyPausedEffect(habbo);
|
||||||
|
|||||||
@@ -421,9 +421,9 @@ public class GuildManager {
|
|||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
|
||||||
statement.setInt(1, guild.getId());
|
statement.setInt(1, guild.getId());
|
||||||
statement.setString(2, "%" + query + "%");
|
statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
|
||||||
statement.setInt(3, page * 14);
|
statement.setInt(3, page * 14);
|
||||||
statement.setInt(4, (page * 14) + 14);
|
statement.setInt(4, 14);
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
while (set.next()) {
|
while (set.next()) {
|
||||||
|
|||||||
@@ -101,20 +101,26 @@ public class ForumThread implements Runnable, ISerialize {
|
|||||||
if (statement.executeUpdate() < 1)
|
if (statement.executeUpdate() < 1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
ResultSet set = statement.getGeneratedKeys();
|
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
int threadId = set.getInt(1);
|
int threadId = set.getInt(1);
|
||||||
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
|
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
|
||||||
cacheThread(createdThread);
|
cacheThread(createdThread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOGGER.error("Caught SQL exception", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForumThreadComment.create() opens its OWN connection; do it after the
|
||||||
|
// thread's connection has been released to avoid holding two pooled
|
||||||
|
// connections simultaneously per forum-thread creation.
|
||||||
|
if (createdThread != null) {
|
||||||
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
|
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
|
||||||
createdThread.addComment(comment);
|
createdThread.addComment(comment);
|
||||||
|
|
||||||
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
|
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
|
||||||
LOGGER.error("Caught SQL exception", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createdThread;
|
return createdThread;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -98,13 +98,14 @@ public class ForumThreadComment implements Runnable, ISerialize {
|
|||||||
if (statement.executeUpdate() < 1)
|
if (statement.executeUpdate() < 1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
ResultSet set = statement.getGeneratedKeys();
|
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
int commentId = set.getInt(1);
|
int commentId = set.getInt(1);
|
||||||
createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
|
createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
|
||||||
|
|
||||||
Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
|
Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,21 +115,31 @@ public class FurnidataWatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onChange() {
|
private void onChange() throws InterruptedException {
|
||||||
|
// Re-index under the shared furnidata lock so the watcher and editor
|
||||||
|
// writes never swap the index concurrently. The lock is released before
|
||||||
|
// the throttle/broadcast below so a slow broadcast can't stall editor saves.
|
||||||
|
List<FurnidataEntry> delta;
|
||||||
FurnidataLock.LOCK.lock();
|
FurnidataLock.LOCK.lock();
|
||||||
try {
|
try {
|
||||||
Path source = this.provider.getSource();
|
Path source = this.provider.getSource();
|
||||||
if (source == null) return;
|
if (source == null) return;
|
||||||
|
delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read());
|
||||||
List<FurnidataEntry> delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read());
|
} finally {
|
||||||
|
FurnidataLock.LOCK.unlock();
|
||||||
|
}
|
||||||
if (delta.isEmpty()) return;
|
if (delta.isEmpty()) return;
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
// Min-interval throttle: the index has already been swapped, so we must
|
||||||
if (now - this.lastBroadcast < this.minIntervalMs) {
|
// not drop this delta (the next reindex would diff against the updated
|
||||||
LOGGER.info("FurnidataWatcher: {} changes indexed but broadcast skipped (min interval) — clients update on next change or reconnect", delta.size());
|
// index and never re-emit it). Instead, defer the broadcast until the
|
||||||
return;
|
// interval elapses. Running on a dedicated daemon thread, sleeping is
|
||||||
|
// safe; file events arriving meanwhile coalesce into the next cycle.
|
||||||
|
long sinceLast = System.currentTimeMillis() - this.lastBroadcast;
|
||||||
|
if (sinceLast < this.minIntervalMs) {
|
||||||
|
Thread.sleep(this.minIntervalMs - sinceLast);
|
||||||
}
|
}
|
||||||
this.lastBroadcast = now;
|
this.lastBroadcast = System.currentTimeMillis();
|
||||||
|
|
||||||
FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap)
|
FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap)
|
||||||
? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
|
? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
|
||||||
@@ -138,9 +148,6 @@ public class FurnidataWatcher {
|
|||||||
broadcast(composer);
|
broadcast(composer);
|
||||||
LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)",
|
LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)",
|
||||||
delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size());
|
delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size());
|
||||||
} finally {
|
|
||||||
FurnidataLock.LOCK.unlock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void broadcast(FurnitureDataReloadComposer composer) {
|
private void broadcast(FurnitureDataReloadComposer composer) {
|
||||||
|
|||||||
+11
@@ -13,11 +13,13 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class InteractionGift extends HabboItem {
|
public class InteractionGift extends HabboItem {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
|
||||||
|
|
||||||
public boolean explode = false;
|
public boolean explode = false;
|
||||||
|
private final AtomicBoolean opening = new AtomicBoolean(false);
|
||||||
private int[] itemId;
|
private int[] itemId;
|
||||||
private int colorId = 0;
|
private int colorId = 0;
|
||||||
private int ribbonId = 0;
|
private int ribbonId = 0;
|
||||||
@@ -46,6 +48,15 @@ public class InteractionGift extends HabboItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims the right to open this gift, returning true exactly once. Guards
|
||||||
|
* against two near-simultaneous OpenRecycleBox packets both scheduling an
|
||||||
|
* (async, delayed) OpenGift before the wrapper is removed from the room.
|
||||||
|
*/
|
||||||
|
public boolean tryStartOpening() {
|
||||||
|
return this.opening.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serializeExtradata(ServerMessage serverMessage) {
|
public void serializeExtradata(ServerMessage serverMessage) {
|
||||||
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
|
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
|
||||||
|
|||||||
+15
-1
@@ -18,6 +18,7 @@ import java.sql.SQLException;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
|
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
|
||||||
@@ -61,7 +62,11 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
*/
|
*/
|
||||||
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
|
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
private long cooldown;
|
private volatile long cooldown;
|
||||||
|
// Ensures one box is processed by a single thread at a time, so the
|
||||||
|
// cooldown check-and-set in WiredHandler can't double-fire when a packet
|
||||||
|
// thread and the room cycle thread trigger the same box concurrently.
|
||||||
|
private final AtomicBoolean processing = new AtomicBoolean(false);
|
||||||
private final ConcurrentHashMap<Long, Long> userExecutionCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, Long> userExecutionCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
InteractionWired(ResultSet set, Item baseItem) throws SQLException {
|
InteractionWired(ResultSet set, Item baseItem) throws SQLException {
|
||||||
@@ -149,6 +154,15 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
this.cooldown = newMillis;
|
this.cooldown = newMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Claims exclusive processing of this box; returns false if another thread is already in it. */
|
||||||
|
public boolean tryBeginProcessing() {
|
||||||
|
return this.processing.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endProcessing() {
|
||||||
|
this.processing.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean allowWiredResetState() {
|
public boolean allowWiredResetState() {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+8
@@ -190,6 +190,14 @@ public class InteractionPetBreedingNest extends HabboItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void breed(Habbo habbo, String name, int petOneId, int petTwoId) {
|
public void breed(Habbo habbo, String name, int petOneId, int petTwoId) {
|
||||||
|
// Guard before the destructive delete below: a crafted packet can call
|
||||||
|
// this on a nest that isn't full, which would delete the nest furni and
|
||||||
|
// then NPE on petOne/petTwo in the async runnable (losing the furni).
|
||||||
|
if (habbo == null || this.petOne == null || this.petTwo == null
|
||||||
|
|| habbo.getHabboInfo().getCurrentRoom() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId()));
|
Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId()));
|
||||||
|
|
||||||
this.setExtradata("2");
|
this.setExtradata("2");
|
||||||
|
|||||||
+8
-2
@@ -65,8 +65,14 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
|
|||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
this.lowerLimit = Integer.parseInt(data[0]);
|
if (data.length >= 2) {
|
||||||
this.upperLimit = Integer.parseInt(data[1]);
|
try {
|
||||||
|
this.lowerLimit = Integer.parseInt(data[0].trim());
|
||||||
|
this.upperLimit = Integer.parseInt(data[1].trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep the constructed defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -263,11 +263,13 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
|
if (data.length >= 5) {
|
||||||
|
try {
|
||||||
int itemCount = Integer.parseInt(data[0]);
|
int itemCount = Integer.parseInt(data[0]);
|
||||||
|
|
||||||
String[] items = data[1].split(";");
|
String[] items = data[1].split(";");
|
||||||
|
|
||||||
for (int i = 0; i < itemCount; i++) {
|
for (int i = 0; i < itemCount && i < items.length; i++) {
|
||||||
String[] stuff = items[i].split("-");
|
String[] stuff = items[i].split("-");
|
||||||
|
|
||||||
if (stuff.length >= 6)
|
if (stuff.length >= 6)
|
||||||
@@ -279,6 +281,11 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
this.state = data[2].equals("1");
|
this.state = data[2].equals("1");
|
||||||
this.direction = data[3].equals("1");
|
this.direction = data[3].equals("1");
|
||||||
this.position = data[4].equals("1");
|
this.position = data[4].equals("1");
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep whatever was parsed plus defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.altitude = false;
|
this.altitude = false;
|
||||||
this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||||
this.quantifier = QUANTIFIER_ALL;
|
this.quantifier = QUANTIFIER_ALL;
|
||||||
|
|||||||
+8
-2
@@ -64,8 +64,14 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
|
|||||||
this.userSource = data.userSource;
|
this.userSource = data.userSource;
|
||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
this.lowerLimit = Integer.parseInt(data[0]);
|
if (data.length >= 2) {
|
||||||
this.upperLimit = Integer.parseInt(data[1]);
|
try {
|
||||||
|
this.lowerLimit = Integer.parseInt(data[0].trim());
|
||||||
|
this.upperLimit = Integer.parseInt(data[1].trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep the constructed defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -190,10 +190,15 @@ public class WiredEffectMoveRotateFurni extends InteractionWiredEffect implement
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (String s : data[3].split("\r")) {
|
for (String s : data[3].split("\r")) {
|
||||||
HabboItem item = room.getHabboItem(Integer.parseInt(s));
|
if (s.trim().isEmpty()) continue;
|
||||||
|
try {
|
||||||
|
HabboItem item = room.getHabboItem(Integer.parseInt(s.trim()));
|
||||||
|
|
||||||
if (item != null)
|
if (item != null)
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// skip malformed furni id token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect {
|
|||||||
@Override
|
@Override
|
||||||
public boolean execute(InteractionWiredTrigger object) {
|
public boolean execute(InteractionWiredTrigger object) {
|
||||||
if (!object.isTriggeredByRoomUnit()) {
|
if (!object.isTriggeredByRoomUnit()) {
|
||||||
invalidTriggers.add(object.getId());
|
invalidTriggers.add(object.getBaseItem().getSpriteId());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -252,7 +252,7 @@ public abstract class WiredEffectUserFurniBase extends InteractionWiredEffect {
|
|||||||
@Override
|
@Override
|
||||||
public boolean execute(InteractionWiredTrigger object) {
|
public boolean execute(InteractionWiredTrigger object) {
|
||||||
if (!object.isTriggeredByRoomUnit()) {
|
if (!object.isTriggeredByRoomUnit()) {
|
||||||
invalidTriggers.add(object.getId());
|
invalidTriggers.add(object.getBaseItem().getSpriteId());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -227,6 +227,18 @@ public final class WiredVariableReferenceSupport {
|
|||||||
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops all cached shared-variable assignments belonging to a room. Both
|
||||||
|
* caches are keyed "roomId:itemId[:userId]", so the trailing colon makes the
|
||||||
|
* prefix match the exact room id. Called on room dispose so the static caches
|
||||||
|
* don't retain entries for the JVM lifetime.
|
||||||
|
*/
|
||||||
|
public static void invalidateRoom(int roomId) {
|
||||||
|
String prefix = roomId + ":";
|
||||||
|
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||||
|
ROOM_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
public static SharedRoomAssignment getSharedRoomAssignment(WiredExtraVariableReference reference) {
|
public static SharedRoomAssignment getSharedRoomAssignment(WiredExtraVariableReference reference) {
|
||||||
if (reference == null || !reference.isRoomReference()) {
|
if (reference == null || !reference.isRoomReference()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class Messenger {
|
|||||||
public static THashSet<MessengerBuddy> searchUsers(String username) {
|
public static THashSet<MessengerBuddy> searchUsers(String username) {
|
||||||
THashSet<MessengerBuddy> users = new THashSet<>();
|
THashSet<MessengerBuddy> users = new THashSet<>();
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE username LIKE ? ORDER BY username ASC LIMIT " + Emulator.getConfig().getInt("hotel.messenger.search.maxresults"))) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE username LIKE ? ORDER BY username ASC LIMIT " + Emulator.getConfig().getInt("hotel.messenger.search.maxresults"))) {
|
||||||
statement.setString(1, username + "%");
|
statement.setString(1, com.eu.habbo.util.SqlLikeEscaper.escape(username) + "%");
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
while (set.next()) {
|
while (set.next()) {
|
||||||
users.add(new MessengerBuddy(set, false));
|
users.add(new MessengerBuddy(set, false));
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.eu.habbo.habbohotel.modtool;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append-only audit trail for privileged housekeeping/admin actions (rank grants,
|
||||||
|
* currency grants, etc.). There was previously no record of which operator did
|
||||||
|
* what to whom. Writes are dispatched off the calling thread; the backing table
|
||||||
|
* is created on first use so no manual migration is required.
|
||||||
|
*/
|
||||||
|
public final class HousekeepingAuditLog {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(HousekeepingAuditLog.class);
|
||||||
|
|
||||||
|
private static volatile boolean tableReady = false;
|
||||||
|
|
||||||
|
private HousekeepingAuditLog() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a privileged action asynchronously.
|
||||||
|
*
|
||||||
|
* @param operatorId the acting staff member's user id
|
||||||
|
* @param operatorName the acting staff member's username
|
||||||
|
* @param action a short action key, e.g. {@code "user.set_rank"}
|
||||||
|
* @param targetUserId the affected user's id (0 if not applicable)
|
||||||
|
* @param detail free-form detail, e.g. {@code "rankId=6"} (capped to 512 chars)
|
||||||
|
* @param ip the operator's IP, for correlation
|
||||||
|
*/
|
||||||
|
public static void log(int operatorId, String operatorName, String action, int targetUserId, String detail, String ip) {
|
||||||
|
Emulator.getThreading().run(() -> writeEntry(operatorId, operatorName, action, targetUserId, detail, ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeEntry(int operatorId, String operatorName, String action, int targetUserId, String detail, String ip) {
|
||||||
|
ensureTable();
|
||||||
|
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"INSERT INTO housekeeping_log (operator_id, operator_name, action, target_user_id, detail, ip, timestamp) " +
|
||||||
|
"VALUES (?, ?, ?, ?, ?, ?, ?)")) {
|
||||||
|
statement.setInt(1, operatorId);
|
||||||
|
statement.setString(2, operatorName != null ? operatorName : "");
|
||||||
|
statement.setString(3, action != null ? action : "");
|
||||||
|
statement.setInt(4, targetUserId);
|
||||||
|
statement.setString(5, truncate(detail));
|
||||||
|
statement.setString(6, ip != null ? ip : "");
|
||||||
|
statement.setInt(7, Emulator.getIntUnixTimestamp());
|
||||||
|
statement.execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOGGER.error("Failed to write housekeeping audit log entry", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String truncate(String detail) {
|
||||||
|
if (detail == null) return "";
|
||||||
|
return detail.length() > 512 ? detail.substring(0, 512) : detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureTable() {
|
||||||
|
if (tableReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (HousekeepingAuditLog.class) {
|
||||||
|
if (tableReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS housekeeping_log (" +
|
||||||
|
"id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
|
||||||
|
"operator_id INT NOT NULL, " +
|
||||||
|
"operator_name VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||||
|
"action VARCHAR(64) NOT NULL, " +
|
||||||
|
"target_user_id INT NOT NULL DEFAULT 0, " +
|
||||||
|
"detail VARCHAR(512) NOT NULL DEFAULT '', " +
|
||||||
|
"ip VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||||
|
"timestamp INT NOT NULL, " +
|
||||||
|
"PRIMARY KEY (id), " +
|
||||||
|
"KEY idx_operator (operator_id), " +
|
||||||
|
"KEY idx_target (target_user_id), " +
|
||||||
|
"KEY idx_timestamp (timestamp)" +
|
||||||
|
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
tableReady = true;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOGGER.error("Failed to create housekeeping_log table", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -378,7 +378,9 @@ public class ModToolManager {
|
|||||||
statement.setString(6, reason);
|
statement.setString(6, reason);
|
||||||
statement.setString(7, type.getType());
|
statement.setString(7, type.getType());
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
statement.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
try (PreparedStatement selectBanStatement = connection.prepareStatement("SELECT * FROM bans WHERE id = ? LIMIT 1")) {
|
try (PreparedStatement selectBanStatement = connection.prepareStatement("SELECT * FROM bans WHERE id = ? LIMIT 1")) {
|
||||||
selectBanStatement.setInt(1, set.getInt(1));
|
selectBanStatement.setInt(1, set.getInt(1));
|
||||||
@@ -434,6 +436,10 @@ public class ModToolManager {
|
|||||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUserId);
|
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUserId);
|
||||||
HabboInfo offlineInfo = target != null ? target.getHabboInfo() : HabboManager.getOfflineHabboInfo(targetUserId);
|
HabboInfo offlineInfo = target != null ? target.getHabboInfo() : HabboManager.getOfflineHabboInfo(targetUserId);
|
||||||
|
|
||||||
|
if (offlineInfo == null) {
|
||||||
|
return bans;
|
||||||
|
}
|
||||||
|
|
||||||
if (moderator.getHabboInfo().getRank().getId() < offlineInfo.getRank().getId()) {
|
if (moderator.getHabboInfo().getRank().getId() < offlineInfo.getRank().getId()) {
|
||||||
return bans;
|
return bans;
|
||||||
}
|
}
|
||||||
@@ -454,7 +460,7 @@ public class ModToolManager {
|
|||||||
bans.add(ban);
|
bans.add(ban);
|
||||||
|
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
Emulator.getGameServer().getGameClientManager().disposeClient(target.getClient());
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(target.getClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type == ModToolBanType.IP || type == ModToolBanType.SUPER) && target != null && !ban.ip.equals("offline")) {
|
if ((type == ModToolBanType.IP || type == ModToolBanType.SUPER) && target != null && !ban.ip.equals("offline")) {
|
||||||
@@ -465,7 +471,7 @@ public class ModToolManager {
|
|||||||
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
|
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
|
||||||
Emulator.getThreading().run(ban);
|
Emulator.getThreading().run(ban);
|
||||||
bans.add(ban);
|
bans.add(ban);
|
||||||
Emulator.getGameServer().getGameClientManager().disposeClient(h.getClient());
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(h.getClient());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +483,7 @@ public class ModToolManager {
|
|||||||
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
|
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
|
||||||
Emulator.getThreading().run(ban);
|
Emulator.getThreading().run(ban);
|
||||||
bans.add(ban);
|
bans.add(ban);
|
||||||
Emulator.getGameServer().getGameClientManager().disposeClient(h.getClient());
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(h.getClient());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,10 +158,12 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
|||||||
private String tags;
|
private String tags;
|
||||||
private boolean publicRoom;
|
private boolean publicRoom;
|
||||||
private boolean staffPromotedRoom;
|
private boolean staffPromotedRoom;
|
||||||
private boolean allowPets;
|
// Read every room cycle (processBots/processPets) but written from settings/
|
||||||
private boolean allowPetsEat;
|
// admin packet handlers on another thread — volatile for cross-thread visibility.
|
||||||
|
private volatile boolean allowPets;
|
||||||
|
private volatile boolean allowPetsEat;
|
||||||
private boolean allowWalkthrough;
|
private boolean allowWalkthrough;
|
||||||
private boolean allowBotsWalk;
|
private volatile boolean allowBotsWalk;
|
||||||
private boolean allowEffects;
|
private boolean allowEffects;
|
||||||
private boolean hideWall;
|
private boolean hideWall;
|
||||||
private int chatMode;
|
private int chatMode;
|
||||||
@@ -1026,6 +1028,10 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
|||||||
com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomDiagnostics(this.id);
|
com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomDiagnostics(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop this room's shared wired-variable assignment caches (otherwise
|
||||||
|
// they accrue per (room, item, user) for the JVM lifetime).
|
||||||
|
com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredVariableReferenceSupport.invalidateRoom(this.id);
|
||||||
|
|
||||||
this.itemManager.clear();
|
this.itemManager.clear();
|
||||||
|
|
||||||
this.unitManager.clearQueue();
|
this.unitManager.clearQueue();
|
||||||
|
|||||||
@@ -300,15 +300,20 @@ public class RoomCycleManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TIntObjectIterator<Bot> botIterator = currentBots.iterator();
|
// Snapshot under the map monitor (currentBots is a synchronizedMap whose
|
||||||
for (int i = currentBots.size(); i-- > 0; ) {
|
// iterator isn't concurrency-safe), then cycle OFF-lock. Holding the
|
||||||
|
// monitor across the whole tick would block bot place/pickup and room
|
||||||
|
// dispose for the tick duration AND invert the lock order vs
|
||||||
|
// roomUnitLock -> currentBots taken by RoomUnitManager.addBot/clear.
|
||||||
|
final ArrayList<Bot> bots;
|
||||||
|
synchronized (currentBots) {
|
||||||
|
bots = new ArrayList<>(currentBots.valueCollection());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Bot bot : bots) {
|
||||||
try {
|
try {
|
||||||
final Bot bot;
|
if (bot == null || bot.getRoomUnit() == null) {
|
||||||
try {
|
continue;
|
||||||
botIterator.advance();
|
|
||||||
bot = botIterator.value();
|
|
||||||
} catch (Exception e) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.room.isAllowBotsWalk() && bot.getRoomUnit().isWalking()) {
|
if (!this.room.isAllowBotsWalk() && bot.getRoomUnit().isWalking()) {
|
||||||
@@ -322,10 +327,8 @@ public class RoomCycleManager {
|
|||||||
if (this.cycleRoomUnit(bot.getRoomUnit(), RoomUnitType.BOT)) {
|
if (this.cycleRoomUnit(bot.getRoomUnit(), RoomUnitType.BOT)) {
|
||||||
updatedUnit.add(bot.getRoomUnit());
|
updatedUnit.add(bot.getRoomUnit());
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
} catch (NoSuchElementException e) {
|
|
||||||
LOGGER.error("Caught exception", e);
|
LOGGER.error("Caught exception", e);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,16 +342,19 @@ public class RoomCycleManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TIntObjectIterator<Pet> petIterator = currentPets.iterator();
|
// Snapshot under the monitor, then cycle off-lock (see processBots): avoids
|
||||||
for (int i = currentPets.size(); i-- > 0; ) {
|
// holding currentPets for the whole tick and the roomUnitLock inversion.
|
||||||
try {
|
final ArrayList<Pet> pets;
|
||||||
petIterator.advance();
|
synchronized (currentPets) {
|
||||||
} catch (NoSuchElementException e) {
|
pets = new ArrayList<>(currentPets.valueCollection());
|
||||||
LOGGER.error("Caught exception", e);
|
}
|
||||||
break;
|
|
||||||
|
for (Pet pet : pets) {
|
||||||
|
try {
|
||||||
|
if (pet == null || pet.getRoomUnit() == null) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Pet pet = petIterator.value();
|
|
||||||
if (this.cycleRoomUnit(pet.getRoomUnit(), RoomUnitType.PET)) {
|
if (this.cycleRoomUnit(pet.getRoomUnit(), RoomUnitType.PET)) {
|
||||||
updatedUnit.add(pet.getRoomUnit());
|
updatedUnit.add(pet.getRoomUnit());
|
||||||
}
|
}
|
||||||
@@ -365,6 +371,9 @@ public class RoomCycleManager {
|
|||||||
pet.getRoomUnit().removeStatus(RoomUnitStatus.GESTURE);
|
pet.getRoomUnit().removeStatus(RoomUnitStatus.GESTURE);
|
||||||
updatedUnit.add(pet.getRoomUnit());
|
updatedUnit.add(pet.getRoomUnit());
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Caught exception", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ public class RoomItemManager {
|
|||||||
*/
|
*/
|
||||||
public THashSet<HabboItem> getFloorItems() {
|
public THashSet<HabboItem> getFloorItems() {
|
||||||
THashSet<HabboItem> items = new THashSet<>();
|
THashSet<HabboItem> items = new THashSet<>();
|
||||||
|
// roomItems is a TCollections.synchronizedMap; its iterator is not safe
|
||||||
|
// against concurrent put/remove (item place/pickup), so hold the map
|
||||||
|
// monitor for the whole traversal, matching the mutation sites.
|
||||||
|
synchronized (this.roomItems) {
|
||||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||||
|
|
||||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||||
@@ -180,6 +184,7 @@ public class RoomItemManager {
|
|||||||
items.add(iterator.value());
|
items.add(iterator.value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -189,6 +194,7 @@ public class RoomItemManager {
|
|||||||
*/
|
*/
|
||||||
public THashSet<HabboItem> getWallItems() {
|
public THashSet<HabboItem> getWallItems() {
|
||||||
THashSet<HabboItem> items = new THashSet<>();
|
THashSet<HabboItem> items = new THashSet<>();
|
||||||
|
synchronized (this.roomItems) {
|
||||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||||
|
|
||||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||||
@@ -202,6 +208,7 @@ public class RoomItemManager {
|
|||||||
items.add(iterator.value());
|
items.add(iterator.value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -211,6 +218,7 @@ public class RoomItemManager {
|
|||||||
*/
|
*/
|
||||||
public THashSet<HabboItem> getPostItNotes() {
|
public THashSet<HabboItem> getPostItNotes() {
|
||||||
THashSet<HabboItem> items = new THashSet<>();
|
THashSet<HabboItem> items = new THashSet<>();
|
||||||
|
synchronized (this.roomItems) {
|
||||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||||
|
|
||||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||||
@@ -225,6 +233,7 @@ public class RoomItemManager {
|
|||||||
items.add(iterator.value());
|
items.add(iterator.value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -276,6 +285,10 @@ public class RoomItemManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache miss: iterate roomItems under its monitor so a concurrent
|
||||||
|
// place/pickup can't rehash the map mid-traversal (which the per-advance
|
||||||
|
// try/catch would otherwise silently swallow into an incomplete result).
|
||||||
|
synchronized (this.roomItems) {
|
||||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||||
|
|
||||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||||
@@ -316,6 +329,7 @@ public class RoomItemManager {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.room.isLoaded()) {
|
if (this.room.isLoaded()) {
|
||||||
this.tileCache.put(tile, items);
|
this.tileCache.put(tile, items);
|
||||||
@@ -956,11 +970,13 @@ public class RoomItemManager {
|
|||||||
public int getUserUniqueFurniCount(int userId) {
|
public int getUserUniqueFurniCount(int userId) {
|
||||||
THashSet<Item> items = new THashSet<>();
|
THashSet<Item> items = new THashSet<>();
|
||||||
|
|
||||||
|
synchronized (this.roomItems) {
|
||||||
for (HabboItem item : this.roomItems.valueCollection()) {
|
for (HabboItem item : this.roomItems.valueCollection()) {
|
||||||
if (!items.contains(item.getBaseItem()) && item.getUserId() == userId) {
|
if (!items.contains(item.getBaseItem()) && item.getUserId() == userId) {
|
||||||
items.add(item.getBaseItem());
|
items.add(item.getBaseItem());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return items.size();
|
return items.size();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,13 +130,16 @@ public class RoomLayout {
|
|||||||
this.roomTiles = new RoomTile[this.mapSizeX][this.mapSizeY];
|
this.roomTiles = new RoomTile[this.mapSizeX][this.mapSizeY];
|
||||||
|
|
||||||
for (short y = 0; y < this.mapSizeY; y++) {
|
for (short y = 0; y < this.mapSizeY; y++) {
|
||||||
if (modelTemp[y].isEmpty() || modelTemp[y].equalsIgnoreCase("\r")) {
|
// A row shorter/longer than the model width (or empty) cannot be parsed
|
||||||
continue;
|
// per-square. Previously such tiles were left null while tileExists()
|
||||||
}
|
// still reported them present, causing NPEs in the coordinate accessors.
|
||||||
|
// Fill them with INVALID tiles so every in-bounds coordinate is non-null.
|
||||||
|
boolean validRow = !modelTemp[y].isEmpty() && modelTemp[y].length() == this.mapSizeX;
|
||||||
|
|
||||||
for (short x = 0; x < this.mapSizeX; x++) {
|
for (short x = 0; x < this.mapSizeX; x++) {
|
||||||
if (modelTemp[y].length() != this.mapSizeX) {
|
if (!validRow) {
|
||||||
break;
|
this.roomTiles[x][y] = new RoomTile(x, y, (short) 0, RoomTileState.INVALID, true);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String square = modelTemp[y].substring(x, x + 1).trim().toLowerCase();
|
String square = modelTemp[y].substring(x, x + 1).trim().toLowerCase();
|
||||||
@@ -159,7 +162,9 @@ public class RoomLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.doorTile = this.roomTiles[this.doorX][this.doorY];
|
this.doorTile = (this.doorX >= 0 && this.doorX < this.mapSizeX && this.doorY >= 0 && this.doorY < this.mapSizeY)
|
||||||
|
? this.roomTiles[this.doorX][this.doorY]
|
||||||
|
: null;
|
||||||
|
|
||||||
if (this.doorTile != null) {
|
if (this.doorTile != null) {
|
||||||
this.doorTile.setAllowStack(false);
|
this.doorTile.setAllowStack(false);
|
||||||
|
|||||||
@@ -272,10 +272,16 @@ public class RoomRightsManager {
|
|||||||
} else if (this.isOwner(habbo)) {
|
} else if (this.isOwner(habbo)) {
|
||||||
habbo.getClient().sendResponse(new RoomOwnerComposer());
|
habbo.getClient().sendResponse(new RoomOwnerComposer());
|
||||||
flatCtrl = RoomRightLevels.MODERATOR;
|
flatCtrl = RoomRightLevels.MODERATOR;
|
||||||
} else if (this.hasRights(habbo) && !this.room.hasGuild()) {
|
|
||||||
flatCtrl = RoomRightLevels.RIGHTS;
|
|
||||||
} else if (this.room.hasGuild()) {
|
} else if (this.room.hasGuild()) {
|
||||||
flatCtrl = this.getGuildRightLevel(habbo);
|
// Explicit room rights must still be honoured in guild rooms (the old
|
||||||
|
// `&& !hasGuild()` guard stripped them for non-guild members) — take
|
||||||
|
// whichever of the two is stronger.
|
||||||
|
RoomRightLevels guildLevel = this.getGuildRightLevel(habbo);
|
||||||
|
flatCtrl = (this.hasRights(habbo) && RoomRightLevels.RIGHTS.isEqualOrGreaterThan(guildLevel))
|
||||||
|
? RoomRightLevels.RIGHTS
|
||||||
|
: guildLevel;
|
||||||
|
} else if (this.hasRights(habbo)) {
|
||||||
|
flatCtrl = RoomRightLevels.RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
habbo.getClient().sendResponse(new RoomRightsComposer(flatCtrl));
|
habbo.getClient().sendResponse(new RoomRightsComposer(flatCtrl));
|
||||||
|
|||||||
@@ -152,15 +152,23 @@ public class RoomSpecialTypes {
|
|||||||
|
|
||||||
|
|
||||||
public InteractionNest getNest(int itemId) {
|
public InteractionNest getNest(int itemId) {
|
||||||
|
synchronized (this.nests) {
|
||||||
return this.nests.get(itemId);
|
return this.nests.get(itemId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void addNest(InteractionNest item) {
|
public void addNest(InteractionNest item) {
|
||||||
this.nests.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
synchronized (this.nests) {
|
||||||
|
this.nests.put(item.getId(), item);
|
||||||
|
}
|
||||||
|
this.specialItemsById.put(item.getId(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeNest(InteractionNest item) {
|
public void removeNest(InteractionNest item) {
|
||||||
this.nests.remove(item.getId()); this.specialItemsById.remove(item.getId());
|
synchronized (this.nests) {
|
||||||
|
this.nests.remove(item.getId());
|
||||||
|
}
|
||||||
|
this.specialItemsById.remove(item.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public THashSet<InteractionNest> getNests() {
|
public THashSet<InteractionNest> getNests() {
|
||||||
@@ -174,15 +182,23 @@ public class RoomSpecialTypes {
|
|||||||
|
|
||||||
|
|
||||||
public InteractionPetDrink getPetDrink(int itemId) {
|
public InteractionPetDrink getPetDrink(int itemId) {
|
||||||
|
synchronized (this.petDrinks) {
|
||||||
return this.petDrinks.get(itemId);
|
return this.petDrinks.get(itemId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void addPetDrink(InteractionPetDrink item) {
|
public void addPetDrink(InteractionPetDrink item) {
|
||||||
this.petDrinks.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
synchronized (this.petDrinks) {
|
||||||
|
this.petDrinks.put(item.getId(), item);
|
||||||
|
}
|
||||||
|
this.specialItemsById.put(item.getId(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removePetDrink(InteractionPetDrink item) {
|
public void removePetDrink(InteractionPetDrink item) {
|
||||||
this.petDrinks.remove(item.getId()); this.specialItemsById.remove(item.getId());
|
synchronized (this.petDrinks) {
|
||||||
|
this.petDrinks.remove(item.getId());
|
||||||
|
}
|
||||||
|
this.specialItemsById.remove(item.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public THashSet<InteractionPetDrink> getPetDrinks() {
|
public THashSet<InteractionPetDrink> getPetDrinks() {
|
||||||
@@ -196,15 +212,23 @@ public class RoomSpecialTypes {
|
|||||||
|
|
||||||
|
|
||||||
public InteractionPetFood getPetFood(int itemId) {
|
public InteractionPetFood getPetFood(int itemId) {
|
||||||
|
synchronized (this.petFoods) {
|
||||||
return this.petFoods.get(itemId);
|
return this.petFoods.get(itemId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void addPetFood(InteractionPetFood item) {
|
public void addPetFood(InteractionPetFood item) {
|
||||||
this.petFoods.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
synchronized (this.petFoods) {
|
||||||
|
this.petFoods.put(item.getId(), item);
|
||||||
|
}
|
||||||
|
this.specialItemsById.put(item.getId(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removePetFood(InteractionPetFood petFood) {
|
public void removePetFood(InteractionPetFood petFood) {
|
||||||
this.petFoods.remove(petFood.getId()); this.specialItemsById.remove(petFood.getId());
|
synchronized (this.petFoods) {
|
||||||
|
this.petFoods.remove(petFood.getId());
|
||||||
|
}
|
||||||
|
this.specialItemsById.remove(petFood.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public THashSet<InteractionPetFood> getPetFoods() {
|
public THashSet<InteractionPetFood> getPetFoods() {
|
||||||
@@ -218,15 +242,23 @@ public class RoomSpecialTypes {
|
|||||||
|
|
||||||
|
|
||||||
public InteractionPetToy getPetToy(int itemId) {
|
public InteractionPetToy getPetToy(int itemId) {
|
||||||
|
synchronized (this.petToys) {
|
||||||
return this.petToys.get(itemId);
|
return this.petToys.get(itemId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void addPetToy(InteractionPetToy item) {
|
public void addPetToy(InteractionPetToy item) {
|
||||||
this.petToys.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
synchronized (this.petToys) {
|
||||||
|
this.petToys.put(item.getId(), item);
|
||||||
|
}
|
||||||
|
this.specialItemsById.put(item.getId(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removePetToy(InteractionPetToy petToy) {
|
public void removePetToy(InteractionPetToy petToy) {
|
||||||
this.petToys.remove(petToy.getId()); this.specialItemsById.remove(petToy.getId());
|
synchronized (this.petToys) {
|
||||||
|
this.petToys.remove(petToy.getId());
|
||||||
|
}
|
||||||
|
this.specialItemsById.remove(petToy.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public THashSet<InteractionPetToy> getPetToys() {
|
public THashSet<InteractionPetToy> getPetToys() {
|
||||||
@@ -240,15 +272,23 @@ public class RoomSpecialTypes {
|
|||||||
|
|
||||||
|
|
||||||
public InteractionPetTree getPetTree(int itemId) {
|
public InteractionPetTree getPetTree(int itemId) {
|
||||||
|
synchronized (this.petTrees) {
|
||||||
return this.petTrees.get(itemId);
|
return this.petTrees.get(itemId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void addPetTree(InteractionPetTree item) {
|
public void addPetTree(InteractionPetTree item) {
|
||||||
this.petTrees.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
synchronized (this.petTrees) {
|
||||||
|
this.petTrees.put(item.getId(), item);
|
||||||
|
}
|
||||||
|
this.specialItemsById.put(item.getId(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removePetTree(InteractionPetTree petTree) {
|
public void removePetTree(InteractionPetTree petTree) {
|
||||||
this.petTrees.remove(petTree.getId()); this.specialItemsById.remove(petTree.getId());
|
synchronized (this.petTrees) {
|
||||||
|
this.petTrees.remove(petTree.getId());
|
||||||
|
}
|
||||||
|
this.specialItemsById.remove(petTree.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public THashSet<InteractionPetTree> getPetTrees() {
|
public THashSet<InteractionPetTree> getPetTrees() {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class RoomTrade {
|
|||||||
|
|
||||||
private final List<RoomTradeUser> users;
|
private final List<RoomTradeUser> users;
|
||||||
private final Room room;
|
private final Room room;
|
||||||
|
private boolean completed = false;
|
||||||
|
|
||||||
public RoomTrade(Habbo userOne, Habbo userTwo, Room room) {
|
public RoomTrade(Habbo userOne, Habbo userTwo, Room room) {
|
||||||
this.users = new ArrayList<>();
|
this.users = new ArrayList<>();
|
||||||
@@ -54,7 +55,7 @@ public class RoomTrade {
|
|||||||
this.sendMessageToUsers(new TradeStartComposer(this));
|
this.sendMessageToUsers(new TradeStartComposer(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void offerItem(Habbo habbo, HabboItem item) {
|
public synchronized void offerItem(Habbo habbo, HabboItem item) {
|
||||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||||
|
|
||||||
if (user.getItems().contains(item))
|
if (user.getItems().contains(item))
|
||||||
@@ -67,7 +68,7 @@ public class RoomTrade {
|
|||||||
this.updateWindow();
|
this.updateWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void offerMultipleItems(Habbo habbo, THashSet<HabboItem> items) {
|
public synchronized void offerMultipleItems(Habbo habbo, THashSet<HabboItem> items) {
|
||||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||||
|
|
||||||
for (HabboItem item : items) {
|
for (HabboItem item : items) {
|
||||||
@@ -81,7 +82,7 @@ public class RoomTrade {
|
|||||||
this.updateWindow();
|
this.updateWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeItem(Habbo habbo, HabboItem item) {
|
public synchronized void removeItem(Habbo habbo, HabboItem item) {
|
||||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||||
|
|
||||||
if (!user.getItems().contains(item))
|
if (!user.getItems().contains(item))
|
||||||
@@ -94,7 +95,7 @@ public class RoomTrade {
|
|||||||
this.updateWindow();
|
this.updateWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void accept(Habbo habbo, boolean value) {
|
public synchronized void accept(Habbo habbo, boolean value) {
|
||||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||||
|
|
||||||
user.setAccepted(value);
|
user.setAccepted(value);
|
||||||
@@ -110,7 +111,13 @@ public class RoomTrade {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void confirm(Habbo habbo) {
|
public synchronized void confirm(Habbo habbo) {
|
||||||
|
// Re-entry guard: both participants confirm on their own EventLoop
|
||||||
|
// threads. Without this (and the method-level lock) two concurrent
|
||||||
|
// confirms could each observe "all confirmed" and run tradeItems()
|
||||||
|
// twice → item/credit duplication.
|
||||||
|
if (this.completed) return;
|
||||||
|
|
||||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||||
|
|
||||||
user.confirm();
|
user.confirm();
|
||||||
@@ -122,6 +129,8 @@ public class RoomTrade {
|
|||||||
accepted = false;
|
accepted = false;
|
||||||
}
|
}
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
|
this.completed = true;
|
||||||
|
|
||||||
if (this.tradeItems()) {
|
if (this.tradeItems()) {
|
||||||
this.closeWindow();
|
this.closeWindow();
|
||||||
this.sendMessageToUsers(new TradeCompleteComposer());
|
this.sendMessageToUsers(new TradeCompleteComposer());
|
||||||
@@ -264,6 +273,10 @@ public class RoomTrade {
|
|||||||
protected void clearAccepted() {
|
protected void clearAccepted() {
|
||||||
for (RoomTradeUser user : this.users) {
|
for (RoomTradeUser user : this.users) {
|
||||||
user.setAccepted(false);
|
user.setAccepted(false);
|
||||||
|
// Any change to the offered items invalidates a prior confirmation;
|
||||||
|
// without this a stale confirmed=true lets a user strip their side
|
||||||
|
// and still complete the trade once the partner re-confirms.
|
||||||
|
user.setConfirmed(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ public class RoomTradeUser {
|
|||||||
this.confirmed = true;
|
this.confirmed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setConfirmed(boolean value) {
|
||||||
|
this.confirmed = value;
|
||||||
|
}
|
||||||
|
|
||||||
public void addItem(HabboItem item) {
|
public void addItem(HabboItem item) {
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -71,7 +72,10 @@ public class RoomUnit {
|
|||||||
private RoomUserRotation headRotation = RoomUserRotation.NORTH;
|
private RoomUserRotation headRotation = RoomUserRotation.NORTH;
|
||||||
private DanceType danceType;
|
private DanceType danceType;
|
||||||
private RoomUnitType roomUnitType;
|
private RoomUnitType roomUnitType;
|
||||||
private Deque<RoomTile> path = new LinkedList<>();
|
// Concurrent + volatile: the room cycle thread polls/clears this path while a
|
||||||
|
// walk packet thread rebuilds it via findPath/setPath. A plain LinkedList would
|
||||||
|
// corrupt under the concurrent structural modification.
|
||||||
|
private volatile Deque<RoomTile> path = new ConcurrentLinkedDeque<>();
|
||||||
private int handItem;
|
private int handItem;
|
||||||
private long handItemTimestamp;
|
private long handItemTimestamp;
|
||||||
private long lastRollerTime;
|
private long lastRollerTime;
|
||||||
@@ -587,7 +591,7 @@ public class RoomUnit {
|
|||||||
Deque<RoomTile> newPath = this.room.getLayout().getPathfinder()
|
Deque<RoomTile> newPath = this.room.getLayout().getPathfinder()
|
||||||
.findPath(this.currentLocation, this.goalLocation, this.goalLocation, this);
|
.findPath(this.currentLocation, this.goalLocation, this.goalLocation, this);
|
||||||
if (newPath != null && !newPath.isEmpty()) {
|
if (newPath != null && !newPath.isEmpty()) {
|
||||||
this.path = newPath;
|
this.path = new ConcurrentLinkedDeque<>(newPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,7 +769,7 @@ public class RoomUnit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setPath(Deque<RoomTile> path) {
|
public void setPath(Deque<RoomTile> path) {
|
||||||
this.path = path;
|
this.path = (path == null) ? new ConcurrentLinkedDeque<>() : new ConcurrentLinkedDeque<>(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RoomRightLevels getRightsLevel() {
|
public RoomRightLevels getRightsLevel() {
|
||||||
|
|||||||
+4
-1
@@ -24,8 +24,11 @@ public class PathfinderImpl implements Pathfinder {
|
|||||||
|
|
||||||
private static final int CACHED_TIMEOUT_MS = Emulator.getConfig()
|
private static final int CACHED_TIMEOUT_MS = Emulator.getConfig()
|
||||||
.getInt(CONFIG_EXECUTION_TIME, 25);
|
.getInt(CONFIG_EXECUTION_TIME, 25);
|
||||||
|
// Default ON: bound A* to CACHED_TIMEOUT_MS (25ms) so a pathological search
|
||||||
|
// can't run unbounded and stall the thread. On timeout findPath returns an
|
||||||
|
// empty path (the unit simply doesn't move there) — graceful degradation.
|
||||||
private static final boolean CACHED_TIMEOUT_ENABLED = Emulator.getConfig()
|
private static final boolean CACHED_TIMEOUT_ENABLED = Emulator.getConfig()
|
||||||
.getBoolean(CONFIG_TIMEOUT_ENABLED, false);
|
.getBoolean(CONFIG_TIMEOUT_ENABLED, true);
|
||||||
private static final long CACHED_TIMEOUT_NANOS = CACHED_TIMEOUT_MS * 1_000_000L;
|
private static final long CACHED_TIMEOUT_NANOS = CACHED_TIMEOUT_MS * 1_000_000L;
|
||||||
|
|
||||||
private final Room room;
|
private final Room room;
|
||||||
|
|||||||
@@ -146,31 +146,23 @@ public class Habbo implements Runnable {
|
|||||||
this.habboInfo.setIpLogin(ip);
|
this.habboInfo.setIpLogin(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.client.getMachineId() == null || this.client.getMachineId().length() == 0) {
|
// The Nitro client sends the UniqueID (machine fingerprint) packet right
|
||||||
return false;
|
// AFTER the SSO ticket, so client.getMachineId() may still be null here.
|
||||||
}
|
// Do NOT reject the login for a missing machineId — MachineIDEvent sets it
|
||||||
|
// and enforces the MAC ban as soon as the UniqueID packet arrives. Only
|
||||||
this.habboInfo.setMachineID(this.client.getMachineId());
|
// MAC-ban check here when the fingerprint is already available.
|
||||||
|
String machineId = this.client.getMachineId();
|
||||||
|
if (machineId != null && !machineId.isEmpty()) {
|
||||||
|
this.habboInfo.setMachineID(machineId);
|
||||||
|
|
||||||
if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
|
if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Emulator.getGameEnvironment().getModToolManager().hasIPBan(this.habboInfo.getIpLogin())) {
|
if (Emulator.getGameEnvironment().getModToolManager().hasIPBan(this.habboInfo.getIpLogin())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.habboInfo.setMachineID(this.client.getMachineId());
|
|
||||||
|
|
||||||
if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Emulator.getGameEnvironment().getModToolManager().hasIPBan(this.habboInfo.getIpLogin())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.habboInfo.setMachineID(this.client.getMachineId());
|
|
||||||
this.isOnline(true);
|
this.isOnline(true);
|
||||||
|
|
||||||
this.messenger.connectionChanged(this, true, false);
|
this.messenger.connectionChanged(this, true, false);
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ public class HabboInfo implements Runnable {
|
|||||||
private RideablePet riding;
|
private RideablePet riding;
|
||||||
private Class<? extends Game> currentGame;
|
private Class<? extends Game> currentGame;
|
||||||
private TIntIntHashMap currencies;
|
private TIntIntHashMap currencies;
|
||||||
|
// Serializes credits + currencies read-modify-write and the saveCurrencies
|
||||||
|
// snapshot so the credit-roller thread and purchase/trade handler threads
|
||||||
|
// can't lose updates or rehash the Trove map mid-iteration. Never held
|
||||||
|
// across run()'s DB I/O.
|
||||||
|
private final Object currencyLock = new Object();
|
||||||
private GamePlayer gamePlayer;
|
private GamePlayer gamePlayer;
|
||||||
private int photoRoomId;
|
private int photoRoomId;
|
||||||
private int photoTimestamp;
|
private int photoTimestamp;
|
||||||
@@ -123,11 +128,16 @@ public class HabboInfo implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void saveCurrencies() {
|
private void saveCurrencies() {
|
||||||
List<int[]> entries = new ArrayList<>(this.currencies.size());
|
// Snapshot under the lock so a concurrent adjustOrPutValue/put can't
|
||||||
|
// rehash the Trove map while we iterate; do the DB batch off-lock.
|
||||||
|
List<int[]> entries;
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
|
entries = new ArrayList<>(this.currencies.size());
|
||||||
this.currencies.forEachEntry((type, amount) -> {
|
this.currencies.forEachEntry((type, amount) -> {
|
||||||
entries.add(new int[]{type, amount});
|
entries.add(new int[]{type, amount});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SqlQueries.batchUpdate(
|
SqlQueries.batchUpdate(
|
||||||
@@ -238,20 +248,30 @@ public class HabboInfo implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getCurrencyAmount(int type) {
|
public int getCurrencyAmount(int type) {
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
return this.currencies.get(type);
|
return this.currencies.get(type);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public TIntIntHashMap getCurrencies() {
|
public TIntIntHashMap getCurrencies() {
|
||||||
return this.currencies;
|
// Return a snapshot under the lock: callers iterate this map, which would
|
||||||
|
// otherwise corrupt during a concurrent adjustOrPutValue rehash.
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
|
return new TIntIntHashMap(this.currencies);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addCurrencyAmount(int type, int amount) {
|
public void addCurrencyAmount(int type, int amount) {
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
this.currencies.adjustOrPutValue(type, amount, amount);
|
this.currencies.adjustOrPutValue(type, amount, amount);
|
||||||
|
}
|
||||||
this.run();
|
this.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCurrencyAmount(int type, int amount) {
|
public void setCurrencyAmount(int type, int amount) {
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
this.currencies.put(type, amount);
|
this.currencies.put(type, amount);
|
||||||
|
}
|
||||||
this.run();
|
this.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,20 +400,26 @@ public class HabboInfo implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean canBuy(CatalogItem item) {
|
public boolean canBuy(CatalogItem item) {
|
||||||
return this.credits >= item.getCredits() && this.getCurrencies().get(item.getPointsType()) >= item.getPoints();
|
return this.getCredits() >= item.getCredits() && this.getCurrencyAmount(item.getPointsType()) >= item.getPoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCredits() {
|
public int getCredits() {
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
return this.credits;
|
return this.credits;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setCredits(int credits) {
|
public void setCredits(int credits) {
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
this.credits = credits;
|
this.credits = credits;
|
||||||
|
}
|
||||||
this.run();
|
this.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addCredits(int credits) {
|
public void addCredits(int credits) {
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
this.credits += credits;
|
this.credits += credits;
|
||||||
|
}
|
||||||
this.run();
|
this.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,6 +626,13 @@ public class HabboInfo implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
this.saveCurrencies();
|
this.saveCurrencies();
|
||||||
|
|
||||||
|
// Read credits under the lock so the persisted value is consistent with
|
||||||
|
// concurrent addCredits/setCredits (matches the currencyLock invariant).
|
||||||
|
final int creditsForSave;
|
||||||
|
synchronized (this.currencyLock) {
|
||||||
|
creditsForSave = this.credits;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SqlQueries.update(
|
SqlQueries.update(
|
||||||
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ?, background_border_id = ? WHERE id = ?",
|
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ?, background_border_id = ? WHERE id = ?",
|
||||||
@@ -607,7 +640,7 @@ public class HabboInfo implements Runnable {
|
|||||||
this.online ? "1" : "0",
|
this.online ? "1" : "0",
|
||||||
this.look,
|
this.look,
|
||||||
this.gender.name(),
|
this.gender.name(),
|
||||||
this.credits,
|
creditsForSave,
|
||||||
Emulator.getIntUnixTimestamp(),
|
Emulator.getIntUnixTimestamp(),
|
||||||
this.lastOnline,
|
this.lastOnline,
|
||||||
this.homeRoom,
|
this.homeRoom,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ public class HabboManager {
|
|||||||
habbo = this.cloneCheck(userId);
|
habbo = this.cloneCheck(userId);
|
||||||
if (habbo != null) {
|
if (habbo != null) {
|
||||||
habbo.alert(Emulator.getTexts().getValue("loggedin.elsewhere"));
|
habbo.alert(Emulator.getTexts().getValue("loggedin.elsewhere"));
|
||||||
Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(habbo.getClient());
|
||||||
habbo = null;
|
habbo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -448,15 +448,27 @@ public class HabboStats implements Runnable {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.achievementProgress.containsKey(achievement))
|
synchronized (this.achievementProgress) {
|
||||||
return this.achievementProgress.get(achievement);
|
Integer progress = this.achievementProgress.get(achievement);
|
||||||
|
return progress != null ? progress : -1;
|
||||||
return -1;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setProgress(Achievement achievement, int progress) {
|
public void setProgress(Achievement achievement, int progress) {
|
||||||
|
synchronized (this.achievementProgress) {
|
||||||
this.achievementProgress.put(achievement, progress);
|
this.achievementProgress.put(achievement, progress);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Atomic read-add-write so concurrent progress sources don't lose updates. Returns the new total. */
|
||||||
|
public int incrementProgress(Achievement achievement, int amount) {
|
||||||
|
synchronized (this.achievementProgress) {
|
||||||
|
Integer current = this.achievementProgress.get(achievement);
|
||||||
|
int next = (current != null ? current : 0) + amount;
|
||||||
|
this.achievementProgress.put(achievement, next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int getRentedTimeEnd() {
|
public int getRentedTimeEnd() {
|
||||||
return this.rentedTimeEnd;
|
return this.rentedTimeEnd;
|
||||||
|
|||||||
@@ -178,6 +178,15 @@ public class WiredHandler {
|
|||||||
private static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff, final LegacyExecutionPlan executionPlan) {
|
private static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff, final LegacyExecutionPlan executionPlan) {
|
||||||
long millis = System.currentTimeMillis();
|
long millis = System.currentTimeMillis();
|
||||||
int roomUnitId = roomUnit != null ? roomUnit.getId() : -1;
|
int roomUnitId = roomUnit != null ? roomUnit.getId() : -1;
|
||||||
|
|
||||||
|
// Only one thread may process a given trigger box at a time, so the
|
||||||
|
// cooldown check (below) and setCooldown (further down) act as one
|
||||||
|
// atomic claim — preventing a concurrent packet/cycle double-fire.
|
||||||
|
if (!trigger.tryBeginProcessing()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (Emulator.isReady && ((Emulator.getConfig().getBoolean("wired.custom.enabled", false) && (trigger.canExecute(millis) || roomUnitId > -1) && trigger.userCanExecute(roomUnitId, millis)) || (!Emulator.getConfig().getBoolean("wired.custom.enabled", false) && trigger.canExecute(millis))) && trigger.execute(roomUnit, room, stuff)) {
|
if (Emulator.isReady && ((Emulator.getConfig().getBoolean("wired.custom.enabled", false) && (trigger.canExecute(millis) || roomUnitId > -1) && trigger.userCanExecute(roomUnitId, millis)) || (!Emulator.getConfig().getBoolean("wired.custom.enabled", false) && trigger.canExecute(millis))) && trigger.execute(roomUnit, room, stuff)) {
|
||||||
THashSet<InteractionWiredCondition> conditions = room.getRoomSpecialTypes().getConditions(trigger.getX(), trigger.getY());
|
THashSet<InteractionWiredCondition> conditions = room.getRoomSpecialTypes().getConditions(trigger.getX(), trigger.getY());
|
||||||
THashSet<InteractionWiredEffect> effects = room.getRoomSpecialTypes().getEffects(trigger.getX(), trigger.getY());
|
THashSet<InteractionWiredEffect> effects = room.getRoomSpecialTypes().getEffects(trigger.getX(), trigger.getY());
|
||||||
@@ -272,6 +281,9 @@ public class WiredHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
trigger.endProcessing();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean evaluateConditions(THashSet<InteractionWiredCondition> conditions, RoomUnit roomUnit, Room room, Object[] stuff, int evaluationMode, int evaluationValue) {
|
private static boolean evaluateConditions(THashSet<InteractionWiredCondition> conditions, RoomUnit roomUnit, Room room, Object[] stuff, int evaluationMode, int evaluationValue) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.eu.habbo.util.PacketUtils;
|
|||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class ClientMessage {
|
public class ClientMessage {
|
||||||
private final int header;
|
private final int header;
|
||||||
private final ByteBuf buffer;
|
private final ByteBuf buffer;
|
||||||
@@ -61,10 +63,17 @@ public class ClientMessage {
|
|||||||
|
|
||||||
public String readString() {
|
public String readString() {
|
||||||
try {
|
try {
|
||||||
int length = this.readShort();
|
// Length is an unsigned short in the protocol; mask to avoid a
|
||||||
|
// negative array size, and clamp to what's actually buffered so a
|
||||||
|
// bogus length can't throw mid-read and desync the remaining fields.
|
||||||
|
int length = this.readShort() & 0xFFFF;
|
||||||
|
int available = this.buffer.readableBytes();
|
||||||
|
if (length > available) {
|
||||||
|
length = available;
|
||||||
|
}
|
||||||
byte[] data = new byte[length];
|
byte[] data = new byte[length];
|
||||||
this.buffer.readBytes(data);
|
this.buffer.readBytes(data);
|
||||||
return new String(data);
|
return new String(data, StandardCharsets.UTF_8);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import io.netty.buffer.ByteBufOutputStream;
|
|||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class ServerMessage {
|
public class ServerMessage {
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ public class ServerMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] data = obj.getBytes();
|
byte[] data = obj.getBytes(StandardCharsets.UTF_8);
|
||||||
this.stream.writeShort(data.length);
|
this.stream.writeShort(data.length);
|
||||||
this.stream.write(data);
|
this.stream.write(data);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ public class CatalogBuyItemEvent extends MessageHandler {
|
|||||||
String extraData = this.packet.readString();
|
String extraData = this.packet.readString();
|
||||||
int count = this.packet.readInt();
|
int count = this.packet.readInt();
|
||||||
|
|
||||||
|
// Clamp the client-supplied quantity. Without this the club-offer
|
||||||
|
// branch accumulates cost in plain ints and a huge count overflows
|
||||||
|
// to a negative total, bypassing the affordability checks and
|
||||||
|
// CREDITING the buyer (free currency/subscription exploit).
|
||||||
|
if (count < 1 || count > 100) {
|
||||||
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.client.getHabbo().getInventory().getItemsComponent().itemCount() > HabboInventory.MAXIMUM_ITEMS) {
|
if (this.client.getHabbo().getInventory().getItemsComponent().itemCount() > HabboInventory.MAXIMUM_ITEMS) {
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
|
|||||||
+5
@@ -29,6 +29,11 @@ public class OpenRecycleBoxEvent extends MessageHandler {
|
|||||||
if (item.getUserId() != this.client.getHabbo().getHabboInfo().getId()) return;
|
if (item.getUserId() != this.client.getHabbo().getHabboInfo().getId()) return;
|
||||||
|
|
||||||
if (item instanceof InteractionGift) {
|
if (item instanceof InteractionGift) {
|
||||||
|
// The actual unwrap (OpenGift) runs async/delayed and only then
|
||||||
|
// removes the wrapper, so a second packet would otherwise pass
|
||||||
|
// the room/owner checks and double-process the gift. Claim it once.
|
||||||
|
if (!((InteractionGift) item).tryStartOpening()) return;
|
||||||
|
|
||||||
if (item.getBaseItem().getName().contains("present_wrap")) {
|
if (item.getBaseItem().getName().contains("present_wrap")) {
|
||||||
((InteractionGift) item).explode = true;
|
((InteractionGift) item).explode = true;
|
||||||
room.updateItem(item);
|
room.updateItem(item);
|
||||||
|
|||||||
+10
-7
@@ -40,23 +40,26 @@ public class RecycleEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.size() == count) {
|
if (items.size() != count) {
|
||||||
for (HabboItem item : items) {
|
|
||||||
this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item);
|
|
||||||
this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId()));
|
|
||||||
Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute the reward BEFORE consuming the inputs. Previously the
|
||||||
|
// inputs were deleted first, so a null reward (misconfiguration)
|
||||||
|
// permanently destroyed the 8 furni with nothing in return.
|
||||||
HabboItem reward = Emulator.getGameEnvironment().getItemManager().handleRecycle(this.client.getHabbo(), Emulator.getGameEnvironment().getCatalogManager().getRandomRecyclerPrize().getId() + "");
|
HabboItem reward = Emulator.getGameEnvironment().getItemManager().handleRecycle(this.client.getHabbo(), Emulator.getGameEnvironment().getCatalogManager().getRandomRecyclerPrize().getId() + "");
|
||||||
if (reward == null) {
|
if (reward == null) {
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (HabboItem item : items) {
|
||||||
|
this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item);
|
||||||
|
this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId()));
|
||||||
|
Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
this.client.sendResponse(new AddHabboItemComposer(reward));
|
this.client.sendResponse(new AddHabboItemComposer(reward));
|
||||||
this.client.getHabbo().getInventory().getItemsComponent().addItem(reward);
|
this.client.getHabbo().getInventory().getItemsComponent().addItem(reward);
|
||||||
this.client.sendResponse(new RecyclerCompleteComposer(RecyclerCompleteComposer.RECYCLING_COMPLETE));
|
this.client.sendResponse(new RecyclerCompleteComposer(RecyclerCompleteComposer.RECYCLING_COMPLETE));
|
||||||
|
|||||||
+17
@@ -37,6 +37,8 @@ public class CraftingCraftItemEvent extends MessageHandler {
|
|||||||
HabboItem habboItem = this.client.getHabbo().getInventory().getItemsComponent().getAndRemoveHabboItem(set.getKey());
|
HabboItem habboItem = this.client.getHabbo().getInventory().getItemsComponent().getAndRemoveHabboItem(set.getKey());
|
||||||
|
|
||||||
if (habboItem == null) {
|
if (habboItem == null) {
|
||||||
|
// Not enough ingredients — give back whatever we already pulled.
|
||||||
|
this.restoreItems(toRemove);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +72,23 @@ public class CraftingCraftItemEvent extends MessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reward creation failed after we already pulled the ingredients —
|
||||||
|
// restore them so the craft isn't a silent item sink.
|
||||||
|
this.restoreItems(toRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.sendResponse(new CraftingResultComposer(null));
|
this.client.sendResponse(new CraftingResultComposer(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void restoreItems(TIntObjectHashMap<HabboItem> items) {
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.forEachValue(item -> {
|
||||||
|
this.client.getHabbo().getInventory().getItemsComponent().addItem(item);
|
||||||
|
this.client.sendResponse(new AddHabboItemComposer(item));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
this.client.sendResponse(new InventoryRefreshComposer());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -49,15 +49,17 @@ public class FurniEditorSearchEvent extends MessageHandler {
|
|||||||
try {
|
try {
|
||||||
int numericQuery = Integer.parseInt(query);
|
int numericQuery = Integer.parseInt(query);
|
||||||
isNumeric = true;
|
isNumeric = true;
|
||||||
|
String likeQuery = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%";
|
||||||
whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)");
|
whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)");
|
||||||
params.add(numericQuery);
|
params.add(numericQuery);
|
||||||
params.add(numericQuery);
|
params.add(numericQuery);
|
||||||
params.add("%" + query + "%");
|
params.add(likeQuery);
|
||||||
params.add("%" + query + "%");
|
params.add(likeQuery);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
|
String likeQuery = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%";
|
||||||
whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)");
|
whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)");
|
||||||
params.add("%" + query + "%");
|
params.add(likeQuery);
|
||||||
params.add("%" + query + "%");
|
params.add(likeQuery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ public class MachineIDEvent extends MessageHandler {
|
|||||||
if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) {
|
if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) {
|
||||||
this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId);
|
this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId);
|
||||||
Emulator.getThreading().run(this.client.getHabbo());
|
Emulator.getThreading().run(this.client.getHabbo());
|
||||||
|
|
||||||
|
// The fingerprint can arrive AFTER login (UniqueID is sent right after the
|
||||||
|
// SSO ticket), so Habbo.connect() may have skipped the MAC-ban check for
|
||||||
|
// lack of a machineId. Enforce it now that the fingerprint is known.
|
||||||
|
if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
|
||||||
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(this.client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.debug("Setting client MachineId to {}", storedMachineId);
|
LOGGER.debug("Setting client MachineId to {}", storedMachineId);
|
||||||
|
|||||||
+1
-1
@@ -306,7 +306,7 @@ public class SecureLoginEvent extends MessageHandler {
|
|||||||
Emulator.getPluginManager().fireEvent(userLoginEvent);
|
Emulator.getPluginManager().fireEvent(userLoginEvent);
|
||||||
|
|
||||||
if(userLoginEvent.isCancelled()) {
|
if(userLoginEvent.isCancelled()) {
|
||||||
Emulator.getGameServer().getGameClientManager().disposeClient(this.client);
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(this.client);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -12,6 +12,7 @@ import java.sql.SQLException;
|
|||||||
|
|
||||||
public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||||
private static final String ACTION_KEY = "user.give_credits";
|
private static final String ACTION_KEY = "user.give_credits";
|
||||||
|
private static final int MAX_GRANT = 1_000_000_000;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
@@ -27,7 +28,7 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
|||||||
int userId = this.packet.readInt();
|
int userId = this.packet.readInt();
|
||||||
int amount = this.packet.readInt();
|
int amount = this.packet.readInt();
|
||||||
|
|
||||||
if (userId <= 0 || amount == 0) {
|
if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) {
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -38,6 +39,7 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
|||||||
// giveCredits already pushes UserCreditsComposer and persists via the
|
// giveCredits already pushes UserCreditsComposer and persists via the
|
||||||
// standard HabboInfo write path; nothing extra needed for the online branch.
|
// standard HabboInfo write path; nothing extra needed for the online branch.
|
||||||
online.giveCredits(amount);
|
online.giveCredits(amount);
|
||||||
|
this.audit(userId, amount);
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,6 +59,15 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.audit(userId, amount);
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void audit(int userId, int amount) {
|
||||||
|
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||||
|
this.client.getHabbo().getHabboInfo().getId(),
|
||||||
|
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||||
|
ACTION_KEY, userId, "amount=" + amount,
|
||||||
|
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -18,6 +18,7 @@ import java.sql.SQLException;
|
|||||||
*/
|
*/
|
||||||
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||||
private static final int CURRENCY_DUCKETS = 0;
|
private static final int CURRENCY_DUCKETS = 0;
|
||||||
|
private static final int MAX_GRANT = 1_000_000_000;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
@@ -36,7 +37,7 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
|||||||
|
|
||||||
String actionKey = "user.give_currency_" + currencyType;
|
String actionKey = "user.give_currency_" + currencyType;
|
||||||
|
|
||||||
if (userId <= 0 || amount == 0) {
|
if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) {
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
|
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -52,6 +53,7 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
|||||||
online.givePoints(currencyType, amount);
|
online.givePoints(currencyType, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.audit(actionKey, userId, currencyType, amount);
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -69,6 +71,15 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.audit(actionKey, userId, currencyType, amount);
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void audit(String actionKey, int userId, int currencyType, int amount) {
|
||||||
|
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||||
|
this.client.getHabbo().getHabboInfo().getId(),
|
||||||
|
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||||
|
actionKey, userId, "type=" + currencyType + " amount=" + amount,
|
||||||
|
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ public class HousekeepingSearchRoomsEvent extends MessageHandler {
|
|||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
statement.setString(1, exactMatch ? query : query + "%");
|
statement.setString(1, exactMatch ? query : com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
|
||||||
statement.setInt(2, limit);
|
statement.setInt(2, limit);
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
|
|||||||
+44
-2
@@ -11,6 +11,7 @@ import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
|
|||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
|
||||||
public class HousekeepingSetUserRankEvent extends MessageHandler {
|
public class HousekeepingSetUserRankEvent extends MessageHandler {
|
||||||
@@ -44,6 +45,43 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
|
|||||||
|
|
||||||
Rank rank = permissions.getRank(rankId);
|
Rank rank = permissions.getRank(rankId);
|
||||||
|
|
||||||
|
// Rank-ceiling guard: an operator must never be able to grant a rank
|
||||||
|
// above their own, nor modify a user who already outranks them. This
|
||||||
|
// mirrors GiveRankCommand and prevents privilege escalation through
|
||||||
|
// the housekeeping path (including self-promotion).
|
||||||
|
int operatorRankId = this.client.getHabbo().getHabboInfo().getRank().getId();
|
||||||
|
|
||||||
|
if (rank.getId() > operatorRankId) {
|
||||||
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||||
|
|
||||||
|
int targetRankId;
|
||||||
|
if (online != null) {
|
||||||
|
targetRankId = online.getHabboInfo().getRank().getId();
|
||||||
|
} else {
|
||||||
|
targetRankId = 0;
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("SELECT rank FROM users WHERE id = ? LIMIT 1")) {
|
||||||
|
statement.setInt(1, userId);
|
||||||
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
|
if (set.next()) {
|
||||||
|
targetRankId = set.getInt("rank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRankId > operatorRankId) {
|
||||||
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Persist for the offline path. Online users get their in-memory
|
// Persist for the offline path. Online users get their in-memory
|
||||||
// HabboInfo.rank rebound below so server-side hasPermission()
|
// HabboInfo.rank rebound below so server-side hasPermission()
|
||||||
// checks land on the new permission set without a relogin.
|
// checks land on the new permission set without a relogin.
|
||||||
@@ -57,8 +95,6 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
|
||||||
|
|
||||||
if (online != null) {
|
if (online != null) {
|
||||||
online.getHabboInfo().setRank(rank);
|
online.getHabboInfo().setRank(rank);
|
||||||
// Ship the refreshed permissions snapshot — same payload the
|
// Ship the refreshed permissions snapshot — same payload the
|
||||||
@@ -66,6 +102,12 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
|
|||||||
online.getClient().sendResponse(new UserPermissionsComposer(online));
|
online.getClient().sendResponse(new UserPermissionsComposer(online));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||||
|
this.client.getHabbo().getHabboInfo().getId(),
|
||||||
|
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||||
|
ACTION_KEY, userId, "rankId=" + rankId,
|
||||||
|
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||||
|
|
||||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -3,6 +3,7 @@ package com.eu.habbo.messages.incoming.modtool;
|
|||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.habbohotel.modtool.ScripterManager;
|
import com.eu.habbo.habbohotel.modtool.ScripterManager;
|
||||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||||
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||||
import com.eu.habbo.messages.outgoing.modtool.ModToolUserChatlogComposer;
|
import com.eu.habbo.messages.outgoing.modtool.ModToolUserChatlogComposer;
|
||||||
@@ -12,7 +13,11 @@ public class ModToolRequestUserChatlogEvent extends MessageHandler {
|
|||||||
public void handle() throws Exception {
|
public void handle() throws Exception {
|
||||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||||
int userId = this.packet.readInt();
|
int userId = this.packet.readInt();
|
||||||
String username = HabboManager.getOfflineHabboInfo(userId).getUsername();
|
HabboInfo habboInfo = HabboManager.getOfflineHabboInfo(userId);
|
||||||
|
if (habboInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String username = habboInfo.getUsername();
|
||||||
|
|
||||||
this.client.sendResponse(new ModToolUserChatlogComposer(Emulator.getGameEnvironment().getModToolManager().getUserRoomVisitsAndChatlogs(userId), userId, username));
|
this.client.sendResponse(new ModToolUserChatlogComposer(Emulator.getGameEnvironment().getModToolManager().getUserRoomVisitsAndChatlogs(userId), userId, username));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ public class RoomRequestBannedUsersEvent extends MessageHandler {
|
|||||||
|
|
||||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||||
if (room == null) return;
|
if (room == null) return;
|
||||||
if (!room.hasRights(this.client.getHabbo()) || !this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) return;
|
if (!room.hasRights(this.client.getHabbo()) && !this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) return;
|
||||||
|
|
||||||
this.client.sendResponse(new RoomBannedUsersComposer(room));
|
this.client.sendResponse(new RoomBannedUsersComposer(room));
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,7 @@
|
|||||||
package com.eu.habbo.messages.incoming.rooms.pets;
|
package com.eu.habbo.messages.incoming.rooms.pets;
|
||||||
|
|
||||||
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetBreedingNest;
|
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetBreedingNest;
|
||||||
|
import com.eu.habbo.habbohotel.rooms.Room;
|
||||||
import com.eu.habbo.habbohotel.users.HabboItem;
|
import com.eu.habbo.habbohotel.users.HabboItem;
|
||||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||||
|
|
||||||
@@ -13,7 +14,10 @@ public class ConfirmPetBreedingEvent extends MessageHandler {
|
|||||||
int petOneId = this.packet.readInt();
|
int petOneId = this.packet.readInt();
|
||||||
int petTwoId = this.packet.readInt();
|
int petTwoId = this.packet.readInt();
|
||||||
|
|
||||||
HabboItem item = this.client.getHabbo().getHabboInfo().getCurrentRoom().getHabboItem(itemId);
|
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
|
||||||
|
if (room == null) return;
|
||||||
|
|
||||||
|
HabboItem item = room.getHabboItem(itemId);
|
||||||
|
|
||||||
if (item instanceof InteractionPetBreedingNest) {
|
if (item instanceof InteractionPetBreedingNest) {
|
||||||
((InteractionPetBreedingNest) item).breed(this.client.getHabbo(), name, petOneId, petTwoId);
|
((InteractionPetBreedingNest) item).breed(this.client.getHabbo(), name, petOneId, petTwoId);
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ public class PetPickupEvent extends MessageHandler {
|
|||||||
Pet pet = room.getPet(petId);
|
Pet pet = room.getPet(petId);
|
||||||
|
|
||||||
if (pet != null) {
|
if (pet != null) {
|
||||||
if (this.client.getHabbo().getHabboInfo().getId() == pet.getId() || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
|
if (this.client.getHabbo().getHabboInfo().getId() == pet.getUserId() || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
|
||||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS) && this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
|
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS) && this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
|
||||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.pets.max.inventory").replace("%amount%", PetManager.MAXIMUM_PET_INVENTORY_SIZE + ""));
|
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.pets.max.inventory").replace("%amount%", PetManager.MAXIMUM_PET_INVENTORY_SIZE + ""));
|
||||||
return;
|
return;
|
||||||
|
|||||||
+7
-1
@@ -163,7 +163,13 @@ public class RoomUserWalkEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (roomUnit.getMoveBlockingTask() != null) {
|
if (roomUnit.getMoveBlockingTask() != null) {
|
||||||
roomUnit.getMoveBlockingTask().get();
|
try {
|
||||||
|
// Bound the wait so a stuck/delayed move-blocking task can't park
|
||||||
|
// the Netty event loop (and thus every client on it) indefinitely.
|
||||||
|
roomUnit.getMoveBlockingTask().get(2, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
|
} catch (java.util.concurrent.TimeoutException | java.util.concurrent.ExecutionException | InterruptedException e) {
|
||||||
|
// proceed with the walk regardless
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean needsLocationResync =
|
boolean needsLocationResync =
|
||||||
|
|||||||
+5
-2
@@ -9,8 +9,8 @@ import com.eu.habbo.messages.ServerMessage;
|
|||||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||||
import gnu.trove.map.hash.THashMap;
|
import gnu.trove.map.hash.THashMap;
|
||||||
import org.joda.time.DateTime;
|
|
||||||
|
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@@ -47,7 +47,10 @@ public class ModToolSanctionInfoComposer extends MessageComposer {
|
|||||||
if (item.probationTimestamp > 0) {
|
if (item.probationTimestamp > 0) {
|
||||||
probationEndTime = new Date((long) item.probationTimestamp * 1000);
|
probationEndTime = new Date((long) item.probationTimestamp * 1000);
|
||||||
|
|
||||||
probationStartTime = new DateTime(probationEndTime).minusDays(modToolSanctions.getProbationDays(modToolSanctionLevelItem)).toDate();
|
probationStartTime = Date.from(probationEndTime.toInstant()
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.minusDays(modToolSanctions.getProbationDays(modToolSanctionLevelItem))
|
||||||
|
.toInstant());
|
||||||
|
|
||||||
Date tradeLockedUntil = null;
|
Date tradeLockedUntil = null;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -99,7 +99,7 @@ public class RoomUsersComposer extends MessageComposer {
|
|||||||
this.response.appendInt(1);
|
this.response.appendInt(1);
|
||||||
this.response.appendString(habbo.getHabboInfo().getGender().name().toUpperCase());
|
this.response.appendString(habbo.getHabboInfo().getGender().name().toUpperCase());
|
||||||
this.response.appendInt(habbo.getHabboStats().guild != 0 ? habbo.getHabboStats().guild : -1);
|
this.response.appendInt(habbo.getHabboStats().guild != 0 ? habbo.getHabboStats().guild : -1);
|
||||||
this.response.appendInt(habbo.getHabboStats().guild != 0 ? habbo.getHabboStats().guild : -1);
|
this.response.appendInt(habbo.getHabboStats().guild != 0 ? 1 : -1);
|
||||||
String name = "";
|
String name = "";
|
||||||
if (habbo.getHabboStats().guild != 0) {
|
if (habbo.getHabboStats().guild != 0) {
|
||||||
Guild g = Emulator.getGameEnvironment().getGuildManager().getGuild(habbo.getHabboStats().guild);
|
Guild g = Emulator.getGameEnvironment().getGuildManager().getGuild(habbo.getHabboStats().guild);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class DisconnectUser extends RCONMessage<DisconnectUser.DisconnectUserJSO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Emulator.getGameServer().getGameClientManager().disposeClient(target.getClient());
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(target.getClient());
|
||||||
this.message = Emulator.getTexts().getValue("commands.succes.cmd_disconnect.disconnected").replace("%user%", target.getHabboInfo().getUsername());
|
this.message = Emulator.getTexts().getValue("commands.succes.cmd_disconnect.disconnected").replace("%user%", target.getHabboInfo().getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.eu.habbo.networking;
|
package com.eu.habbo.networking;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
import io.netty.bootstrap.ServerBootstrap;
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
|
import io.netty.buffer.ByteBufAllocator;
|
||||||
|
import io.netty.buffer.PooledByteBufAllocator;
|
||||||
import io.netty.buffer.UnpooledByteBufAllocator;
|
import io.netty.buffer.UnpooledByteBufAllocator;
|
||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
import io.netty.channel.ChannelOption;
|
import io.netty.channel.ChannelOption;
|
||||||
import io.netty.channel.EventLoopGroup;
|
import io.netty.channel.EventLoopGroup;
|
||||||
import io.netty.channel.FixedRecvByteBufAllocator;
|
import io.netty.channel.FixedRecvByteBufAllocator;
|
||||||
import io.netty.channel.nio.NioEventLoopGroup;
|
import io.netty.channel.MultiThreadIoEventLoopGroup;
|
||||||
|
import io.netty.channel.nio.NioIoHandler;
|
||||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||||
import io.netty.util.concurrent.DefaultThreadFactory;
|
import io.netty.util.concurrent.DefaultThreadFactory;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -18,6 +22,30 @@ public abstract class Server {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
|
||||||
|
|
||||||
|
private static volatile ByteBufAllocator sharedAllocator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared channel allocator. Defaults to unpooled-heap (the long-standing
|
||||||
|
* behaviour); set {@code io.netty.allocator.pooled=true} to switch to a
|
||||||
|
* pooled HEAP allocator (preferDirect=false, so the array-backed crypto
|
||||||
|
* paths keep working) which removes the per-packet alloc/GC churn. Opt-in
|
||||||
|
* until validated under load with the Netty leak detector, since pooled
|
||||||
|
* buffers that aren't released accumulate instead of being GC-reclaimed.
|
||||||
|
*/
|
||||||
|
protected static ByteBufAllocator allocator() {
|
||||||
|
if (sharedAllocator == null) {
|
||||||
|
synchronized (Server.class) {
|
||||||
|
if (sharedAllocator == null) {
|
||||||
|
boolean pooled = Emulator.getConfig() != null
|
||||||
|
&& "true".equalsIgnoreCase(Emulator.getConfig().getValue("io.netty.allocator.pooled", "false"));
|
||||||
|
sharedAllocator = pooled ? new PooledByteBufAllocator(false) : new UnpooledByteBufAllocator(false);
|
||||||
|
LOGGER.info("Netty ByteBuf allocator: {}", pooled ? "pooled-heap" : "unpooled-heap");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sharedAllocator;
|
||||||
|
}
|
||||||
|
|
||||||
protected final ServerBootstrap serverBootstrap;
|
protected final ServerBootstrap serverBootstrap;
|
||||||
protected final EventLoopGroup bossGroup;
|
protected final EventLoopGroup bossGroup;
|
||||||
protected final EventLoopGroup workerGroup;
|
protected final EventLoopGroup workerGroup;
|
||||||
@@ -32,8 +60,10 @@ public abstract class Server {
|
|||||||
|
|
||||||
String threadName = name.replace("Server", "").replace(" ", "");
|
String threadName = name.replace("Server", "").replace(" ", "");
|
||||||
|
|
||||||
this.bossGroup = new NioEventLoopGroup(bossGroupThreads, new DefaultThreadFactory(threadName + "Boss"));
|
// Netty 4.2: NioEventLoopGroup is deprecated in favour of the generic
|
||||||
this.workerGroup = new NioEventLoopGroup(workerGroupThreads, new DefaultThreadFactory(threadName + "Worker"));
|
// MultiThreadIoEventLoopGroup driven by an IoHandlerFactory (NIO here).
|
||||||
|
this.bossGroup = new MultiThreadIoEventLoopGroup(bossGroupThreads, new DefaultThreadFactory(threadName + "Boss"), NioIoHandler.newFactory());
|
||||||
|
this.workerGroup = new MultiThreadIoEventLoopGroup(workerGroupThreads, new DefaultThreadFactory(threadName + "Worker"), NioIoHandler.newFactory());
|
||||||
this.serverBootstrap = new ServerBootstrap();
|
this.serverBootstrap = new ServerBootstrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +75,7 @@ public abstract class Server {
|
|||||||
this.serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
|
this.serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
|
||||||
this.serverBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096);
|
this.serverBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096);
|
||||||
this.serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096));
|
this.serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096));
|
||||||
this.serverBootstrap.childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false));
|
this.serverBootstrap.childOption(ChannelOption.ALLOCATOR, allocator());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void connect() {
|
public void connect() {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public class GameServer extends Server {
|
|||||||
this.webSocketBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
|
this.webSocketBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
|
||||||
this.webSocketBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096);
|
this.webSocketBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096);
|
||||||
this.webSocketBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096));
|
this.webSocketBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096));
|
||||||
this.webSocketBootstrap.childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false));
|
this.webSocketBootstrap.childOption(ChannelOption.ALLOCATOR, allocator());
|
||||||
this.webSocketBootstrap.childHandler(wsInitializer);
|
this.webSocketBootstrap.childHandler(wsInitializer);
|
||||||
|
|
||||||
ChannelFuture wsFuture = this.webSocketBootstrap.bind(wsHost, wsPort);
|
ChannelFuture wsFuture = this.webSocketBootstrap.bind(wsHost, wsPort);
|
||||||
|
|||||||
+26
-1
@@ -26,12 +26,37 @@ import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
|||||||
import io.netty.handler.logging.LoggingHandler;
|
import io.netty.handler.logging.LoggingHandler;
|
||||||
import io.netty.handler.ssl.SslContext;
|
import io.netty.handler.ssl.SslContext;
|
||||||
import io.netty.handler.ssl.SslHandler;
|
import io.netty.handler.ssl.SslHandler;
|
||||||
|
import io.netty.util.concurrent.DefaultEventExecutorGroup;
|
||||||
|
import io.netty.util.concurrent.DefaultThreadFactory;
|
||||||
|
import io.netty.util.concurrent.EventExecutorGroup;
|
||||||
|
|
||||||
import javax.net.ssl.SSLEngine;
|
import javax.net.ssl.SSLEngine;
|
||||||
|
|
||||||
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
|
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
private static final int MAX_FRAME_SIZE = 500000;
|
private static final int MAX_FRAME_SIZE = 500000;
|
||||||
|
|
||||||
|
// Runs the game packet handler OFF the Netty I/O event loop, so a blocking
|
||||||
|
// handler (login/friends/catalog/guild JDBC, A* pathfinding, etc.) can no
|
||||||
|
// longer stall socket I/O for every other client sharing that I/O thread.
|
||||||
|
// A DefaultEventExecutorGroup pins each channel to one executor, so a single
|
||||||
|
// client's packets stay strictly ordered (no new intra-client races); the
|
||||||
|
// cross-client concurrency degree is the same the multi-threaded I/O group
|
||||||
|
// already had. Daemon threads so they don't block JVM shutdown.
|
||||||
|
private static final EventExecutorGroup PACKET_HANDLER_GROUP = new DefaultEventExecutorGroup(
|
||||||
|
packetHandlerThreads(),
|
||||||
|
new DefaultThreadFactory("GamePacketHandler", true));
|
||||||
|
|
||||||
|
// Size of the packet-handler pool. Defaults to max(16, 2x CPU cores); set
|
||||||
|
// the optional `io.packet.handler.threads` config key to override.
|
||||||
|
private static int packetHandlerThreads() {
|
||||||
|
int fallback = Math.max(16, Runtime.getRuntime().availableProcessors() * 2);
|
||||||
|
if (Emulator.getConfig() == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
int configured = Emulator.getConfig().getInt("io.packet.handler.threads", fallback);
|
||||||
|
return configured > 0 ? configured : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
private final SslContext sslContext;
|
private final SslContext sslContext;
|
||||||
private final boolean sslEnabled;
|
private final boolean sslEnabled;
|
||||||
private final WebSocketServerProtocolConfig wsConfig;
|
private final WebSocketServerProtocolConfig wsConfig;
|
||||||
@@ -82,7 +107,7 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
|
|||||||
|
|
||||||
ch.pipeline().addLast("idleEventHandler", new IdleTimeoutHandler(30, 60));
|
ch.pipeline().addLast("idleEventHandler", new IdleTimeoutHandler(30, 60));
|
||||||
ch.pipeline().addLast(new GameMessageRateLimit());
|
ch.pipeline().addLast(new GameMessageRateLimit());
|
||||||
ch.pipeline().addLast(new GameMessageHandler());
|
ch.pipeline().addLast(PACKET_HANDLER_GROUP, "gameMessageHandler", new GameMessageHandler());
|
||||||
ch.pipeline().addLast("messageEncoder", new GameServerMessageEncoder());
|
ch.pipeline().addLast("messageEncoder", new GameServerMessageEncoder());
|
||||||
|
|
||||||
if (PacketManager.DEBUG_SHOW_PACKETS) {
|
if (PacketManager.DEBUG_SHOW_PACKETS) {
|
||||||
|
|||||||
+1
-1
@@ -467,7 +467,7 @@ final class AccountChangeEndpoints {
|
|||||||
com.eu.habbo.habbohotel.users.Habbo habbo =
|
com.eu.habbo.habbohotel.users.Habbo habbo =
|
||||||
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||||
if (habbo != null && habbo.getClient() != null) {
|
if (habbo != null && habbo.getClient() != null) {
|
||||||
Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(habbo.getClient());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ import io.netty.handler.codec.http.HttpMethod;
|
|||||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||||
import io.netty.util.ReferenceCountUtil;
|
import io.netty.util.ReferenceCountUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.MAX_BODY_BYTES;
|
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.MAX_BODY_BYTES;
|
||||||
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.errorPayload;
|
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.errorPayload;
|
||||||
@@ -21,6 +28,37 @@ import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.sendJson;
|
|||||||
|
|
||||||
public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpHandler.class);
|
||||||
|
|
||||||
|
// Dedicated, bounded pool for the auth endpoints. Their work blocks on
|
||||||
|
// BCrypt, JDBC, the Turnstile HTTPS round-trip and SMTP — running that on the
|
||||||
|
// Netty event loop stalls every client on the same worker. A SEPARATE pool
|
||||||
|
// (not the shared game ThreadPooling) also keeps it from starving room cycles.
|
||||||
|
private static final int AUTH_POOL_MAX = authPoolMax();
|
||||||
|
private static final ThreadPoolExecutor AUTH_EXECUTOR = new ThreadPoolExecutor(
|
||||||
|
Math.min(4, AUTH_POOL_MAX), AUTH_POOL_MAX, 60L, TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<>(512),
|
||||||
|
new java.util.concurrent.ThreadFactory() {
|
||||||
|
private final AtomicInteger counter = new AtomicInteger(1);
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable r) {
|
||||||
|
Thread t = new Thread(r, "auth-http-worker-" + counter.getAndIncrement());
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Max threads for the auth pool. Defaults to 16; set the optional
|
||||||
|
// `auth.http.pool.size` config key to override.
|
||||||
|
private static int authPoolMax() {
|
||||||
|
int fallback = 16;
|
||||||
|
if (com.eu.habbo.Emulator.getConfig() == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
int configured = com.eu.habbo.Emulator.getConfig().getInt("auth.http.pool.size", fallback);
|
||||||
|
return configured > 0 ? configured : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
static final String LOGIN_PATH = "/api/auth/login";
|
static final String LOGIN_PATH = "/api/auth/login";
|
||||||
static final String REGISTER_PATH = "/api/auth/register";
|
static final String REGISTER_PATH = "/api/auth/register";
|
||||||
static final String FORGOT_PATH = "/api/auth/forgot-password";
|
static final String FORGOT_PATH = "/api/auth/forgot-password";
|
||||||
@@ -52,11 +90,31 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Offload the (potentially blocking) auth work off the event loop. Netty
|
||||||
|
// writes are thread-safe, so the endpoints' sendJson/writeAndFlush calls
|
||||||
|
// are fine from the worker; the request is released once the work ends.
|
||||||
|
try {
|
||||||
|
AUTH_EXECUTOR.execute(() -> {
|
||||||
try {
|
try {
|
||||||
handle(ctx, req, path);
|
handle(ctx, req, path);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
LOGGER.error("Auth handler failed for {}", path, t);
|
||||||
|
try {
|
||||||
|
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Internal error."));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// response may already be partially written — nothing else to do
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
ReferenceCountUtil.release(req);
|
ReferenceCountUtil.release(req);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} catch (RejectedExecutionException rejected) {
|
||||||
|
try {
|
||||||
|
sendJson(ctx, req, HttpResponseStatus.SERVICE_UNAVAILABLE, errorPayload("Server busy, try again shortly."));
|
||||||
|
} finally {
|
||||||
|
ReferenceCountUtil.release(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isOurRoute(String path) {
|
private static boolean isOurRoute(String path) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.sql.SQLException;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
final class AuthHttpUtil {
|
public final class AuthHttpUtil {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpUtil.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpUtil.class);
|
||||||
|
|
||||||
@@ -132,7 +132,10 @@ final class AuthHttpUtil {
|
|||||||
String ipHeader = Emulator.getConfig() != null
|
String ipHeader = Emulator.getConfig() != null
|
||||||
? Emulator.getConfig().getValue("ws.ip.header", "")
|
? Emulator.getConfig().getValue("ws.ip.header", "")
|
||||||
: "";
|
: "";
|
||||||
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
|
// Only trust a client-supplied forwarded-IP header when the DIRECT peer
|
||||||
|
// is a trusted reverse proxy; otherwise an attacker hitting the port
|
||||||
|
// directly could spoof it to evade per-IP rate limiting and IP bans.
|
||||||
|
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader) && isTrustedProxy(ctx)) {
|
||||||
String hv = req.headers().get(ipHeader);
|
String hv = req.headers().get(ipHeader);
|
||||||
if (hv != null && !hv.isEmpty()) {
|
if (hv != null && !hv.isEmpty()) {
|
||||||
int comma = hv.indexOf(',');
|
int comma = hv.indexOf(',');
|
||||||
@@ -148,6 +151,37 @@ final class AuthHttpUtil {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the channel's direct peer may set a forwarded-IP header. Loopback
|
||||||
|
* is always trusted; additional proxies can be allow-listed (exact IP or
|
||||||
|
* string prefix, comma-separated) via the {@code ws.ip.header.trusted}
|
||||||
|
* config key. Default-deny so the header can't be spoofed from the open net.
|
||||||
|
*/
|
||||||
|
public static boolean isTrustedProxy(ChannelHandlerContext ctx) {
|
||||||
|
String peerIp = (ctx.channel().remoteAddress() instanceof InetSocketAddress a)
|
||||||
|
? a.getAddress().getHostAddress() : null;
|
||||||
|
if (peerIp == null || peerIp.isEmpty()) return false;
|
||||||
|
if (peerIp.equals("127.0.0.1") || peerIp.equals("::1") || peerIp.equals("0:0:0:0:0:0:0:1")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String trusted = Emulator.getConfig() != null
|
||||||
|
? Emulator.getConfig().getValue("ws.ip.header.trusted", "")
|
||||||
|
: "";
|
||||||
|
if (trusted.isEmpty()) return false;
|
||||||
|
for (String entry : trusted.split(",")) {
|
||||||
|
String t = entry.trim();
|
||||||
|
if (t.isEmpty()) continue;
|
||||||
|
// Exact IP match, or a dotted/colon prefix range (e.g. "10.0.0." or
|
||||||
|
// "2001:db8:") — never a bare-IP prefix, so "10.0.0.1" can't also
|
||||||
|
// trust "10.0.0.12".
|
||||||
|
boolean isRange = t.endsWith(".") || t.endsWith(":");
|
||||||
|
if (peerIp.equals(t) || (isRange && peerIp.startsWith(t))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static boolean checkPassword(String plain, String stored) {
|
static boolean checkPassword(String plain, String stored) {
|
||||||
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
|
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,8 +11,34 @@ public final class AuthRateLimiter {
|
|||||||
private static final Map<String, AtomicReference<State>> STATE = new ConcurrentHashMap<>();
|
private static final Map<String, AtomicReference<State>> STATE = new ConcurrentHashMap<>();
|
||||||
private static final Map<String, AtomicReference<ProbeState>> PROBE_STATE = new ConcurrentHashMap<>();
|
private static final Map<String, AtomicReference<ProbeState>> PROBE_STATE = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Both maps are keyed by client IP and reachable by unauthenticated traffic.
|
||||||
|
// recordSuccess removes STATE on login, but failed-only and probe-only IPs
|
||||||
|
// never get removed otherwise — unbounded growth over the JVM lifetime.
|
||||||
|
// Opportunistically evict window-expired entries once the maps get large.
|
||||||
|
private static final int SWEEP_THRESHOLD = 10_000;
|
||||||
|
private static final long SWEEP_MIN_INTERVAL_MS = 60_000L;
|
||||||
|
private static volatile long lastSweepMillis = 0L;
|
||||||
|
|
||||||
private AuthRateLimiter() {}
|
private AuthRateLimiter() {}
|
||||||
|
|
||||||
|
private static void maybeSweep(long now) {
|
||||||
|
if (STATE.size() < SWEEP_THRESHOLD && PROBE_STATE.size() < SWEEP_THRESHOLD) return;
|
||||||
|
if (now - lastSweepMillis < SWEEP_MIN_INTERVAL_MS) return;
|
||||||
|
lastSweepMillis = now;
|
||||||
|
|
||||||
|
long stateWindowMs = configInt("login.ratelimit.window_sec", 60) * 1000L;
|
||||||
|
STATE.entrySet().removeIf(e -> {
|
||||||
|
State s = e.getValue().get();
|
||||||
|
return s == null || (s.lockedUntilMillis <= now && (now - s.windowStartMillis) > stateWindowMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
long probeWindowMs = configInt("login.probe.window_sec", 60) * 1000L;
|
||||||
|
PROBE_STATE.entrySet().removeIf(e -> {
|
||||||
|
ProbeState p = e.getValue().get();
|
||||||
|
return p == null || (now - p.windowStartMillis) > probeWindowMs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isLocked(String ip) {
|
public static boolean isLocked(String ip) {
|
||||||
if (!isEnabled() || ip == null || ip.isEmpty()) return false;
|
if (!isEnabled() || ip == null || ip.isEmpty()) return false;
|
||||||
|
|
||||||
@@ -38,6 +64,7 @@ public final class AuthRateLimiter {
|
|||||||
if (!isEnabled() || ip == null || ip.isEmpty()) return;
|
if (!isEnabled() || ip == null || ip.isEmpty()) return;
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
maybeSweep(now);
|
||||||
long windowMs = configInt("login.ratelimit.window_sec", 60) * 1000L;
|
long windowMs = configInt("login.ratelimit.window_sec", 60) * 1000L;
|
||||||
int maxAttempts = configInt("login.ratelimit.max_attempts", 5);
|
int maxAttempts = configInt("login.ratelimit.max_attempts", 5);
|
||||||
long lockoutMs = configInt("login.ratelimit.lockout_sec", 120) * 1000L;
|
long lockoutMs = configInt("login.ratelimit.lockout_sec", 120) * 1000L;
|
||||||
@@ -64,6 +91,7 @@ public final class AuthRateLimiter {
|
|||||||
if (isLocked(ip)) return false;
|
if (isLocked(ip)) return false;
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
maybeSweep(now);
|
||||||
long windowMs = configInt("login.probe.window_sec", 60) * 1000L;
|
long windowMs = configInt("login.probe.window_sec", 60) * 1000L;
|
||||||
int maxAttempts = configInt("login.probe.max_attempts", 20);
|
int maxAttempts = configInt("login.probe.max_attempts", 20);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ final class SessionEndpoints {
|
|||||||
com.eu.habbo.habbohotel.users.Habbo habbo =
|
com.eu.habbo.habbohotel.users.Habbo habbo =
|
||||||
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||||
if (habbo != null && habbo.getClient() != null) {
|
if (habbo != null && habbo.getClient() != null) {
|
||||||
Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
|
Emulator.getGameServer().getGameClientManager().forceDisposeClient(habbo.getClient());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-5
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.decoders;
|
|||||||
|
|
||||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||||
|
|
||||||
@@ -15,14 +16,17 @@ public class GameByteDecryption extends ByteToMessageDecoder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||||
// Read all available bytes.
|
// Copy the readable region into a plain array (offset-safe, so this is
|
||||||
ByteBuf data = in.readBytes(in.readableBytes());
|
// correct for pooled buffers too — buf.array() would have read the wrong
|
||||||
|
// region for a pooled/sliced buffer).
|
||||||
|
byte[] bytes = new byte[in.readableBytes()];
|
||||||
|
in.readBytes(bytes);
|
||||||
|
|
||||||
// Decrypt.
|
// Decrypt in place.
|
||||||
ctx.channel().attr(GameServerAttributes.CRYPTO_CLIENT).get().parse(data.array());
|
ctx.channel().attr(GameServerAttributes.CRYPTO_CLIENT).get().parse(bytes);
|
||||||
|
|
||||||
// Continue in the pipeline.
|
// Continue in the pipeline.
|
||||||
out.add(data);
|
out.add(Unpooled.wrappedBuffer(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-8
@@ -56,14 +56,13 @@ public class GameMessageHandler extends ChannelInboundHandlerAdapter {
|
|||||||
ClientMessage message = (ClientMessage) msg;
|
ClientMessage message = (ClientMessage) msg;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ChannelReadHandler handler = new ChannelReadHandler(ctx, message);
|
// This handler is registered on a dedicated EventExecutorGroup
|
||||||
|
// (see WebSocketChannelInitializer), so channelRead already runs OFF
|
||||||
if (PacketManager.MULTI_THREADED_PACKET_HANDLING) {
|
// the Netty I/O event loop, serialized per channel. Running the
|
||||||
Emulator.getThreading().run(handler);
|
// handler inline here keeps that per-channel ordering — submitting to
|
||||||
return;
|
// the shared game pool instead would break ordering, so we no longer
|
||||||
}
|
// branch on MULTI_THREADED_PACKET_HANDLING.
|
||||||
|
new ChannelReadHandler(ctx, message).run();
|
||||||
handler.run();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Caught exception", e);
|
LOGGER.error("Caught exception", e);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -23,7 +23,12 @@ public class GameMessageRateLimit extends MessageToMessageDecoder<ClientMessage>
|
|||||||
protected void decode(ChannelHandlerContext ctx, ClientMessage message, List<Object> out) throws Exception {
|
protected void decode(ChannelHandlerContext ctx, ClientMessage message, List<Object> out) throws Exception {
|
||||||
GameClient client = ctx.channel().attr(GameServerAttributes.CLIENT).get();
|
GameClient client = ctx.channel().attr(GameServerAttributes.CLIENT).get();
|
||||||
|
|
||||||
|
// ClientMessage is not ReferenceCounted, so MessageToMessageDecoder's
|
||||||
|
// auto-release is a no-op for it; on every drop path we must release the
|
||||||
|
// wrapped ByteBuf ourselves or it leaks (it is only released downstream
|
||||||
|
// in ChannelReadHandler on the success path).
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
|
message.release();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +47,7 @@ public class GameMessageRateLimit extends MessageToMessageDecoder<ClientMessage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (count > MAX_COUNTER) {
|
if (count > MAX_COUNTER) {
|
||||||
|
message.release();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +59,7 @@ public class GameMessageRateLimit extends MessageToMessageDecoder<ClientMessage>
|
|||||||
LOGGER.warn("Global packet rate limit exceeded for {} ({} packets/sec) — dropping excess packets",
|
LOGGER.warn("Global packet rate limit exceeded for {} ({} packets/sec) — dropping excess packets",
|
||||||
username, globalCount);
|
username, globalCount);
|
||||||
}
|
}
|
||||||
|
message.release();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-5
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.encoders;
|
|||||||
|
|
||||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
@@ -14,16 +15,19 @@ public class GameByteEncryption extends ChannelOutboundHandlerAdapter {
|
|||||||
// convert to Bytebuf
|
// convert to Bytebuf
|
||||||
ByteBuf in = (ByteBuf) msg;
|
ByteBuf in = (ByteBuf) msg;
|
||||||
|
|
||||||
// read available bytes
|
// Copy the readable region into a plain array (respects readerIndex /
|
||||||
ByteBuf data = (in).readBytes(in.readableBytes());
|
// arrayOffset, so this is correct for pooled buffers too — buf.array()
|
||||||
|
// would have returned the wrong region for a pooled/sliced buffer).
|
||||||
|
byte[] bytes = new byte[in.readableBytes()];
|
||||||
|
in.readBytes(bytes);
|
||||||
|
|
||||||
//release old object
|
//release old object
|
||||||
ReferenceCountUtil.release(in);
|
ReferenceCountUtil.release(in);
|
||||||
|
|
||||||
// Encrypt.
|
// Encrypt in place.
|
||||||
ctx.channel().attr(GameServerAttributes.CRYPTO_SERVER).get().parse(data.array());
|
ctx.channel().attr(GameServerAttributes.CRYPTO_SERVER).get().parse(bytes);
|
||||||
|
|
||||||
// Continue in the pipeline.
|
// Continue in the pipeline.
|
||||||
ctx.write(data, promise);
|
ctx.write(Unpooled.wrappedBuffer(bytes), promise);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.handlers;
|
|||||||
|
|
||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||||
|
import com.eu.habbo.networking.gameserver.auth.AuthHttpUtil;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
@@ -53,7 +54,7 @@ public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
|
|||||||
FullHttpResponse response = new DefaultFullHttpResponse(
|
FullHttpResponse response = new DefaultFullHttpResponse(
|
||||||
HttpVersion.HTTP_1_1,
|
HttpVersion.HTTP_1_1,
|
||||||
HttpResponseStatus.FORBIDDEN,
|
HttpResponseStatus.FORBIDDEN,
|
||||||
Unpooled.wrappedBuffer("Origin forbidden".getBytes())
|
Unpooled.wrappedBuffer("Origin forbidden".getBytes(java.nio.charset.StandardCharsets.UTF_8))
|
||||||
);
|
);
|
||||||
response.headers().set("Vary", "Origin");
|
response.headers().set("Vary", "Origin");
|
||||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||||
@@ -65,9 +66,14 @@ public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
|
|||||||
|
|
||||||
private static void captureForwardedIp(ChannelHandlerContext ctx, HttpMessage req) {
|
private static void captureForwardedIp(ChannelHandlerContext ctx, HttpMessage req) {
|
||||||
String ipHeader = Emulator.getConfig().getValue("ws.ip.header", "");
|
String ipHeader = Emulator.getConfig().getValue("ws.ip.header", "");
|
||||||
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
|
// Only honour the forwarded-IP header from a trusted reverse proxy,
|
||||||
|
// otherwise the game-session IP (used for bans/rate-limits) is spoofable.
|
||||||
|
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader) && AuthHttpUtil.isTrustedProxy(ctx)) {
|
||||||
String ip = req.headers().get(ipHeader);
|
String ip = req.headers().get(ipHeader);
|
||||||
ctx.channel().attr(GameServerAttributes.WS_IP).set(ip);
|
if (ip != null && !ip.isEmpty()) {
|
||||||
|
int comma = ip.indexOf(',');
|
||||||
|
ctx.channel().attr(GameServerAttributes.WS_IP).set((comma > 0 ? ip.substring(0, comma) : ip).trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(RCONServerHandler.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(RCONServerHandler.class);
|
||||||
|
|
||||||
|
// Gson is thread-safe and immutable once built — share one instance instead
|
||||||
|
// of allocating a parser per RCON request.
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
||||||
String adress = ctx.channel().remoteAddress().toString().split(":")[0].replace("/", "");
|
String adress = ctx.channel().remoteAddress().toString().split(":")[0].replace("/", "");
|
||||||
@@ -37,8 +41,8 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
|||||||
|
|
||||||
byte[] d = new byte[data.readableBytes()];
|
byte[] d = new byte[data.readableBytes()];
|
||||||
data.getBytes(0, d);
|
data.getBytes(0, d);
|
||||||
String message = new String(d);
|
String message = new String(d, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
Gson gson = new Gson();
|
Gson gson = GSON;
|
||||||
String response = "ERROR";
|
String response = "ERROR";
|
||||||
String key = "";
|
String key = "";
|
||||||
try {
|
try {
|
||||||
@@ -52,7 +56,7 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes()), ctx.channel().voidPromise());
|
ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise());
|
||||||
ctx.channel().flush();
|
ctx.channel().flush();
|
||||||
ctx.flush();
|
ctx.flush();
|
||||||
f.channel().close();
|
f.channel().close();
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ public class PluginManager {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(PluginManager.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(PluginManager.class);
|
||||||
|
|
||||||
|
// Gson is thread-safe and immutable once built — reuse one instance instead
|
||||||
|
// of building a parser per plugin-config load.
|
||||||
|
private static final Gson PLUGIN_GSON = new GsonBuilder().create();
|
||||||
|
|
||||||
private final THashSet<HabboPlugin> plugins = new THashSet<>();
|
private final THashSet<HabboPlugin> plugins = new THashSet<>();
|
||||||
private final THashSet<Method> methods = new THashSet<>();
|
private final THashSet<Method> methods = new THashSet<>();
|
||||||
|
|
||||||
@@ -273,10 +277,9 @@ public class PluginManager {
|
|||||||
byte[] content = new byte[stream.available()];
|
byte[] content = new byte[stream.available()];
|
||||||
|
|
||||||
if (stream.read(content) > 0) {
|
if (stream.read(content) > 0) {
|
||||||
String body = new String(content);
|
String body = new String(content, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
|
||||||
Gson gson = new GsonBuilder().create();
|
HabboPluginConfiguration pluginConfigurtion = PLUGIN_GSON.fromJson(body, HabboPluginConfiguration.class);
|
||||||
HabboPluginConfiguration pluginConfigurtion = gson.fromJson(body, HabboPluginConfiguration.class);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Class<?> clazz = urlClassLoader.loadClass(pluginConfigurtion.main);
|
Class<?> clazz = urlClassLoader.loadClass(pluginConfigurtion.main);
|
||||||
|
|||||||
@@ -165,12 +165,10 @@ public class RebugKickBallAction implements Runnable {
|
|||||||
this.dead = true;
|
this.dead = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
THashSet<HabboItem> oldItems = this.room.getItemsAt(oldTile);
|
// updateTile() below removes both tiles from the item cache (rebuilt
|
||||||
if (oldItems != null && !oldItems.isEmpty()) {
|
// lazily from the ball's already-updated position), so mutating the
|
||||||
oldItems.remove(this.ball);
|
// shared cached THashSets here is both redundant and a data race
|
||||||
}
|
// against the room-cycle/IO threads iterating those same sets.
|
||||||
this.room.getItemsAt(nextTile).add(this.ball);
|
|
||||||
|
|
||||||
this.room.updateTile(oldTile);
|
this.room.updateTile(oldTile);
|
||||||
this.room.updateTile(nextTile);
|
this.room.updateTile(nextTile);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.eu.habbo.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes the LIKE wildcards {@code %} and {@code _} (and the escape char itself)
|
||||||
|
* in user-supplied search input, so they are matched literally instead of acting
|
||||||
|
* as wildcards. Prevents wildcard-driven over-broad matches and the expensive
|
||||||
|
* full-scans an attacker could trigger with a query like {@code "%"}. Uses
|
||||||
|
* MariaDB's default escape character {@code \}.
|
||||||
|
*/
|
||||||
|
public final class SqlLikeEscaper {
|
||||||
|
|
||||||
|
private SqlLikeEscaper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String escape(String input) {
|
||||||
|
if (input == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("%", "\\%")
|
||||||
|
.replace("_", "\\_");
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.eu.habbo.habbohotel.catalog.marketplace;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
|
||||||
|
class MarketPlaceOfferContractTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesPersistenceState() {
|
||||||
|
assertDoesNotThrow(() -> MarketPlaceOffer.class.getDeclaredMethod("isPersisted"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.eu.habbo.habbohotel.gameclients;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
|
||||||
|
class GameClientManagerContractTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesExplicitForcedDisposePath() {
|
||||||
|
assertDoesNotThrow(() -> GameClient.class.getDeclaredMethod("dispose", boolean.class));
|
||||||
|
assertDoesNotThrow(() -> GameClientManager.class.getDeclaredMethod("forceDisposeClient", GameClient.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void disposeMethodsIgnoreNullClient() {
|
||||||
|
GameClientManager manager = new GameClientManager();
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> manager.disposeClient(null));
|
||||||
|
assertDoesNotThrow(() -> manager.forceDisposeClient(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,3 +68,9 @@ login.news.limit=5
|
|||||||
### ws.port=2096
|
### ws.port=2096
|
||||||
### ws.whitelist=localhost #Comma-separated whitelist of allowed origins. Supports wildcards: *.example.com, * (allow all)
|
### ws.whitelist=localhost #Comma-separated whitelist of allowed origins. Supports wildcards: *.example.com, * (allow all)
|
||||||
### ws.ip.header=X-Forwarded-For #Header name for real client IP when behind a proxy (e.g., X-Forwarded-For, CF-Connecting-IP). Leave empty if not using a proxy.
|
### ws.ip.header=X-Forwarded-For #Header name for real client IP when behind a proxy (e.g., X-Forwarded-For, CF-Connecting-IP). Leave empty if not using a proxy.
|
||||||
|
### ws.ip.header.trusted= #Comma-separated trusted reverse-proxy IPs/prefixes (entries ending in '.' or ':' are prefix ranges, e.g. 10.0.0.) allowed to set ws.ip.header. Loopback (127.0.0.1/::1) is ALWAYS trusted; default-deny otherwise so the forwarded header can't be spoofed from the open net.
|
||||||
|
|
||||||
|
#Performance / concurrency (optional — sensible defaults apply if unset; adjust in the Database).
|
||||||
|
### io.packet.handler.threads=24 #Game packet-handler pool size; runs game handlers OFF the Netty I/O loop. Default max(16, 2 x CPU cores).
|
||||||
|
### auth.http.pool.size=16 #Dedicated worker pool for the /api/auth/* HTTP endpoints (BCrypt/JDBC/Turnstile/SMTP run off the event loop). Default 16.
|
||||||
|
### io.netty.allocator.pooled=false #Set true to opt into Netty's pooled ByteBuf allocator. Default false (unpooled-heap).
|
||||||
|
|||||||
Reference in New Issue
Block a user