You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 07:26:18 +00:00
Merge branch 'dev' into fix/gameclients-inputs
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CommandTargetGuardContractTest {
|
||||
@Test
|
||||
void highRiskUserCommandsUseCentralTargetGuard() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/habbohotel/commands");
|
||||
|
||||
for (String command : List.of(
|
||||
"AlertCommand.java",
|
||||
"BanCommand.java",
|
||||
"DisconnectCommand.java",
|
||||
"GivePrefixCommand.java",
|
||||
"GiveRankCommand.java",
|
||||
"IPBanCommand.java",
|
||||
"MachineBanCommand.java",
|
||||
"MuteCommand.java",
|
||||
"RemovePrefixCommand.java",
|
||||
"SuperbanCommand.java",
|
||||
"UnmuteCommand.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(command));
|
||||
|
||||
assertTrue(source.contains("CommandTargetGuard.canTarget"),
|
||||
command + " must use the central command target guard for staff/core rank handling");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rankGrantingUsesCentralAssignmentGuard() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java"));
|
||||
|
||||
assertTrue(source.contains("CommandTargetGuard.canAssignRank"),
|
||||
"GiveRankCommand must guard the assigned rank with the same core-rank semantics");
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetGuardKeepsCorePeerOverrideCentralized() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java"));
|
||||
String rule = "targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId";
|
||||
|
||||
assertTrue(countOccurrences(source, rule) >= 2,
|
||||
"non-core command users must only target lower ranks while the highest/core rank may target peer ranks");
|
||||
}
|
||||
|
||||
private static int countOccurrences(String source, String needle) {
|
||||
int count = 0;
|
||||
int index = 0;
|
||||
|
||||
while ((index = source.indexOf(needle, index)) >= 0) {
|
||||
count++;
|
||||
index += needle.length();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.eu.habbo.messages.incoming.catalog;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CatalogSearchOfferIdContractTest {
|
||||
private static String source(String path) throws Exception {
|
||||
return Files.readString(Path.of(path));
|
||||
}
|
||||
|
||||
@Test
|
||||
void catalogItemsExposeStableSearchOfferIdWhenDatabaseOfferIdIsMissing() throws Exception {
|
||||
String source = source("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java");
|
||||
|
||||
int method = source.indexOf("public int getSearchOfferId()");
|
||||
int rawGuard = source.indexOf("this.offerId > 0", method);
|
||||
int fallback = source.indexOf("return haveOffer(this) ? this.id : -1", rawGuard);
|
||||
|
||||
assertTrue(method > -1, "CatalogItem should expose a search-safe offer id");
|
||||
assertTrue(rawGuard > method, "CatalogItem should preserve valid positive database offer ids");
|
||||
assertTrue(fallback > rawGuard,
|
||||
"CatalogItem should fall back to catalog item id when offer_id is missing but the item can be offered");
|
||||
}
|
||||
|
||||
@Test
|
||||
void catalogManagerIndexesSearchOfferIdsInsteadOfRawOfferIds() throws Exception {
|
||||
String source = source("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java");
|
||||
|
||||
int searchOffer = source.indexOf("int searchOfferId = item.getSearchOfferId()");
|
||||
int addOffer = source.indexOf("page.addOfferId(searchOfferId)", searchOffer);
|
||||
int offerDefs = source.indexOf("this.offerDefs.put(searchOfferId, item.getId())", addOffer);
|
||||
|
||||
assertTrue(searchOffer > -1, "CatalogManager should calculate the runtime search offer id");
|
||||
assertTrue(addOffer > searchOffer, "CatalogManager should expose runtime search offer ids in catalog pages");
|
||||
assertTrue(offerDefs > addOffer, "CatalogManager should map runtime search offer ids back to catalog items");
|
||||
assertTrue(!source.contains("this.offerDefs.put(item.getOfferId(), item.getId())"),
|
||||
"CatalogManager must not index raw -1 offer ids for catalog search");
|
||||
}
|
||||
|
||||
@Test
|
||||
void catalogSearchLookupResolvesCatalogItemIdsAndComparesSearchOfferIds() throws Exception {
|
||||
String source = source("src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchedItemEvent.java");
|
||||
|
||||
assertTrue(source.contains("int catalogItemId ="),
|
||||
"Catalog search lookup should name offerDefs values as catalog item ids");
|
||||
assertTrue(source.contains("getCatalogItem(catalogItemId)"),
|
||||
"Catalog search should resolve the mapped catalog item directly");
|
||||
assertTrue(source.contains("item.getSearchOfferId() == offerId"),
|
||||
"Catalog search should compare runtime search offer ids, not raw database offer ids");
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.incoming.catalog.marketplace;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MarketplaceInputContractTest {
|
||||
@Test
|
||||
void marketplaceIdHandlersRejectNonPositiveIds() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace");
|
||||
|
||||
for (String handler : List.of(
|
||||
"BuyItemEvent.java",
|
||||
"RequestItemInfoEvent.java",
|
||||
"SellItemEvent.java",
|
||||
"TakeBackItemEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("MarketplaceInputGuard.isPositiveId"),
|
||||
handler + " must reject zero or negative ids before marketplace or inventory lookup");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void offerSearchNormalizesCacheKeyInputs() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestOffersEvent.java"));
|
||||
|
||||
assertTrue(source.contains("MarketplaceInputGuard.normalizeMinPrice"),
|
||||
"marketplace offer search must normalize minimum price");
|
||||
assertTrue(source.contains("MarketplaceInputGuard.normalizeMaxPrice"),
|
||||
"marketplace offer search must normalize maximum price");
|
||||
assertTrue(source.contains("MarketplaceInputGuard.normalizeSearch"),
|
||||
"marketplace offer search must trim and bound search text");
|
||||
assertTrue(source.contains("MarketplaceInputGuard.normalizeSort"),
|
||||
"marketplace offer search must normalize sort before using it as a cache key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void takeBackDoesNotFirePluginEventForMissingOffer() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java"));
|
||||
|
||||
assertTrue(source.contains("if (offer == null)"),
|
||||
"takeBackItem must ignore missing offers before constructing plugin events");
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.eu.habbo.messages.incoming.catalog.marketplace;
|
||||
|
||||
import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MarketplaceInputGuardTest {
|
||||
@Test
|
||||
void idsMustBePositive() {
|
||||
assertFalse(MarketplaceInputGuard.isPositiveId(0));
|
||||
assertFalse(MarketplaceInputGuard.isPositiveId(-1));
|
||||
assertTrue(MarketplaceInputGuard.isPositiveId(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchIsTrimmedAndBounded() {
|
||||
assertEquals("", MarketplaceInputGuard.normalizeSearch(null));
|
||||
assertEquals("rare", MarketplaceInputGuard.normalizeSearch(" rare "));
|
||||
assertEquals(MarketplaceInputGuard.MAX_SEARCH_LENGTH, MarketplaceInputGuard.normalizeSearch("a".repeat(80)).length());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortFallsBackToDefaultOutsideKnownRange() {
|
||||
assertEquals(MarketplaceInputGuard.DEFAULT_SORT, MarketplaceInputGuard.normalizeSort(0));
|
||||
assertEquals(3, MarketplaceInputGuard.normalizeSort(3));
|
||||
assertEquals(MarketplaceInputGuard.DEFAULT_SORT, MarketplaceInputGuard.normalizeSort(7));
|
||||
}
|
||||
|
||||
@Test
|
||||
void priceRangesPreserveCacheSentinelAndStayBounded() {
|
||||
assertEquals(-1, MarketplaceInputGuard.normalizeMinPrice(-1));
|
||||
assertEquals(0, MarketplaceInputGuard.normalizeMinPrice(-100));
|
||||
assertEquals(MarketPlace.MAXIMUM_LISTING_PRICE, MarketplaceInputGuard.normalizeMinPrice(Integer.MAX_VALUE));
|
||||
|
||||
assertEquals(-1, MarketplaceInputGuard.normalizeMaxPrice(-1, -1));
|
||||
assertEquals(500, MarketplaceInputGuard.normalizeMaxPrice(100, 500));
|
||||
assertEquals(MarketPlace.MAXIMUM_LISTING_PRICE, MarketplaceInputGuard.normalizeMaxPrice(Integer.MAX_VALUE, 0));
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package com.eu.habbo.messages.incoming.friends;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FriendBatchGuardContractTest {
|
||||
private static String source(String name) throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/friends/" + name + ".java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void declineFriendRequestsBoundsClientSuppliedBatchCount() throws Exception {
|
||||
String source = source("DeclineFriendRequestEvent");
|
||||
|
||||
int count = source.indexOf("int count = this.packet.readInt()");
|
||||
int guard = source.indexOf("count <= 0 || count > MAX_BATCH_SIZE", count);
|
||||
int loop = source.indexOf("for (int i = 0; i < count; i++)", count);
|
||||
int delete = source.indexOf("deleteFriendRequests", loop);
|
||||
|
||||
assertTrue(source.contains("MAX_BATCH_SIZE = 100"),
|
||||
"Friend request decline batches should have a conservative cap");
|
||||
assertTrue(count > -1, "DeclineFriendRequestEvent must read the client supplied count");
|
||||
assertTrue(guard > count, "DeclineFriendRequestEvent must validate the count after reading it");
|
||||
assertTrue(guard < loop, "DeclineFriendRequestEvent must validate the count before looping");
|
||||
assertTrue(loop < delete, "DeclineFriendRequestEvent should only mutate after the bounded loop starts");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeFriendsBoundsClientSuppliedBatchCountBeforeMutations() throws Exception {
|
||||
String source = source("RemoveFriendEvent");
|
||||
|
||||
int count = source.indexOf("int count = this.packet.readInt()");
|
||||
int guard = source.indexOf("count <= 0 || count > MAX_BATCH_SIZE", count);
|
||||
int loop = source.indexOf("for (int i = 0; i < count; i++)", count);
|
||||
int idGuard = source.indexOf("habboId <= 0", loop);
|
||||
int unfriend = source.indexOf("Messenger.unfriend", loop);
|
||||
|
||||
assertTrue(source.contains("MAX_BATCH_SIZE = 100"),
|
||||
"Friend removal batches should have a conservative cap");
|
||||
assertTrue(count > -1, "RemoveFriendEvent must read the client supplied count");
|
||||
assertTrue(guard > count, "RemoveFriendEvent must validate the count after reading it");
|
||||
assertTrue(guard < loop, "RemoveFriendEvent must validate the count before looping");
|
||||
assertTrue(idGuard > loop && idGuard < unfriend,
|
||||
"RemoveFriendEvent must skip invalid ids before mutating friendships");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptFriendRequestsBoundsClientSuppliedBatchCountBeforeLoadingTargets() throws Exception {
|
||||
String source = source("AcceptFriendRequestEvent");
|
||||
|
||||
int count = source.indexOf("int count = this.packet.readInt()");
|
||||
int guard = source.indexOf("count <= 0 || count > 100", count);
|
||||
int loop = source.indexOf("for (int i = 0; i < count; i++)", count);
|
||||
int idGuard = source.indexOf("userId <= 0", loop);
|
||||
int loadTarget = source.indexOf("getHabbo(userId)", loop);
|
||||
|
||||
assertTrue(count > -1, "AcceptFriendRequestEvent must read the client supplied count");
|
||||
assertTrue(guard > count && guard < loop,
|
||||
"AcceptFriendRequestEvent must validate the count before looping");
|
||||
assertTrue(idGuard > loop && idGuard < loadTarget,
|
||||
"AcceptFriendRequestEvent must skip invalid ids before loading targets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void friendRequestAndMessagesUseSharedInputGuards() throws Exception {
|
||||
String guard = source("FriendInputGuard");
|
||||
String request = source("FriendRequestEvent");
|
||||
String privateMessage = source("FriendPrivateMessageEvent");
|
||||
String invite = source("InviteFriendsEvent");
|
||||
|
||||
assertTrue(guard.contains("MAX_USERNAME_LENGTH = 15"),
|
||||
"Friend request usernames should keep the Habbo username length bound");
|
||||
assertTrue(guard.contains("MAX_MESSAGE_LENGTH = 255"),
|
||||
"Messenger payloads should keep the client message length bound");
|
||||
assertTrue(request.contains("FriendInputGuard.normalizeUsername"),
|
||||
"Friend requests should normalize usernames before lookup");
|
||||
assertTrue(request.contains("FriendInputGuard.isValidUsername"),
|
||||
"Friend requests should reject empty or oversized usernames before DB lookup");
|
||||
assertTrue(request.contains("Messenger.friendRequested(targetId, this.client.getHabbo().getHabboInfo().getId())"),
|
||||
"Friend requests should reject duplicate outgoing requests");
|
||||
assertTrue(privateMessage.contains("FriendInputGuard.normalizeMessage"),
|
||||
"Private messages should be normalized and capped before plugin dispatch");
|
||||
assertTrue(invite.contains("FriendInputGuard.normalizeMessage"),
|
||||
"Room invites should be normalized and capped before fan-out");
|
||||
}
|
||||
|
||||
@Test
|
||||
void relationshipChangesFirePluginEventAndValidatePluginMutation() throws Exception {
|
||||
String source = source("ChangeRelationEvent");
|
||||
|
||||
int event = source.indexOf("new UserRelationShipEvent");
|
||||
int fire = source.indexOf("Emulator.getPluginManager().fireEvent(event)", event);
|
||||
int pluginGuard = source.indexOf("FriendInputGuard.isValidRelation(event.relationShip)", fire);
|
||||
int setRelation = source.indexOf("buddy.setRelation(event.relationShip)", pluginGuard);
|
||||
|
||||
assertTrue(source.contains("FriendInputGuard.isValidRelation(relationId)"),
|
||||
"Relationship changes should reject invalid client relation ids");
|
||||
assertTrue(event > -1 && fire > event,
|
||||
"Relationship changes should dispatch the plugin event before applying changes");
|
||||
assertTrue(pluginGuard > fire && pluginGuard < setRelation,
|
||||
"Relationship changes should reject invalid plugin-mutated relation ids");
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.eu.habbo.messages.incoming.guilds;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GuildManagementInputGuardContractTest {
|
||||
private static String source(String file) throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/" + file));
|
||||
}
|
||||
|
||||
@Test
|
||||
void guildCreateAndRenameShareNameAndDescriptionBounds() throws Exception {
|
||||
String limits = source("GuildInputLimits.java");
|
||||
String buy = source("RequestGuildBuyEvent.java");
|
||||
String rename = source("GuildChangeNameDescEvent.java");
|
||||
|
||||
assertTrue(limits.contains("MAX_GUILD_NAME_LENGTH = 29"),
|
||||
"Guild names should keep the existing 29 character protocol bound");
|
||||
assertTrue(limits.contains("MAX_GUILD_DESCRIPTION_LENGTH = 254"),
|
||||
"Guild descriptions should keep the existing database/protocol bound");
|
||||
assertTrue(limits.contains("!name.isBlank()"),
|
||||
"Guild names must not be empty or whitespace-only");
|
||||
assertTrue(buy.contains("GuildInputLimits.isValidGuildName(name)"),
|
||||
"Guild purchase should use the shared name guard");
|
||||
assertTrue(buy.contains("GuildInputLimits.isValidGuildDescription(description)"),
|
||||
"Guild purchase should use the shared description guard");
|
||||
assertTrue(rename.contains("GuildInputLimits.isValidGuildName(newName)"),
|
||||
"Guild rename should reject invalid client names before plugin events");
|
||||
assertTrue(rename.contains("GuildInputLimits.isValidGuildName(nameEvent.name)"),
|
||||
"Guild rename should reject invalid plugin-mutated names before persistence");
|
||||
}
|
||||
|
||||
@Test
|
||||
void guildColorInputsAreCheckedAgainstLoadedPalette() throws Exception {
|
||||
String buy = source("RequestGuildBuyEvent.java");
|
||||
String colors = source("GuildChangeColorsEvent.java");
|
||||
|
||||
assertTrue(buy.contains("symbolColor(colorOne)") && buy.contains("backgroundColor(colorTwo)"),
|
||||
"Guild purchase should reject color ids that are not in the loaded guild palette");
|
||||
assertTrue(colors.contains("symbolColor(colorOne)") && colors.contains("backgroundColor(colorTwo)"),
|
||||
"Guild color changes should reject invalid client color ids");
|
||||
assertTrue(colors.contains("symbolColor(colorsEvent.colorOne)") && colors.contains("backgroundColor(colorsEvent.colorTwo)"),
|
||||
"Guild color changes should reject invalid plugin-mutated color ids");
|
||||
}
|
||||
|
||||
@Test
|
||||
void guildStateInputsStayInsideKnownEnumRange() throws Exception {
|
||||
String settings = source("GuildChangeSettingsEvent.java");
|
||||
|
||||
assertTrue(settings.contains("state < 0 || state >= GuildState.values().length"),
|
||||
"Guild settings should reject invalid client state ids before event dispatch");
|
||||
assertTrue(settings.contains("settingsEvent.state < 0 || settingsEvent.state >= GuildState.values().length"),
|
||||
"Guild settings should reject invalid plugin-mutated state ids before applying them");
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.eu.habbo.messages.incoming.guilds;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GuildMembersInputGuardContractTest {
|
||||
private static String eventSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildMembersEvent.java"));
|
||||
}
|
||||
|
||||
private static String managerSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void guildMemberListInputsAreBoundedBeforeManagerQueries() throws Exception {
|
||||
String source = eventSource();
|
||||
|
||||
int pageRead = source.indexOf("int pageId = this.packet.readInt()");
|
||||
int queryRead = source.indexOf("String query = this.packet.readString()", pageRead);
|
||||
int levelRead = source.indexOf("int levelId = this.packet.readInt()", queryRead);
|
||||
int guard = source.indexOf("pageId < 0 || pageId > MAX_PAGE_ID", levelRead);
|
||||
int managerCall = source.indexOf("getGuildMembers(g, pageId, levelId, query)", guard);
|
||||
|
||||
assertTrue(source.contains("MAX_PAGE_ID = 1000"),
|
||||
"Guild member pagination should have a server-side upper bound");
|
||||
assertTrue(source.contains("MAX_QUERY_LENGTH = 32"),
|
||||
"Guild member search query should have a server-side length bound");
|
||||
assertTrue(source.contains("MAX_LEVEL_ID = 2"),
|
||||
"Guild member rank filter should be bounded to known levels");
|
||||
assertTrue(pageRead > -1 && queryRead > pageRead && levelRead > queryRead,
|
||||
"Guild member handler must read page/query/level from the packet");
|
||||
assertTrue(guard > levelRead && guard < managerCall,
|
||||
"Guild member handler must validate inputs before querying the manager");
|
||||
}
|
||||
|
||||
@Test
|
||||
void guildMemberCountEscapesLikeQuerySameAsListQuery() throws Exception {
|
||||
String source = managerSource();
|
||||
|
||||
int listMethod = source.indexOf("public ArrayList<GuildMember> getGuildMembers(Guild guild, int page, int levelId, String query)");
|
||||
int listEscape = source.indexOf("SqlLikeEscaper.escape(query)", listMethod);
|
||||
int countMethod = source.indexOf("public int getGuildMembersCount(Guild guild, int page, int levelId, String query)");
|
||||
int countEscape = source.indexOf("SqlLikeEscaper.escape(query)", countMethod);
|
||||
|
||||
assertTrue(listEscape > listMethod,
|
||||
"Guild member list query should escape SQL LIKE wildcards");
|
||||
assertTrue(countEscape > countMethod,
|
||||
"Guild member count query should escape SQL LIKE wildcards too");
|
||||
}
|
||||
}
|
||||
+16
@@ -15,6 +15,7 @@ class GuildForumInputGuardContractTest {
|
||||
|
||||
for (String handler : List.of(
|
||||
"GuildForumPostThreadEvent.java",
|
||||
"GuildForumDataEvent.java",
|
||||
"GuildForumModerateMessageEvent.java",
|
||||
"GuildForumModerateThreadEvent.java",
|
||||
"GuildForumThreadUpdateEvent.java",
|
||||
@@ -39,9 +40,12 @@ class GuildForumInputGuardContractTest {
|
||||
String settings = Files.readString(base.resolve("GuildForumUpdateSettingsEvent.java"));
|
||||
String moderateThread = Files.readString(base.resolve("GuildForumModerateThreadEvent.java"));
|
||||
String moderateMessage = Files.readString(base.resolve("GuildForumModerateMessageEvent.java"));
|
||||
String threads = Files.readString(base.resolve("GuildForumThreadsEvent.java"));
|
||||
|
||||
assertTrue(messages.contains("GuildForumInputGuard.isValidPage(index, limit)"),
|
||||
"thread message reads must bound index/limit before fetching comments");
|
||||
assertTrue(threads.contains("GuildForumInputGuard.isValidThreadIndex(index)"),
|
||||
"thread list reads must bound the client-provided index before composing results");
|
||||
assertTrue(markRead.contains("GuildForumInputGuard.isValidMarkReadBatch(count)"),
|
||||
"mark-as-read must bound the client-provided batch count before DB writes");
|
||||
assertTrue(settings.contains("GuildForumInputGuard.isSettingsState"),
|
||||
@@ -59,4 +63,16 @@ class GuildForumInputGuardContractTest {
|
||||
assertTrue(source.contains("GuildForumInputGuard.normalize(this.packet.readString())"),
|
||||
"forum post subject and body should be normalized before word filtering and length checks");
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsReadRequiresForumReadAccessBeforeWritingViews() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java"));
|
||||
|
||||
int guildLookup = source.indexOf("Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId)");
|
||||
int readGuard = source.indexOf("guild.canHabboReadForum(userId, member, staff)");
|
||||
int insert = source.indexOf("INSERT INTO `guild_forum_views`");
|
||||
|
||||
assertTrue(guildLookup > -1 && readGuard > guildLookup && readGuard < insert,
|
||||
"mark-as-read should confirm the user can read the forum before inserting view rows");
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -21,6 +21,8 @@ class GuildForumInputGuardTest {
|
||||
assertFalse(GuildForumInputGuard.isValidPage(0, 0));
|
||||
assertTrue(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT));
|
||||
assertFalse(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT + 1));
|
||||
assertTrue(GuildForumInputGuard.isValidThreadIndex(GuildForumInputGuard.MAX_THREAD_INDEX));
|
||||
assertFalse(GuildForumInputGuard.isValidThreadIndex(GuildForumInputGuard.MAX_THREAD_INDEX + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.eu.habbo.messages.incoming.handshake;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SecureLoginGuardContractTest {
|
||||
|
||||
private static String source() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void websocketSsoTicketIsLengthBoundedBeforeDatabaseLookup() throws Exception {
|
||||
String source = source();
|
||||
|
||||
int maxConstant = source.indexOf("MAX_SSO_TICKET_LENGTH = 128");
|
||||
int guard = source.indexOf("sso.isEmpty() || sso.length() > MAX_SSO_TICKET_LENGTH");
|
||||
int lookup = source.indexOf("SELECT id FROM users WHERE auth_ticket = ?");
|
||||
|
||||
assertTrue(maxConstant > -1, "Secure login should define the same SSO length cap used by HTTP auth");
|
||||
assertTrue(guard > -1, "Secure login must reject missing or oversized SSO tickets");
|
||||
assertTrue(guard < lookup, "SSO length must be validated before database lookup");
|
||||
}
|
||||
}
|
||||
+24
@@ -109,4 +109,28 @@ class ModToolPermissionContractTest {
|
||||
handler + " must reject empty or oversized staff-supplied text");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void staffSuppliedModToolTargetsArePositiveBeforeLookupOrMutation() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
|
||||
|
||||
for (String handler : List.of(
|
||||
"ModToolAlertEvent.java",
|
||||
"ModToolWarnEvent.java",
|
||||
"ModToolKickEvent.java",
|
||||
"ModToolChangeRoomSettingsEvent.java",
|
||||
"ModToolRequestRoomInfoEvent.java",
|
||||
"ModToolRequestRoomVisitsEvent.java",
|
||||
"ModToolIssueDefaultSanctionEvent.java",
|
||||
"ModToolSanctionAlertEvent.java",
|
||||
"ModToolSanctionBanEvent.java",
|
||||
"ModToolSanctionMuteEvent.java",
|
||||
"ModToolSanctionTradeLockEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("ModToolTicketGuard.isPositiveId"),
|
||||
handler + " must reject zero or negative client-provided ids before manager/database lookups");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.eu.habbo.messages.incoming.navigator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class NavigatorInputGuardTest {
|
||||
@Test
|
||||
void searchValuesAreTrimmedAndBounded() {
|
||||
assertEquals("", NavigatorInputGuard.normalizeSearch(null));
|
||||
assertEquals("rare", NavigatorInputGuard.normalizeSearch(" rare "));
|
||||
assertEquals(NavigatorInputGuard.MAX_SEARCH_LENGTH, NavigatorInputGuard.normalizeSearch("a".repeat(100)).length());
|
||||
}
|
||||
|
||||
@Test
|
||||
void savedSearchValuesUseLargerBound() {
|
||||
assertEquals("", NavigatorInputGuard.normalizeSavedSearchValue(null));
|
||||
assertEquals("owner:duckie", NavigatorInputGuard.normalizeSavedSearchValue(" owner:duckie "));
|
||||
assertEquals(NavigatorInputGuard.MAX_SAVED_SEARCH_LENGTH, NavigatorInputGuard.normalizeSavedSearchValue("a".repeat(400)).length());
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.eu.habbo.messages.incoming.navigator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class NavigatorSearchInputContractTest {
|
||||
@Test
|
||||
void classicSearchNormalizesInputAndPassesUnprefixedQueriesToManagers() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsEvent.java"));
|
||||
|
||||
assertTrue(source.contains("NavigatorInputGuard.normalizeSearch(this.packet.readString())"),
|
||||
"classic room search must normalize raw client text before cache or manager lookups");
|
||||
assertTrue(source.contains("getRoomsForHabbo(query)"),
|
||||
"owner search must pass only the unprefixed owner query");
|
||||
assertTrue(source.contains("getRoomsWithTag(query)"),
|
||||
"tag search must pass only the unprefixed tag query");
|
||||
assertTrue(source.contains("getGroupRoomsWithName(query)"),
|
||||
"group search must pass only the unprefixed group query");
|
||||
assertTrue(source.contains("buildCacheKey(prefix, query)"),
|
||||
"classic room search must cache using normalized prefix/query pairs");
|
||||
}
|
||||
|
||||
@Test
|
||||
void savedAndTagSearchesNormalizeText() throws Exception {
|
||||
String saved = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java"));
|
||||
String tag = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsByTagEvent.java"));
|
||||
|
||||
assertTrue(saved.contains("NavigatorInputGuard.normalizeSavedSearchValue"),
|
||||
"saved searches must trim and bound search code/filter values");
|
||||
assertTrue(tag.contains("NavigatorInputGuard.normalizeSearch"),
|
||||
"tag search must trim and bound tag values");
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.eu.habbo.messages.incoming.polls;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PollAnswerInputGuardContractTest {
|
||||
private static String source() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/polls/AnswerPollEvent.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollAnswerCountAndPartLengthAreBoundedBeforeBuildingCombinedAnswer() throws Exception {
|
||||
String source = source();
|
||||
|
||||
int count = source.indexOf("int count = this.packet.readInt()");
|
||||
int answers = source.indexOf("String answers = this.packet.readString()", count);
|
||||
int guard = source.indexOf("count <= 0 || count > MAX_ANSWER_COUNT", answers);
|
||||
int builder = source.indexOf("StringBuilder answer = new StringBuilder()", guard);
|
||||
int loop = source.indexOf("for (int i = 0; i < count; i++)", builder);
|
||||
|
||||
assertTrue(source.contains("MAX_ANSWER_COUNT = 20"),
|
||||
"Poll answers should have a bounded answer count");
|
||||
assertTrue(source.contains("MAX_ANSWER_PART_LENGTH = 255"),
|
||||
"Poll answer fragments should have a bounded length");
|
||||
assertTrue(source.contains("MAX_COMBINED_ANSWER_LENGTH = 2048"),
|
||||
"Poll combined answer should have a bounded final length");
|
||||
assertTrue(count > -1 && answers > count, "Poll handler must read count and answer string");
|
||||
assertTrue(guard > answers, "Poll handler must validate count and answer string after reading them");
|
||||
assertTrue(guard < builder && builder < loop,
|
||||
"Poll handler must validate inputs before building the repeated answer string");
|
||||
}
|
||||
|
||||
@Test
|
||||
void combinedAnswerLengthIsCheckedBeforeWordQuizOrDatabaseWrite() throws Exception {
|
||||
String source = source();
|
||||
|
||||
int append = source.indexOf("answer.append(\":\").append(answers)");
|
||||
int combinedGuard = source.indexOf("answer.length() > MAX_COMBINED_ANSWER_LENGTH", append);
|
||||
int wordQuiz = source.indexOf("handleWordQuiz", combinedGuard);
|
||||
int dbWrite = source.indexOf("INSERT INTO polls_answers", combinedGuard);
|
||||
|
||||
assertTrue(combinedGuard > append,
|
||||
"Poll handler must check combined answer length while building it");
|
||||
assertTrue(combinedGuard < wordQuiz,
|
||||
"Poll handler must bound word quiz answers before dispatching them");
|
||||
assertTrue(combinedGuard < dbWrite,
|
||||
"Poll handler must bound poll answers before persisting them");
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.eu.habbo.messages.incoming.rooms;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RoomSettingsInputGuardContractTest {
|
||||
private static String source() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomStateAndTagCountAreValidatedWithoutModuloOrTruncation() throws Exception {
|
||||
String source = source();
|
||||
|
||||
int stateRead = source.indexOf("int stateId = this.packet.readInt()");
|
||||
int stateGuard = source.indexOf("stateId < 0 || stateId >= RoomState.values().length", stateRead);
|
||||
int stateAssign = source.indexOf("RoomState state = RoomState.values()[stateId]", stateGuard);
|
||||
int tagCount = source.indexOf("int count = this.packet.readInt()", stateAssign);
|
||||
int tagGuard = source.indexOf("count < 0 || count > MAX_TAGS", tagCount);
|
||||
int tagLoop = source.indexOf("for (int i = 0; i < count; i++)", tagGuard);
|
||||
|
||||
assertTrue(source.contains("MAX_TAGS = 2"), "Room settings tag count should have an explicit cap");
|
||||
assertTrue(!source.contains("% RoomState.values().length"),
|
||||
"Room state must not use modulo because negative values can crash or remap input");
|
||||
assertTrue(!source.contains("Math.min(this.packet.readInt(), 2)"),
|
||||
"Room settings must reject oversized tag counts instead of truncating and desynchronizing the packet");
|
||||
assertTrue(stateRead > -1 && stateGuard > stateRead && stateAssign > stateGuard,
|
||||
"Room state must be range-checked before indexing RoomState.values()");
|
||||
assertTrue(tagCount > -1 && tagGuard > tagCount && tagLoop > tagGuard,
|
||||
"Tag count must be range-checked before reading tag strings");
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomSettingsOptionsAreValidatedBeforeMutatingRoom() throws Exception {
|
||||
String source = source();
|
||||
|
||||
int tradeMode = source.indexOf("int tradeMode = this.packet.readInt()");
|
||||
int validation = source.indexOf("!isInRange(tradeMode, 0, MAX_OPTION_LEVEL)", tradeMode);
|
||||
int setTags = source.indexOf("room.setTags(tags.toString())", validation);
|
||||
int setChatDistance = source.indexOf("room.setChatDistance(chatDistance)", setTags);
|
||||
|
||||
assertTrue(source.contains("MAX_ROOM_PASSWORD_LENGTH = 64"),
|
||||
"Room password should have a bounded server-side length");
|
||||
assertTrue(source.contains("MAX_USERS_MAX = 200"),
|
||||
"Room capacity should have a bounded server-side maximum");
|
||||
assertTrue(source.contains("MIN_CHAT_DISTANCE = 1") && source.contains("MAX_CHAT_DISTANCE = 99"),
|
||||
"Room chat distance should be explicitly bounded");
|
||||
assertTrue(!source.contains("Math.abs(this.packet.readInt())"),
|
||||
"Room settings must reject invalid chat distance instead of converting negative values");
|
||||
assertTrue(validation > tradeMode, "Room options must be validated after reading them");
|
||||
assertTrue(validation < setTags, "Room options must be validated before mutating room fields");
|
||||
assertTrue(setChatDistance > setTags, "Validated chat distance should be applied after the guard block");
|
||||
assertTrue(source.contains("private static boolean isInRange"),
|
||||
"Room settings should use one clear range helper for numeric option guards");
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package com.eu.habbo.messages.incoming.rooms.items;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RoomItemInputGuardContractTest {
|
||||
private static String source(String name) throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/" + name + ".java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void itemMutationHandlersRejectInvalidIdsBeforeRoomLookups() throws Exception {
|
||||
for (String handler : new String[]{"RoomPickupItemEvent", "RotateMoveItemEvent", "UpdateFurniturePositionEvent", "MoveWallItemEvent", "ToggleFloorItemEvent", "ToggleWallItemEvent", "AdvertisingSaveEvent", "MannequinSaveNameEvent", "MannequinSaveLookEvent", "FootballGateSaveLookEvent", "PostItSaveDataEvent", "PostItPlaceEvent", "PostItDeleteEvent"}) {
|
||||
String source = source(handler);
|
||||
int idRead = source.indexOf("this.packet.readInt()");
|
||||
int guard = source.indexOf("RoomItemInputGuard.isPositiveId", idRead);
|
||||
int lookup = source.indexOf("getHabboItem", guard);
|
||||
|
||||
assertTrue(guard > idRead, handler + " should validate item ids after reading them");
|
||||
assertTrue(lookup == -1 || guard < lookup, handler + " should validate item ids before room item lookups");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void specialItemHandlersRejectInvalidIdsBeforeLookups() throws Exception {
|
||||
for (String handler : new String[]{
|
||||
"rentablespace/RentSpaceEvent",
|
||||
"rentablespace/RentSpaceCancelEvent",
|
||||
"lovelock/LoveLockStartConfirmEvent",
|
||||
"youtube/YoutubeRequestPlaylistChange",
|
||||
"youtube/YoutubeRequestPlaylists",
|
||||
"youtube/YoutubeRequestStateChange",
|
||||
"jukebox/JukeBoxAddSoundTrackEvent",
|
||||
"RedeemItemEvent",
|
||||
"RedeemClothingEvent"
|
||||
}) {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/" + handler + ".java"));
|
||||
int idRead = source.indexOf("this.packet.readInt()");
|
||||
int guard = source.indexOf("RoomItemInputGuard.isPositiveId", idRead);
|
||||
|
||||
assertTrue(guard > idRead, handler + " should validate item ids after reading them");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomPlacementParsesClientPayloadSafely() throws Exception {
|
||||
String source = source("RoomPlaceItemEvent");
|
||||
|
||||
assertTrue(source.contains("RoomItemInputGuard.parseInt(values[0])"),
|
||||
"item placement should parse item id without throwing on malformed packets");
|
||||
assertTrue(source.contains("values.length < 4"),
|
||||
"item placement should require complete coordinate payloads");
|
||||
assertTrue(source.contains("RoomItemInputGuard.parseShort(values[1])"),
|
||||
"floor placement should parse x coordinate safely");
|
||||
assertTrue(source.contains("RoomItemInputGuard.parseShort(values[2])"),
|
||||
"floor placement should parse y coordinate safely");
|
||||
assertTrue(source.contains("RoomItemInputGuard.parseInt(values[3])"),
|
||||
"floor placement should parse rotation safely");
|
||||
}
|
||||
|
||||
@Test
|
||||
void advertisingCustomValuesAreBoundedBeforeMutation() throws Exception {
|
||||
String source = source("AdvertisingSaveEvent");
|
||||
|
||||
int count = source.indexOf("int count = this.packet.readInt()");
|
||||
int guard = source.indexOf("RoomItemInputGuard.isValidCustomValueCount(count)", count);
|
||||
int loop = source.indexOf("for (int i = 0; i < count / 2; i++)", guard);
|
||||
int mutate = source.indexOf(".values.put(key, value)", loop);
|
||||
|
||||
assertTrue(guard > count && guard < loop,
|
||||
"custom value pair count should be bounded before reading key/value pairs");
|
||||
assertTrue(source.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_CUSTOM_KEY_LENGTH)"),
|
||||
"custom value keys should be trimmed and capped");
|
||||
assertTrue(source.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_CUSTOM_VALUE_LENGTH)"),
|
||||
"custom value values should be trimmed and capped");
|
||||
assertTrue(mutate > loop,
|
||||
"custom values should only mutate after bounded reads");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stickyPoleMultiCommandPayloadIsBounded() throws Exception {
|
||||
String source = source("SavePostItStickyPoleEvent");
|
||||
|
||||
int split = source.indexOf("String[] commands = this.packet.readString().split");
|
||||
int countGuard = source.indexOf("commands.length > RoomItemInputGuard.MAX_STICKY_POLE_COMMANDS", split);
|
||||
int trim = source.indexOf("RoomItemInputGuard.trimToMax(command.replace", countGuard);
|
||||
int execute = source.indexOf("CommandHandler.handleCommand", trim);
|
||||
|
||||
assertTrue(split > -1 && countGuard > split,
|
||||
"sticky-pole multi-command packets should cap command count before looping");
|
||||
assertTrue(trim > countGuard && trim < execute,
|
||||
"sticky-pole multi-command packets should cap each command before execution");
|
||||
}
|
||||
|
||||
@Test
|
||||
void specialLookPayloadsAreValidatedBeforeMutation() throws Exception {
|
||||
String football = source("FootballGateSaveLookEvent");
|
||||
String mannequinName = source("MannequinSaveNameEvent");
|
||||
String moodlight = source("MoodLightSaveSettingsEvent");
|
||||
|
||||
assertTrue(football.contains("RoomItemInputGuard.isValidGender(gender)"),
|
||||
"football gates should reject unknown gender keys instead of defaulting to male");
|
||||
assertTrue(football.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_LOOK_LENGTH)"),
|
||||
"football gate looks should be capped before persistence");
|
||||
assertTrue(mannequinName.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), 32)"),
|
||||
"mannequin names should be capped before extradata persistence");
|
||||
assertTrue(moodlight.contains("if (room == null)"),
|
||||
"moodlight saves should null-check current room before inspecting rights");
|
||||
}
|
||||
|
||||
@Test
|
||||
void youtubeAndJukeboxInputsAreBoundedBeforeListAccess() throws Exception {
|
||||
String playlistChange = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java"));
|
||||
String jukeboxRemove = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRemoveSoundTrackEvent.java"));
|
||||
String jukeboxRequest = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRequestPlayListEvent.java"));
|
||||
|
||||
int playlistTrim = playlistChange.indexOf("RoomItemInputGuard.trimToMax");
|
||||
int emptyVideos = playlistChange.indexOf("playlist.get().getVideos().isEmpty()");
|
||||
int getFirst = playlistChange.indexOf("playlist.get().getVideos().get(0)");
|
||||
|
||||
assertTrue(playlistTrim > -1,
|
||||
"youtube playlist ids should be capped before lookup");
|
||||
assertTrue(emptyVideos > playlistTrim && emptyVideos < getFirst,
|
||||
"youtube playlist changes should reject empty playlists before get(0)");
|
||||
assertTrue(jukeboxRemove.contains("index < 0 || index >= room.getTraxManager().getSongs().size()"),
|
||||
"jukebox remove should bound client-provided indexes before list access");
|
||||
assertTrue(jukeboxRequest.contains("if (room == null)"),
|
||||
"jukebox playlist requests should null-check current room");
|
||||
}
|
||||
|
||||
@Test
|
||||
void helperRejectsMalformedValues() {
|
||||
assertFalse(RoomItemInputGuard.isPositiveId(0));
|
||||
assertTrue(RoomItemInputGuard.isPositiveId(1));
|
||||
assertFalse(RoomItemInputGuard.isValidCustomValueCount(0));
|
||||
assertFalse(RoomItemInputGuard.isValidCustomValueCount(3));
|
||||
assertTrue(RoomItemInputGuard.isValidCustomValueCount(RoomItemInputGuard.MAX_CUSTOM_VALUE_PAIRS * 2));
|
||||
assertFalse(RoomItemInputGuard.isValidCustomValueCount((RoomItemInputGuard.MAX_CUSTOM_VALUE_PAIRS + 1) * 2));
|
||||
assertEquals(123, RoomItemInputGuard.parseInt("123"));
|
||||
assertNull(RoomItemInputGuard.parseInt("abc"));
|
||||
assertEquals((short) 12, RoomItemInputGuard.parseShort("12"));
|
||||
assertNull(RoomItemInputGuard.parseShort("40000"));
|
||||
assertTrue(RoomItemInputGuard.isValidGender("m"));
|
||||
assertTrue(RoomItemInputGuard.isValidGender("F"));
|
||||
assertFalse(RoomItemInputGuard.isValidGender("x"));
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.eu.habbo.messages.incoming.rooms.users;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RoomUserInputGuardContractTest {
|
||||
private static String source(String name) throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/users/" + name + ".java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomModerationHandlersRejectInvalidUserAndRoomIds() throws Exception {
|
||||
for (String handler : new String[]{"RoomUserBanEvent", "UnbanRoomUserEvent", "RoomUserMuteEvent"}) {
|
||||
String source = source(handler);
|
||||
int userRead = source.indexOf("int userId = this.packet.readInt()");
|
||||
int roomRead = source.indexOf("int roomId = this.packet.readInt()", userRead);
|
||||
int guard = source.indexOf("RoomUserInputGuard.isPositiveId(userId)", roomRead);
|
||||
int roomLookup = source.indexOf("getCurrentRoom()", guard);
|
||||
|
||||
assertTrue(userRead > -1 && roomRead > userRead, handler + " should read user and room ids");
|
||||
assertTrue(guard > roomRead && guard < roomLookup,
|
||||
handler + " should reject invalid ids before resolving room state");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rightsMutationHandlersRejectInvalidUserIds() throws Exception {
|
||||
String giveRights = source("RoomUserGiveRightsEvent");
|
||||
String removeRights = source("RoomUserRemoveRightsEvent");
|
||||
|
||||
int giveRead = giveRights.indexOf("int userId = this.packet.readInt()");
|
||||
int giveGuard = giveRights.indexOf("RoomUserInputGuard.isPositiveId(userId)", giveRead);
|
||||
int giveTarget = giveRights.indexOf("room.getHabbo(userId)", giveGuard);
|
||||
|
||||
int removeRead = removeRights.indexOf("int userId = this.packet.readInt()");
|
||||
int removeGuard = removeRights.indexOf("RoomUserInputGuard.isPositiveId(userId)", removeRead);
|
||||
int removeCall = removeRights.indexOf("room.removeRights(userId)", removeGuard);
|
||||
|
||||
assertTrue(giveGuard > giveRead && giveGuard < giveTarget,
|
||||
"give-rights should validate target id before online/friend lookups");
|
||||
assertTrue(removeGuard > removeRead && removeGuard < removeCall,
|
||||
"remove-rights should skip invalid ids before removing rights");
|
||||
}
|
||||
|
||||
@Test
|
||||
void kickPluginEventOnlyFiresAfterPermissionCheck() throws Exception {
|
||||
String source = source("RoomUserKickEvent");
|
||||
|
||||
int userRead = source.indexOf("int userId = this.packet.readInt()");
|
||||
int idGuard = source.indexOf("RoomUserInputGuard.isPositiveId(userId)", userRead);
|
||||
int targetLookup = source.indexOf("room.getHabbo(userId)", idGuard);
|
||||
int permissionCheck = source.indexOf("room.hasRights(this.client.getHabbo())", targetLookup);
|
||||
int event = source.indexOf("new UserKickEvent", permissionCheck);
|
||||
int kick = source.indexOf("room.kickHabbo(target, true)", event);
|
||||
|
||||
assertTrue(idGuard > userRead && idGuard < targetLookup,
|
||||
"kick should validate target id before room lookup");
|
||||
assertTrue(permissionCheck > targetLookup && event > permissionCheck && event < kick,
|
||||
"kick plugin event should only fire once the actor is authorized");
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomActionsRejectUnknownActionIdsBeforeComposersAndWired() throws Exception {
|
||||
String source = source("RoomUserActionEvent");
|
||||
|
||||
int actionRead = source.indexOf("int action = this.packet.readInt()");
|
||||
int guard = source.indexOf("RoomUserInputGuard.isValidAction(action)", actionRead);
|
||||
int composer = source.indexOf("new RoomUserActionComposer", guard);
|
||||
int wired = source.indexOf("WiredManager.triggerUserPerformsAction", guard);
|
||||
|
||||
assertTrue(guard > actionRead && guard < composer,
|
||||
"room actions should reject unknown ids before composing room state");
|
||||
assertTrue(guard < wired,
|
||||
"room actions should reject unknown ids before wired triggers");
|
||||
}
|
||||
|
||||
@Test
|
||||
void helperBoundsExpectedRanges() {
|
||||
assertFalse(RoomUserInputGuard.isPositiveId(0));
|
||||
assertTrue(RoomUserInputGuard.isPositiveId(1));
|
||||
assertFalse(RoomUserInputGuard.isValidAction(-1));
|
||||
assertTrue(RoomUserInputGuard.isValidAction(0));
|
||||
assertTrue(RoomUserInputGuard.isValidAction(7));
|
||||
assertFalse(RoomUserInputGuard.isValidAction(8));
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.incoming.trading;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class TradeOfferGuardContractTest {
|
||||
private static String incomingSource(String name) throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/trading/" + name + ".java"));
|
||||
}
|
||||
|
||||
private static String roomTradeSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleTradeOfferPacketBoundsClientSuppliedCountBeforeInventoryLookups() throws Exception {
|
||||
String source = incomingSource("TradeOfferMultipleItemsEvent");
|
||||
|
||||
int count = source.indexOf("int count = this.packet.readInt()");
|
||||
int guard = source.indexOf("count <= 0 || count > RoomTrade.MAX_OFFERED_ITEMS", count);
|
||||
int loop = source.indexOf("for (int i = 0; i < count; i++)", count);
|
||||
int lookup = source.indexOf("getHabboItem(itemId)", loop);
|
||||
|
||||
assertTrue(count > -1, "TradeOfferMultipleItemsEvent must read the client supplied count");
|
||||
assertTrue(guard > count, "TradeOfferMultipleItemsEvent must validate the count after reading it");
|
||||
assertTrue(guard < loop, "TradeOfferMultipleItemsEvent must validate the count before looping");
|
||||
assertTrue(loop < lookup, "TradeOfferMultipleItemsEvent should only resolve inventory items inside the bounded loop");
|
||||
assertTrue(source.contains("itemId <= 0"),
|
||||
"TradeOfferMultipleItemsEvent must skip invalid item ids before inventory lookup");
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomTradeEnforcesServerSideOfferedItemCapBeforeInventoryMutation() throws Exception {
|
||||
String source = roomTradeSource();
|
||||
|
||||
int constant = source.indexOf("MAX_OFFERED_ITEMS = 100");
|
||||
int singleGuard = source.indexOf("user.getItems().size() >= MAX_OFFERED_ITEMS");
|
||||
int multipleGuard = source.indexOf("user.getItems().size() >= MAX_OFFERED_ITEMS", singleGuard + 1);
|
||||
int remove = source.indexOf("removeHabboItem(item)", multipleGuard);
|
||||
|
||||
assertTrue(constant > -1, "RoomTrade must define a server-side offered item cap");
|
||||
assertTrue(singleGuard > constant, "RoomTrade.offerItem must enforce the item cap");
|
||||
assertTrue(multipleGuard > singleGuard, "RoomTrade.offerMultipleItems must enforce the item cap");
|
||||
assertTrue(multipleGuard < remove, "RoomTrade must enforce the cap before mutating inventory");
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.eu.habbo.messages.incoming.users;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class UserInputGuardContractTest {
|
||||
private static String source(String name) throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/users/" + name + ".java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void userProfileLookupsRejectInvalidIdsBeforeOfflineQueries() throws Exception {
|
||||
for (String handler : new String[]{"RequestUserProfileEvent", "RequestProfileFriendsEvent", "RequestWearingBadgesEvent"}) {
|
||||
String source = source(handler);
|
||||
int read = source.indexOf("this.packet.readInt()");
|
||||
int guard = source.indexOf("UserInputGuard.isPositiveId", read);
|
||||
int lookup = Math.max(source.indexOf("getOfflineHabboInfo", guard), source.indexOf("getBadgesOfflineHabbo", guard));
|
||||
|
||||
assertTrue(guard > read, handler + " should validate the packet id after reading it");
|
||||
assertTrue(lookup == -1 || guard < lookup, handler + " should validate ids before offline lookups");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingsInputsAreNormalizedBeforePersistence() throws Exception {
|
||||
String volumes = source("SaveUserVolumesEvent");
|
||||
String flags = source("UpdateUIFlagsEvent");
|
||||
|
||||
assertTrue(volumes.contains("UserInputGuard.clampVolume(this.packet.readInt())"),
|
||||
"volume settings should be clamped to the client-supported range");
|
||||
assertTrue(flags.contains("UserInputGuard.sanitizeUiFlags(this.packet.readInt())"),
|
||||
"UI flags should be sanitized before persistence");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mottoIsNormalizedAndRejectedBeforeSaveSideEffects() throws Exception {
|
||||
String motto = source("SaveMottoEvent");
|
||||
|
||||
int pluginValue = motto.indexOf("UserInputGuard.normalizeText(event.newMotto)");
|
||||
int lengthGuard = motto.indexOf("motto.length() > Emulator.getConfig().getInt", pluginValue);
|
||||
int save = motto.indexOf("setMotto(motto)", lengthGuard);
|
||||
int achievement = motto.indexOf("AchievementManager.progressAchievement", save);
|
||||
|
||||
assertTrue(pluginValue > -1, "plugin-mutated motto should be normalized");
|
||||
assertTrue(lengthGuard > pluginValue && lengthGuard < save,
|
||||
"motto length should be validated before saving");
|
||||
assertTrue(save < achievement,
|
||||
"motto achievement should only progress after a valid save");
|
||||
}
|
||||
|
||||
@Test
|
||||
void helperClampsAndMasksValues() {
|
||||
assertEquals(0, UserInputGuard.clampVolume(-20));
|
||||
assertEquals(40, UserInputGuard.clampVolume(40));
|
||||
assertEquals(100, UserInputGuard.clampVolume(101));
|
||||
assertEquals(0, UserInputGuard.sanitizeUiFlags(-1));
|
||||
assertEquals(0xFFFF, UserInputGuard.sanitizeUiFlags(0x1FFFF));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AuthTokenGuardContractTest {
|
||||
|
||||
@Test
|
||||
void accessTokenRejectsOversizedTokensBeforeSplitAndDecode() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/AccessTokenService.java"));
|
||||
|
||||
int maxConstant = source.indexOf("MAX_TOKEN_CHARS = 2048");
|
||||
int lengthGuard = source.indexOf("token.length() > MAX_TOKEN_CHARS");
|
||||
int split = source.indexOf("token.split");
|
||||
int decode = source.indexOf("URL_DEC.decode");
|
||||
|
||||
assertTrue(maxConstant > -1, "Access tokens should have a bounded serialized size");
|
||||
assertTrue(lengthGuard > -1, "Access token verification must reject oversized tokens");
|
||||
assertTrue(lengthGuard < split, "Access token length guard must run before split");
|
||||
assertTrue(lengthGuard < decode, "Access token length guard must run before Base64 decode");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rememberTokenRejectsOversizedTokensBeforeSplitAndDecode() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java"));
|
||||
|
||||
int maxConstant = source.indexOf("MAX_TOKEN_CHARS = 2048");
|
||||
int lengthGuard = source.indexOf("jwt.length() > MAX_TOKEN_CHARS");
|
||||
int split = source.indexOf("jwt.split");
|
||||
int decode = source.indexOf("URL_DEC.decode");
|
||||
|
||||
assertTrue(maxConstant > -1, "Remember tokens should have a bounded serialized size");
|
||||
assertTrue(lengthGuard > -1, "Remember token verification must reject oversized tokens");
|
||||
assertTrue(lengthGuard < split, "Remember token length guard must run before split");
|
||||
assertTrue(lengthGuard < decode, "Remember token length guard must run before Base64 decode");
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class NitroSecureApiHandlerContractTest {
|
||||
private static String handlerSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java"));
|
||||
}
|
||||
|
||||
private static String emulatorSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void encryptedApiPayloadSizeIsBoundedBeforeCopyAndDecrypt() throws Exception {
|
||||
String handler = handlerSource();
|
||||
String emulator = emulatorSource();
|
||||
|
||||
int readableBytes = handler.indexOf("int readableBytes = req.content().readableBytes()");
|
||||
int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes);
|
||||
int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload);
|
||||
int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes);
|
||||
int decrypt = handler.indexOf("NitroSecureAssetHandler.decrypt", byteArray);
|
||||
|
||||
assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"),
|
||||
"Secure API handler should have a conservative default payload cap");
|
||||
assertTrue(handler.contains("nitro.secure.api.max_payload_bytes"),
|
||||
"Secure API max payload should be configurable");
|
||||
assertTrue(readableBytes > -1, "Secure API handler must read content size before allocation");
|
||||
assertTrue(maxPayload > readableBytes, "Secure API handler must resolve max payload before allocation");
|
||||
assertTrue(oversizedGuard > maxPayload, "Secure API handler must reject oversized encrypted payloads");
|
||||
assertTrue(oversizedGuard < byteArray, "Oversized encrypted payloads must be rejected before byte array allocation");
|
||||
assertTrue(byteArray < decrypt, "Secure API payload must be bounded before decrypting");
|
||||
assertTrue(handler.contains("REQUEST_ENTITY_TOO_LARGE"),
|
||||
"Secure API callers need a deterministic status for oversized encrypted payloads");
|
||||
assertTrue(emulator.contains("register(\"nitro.secure.api.max_payload_bytes\", \"65536\")"),
|
||||
"Secure API max payload default must be registered before startup");
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class NitroSecureAssetHandlerContractTest {
|
||||
private static String handlerSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java"));
|
||||
}
|
||||
|
||||
private static String emulatorSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void secureAssetFilesAreSizeCheckedBeforeReadAndCache() throws Exception {
|
||||
String handler = handlerSource();
|
||||
String emulator = emulatorSource();
|
||||
|
||||
int size = handler.indexOf("long size = Files.size(target)");
|
||||
int maxBytes = handler.indexOf("int maxBytes = maxAssetBytes(kind)", size);
|
||||
int oversizedGuard = handler.indexOf("size > maxBytes", maxBytes);
|
||||
int cacheLookup = handler.indexOf("CACHE.get(cacheKey)", oversizedGuard);
|
||||
int readAllBytes = handler.indexOf("Files.readAllBytes(target)", oversizedGuard);
|
||||
|
||||
assertTrue(handler.contains("DEFAULT_MAX_CONFIG_BYTES = 2 * 1024 * 1024"),
|
||||
"Secure config assets should have a conservative default file cap");
|
||||
assertTrue(handler.contains("DEFAULT_MAX_GAMEDATA_BYTES = 16 * 1024 * 1024"),
|
||||
"Secure gamedata assets should have a bounded default file cap");
|
||||
assertTrue(handler.contains("nitro.secure.config.max_file_bytes"),
|
||||
"Secure config max file size should be configurable");
|
||||
assertTrue(handler.contains("nitro.secure.gamedata.max_file_bytes"),
|
||||
"Secure gamedata max file size should be configurable");
|
||||
assertTrue(size > -1, "Secure assets must inspect file size before loading bytes");
|
||||
assertTrue(maxBytes > size, "Secure assets must resolve the configured cap before loading bytes");
|
||||
assertTrue(oversizedGuard > maxBytes, "Secure assets must reject oversized files");
|
||||
assertTrue(oversizedGuard < cacheLookup, "Oversized secure assets must not be served from cache");
|
||||
assertTrue(oversizedGuard < readAllBytes, "Oversized secure assets must be rejected before readAllBytes");
|
||||
assertTrue(emulator.contains("register(\"nitro.secure.config.max_file_bytes\", \"2097152\")"),
|
||||
"Secure config max file size default must be registered before startup");
|
||||
assertTrue(emulator.contains("register(\"nitro.secure.gamedata.max_file_bytes\", \"16777216\")"),
|
||||
"Secure gamedata max file size default must be registered before startup");
|
||||
}
|
||||
}
|
||||
+36
@@ -50,6 +50,30 @@ class RCONServerHandlerContractTest {
|
||||
assertTrue(registerIndex < serverIndex, "RCON rate limit defaults must be registered before RCONServer is constructed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rconPayloadSizeIsBoundedBeforeBufferCopy() throws Exception {
|
||||
String handler = handlerSource();
|
||||
String emulator = emulatorSource();
|
||||
|
||||
int readableBytes = handler.indexOf("int readableBytes = data.readableBytes()");
|
||||
int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes);
|
||||
int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload);
|
||||
int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes);
|
||||
|
||||
assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"),
|
||||
"RCON handler should have a conservative default payload cap");
|
||||
assertTrue(handler.contains("rcon.max_payload_bytes"),
|
||||
"RCON max payload should be configurable");
|
||||
assertTrue(readableBytes > -1, "RCON handler must read ByteBuf size before allocation");
|
||||
assertTrue(maxPayload > readableBytes, "RCON handler must resolve max payload before allocation");
|
||||
assertTrue(oversizedGuard > maxPayload, "RCON handler must reject oversized payloads");
|
||||
assertTrue(oversizedGuard < byteArray, "Oversized RCON payloads must be rejected before byte array allocation");
|
||||
assertTrue(handler.contains("PAYLOAD_TOO_LARGE"),
|
||||
"RCON callers need a deterministic response for oversized payloads");
|
||||
assertTrue(emulator.contains("register(\"rcon.max_payload_bytes\", \"65536\")"),
|
||||
"RCON max payload default must be registered before startup");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inboundByteBufIsReleasedFromFinallyBlock() throws Exception {
|
||||
String source = handlerSource();
|
||||
@@ -59,4 +83,16 @@ class RCONServerHandlerContractTest {
|
||||
assertTrue(finallyIndex >= 0, "RCON channelRead must release inbound ByteBufs from a finally block");
|
||||
assertTrue(releaseIndex > finallyIndex, "RCON channelRead must release the inbound ByteBuf after finally starts");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rconWhitelistUsesSocketAddressInsteadOfStringSplitting() throws Exception {
|
||||
String source = handlerSource();
|
||||
|
||||
assertTrue(source.contains("InetSocketAddress"),
|
||||
"RCON whitelist should resolve socket addresses instead of parsing remoteAddress.toString()");
|
||||
assertTrue(source.contains("getHostAddress()"),
|
||||
"RCON whitelist should compare the resolved host address");
|
||||
assertTrue(!source.contains(".toString().split(\":\")"),
|
||||
"RCON whitelist must not split host:port strings because that breaks IPv6 addresses");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user