diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildBadgeBuilder.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildBadgeBuilder.java new file mode 100644 index 00000000..0e317809 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildBadgeBuilder.java @@ -0,0 +1,52 @@ +package com.eu.habbo.habbohotel.guilds; + +import com.eu.habbo.messages.ClientMessage; +import com.eu.habbo.util.PacketGuard; + +public final class GuildBadgeBuilder { + public static final int MAX_BADGE_PARTS = 5; + private static final int INTS_PER_PART = 3; + private static final int BYTES_PER_INT = 4; + private static final int MAX_PART_ID = 999; + private static final int MAX_COLOR_ID = 99; + private static final int MAX_POSITION = 8; + + private GuildBadgeBuilder() { + } + + public static String readBadge(ClientMessage packet, int flatPartValueCount) { + if (flatPartValueCount % INTS_PER_PART != 0) { + return null; + } + + int partCount = flatPartValueCount / INTS_PER_PART; + if (!PacketGuard.isCountInRange(partCount, 1, MAX_BADGE_PARTS) + || !PacketGuard.hasFixedWidthEntries(packet, flatPartValueCount, BYTES_PER_INT)) { + return null; + } + + StringBuilder badge = new StringBuilder(partCount * 6); + for (int partIndex = 0; partIndex < partCount; partIndex++) { + int id = packet.readInt(); + int color = packet.readInt(); + int position = packet.readInt(); + + if (!isValidPart(id, color, position)) { + return null; + } + + badge.append(partIndex == 0 ? "b" : "s"); + badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id); + badge.append(color < 10 ? "0" : "").append(color); + badge.append(position); + } + + return badge.toString(); + } + + private static boolean isValidPart(int id, int color, int position) { + return id >= 0 && id <= MAX_PART_ID + && color >= 0 && color <= MAX_COLOR_ID + && position >= 0 && position <= MAX_POSITION; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java index b835a5f5..9441fc11 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java @@ -291,11 +291,12 @@ public class GuildManager { } } } else if (!error) { - try (PreparedStatement statement = connection.prepareStatement("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ?")) { + try (PreparedStatement statement = connection.prepareStatement("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ? AND level_id = ?")) { statement.setInt(1, GuildRank.MEMBER.type); statement.setInt(2, Emulator.getIntUnixTimestamp()); statement.setInt(3, userId); statement.setInt(4, guild.getId()); + statement.setInt(5, GuildRank.REQUESTED.type); statement.execute(); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeBadgeEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeBadgeEvent.java index 9aa68c8e..5dde4604 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeBadgeEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeBadgeEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.guilds; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.guilds.Guild; +import com.eu.habbo.habbohotel.guilds.GuildBadgeBuilder; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.messages.incoming.MessageHandler; @@ -27,25 +28,8 @@ public class GuildChangeBadgeEvent extends MessageHandler { int count = this.packet.readInt(); - String badge = ""; - - byte base = 1; - - while (base < count) { - int id = this.packet.readInt(); - int color = this.packet.readInt(); - int pos = this.packet.readInt(); - - if (base == 1) { - badge += "b"; - } else { - badge += "s"; - } - - badge += (id < 100 ? "0" : "") + (id < 10 ? "0" : "") + id + (color < 10 ? "0" : "") + color + "" + pos; - - base += 3; - } + String badge = GuildBadgeBuilder.readBadge(this.packet, count); + if (badge == null) return; if (guild.getBadge().equalsIgnoreCase(badge)) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java index 976170c1..1b7a8c0b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.guilds; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.guilds.Guild; +import com.eu.habbo.habbohotel.guilds.GuildBadgeBuilder; import com.eu.habbo.habbohotel.modtool.ScripterManager; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; @@ -69,24 +70,10 @@ public class RequestGuildBuyEvent extends MessageHandler { int count = this.packet.readInt(); - StringBuilder badge = new StringBuilder(); - - byte base = 1; - - while (base < count) { - int id = this.packet.readInt(); - int color = this.packet.readInt(); - int pos = this.packet.readInt(); - - if (base == 1) { - badge.append("b"); - } else { - badge.append("s"); - } - - badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos); - - base += 3; + String badge = GuildBadgeBuilder.readBadge(this.packet, count); + if (badge == null) { + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; } // Only charge the player once every step has been validated. Previously the @@ -103,7 +90,7 @@ public class RequestGuildBuyEvent extends MessageHandler { } } - Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo); + Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge, colorOne, colorTwo); r.setGuild(guild.getId()); r.removeAllRights(); diff --git a/Emulator/src/main/java/com/eu/habbo/util/PacketGuard.java b/Emulator/src/main/java/com/eu/habbo/util/PacketGuard.java new file mode 100644 index 00000000..e35d5bae --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/util/PacketGuard.java @@ -0,0 +1,25 @@ +package com.eu.habbo.util; + +import com.eu.habbo.messages.ClientMessage; + +public final class PacketGuard { + private PacketGuard() { + } + + public static boolean isCountInRange(int count, int min, int max) { + return count >= min && count <= max; + } + + public static boolean hasReadableBytes(ClientMessage packet, int requiredBytes) { + return packet != null && requiredBytes >= 0 && packet.bytesAvailable() >= requiredBytes; + } + + public static boolean hasFixedWidthEntries(ClientMessage packet, int entryCount, int bytesPerEntry) { + if (packet == null || entryCount < 0 || bytesPerEntry < 0) { + return false; + } + + long requiredBytes = (long) entryCount * bytesPerEntry; + return requiredBytes <= Integer.MAX_VALUE && packet.bytesAvailable() >= requiredBytes; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/guilds/GuildBadgeBuilderTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/guilds/GuildBadgeBuilderTest.java new file mode 100644 index 00000000..642a6a4c --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/guilds/GuildBadgeBuilderTest.java @@ -0,0 +1,63 @@ +package com.eu.habbo.habbohotel.guilds; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.eu.habbo.messages.ClientMessage; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; + +class GuildBadgeBuilderTest { + @Test + void buildsBadgeFromFlatPartTriplets() { + ClientMessage packet = messageWithInts( + 1, 2, 4, + 35, 8, 0 + ); + + assertEquals("b001024s035080", GuildBadgeBuilder.readBadge(packet, 6)); + } + + @Test + void rejectsCountThatDoesNotRepresentCompleteTriplets() { + ClientMessage packet = messageWithInts(1, 2, 4); + + assertNull(GuildBadgeBuilder.readBadge(packet, 4)); + } + + @Test + void rejectsPayloadShorterThanDeclaredCount() { + ClientMessage packet = messageWithInts(1, 2); + + assertNull(GuildBadgeBuilder.readBadge(packet, 3)); + } + + @Test + void rejectsTooManyBadgeParts() { + ClientMessage packet = messageWithInts( + 1, 1, 4, + 2, 1, 4, + 3, 1, 4, + 4, 1, 4, + 5, 1, 4, + 6, 1, 4 + ); + + assertNull(GuildBadgeBuilder.readBadge(packet, 18)); + } + + @Test + void rejectsPartValuesOutsideBadgeCodeRanges() { + assertNull(GuildBadgeBuilder.readBadge(messageWithInts(1000, 1, 4), 3)); + assertNull(GuildBadgeBuilder.readBadge(messageWithInts(1, 100, 4), 3)); + assertNull(GuildBadgeBuilder.readBadge(messageWithInts(1, 1, 9), 3)); + } + + private static ClientMessage messageWithInts(int... values) { + var buffer = Unpooled.buffer(values.length * Integer.BYTES); + for (int value : values) { + buffer.writeInt(value); + } + return new ClientMessage(0, buffer); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/guilds/GuildManagerMembershipContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/guilds/GuildManagerMembershipContractTest.java new file mode 100644 index 00000000..20a599c1 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/guilds/GuildManagerMembershipContractTest.java @@ -0,0 +1,24 @@ +package com.eu.habbo.habbohotel.guilds; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class GuildManagerMembershipContractTest { + private static String guildManagerSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java")); + } + + @Test + void acceptRequestOnlyPromotesPendingMembershipRows() throws Exception { + String source = guildManagerSource(); + + assertTrue(source.contains("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ? AND level_id = ?"), + "accepting a guild request must only promote rows still in REQUESTED state"); + assertTrue(source.contains("statement.setInt(5, GuildRank.REQUESTED.type);"), + "the accept-request update must bind the expected REQUESTED rank guard"); + } +}