V4.0 安全扩展:29个新模块 + 60项API端点 + 164项新测试全部通过
新增模块(5组29个): 反作弊扩展(6个): - AntiKillAuraService: 杀戮光环检测(攻击频率/目标切换/旋转角度) - AntiReachService: 超距攻击检测(攻击距离/位置回溯验证) - AntiSpeedService: 水平速度检测(移动速度/GroundSpoof异常) - AntiJesusService: 水上行走检测(水面/岩浆面移动验证) - AntiNoFallService: 无摔落伤害检测(落地检测/伤害验证) - AntiScaffoldService: 自动搭路检测(方块放置频率/角度/速度) 玩家行为安全(6个): - AntiNukerService: 快速破坏检测(挖矿速度/范围/模式识别) - AntiAutoClickerService: 自动点击检测(CPS统计/点击间隔熵值分析) - AntiChestStealService: 自动偷箱检测(开箱/取物速度模式识别) - AntiAutoFishService: 自动钓鱼检测(钓鱼行为时序/反应速度) - AntiInventoryManipulationService: 背包操作检测(非法丢弃/槽位异常) - AntiBaritoneService: AI寻路机器人检测(路径平滑度/行为重复率) 服务器保护(6个): - PacketFloodProtectionService: 数据包洪水防护(窗口/递增惩罚机制) - AntiSignExploitService: 告示牌漏洞防护(超长JSON/NBT/命令注入) - AntiBookBanService: 书与笔漏洞防护(超大页码/翻译键滥用/命令注入) - AntiResourcePackExploitService: 资源包漏洞防护(内网SSRF/SHA-1黑名单) - AntiTabCompleteCrashService: Tab补全崩溃防护(深度嵌套/洪水/无效UTF-8) - AntiOfflineModeSpoofService: 离线UUID欺诈防护(格式校验/IP-UUID关联) 访问控制(6个): - OPPrivilegeMonitorService: OP权限监控(权限变更/敏感命令审计) - ConfigTamperDetectionService: 配置文件篡改检测(SHA-256基线监控) - BackdoorPluginScannerService: 后门插件扫描(恶意类名/远程执行检测) - AntiVPNProxyService: VPN代理检测(托管ASN/IP范围匹配) - AntiAltAccountService: 小号检测(IP关联/行为相似度/封禁逃逸) - AntiNameSpoofService: 名称冒充检测(Unicode同形字/空白字符/编辑距离) 聊天社交安全(5个): - ChatFloodProtectionService: 聊天洪水防护(双窗口频率/相似度/递增静音) - AntiAdvertisementService: 广告检测(IP/域名/QQ群/Discord邀请正则) - AntiPhishingLinkService: 钓鱼链接检测(短链接/可疑域名/显示文本不匹配) - AntiCommandAbuseService: 命令滥用检测(洪水/插件探测/Shell注入) - PlayerPrivacyService: 玩家隐私保护(IP脱敏/GDPR合规/数据保留策略) 配置与API: - ServerGuardConfig新增29个boolean开关(默认true),全部独立可控 - application.yml新增29行YAML配置(含中文注释说明功能) - DashboardController整合29个新服务,新增60个REST API端点(总计108个端点) - 所有模块使用双构造函数(无参+@Autowired)、静态结果类工厂方法 测试:新增5个测试类164项测试,全部通过(全项目278项/17测试类/159源文件) 修复: - AntiBookBanService: helper方法改为返回int得分贡献,修复translate/score/clickEvent检测不生效 - AntiResourcePackExploitService: 修复Set.of()中.vbs重复导致ExceptionInInitializer - AntiTabCompleteCrashService: 深度嵌套得分提高至75(触发blocked阈值) - AntiOfflineModeSpoofService: 快速UUID切换得分提高至75(触发blocked阈值) - PacketFloodProtectionService: 重构检测流程,移除提前返回导致的检测屏蔽
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
package com.aluer.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* V4.0 访问控制模块测试类
|
||||
* 覆盖 OPPrivilegeMonitor, ConfigTamperDetection, BackdoorPluginScanner,
|
||||
* AntiVPNProxy, AntiAltAccount, AntiNameSpoof 六个安全服务
|
||||
*/
|
||||
class V40AccessControlTest {
|
||||
|
||||
// ==================== OPPrivilegeMonitorService Tests ====================
|
||||
|
||||
@Test
|
||||
void opMonitorShouldDetectUnauthorizedGrantViaRcon() {
|
||||
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
|
||||
// OP granted via RCON (non-console source) should be flagged suspicious
|
||||
OPPrivilegeMonitorService.OPMonitorResult result = service.recordOPChange(
|
||||
"NewAdmin", 4, "grant", "RCON", "UnknownPlayer");
|
||||
assertTrue(result.isSuspicious());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("UNAUTHORIZED_OP_GRANT")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void opMonitorShouldAllowConsoleGrant() {
|
||||
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
|
||||
// OP granted via CONSOLE should be considered clean
|
||||
OPPrivilegeMonitorService.OPMonitorResult result = service.recordOPChange(
|
||||
"LegitAdmin", 4, "grant", "CONSOLE", "Server");
|
||||
assertTrue(result.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void opMonitorShouldDetectOpLevelJump() {
|
||||
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
|
||||
// First grant Level 1 via console (clean)
|
||||
service.recordOPChange("Player1", 1, "grant", "CONSOLE", "Server");
|
||||
// Then jump to Level 4 via console — should be suspicious
|
||||
OPPrivilegeMonitorService.OPMonitorResult result = service.recordOPChange(
|
||||
"Player1", 4, "grant", "CONSOLE", "Server");
|
||||
assertTrue(result.isSuspicious());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("OP_LEVEL_JUMP")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void opMonitorShouldDetectCommandStorm() {
|
||||
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
|
||||
// Simulate 6 sensitive commands from same player within 1 minute
|
||||
for (int i = 0; i < 6; i++) {
|
||||
OPPrivilegeMonitorService.OPMonitorResult result = service.recordSensitiveCommand(
|
||||
"SuspiciousAdmin", "ban Player" + i, "IN_GAME");
|
||||
if (i >= 5) {
|
||||
assertTrue(result.isSuspicious());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("COMMAND_STORM")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void opMonitorShouldIdentifySensitiveCommands() {
|
||||
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
|
||||
assertTrue(service.isSensitiveCommand("ban BadPlayer"));
|
||||
assertTrue(service.isSensitiveCommand("/stop"));
|
||||
assertTrue(service.isSensitiveCommand("/reload"));
|
||||
assertTrue(service.isSensitiveCommand("/whitelist add Someone"));
|
||||
assertFalse(service.isSensitiveCommand("list"));
|
||||
assertFalse(service.isSensitiveCommand("say hello"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void opMonitorGetStatusReturnsCorrectStructure() {
|
||||
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
|
||||
service.recordOPChange("Admin1", 4, "grant", "CONSOLE", "Server");
|
||||
service.recordOPChange("Admin2", 2, "grant", "CONSOLE", "Server");
|
||||
|
||||
var status = service.getStatus();
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalOps"));
|
||||
assertTrue(status.containsKey("opChanges"));
|
||||
assertTrue(status.containsKey("sensitiveCommandLog"));
|
||||
assertEquals(2L, status.get("totalOps"));
|
||||
}
|
||||
|
||||
// ==================== ConfigTamperDetectionService Tests ====================
|
||||
|
||||
@Test
|
||||
void configTamperShouldDetectHashMismatch() {
|
||||
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
|
||||
byte[] original = "online-mode=true\nmax-players=20\n".getBytes();
|
||||
byte[] tampered = "online-mode=false\nmax-players=20\n".getBytes();
|
||||
|
||||
service.registerBaseline("server.properties", original);
|
||||
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
|
||||
"server.properties", tampered, "system");
|
||||
|
||||
assertTrue(result.isTampered());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("HASH_MISMATCH")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configTamperShouldDetectOnlineModeDisabled() {
|
||||
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
|
||||
byte[] props = "online-mode=false\nenable-rcon=true\nrcon.password=123\n".getBytes();
|
||||
|
||||
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
|
||||
"server.properties", props, "system");
|
||||
|
||||
assertTrue(result.isTampered());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("ONLINE_MODE_DISABLED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configTamperShouldDetectWeakRconPassword() {
|
||||
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
|
||||
byte[] props = "online-mode=true\nenable-rcon=true\nrcon.password=abc\n".getBytes();
|
||||
|
||||
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
|
||||
"server.properties", props, "system");
|
||||
|
||||
assertTrue(result.isTampered());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("WEAK_RCON_PASSWORD")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configTamperShouldDetectSecureProfileDisabled() {
|
||||
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
|
||||
byte[] props = "online-mode=true\nenforce-secure-profile=false\n".getBytes();
|
||||
|
||||
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
|
||||
"server.properties", props, "system");
|
||||
|
||||
assertTrue(result.isTampered());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("SECURE_PROFILE_DISABLED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configTamperShouldAllowCleanConfig() {
|
||||
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
|
||||
byte[] props = "online-mode=true\nmax-players=20\nenforce-secure-profile=true\n".getBytes();
|
||||
|
||||
service.registerBaseline("server.properties", props);
|
||||
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
|
||||
"server.properties", props, "system");
|
||||
|
||||
assertTrue(result.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void configTamperShouldDetectUnauthorizedOpInOpsJson() {
|
||||
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
|
||||
service.setLegitimateOps(Set.of("Owner", "Admin1"));
|
||||
|
||||
byte[] opsJson = ("[{\"uuid\":\"aaa-bbb-ccc\",\"name\":\"UnknownHacker\",\"level\":4}]").getBytes();
|
||||
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
|
||||
"ops.json", opsJson, "system");
|
||||
|
||||
assertTrue(result.isTampered());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("UNAUTHORIZED_OP")));
|
||||
}
|
||||
|
||||
// ==================== BackdoorPluginScannerService Tests ====================
|
||||
|
||||
@Test
|
||||
void backdoorScannerShouldDetectKnownMaliciousPlugin() {
|
||||
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
|
||||
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
|
||||
"ForceOP", "ForceOP.jar", true, "com.force.op.Main",
|
||||
List.of("com.force.op.Main"), List.of());
|
||||
|
||||
assertTrue(result.isMalicious());
|
||||
assertEquals(BackdoorPluginScannerService.RiskLevel.MALICIOUS, result.getRiskLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void backdoorScannerShouldDetectMissingPluginYml() {
|
||||
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
|
||||
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
|
||||
"MysteryPlugin", "MysteryPlugin.jar", false, "com.test.Main",
|
||||
List.of("com.test.Main"), List.of());
|
||||
|
||||
assertTrue(result.isSuspicious());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("MISSING_PLUGIN_YML")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void backdoorScannerShouldDetectRCECapability() {
|
||||
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
|
||||
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
|
||||
"RemoteTool", "RemoteTool.jar", true, "com.remote.Main",
|
||||
List.of("com.remote.Main", "Runtime.exec.Payload", "com.remote.Cleanup"),
|
||||
List.of());
|
||||
|
||||
assertTrue(result.isSuspicious());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("RCE_CAPABILITY")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void backdoorScannerShouldDetectSuspiciousConfigKeys() {
|
||||
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
|
||||
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
|
||||
"NormalPlugin", "NormalPlugin.jar", true, "com.normal.Main",
|
||||
List.of("com.normal.Main"),
|
||||
List.of("database-url", "backdoor-enabled", "webhook-url"));
|
||||
|
||||
assertTrue(result.isSuspicious());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("SUSPICIOUS_CONFIG_KEY")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void backdoorScannerShouldAllowSafePlugin() {
|
||||
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
|
||||
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
|
||||
"WorldEdit", "WorldEdit.jar", true, "com.sk89q.worldedit.bukkit.WorldEditPlugin",
|
||||
List.of("com.sk89q.worldedit.bukkit.WorldEditPlugin"), List.of("max-blocks", "nav-wand"));
|
||||
|
||||
assertTrue(result.isSafe());
|
||||
assertEquals(BackdoorPluginScannerService.RiskLevel.SAFE, result.getRiskLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void backdoorScannerQuickScanShouldDetectMalicious() {
|
||||
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
|
||||
BackdoorPluginScannerService.PluginScanResult result = service.quickScan("OPMe");
|
||||
|
||||
assertTrue(result.isMalicious());
|
||||
}
|
||||
|
||||
// ==================== AntiVPNProxyService Tests ====================
|
||||
|
||||
@Test
|
||||
void antiVpnShouldDetectDigitalOceanASN() {
|
||||
AntiVPNProxyService service = new AntiVPNProxyService();
|
||||
// AS14061 = DigitalOcean
|
||||
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
|
||||
"159.65.100.50", "14061", "DigitalOcean, LLC", "Player1");
|
||||
|
||||
assertTrue(result.isHosting());
|
||||
assertFalse(result.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiVpnShouldDetectDatacenterIPRange() {
|
||||
AntiVPNProxyService service = new AntiVPNProxyService();
|
||||
// Vultr IP prefix
|
||||
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
|
||||
"45.76.100.200", null, null, "Player2");
|
||||
|
||||
assertTrue(result.isHosting());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiVpnShouldAllowTrustedIP() {
|
||||
AntiVPNProxyService service = new AntiVPNProxyService();
|
||||
// Add to trusted list first
|
||||
service.addTrustedIP("159.65.100.50");
|
||||
|
||||
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
|
||||
"159.65.100.50", "14061", "DigitalOcean, LLC", "Player3");
|
||||
|
||||
assertTrue(result.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiVpnShouldDetectHostingISP() {
|
||||
AntiVPNProxyService service = new AntiVPNProxyService();
|
||||
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
|
||||
"198.51.100.50", null, "SomeHosting Cloud LTD", "Player4");
|
||||
|
||||
assertTrue(result.isHosting());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiVpnShouldAllowResidentialIP() {
|
||||
AntiVPNProxyService service = new AntiVPNProxyService();
|
||||
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
|
||||
"203.0.113.1", "12345", "Comcast Cable", "LegitPlayer");
|
||||
|
||||
assertTrue(result.isClean());
|
||||
}
|
||||
|
||||
// ==================== AntiAltAccountService Tests ====================
|
||||
|
||||
@Test
|
||||
void antiAltShouldDetectBanEvasion() {
|
||||
AntiAltAccountService service = new AntiAltAccountService();
|
||||
// Ban this IP first
|
||||
service.recordBannedIP("10.0.0.100", "Griefing");
|
||||
// New player joins from the same IP immediately after
|
||||
AntiAltAccountService.AltAccountResult result = service.onPlayerLogin(
|
||||
"NewAltPlayer", "uuid-new", "10.0.0.100");
|
||||
|
||||
assertTrue(result.isAltDetected());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("BAN_EVASION")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAltShouldDetectRapidAccountSwitch() {
|
||||
AntiAltAccountService service = new AntiAltAccountService();
|
||||
// Same IP, multiple accounts within 5 minutes
|
||||
service.onPlayerLogin("Alt1", "uuid-1", "10.0.0.200");
|
||||
service.onPlayerLogin("Alt2", "uuid-2", "10.0.0.200");
|
||||
AntiAltAccountService.AltAccountResult result = service.onPlayerLogin(
|
||||
"Alt3", "uuid-3", "10.0.0.200");
|
||||
|
||||
assertTrue(result.isAltDetected());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("RAPID_ACCOUNT_SWITCH")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAltShouldAllowSingleAccountLogin() {
|
||||
AntiAltAccountService service = new AntiAltAccountService();
|
||||
AntiAltAccountService.AltAccountResult result = service.onPlayerLogin(
|
||||
"LegitPlayer", "uuid-real", "192.168.1.1");
|
||||
|
||||
assertTrue(result.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAltShouldTrackTroublemakerIPs() {
|
||||
AntiAltAccountService service = new AntiAltAccountService();
|
||||
// First, login with account and create IP association
|
||||
service.onPlayerLogin("Griefer1", "uuid-g1", "10.0.0.50");
|
||||
// Ban the IP
|
||||
service.recordBannedIP("10.0.0.50", "Griefing");
|
||||
// Mark as troublemaker
|
||||
service.markAsTroublemaker("Griefer1");
|
||||
|
||||
// New account from same IP should be detected
|
||||
AntiAltAccountService.AltAccountResult result = service.onPlayerLogin(
|
||||
"GrieferAlt", "uuid-g2", "10.0.0.50");
|
||||
|
||||
assertTrue(result.isAltDetected());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("TROUBLEMAKER_IP")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAltShouldReturnAssociatedAccounts() {
|
||||
AntiAltAccountService service = new AntiAltAccountService();
|
||||
service.onPlayerLogin("AccA", "uuid-a", "10.0.0.77");
|
||||
service.onPlayerLogin("AccB", "uuid-b", "10.0.0.77");
|
||||
|
||||
Set<String> accounts = service.getAccountsForIP("10.0.0.77");
|
||||
assertTrue(accounts.contains("AccA"));
|
||||
assertTrue(accounts.contains("AccB"));
|
||||
}
|
||||
|
||||
// ==================== AntiNameSpoofService Tests ====================
|
||||
|
||||
@Test
|
||||
void antiNameSpoofShouldDetectUnicodeHomoglyph() {
|
||||
AntiNameSpoofService service = new AntiNameSpoofService();
|
||||
service.addProtectedName("Admin");
|
||||
|
||||
// Use Cyrillic 'a' (U+0430) instead of Latin 'a'
|
||||
String spoofedName = "Admin"; // Note: uses Cyrillic 'a' when built with proper Unicode
|
||||
// For the test, we check a player name that contains Unicode normalization differences
|
||||
AntiNameSpoofService.NameSpoofResult result = service.checkPlayerName(
|
||||
"Admın", "10.0.0.1"); // Uses dotless i (U+0131) instead of normal i
|
||||
|
||||
assertTrue(result.isSpoofed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiNameSpoofShouldDetectInvisibleCharacters() {
|
||||
AntiNameSpoofService service = new AntiNameSpoofService();
|
||||
// Insert zero-width space U+200B into the name
|
||||
String spoofedName = "Admintest";
|
||||
AntiNameSpoofService.NameSpoofResult result = service.checkPlayerName(
|
||||
spoofedName, "10.0.0.2");
|
||||
|
||||
assertTrue(result.isSpoofed());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("INVISIBLE_CHARS")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiNameSpoofShouldAllowNormalPlayerName() {
|
||||
AntiNameSpoofService service = new AntiNameSpoofService();
|
||||
service.addProtectedName("Admin");
|
||||
|
||||
AntiNameSpoofService.NameSpoofResult result = service.checkPlayerName(
|
||||
"Player123", "10.0.0.3");
|
||||
|
||||
assertTrue(result.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiNameSpoofShouldDetectLevenshteinSimilarity() {
|
||||
AntiNameSpoofService service = new AntiNameSpoofService();
|
||||
service.addProtectedName("Administrator");
|
||||
|
||||
// "Administrat0r" (using zero instead of 'o') is very similar
|
||||
AntiNameSpoofService.NameSpoofResult result = service.checkPlayerName(
|
||||
"Administrat0r", "10.0.0.4");
|
||||
|
||||
assertTrue(result.isSpoofed());
|
||||
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("SIMILARITY_SPOOF")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiNameSpoofShouldBlockSpoofedName() {
|
||||
AntiNameSpoofService service = new AntiNameSpoofService();
|
||||
service.addProtectedName("Notch");
|
||||
|
||||
service.checkPlayerName("Notch", "10.0.0.5");
|
||||
service.checkPlayerName("NOTCH", "10.0.0.6");
|
||||
|
||||
var status = service.getStatus();
|
||||
assertTrue((Long) status.get("spoofAttempts") >= 1);
|
||||
assertTrue((Integer) status.get("blockedNames") >= 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
package com.aluer.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* V4.0 反作弊扩展模块测试
|
||||
* 测试 6 个新增反作弊模块:AntiKillAura, AntiReach, AntiSpeed, AntiJesus, AntiNoFall, AntiScaffold
|
||||
*
|
||||
* 每个模块至少3个测试用例,覆盖正常行为、异常行为和 getStatus() 功能
|
||||
*/
|
||||
class V40AntiCheatTest {
|
||||
|
||||
// ========================================================================
|
||||
// AntiKillAuraService 测试
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 正常攻击行为 — 攻击单一目标,角度有变化,距离在合理范围
|
||||
*/
|
||||
@Test
|
||||
void killAuraNormalAttackReturnsClean() {
|
||||
AntiKillAuraService svc = new AntiKillAuraService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 连续攻击同一个目标
|
||||
AntiKillAuraService.KillAuraCheckResult r = null;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
r = svc.detect("Player1", "uuid-1", "TargetA",
|
||||
45.0 + i * 10, 10.0 + i * 2,
|
||||
2.5, now.plusMillis(i * 500));
|
||||
}
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
assertFalse(r.isFlagged());
|
||||
assertFalse(r.isSuspicious());
|
||||
}
|
||||
|
||||
/**
|
||||
* 短时间内攻击过多不同目标 — 应标记为可疑或flagged
|
||||
*/
|
||||
@Test
|
||||
void killAuraRapidTargetSwitchDetected() {
|
||||
AntiKillAuraService svc = new AntiKillAuraService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 在3秒内切换5个不同目标 — 超过MAX_TARGET_SWITCHES(3)
|
||||
String[] targets = {"A", "B", "C", "D", "E"};
|
||||
AntiKillAuraService.KillAuraCheckResult lastResult = null;
|
||||
|
||||
for (int i = 0; i < targets.length; i++) {
|
||||
lastResult = svc.detect("Player1", "uuid-1", targets[i],
|
||||
45.0 + i * 3, 10.0,
|
||||
2.5, now.plusMillis(i * 200));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
assertFalse(lastResult.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* 攻击角度高度一致 — 模拟Aimbot(自动瞄准),应标记为flagged
|
||||
*/
|
||||
@Test
|
||||
void killAuraAimbotAngleDetected() {
|
||||
AntiKillAuraService svc = new AntiKillAuraService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiKillAuraService.KillAuraCheckResult lastResult = null;
|
||||
|
||||
// 多次攻击角度几乎不变 — Aimbot特征
|
||||
for (int i = 0; i < 8; i++) {
|
||||
lastResult = svc.detect("Player2", "uuid-2", "Target1",
|
||||
45.0 + (i * 0.5 % 1.0), // yaw几乎不变
|
||||
10.0 + (i * 0.3 % 0.5), // pitch几乎不变
|
||||
2.0, now.plusMillis(i * 600));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
// 角度一致性应被检测
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 返回非空Map包含必要字段
|
||||
*/
|
||||
@Test
|
||||
void killAuraGetStatusReturnsValidMap() {
|
||||
AntiKillAuraService svc = new AntiKillAuraService();
|
||||
svc.detect("P1", "u1", "T1", 45, 10, 2.0, Instant.now());
|
||||
|
||||
Map<String, Object> status = svc.getStatus();
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalChecks"));
|
||||
assertTrue(status.containsKey("flaggedCount"));
|
||||
assertTrue(status.containsKey("activeTrackedPlayers"));
|
||||
assertNotNull(status.get("totalChecks"));
|
||||
assertNotNull(status.get("flaggedCount"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 完美极限距离模式 — 多次在最大距离范围攻击
|
||||
*/
|
||||
@Test
|
||||
void killAuraPerfectReachPatternDetected() {
|
||||
AntiKillAuraService svc = new AntiKillAuraService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiKillAuraService.KillAuraCheckResult lastResult = null;
|
||||
|
||||
// 多次在2.8-3.0精确距离攻击 — perfect reach
|
||||
for (int i = 0; i < 12; i++) {
|
||||
lastResult = svc.detect("Player3", "uuid-3", "TargetX",
|
||||
30.0 + i * 5, 5.0,
|
||||
2.9, // 接近极限距离
|
||||
now.plusMillis(i * 400));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AntiReachService 测试
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 正常攻击距离 — 3.0方块以内
|
||||
*/
|
||||
@Test
|
||||
void reachNormalDistanceReturnsClean() {
|
||||
AntiReachService svc = new AntiReachService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiReachService.ReachCheckResult r = svc.detect(
|
||||
"Player1", "uuid-1",
|
||||
0, 64, 0,
|
||||
0, 64, 2.5,
|
||||
2.5, now);
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
assertFalse(r.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* 绝对超距攻击 — 超过6.0方块,应标记为flagged
|
||||
*/
|
||||
@Test
|
||||
void reachAbsoluteExceedDetected() {
|
||||
AntiReachService svc = new AntiReachService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 攻击距离超过6.0 — 绝对超距
|
||||
AntiReachService.ReachCheckResult r = svc.detect(
|
||||
"Hacker1", "uuid-h1",
|
||||
0, 64, 0,
|
||||
0, 64, 7.0,
|
||||
7.0, now);
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isFlagged());
|
||||
assertTrue(r.getReasons().size() > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可疑超距 — 超过3.8但不到6.0方块
|
||||
*/
|
||||
@Test
|
||||
void reachSuspiciousDistanceDetected() {
|
||||
AntiReachService svc = new AntiReachService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiReachService.ReachCheckResult r = svc.detect(
|
||||
"Player2", "uuid-2",
|
||||
0, 64, 0,
|
||||
0, 64, 4.5,
|
||||
4.5, now);
|
||||
|
||||
assertTrue(r.isSuspicious() || r.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* 频繁超距攻击 — 多次超过阈值
|
||||
*/
|
||||
@Test
|
||||
void reachFrequentViolationFlagged() {
|
||||
AntiReachService svc = new AntiReachService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiReachService.ReachCheckResult lastResult = null;
|
||||
|
||||
// 多次超过3.8可疑阈值
|
||||
for (int i = 0; i < 3; i++) {
|
||||
lastResult = svc.detect("Hacker2", "uuid-h2",
|
||||
0, 64, 0,
|
||||
0, 64, 4.0 + i * 0.2,
|
||||
4.0 + i * 0.2, now.plusMillis(i * 200));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertFalse(lastResult.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 返回有效状态Map
|
||||
*/
|
||||
@Test
|
||||
void reachGetStatusReturnsValidMap() {
|
||||
AntiReachService svc = new AntiReachService();
|
||||
svc.detect("P1", "u1", 0, 64, 0, 0, 64, 2.0, 2.0, Instant.now());
|
||||
svc.detect("P1", "u1", 0, 64, 0, 0, 64, 7.0, 7.0, Instant.now());
|
||||
|
||||
Map<String, Object> status = svc.getStatus();
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalChecks"));
|
||||
assertTrue(status.containsKey("violations"));
|
||||
assertTrue(status.containsKey("recentViolations"));
|
||||
assertNotNull(status.get("totalChecks"));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AntiSpeedService 测试
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 正常步行速度 — 应返回clean
|
||||
*/
|
||||
@Test
|
||||
void speedNormalWalkingReturnsClean() {
|
||||
AntiSpeedService svc = new AntiSpeedService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 模拟正常步行 ~4.0 m/s
|
||||
AntiSpeedService.SpeedCheckResult r = svc.detect(
|
||||
"Player1", "uuid-1",
|
||||
0, 64, 0,
|
||||
0.2, 64, 0.2,
|
||||
true, 0.05, now);
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* 持续超速 — 超过7.0 m/s超过1秒
|
||||
*/
|
||||
@Test
|
||||
void speedSustainedOverspeedFlagged() {
|
||||
AntiSpeedService svc = new AntiSpeedService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiSpeedService.SpeedCheckResult lastResult = null;
|
||||
|
||||
// 持续超速,每次移动0.5方块,间隔50ms = 10 m/s
|
||||
for (int i = 0; i < 20; i++) {
|
||||
lastResult = svc.detect("Hacker1", "uuid-h1",
|
||||
i * 0.5, 64, 0,
|
||||
(i + 1) * 0.5, 64, 0,
|
||||
true, 0.05, now.plusMillis(i * 50));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* GroundSpoof检测 — 声称在地面但垂直速度异常
|
||||
*/
|
||||
@Test
|
||||
void speedGroundSpoofDetected() {
|
||||
AntiSpeedService svc = new AntiSpeedService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// onGround=true但Y轴有显著变化
|
||||
AntiSpeedService.SpeedCheckResult r = svc.detect(
|
||||
"Hacker2", "uuid-h2",
|
||||
0, 64, 0,
|
||||
0.2, 64.5, 0.2,
|
||||
true, 0.05, now);
|
||||
|
||||
assertTrue(r.isSuspicious() || r.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* 机械性恒速模式 — 速度恒定
|
||||
*/
|
||||
@Test
|
||||
void speedMechanicalPatternDetected() {
|
||||
AntiSpeedService svc = new AntiSpeedService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiSpeedService.SpeedCheckResult lastResult = null;
|
||||
|
||||
// 每次都以完全相同速度移动 — 机械性Speed hack
|
||||
for (int i = 0; i < 12; i++) {
|
||||
lastResult = svc.detect("Hacker3", "uuid-h3",
|
||||
i * 0.3, 64, 0,
|
||||
i * 0.3 + 0.3, 64, 0,
|
||||
true, 0.05, now.plusMillis(i * 50));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 返回包含统计信息的Map
|
||||
*/
|
||||
@Test
|
||||
void speedGetStatusReturnsValidMap() {
|
||||
AntiSpeedService svc = new AntiSpeedService();
|
||||
svc.detect("P1", "u1", 0, 64, 0, 0.2, 64, 0.2, true, 0.05, Instant.now());
|
||||
svc.detect("P1", "u1", 0.2, 64, 0.2, 0.4, 64, 0.4, true, 0.05, Instant.now());
|
||||
|
||||
Map<String, Object> status = svc.getStatus();
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalSamples"));
|
||||
assertTrue(status.containsKey("speedViolations"));
|
||||
assertTrue(status.containsKey("averageSpeed"));
|
||||
assertTrue(status.containsKey("maxSpeed"));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AntiJesusService 测试
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 正常在水中游泳 — 不在液体表面行走
|
||||
*/
|
||||
@Test
|
||||
void jesusNormalSwimmingReturnsClean() {
|
||||
AntiJesusService svc = new AntiJesusService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 玩家在水中(Y=61.5, 水面Y=63)— 正在下沉/游泳中
|
||||
AntiJesusService.JesusCheckResult r = svc.detect(
|
||||
"Player1", "uuid-1",
|
||||
61.5, 63.0,
|
||||
"WATER", false, true, now);
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* 水上行走 — 站在水面高度且声称在地面
|
||||
*/
|
||||
@Test
|
||||
void jesusWalkingOnWaterSuspicious() {
|
||||
AntiJesusService svc = new AntiJesusService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiJesusService.JesusCheckResult lastResult = null;
|
||||
|
||||
// 连续站在水面上 — Jesus hack特征
|
||||
for (int i = 0; i < 5; i++) {
|
||||
lastResult = svc.detect("Hacker1", "uuid-h1",
|
||||
63.1, 63.0,
|
||||
"WATER", true, true, now.plusMillis(i * 500));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* 持续水上行走超过1秒+抖动模式 — 双规则命中应标记为flagged
|
||||
*/
|
||||
@Test
|
||||
void jesusSustainedWaterWalkingFlagged() {
|
||||
AntiJesusService svc = new AntiJesusService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiJesusService.JesusCheckResult lastResult = null;
|
||||
|
||||
// 持续站在水面上超过1秒 + 抖动振荡(触发双规则)
|
||||
double[] yValues = {63.0, 63.05, 62.95, 63.02, 62.98, 63.03, 62.97, 63.01, 63.04, 62.96};
|
||||
for (int i = 0; i < yValues.length; i++) {
|
||||
lastResult = svc.detect("Hacker2", "uuid-h2",
|
||||
yValues[i], 63.0,
|
||||
"WATER", true, true, now.plusMillis(i * 200));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isFlagged());
|
||||
|
||||
assertTrue(lastResult.getReasons().stream()
|
||||
.anyMatch(r -> r.contains("SUSTAINED_JESUS")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 抖动模式检测 — Y坐标在水面附近振荡
|
||||
*/
|
||||
@Test
|
||||
void jesusJitterPatternDetected() {
|
||||
AntiJesusService svc = new AntiJesusService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiJesusService.JesusCheckResult lastResult = null;
|
||||
|
||||
// 模拟抖动:Y值在水面附近反复小幅变化
|
||||
double[] yValues = {63.0, 63.05, 62.95, 63.02, 62.98, 63.03, 62.97, 63.01};
|
||||
for (int i = 0; i < yValues.length; i++) {
|
||||
lastResult = svc.detect("Hacker3", "uuid-h3",
|
||||
yValues[i], 63.0,
|
||||
"WATER", true, true, now.plusMillis(i * 300));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 返回有效状态Map
|
||||
*/
|
||||
@Test
|
||||
void jesusGetStatusReturnsValidMap() {
|
||||
AntiJesusService svc = new AntiJesusService();
|
||||
svc.detect("P1", "u1", 62.0, 63.0, "WATER", false, true, Instant.now());
|
||||
|
||||
Map<String, Object> status = svc.getStatus();
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalChecks"));
|
||||
assertTrue(status.containsKey("jesusViolations"));
|
||||
assertTrue(status.containsKey("activeTrackedPlayers"));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AntiNoFallService 测试
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 正常坠落并受伤害 — 应返回clean
|
||||
*/
|
||||
@Test
|
||||
void noFallNormalFallWithDamageReturnsClean() {
|
||||
AntiNoFallService svc = new AntiNoFallService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 记录从Y=80坠落
|
||||
svc.recordFallStart("Player1", "uuid-1", 80.0, "survival", now);
|
||||
// 著陆在Y=70,受到伤害
|
||||
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
|
||||
"Player1", "uuid-1", 70.0,
|
||||
true, 5.0, // tookDamage=true
|
||||
false, false, false, false, false, false, // 无合法无伤害减免
|
||||
now.plusSeconds(2));
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* 短距离坠落不受伤 — 正常行为
|
||||
*/
|
||||
@Test
|
||||
void noFallShortFallNoDamageNormal() {
|
||||
AntiNoFallService svc = new AntiNoFallService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 坠落不足3方块 — 不会产生伤害
|
||||
svc.recordFallStart("Player2", "uuid-2", 66.0, "survival", now);
|
||||
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
|
||||
"Player2", "uuid-2", 65.0,
|
||||
false, 0,
|
||||
false, false, false, false, false, false,
|
||||
now.plusMillis(500));
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* 高空坠落无伤害 — NoFall hack特征
|
||||
*/
|
||||
@Test
|
||||
void noFallHighFallNoDamageFlagged() {
|
||||
AntiNoFallService svc = new AntiNoFallService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 从12方块高度坠落但不受伤 — 无合法理由
|
||||
svc.recordFallStart("Hacker1", "uuid-h1", 82.0, "survival", now);
|
||||
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
|
||||
"Hacker1", "uuid-h1", 70.0,
|
||||
false, 0, // 没有受到伤害
|
||||
false, false, false, false, false, false,
|
||||
now.plusSeconds(3));
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isSuspicious() || r.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合法无伤害 — 落入水中
|
||||
*/
|
||||
@Test
|
||||
void noFallWaterLandingNoDamageIsLegitimate() {
|
||||
AntiNoFallService svc = new AntiNoFallService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 掉入水中 — 合法免疫摔落伤害
|
||||
svc.recordFallStart("Player3", "uuid-3", 80.0, "survival", now);
|
||||
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
|
||||
"Player3", "uuid-3", 62.0,
|
||||
false, 0,
|
||||
true, // isInLiquid=true (水中)
|
||||
false, false, false, false, false,
|
||||
now.plusSeconds(2));
|
||||
|
||||
// 落入水中是合法的无伤害情况,不应标记
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合法无伤害 — 鞘翅飞行
|
||||
*/
|
||||
@Test
|
||||
void noFallElytraFlyingIsLegitimate() {
|
||||
AntiNoFallService svc = new AntiNoFallService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 鞘翅飞行期间不受伤 — 合法
|
||||
svc.recordFallStart("Player4", "uuid-4", 100.0, "survival", now);
|
||||
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
|
||||
"Player4", "uuid-4", 70.0,
|
||||
false, 0,
|
||||
false, false, false, false, false,
|
||||
true, // isElytraFlying=true
|
||||
now.plusSeconds(5));
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* 粘液块上无伤害 — 合法
|
||||
*/
|
||||
@Test
|
||||
void noFallSlimeBlockLandingIsLegitimate() {
|
||||
AntiNoFallService svc = new AntiNoFallService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 落在粘液块上 — 合法免疫摔落伤害
|
||||
svc.recordFallStart("Player5", "uuid-5", 80.0, "survival", now);
|
||||
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
|
||||
"Player5", "uuid-5", 60.0,
|
||||
false, 0,
|
||||
false, true, // isOnSlimeBlock=true
|
||||
false, false, false, false,
|
||||
now.plusSeconds(2));
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 返回有效状态Map
|
||||
*/
|
||||
@Test
|
||||
void noFallGetStatusReturnsValidMap() {
|
||||
AntiNoFallService svc = new AntiNoFallService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
svc.recordFallStart("P1", "u1", 80.0, "survival", now);
|
||||
svc.recordLanding("P1", "u1", 70.0, true, 5.0,
|
||||
false, false, false, false, false, false, now.plusSeconds(2));
|
||||
|
||||
Map<String, Object> status = svc.getStatus();
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalFalls"));
|
||||
assertTrue(status.containsKey("noFallViolations"));
|
||||
assertTrue(status.containsKey("falsePositives"));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AntiScaffoldService 测试
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 正常方块放置 — 间隔合理,角度正常
|
||||
*/
|
||||
@Test
|
||||
void scaffoldNormalPlacementReturnsClean() {
|
||||
AntiScaffoldService svc = new AntiScaffoldService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiScaffoldService.ScaffoldCheckResult r = svc.detect(
|
||||
"Player1", "uuid-1",
|
||||
10, 63, 10,
|
||||
180, 30, // yaw=180, pitch=30(正常视角)
|
||||
false, true, "COBBLESTONE",
|
||||
now);
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
/**
|
||||
* 高速连续放置 — 超过5方块/秒持续3秒
|
||||
*/
|
||||
@Test
|
||||
void scaffoldRapidPlacementFlagged() {
|
||||
AntiScaffoldService svc = new AntiScaffoldService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiScaffoldService.ScaffoldCheckResult lastResult = null;
|
||||
|
||||
// 高速放置方块:每次间隔100ms = 10方块/秒
|
||||
for (int i = 0; i < 30; i++) {
|
||||
lastResult = svc.detect("Hacker1", "uuid-h1",
|
||||
10 + i, 63, 10,
|
||||
90, -85, // pitch=-85 (近乎垂直向下看)
|
||||
true, true, "COBBLESTONE",
|
||||
now.plusMillis(i * 100));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scaffold角度检测 — 向下看并精确放置
|
||||
*/
|
||||
@Test
|
||||
void scaffoldDownwardAngleAndPlacementPattern() {
|
||||
AntiScaffoldService svc = new AntiScaffoldService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiScaffoldService.ScaffoldCheckResult lastResult = null;
|
||||
|
||||
// 向下看 (pitch接近-90) + 脚下放置 — 典型Scaffold
|
||||
for (int i = 0; i < 10; i++) {
|
||||
lastResult = svc.detect("Hacker2", "uuid-h2",
|
||||
10 + i, 63, 10,
|
||||
0, -88, // 几乎是正下方
|
||||
true, true, "COBBLESTONE",
|
||||
now.plusMillis(i * 150));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Safewalk检测 — 连续边缘放置
|
||||
*/
|
||||
@Test
|
||||
void scaffoldSafewalkPattern() {
|
||||
AntiScaffoldService svc = new AntiScaffoldService();
|
||||
Instant now = Instant.now();
|
||||
|
||||
AntiScaffoldService.ScaffoldCheckResult lastResult = null;
|
||||
|
||||
// 大量在脚下放置方块(Safewalk行为)
|
||||
for (int i = 0; i < 20; i++) {
|
||||
lastResult = svc.detect("Hacker3", "uuid-h3",
|
||||
10 + i, 63, 10,
|
||||
90, -75,
|
||||
true, // isPlaceBelowPlayer=true(脚下放置)
|
||||
false, "COBBLESTONE",
|
||||
now.plusMillis(i * 80));
|
||||
}
|
||||
|
||||
assertNotNull(lastResult);
|
||||
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 返回有效状态Map
|
||||
*/
|
||||
@Test
|
||||
void scaffoldGetStatusReturnsValidMap() {
|
||||
AntiScaffoldService svc = new AntiScaffoldService();
|
||||
svc.detect("P1", "u1", 10, 63, 10, 180, 30,
|
||||
false, true, "STONE", Instant.now());
|
||||
svc.detect("P1", "u1", 11, 63, 10, 180, 32,
|
||||
false, true, "STONE", Instant.now());
|
||||
|
||||
Map<String, Object> status = svc.getStatus();
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalPlacements"));
|
||||
assertTrue(status.containsKey("scaffoldViolations"));
|
||||
assertTrue(status.containsKey("trackedBuilders"));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 模块关闭验证测试 — 通过直接创建带无参构造的服务实例测试
|
||||
// 无参构造函数使用 new ServerGuardConfig(),默认所有开关为true
|
||||
// 此处验证配置默认为true时服务正常执行检测
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* All services instantiable and return clean for trivial inputs
|
||||
*/
|
||||
@Test
|
||||
void allServicesInstantiableAndFunctional() {
|
||||
// 验证所有服务可通过无参构造实例化
|
||||
AntiKillAuraService killAura = new AntiKillAuraService();
|
||||
AntiReachService reach = new AntiReachService();
|
||||
AntiSpeedService speed = new AntiSpeedService();
|
||||
AntiJesusService jesus = new AntiJesusService();
|
||||
AntiNoFallService noFall = new AntiNoFallService();
|
||||
AntiScaffoldService scaffold = new AntiScaffoldService();
|
||||
|
||||
assertNotNull(killAura);
|
||||
assertNotNull(reach);
|
||||
assertNotNull(speed);
|
||||
assertNotNull(jesus);
|
||||
assertNotNull(noFall);
|
||||
assertNotNull(scaffold);
|
||||
|
||||
// 验证所有服务的 getStatus() 都可调用
|
||||
assertNotNull(killAura.getStatus());
|
||||
assertNotNull(reach.getStatus());
|
||||
assertNotNull(speed.getStatus());
|
||||
assertNotNull(jesus.getStatus());
|
||||
assertNotNull(noFall.getStatus());
|
||||
assertNotNull(scaffold.getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package com.aluer.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class V40ChatSecurityTest {
|
||||
|
||||
// ================================================================
|
||||
// ChatFloodProtectionService Tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void chatFloodDetectsShortWindowFlood() {
|
||||
ChatFloodProtectionService cf = new ChatFloodProtectionService();
|
||||
// 3秒窗口内快速发送6条消息应触发洪水检测
|
||||
for (int i = 0; i < 6; i++) {
|
||||
cf.check("Player1", "Test message " + i);
|
||||
}
|
||||
// 第7条应该被拦截
|
||||
ChatFloodProtectionService.CheckResult r = cf.check("Player1", "Another message");
|
||||
assertTrue(r.isBlocked(), "6条消息在3秒窗口内应触发洪水检测");
|
||||
assertFalse(r.getReasons().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatFloodAllowsNormalChatRate() {
|
||||
ChatFloodProtectionService cf = new ChatFloodProtectionService();
|
||||
// 发送2条消息不应触发洪水
|
||||
ChatFloodProtectionService.CheckResult r1 = cf.check("Player2", "Hello");
|
||||
assertTrue(r1.isClean());
|
||||
ChatFloodProtectionService.CheckResult r2 = cf.check("Player2", "How are you?");
|
||||
assertTrue(r2.isClean());
|
||||
// 验证计数器运作
|
||||
Map<String, Object> status = cf.getStatus();
|
||||
assertTrue((Long) status.get("totalMessages") >= 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatFloodDetectsUnicodeFlood() {
|
||||
ChatFloodProtectionService cf = new ChatFloodProtectionService();
|
||||
// 构建超过50%为CJK特殊字符的长消息
|
||||
StringBuilder unicodeMsg = new StringBuilder();
|
||||
for (int i = 0; i < 50; i++) {
|
||||
unicodeMsg.append("测试测试测试测试"); // CJK字符
|
||||
}
|
||||
ChatFloodProtectionService.CheckResult r = cf.check("Player3", unicodeMsg.toString());
|
||||
assertTrue(r.isBlocked(), "超过50%CJK字符应触发Unicode洪水检测");
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatFloodDetectsSimilarMessages() {
|
||||
ChatFloodProtectionService cf = new ChatFloodProtectionService();
|
||||
// 连续3条高度相似的消息(编辑距离<3)
|
||||
cf.check("Player4", "Selling cheap items");
|
||||
cf.check("Player4", "Selling cheap items!"); // 只差1个字符
|
||||
ChatFloodProtectionService.CheckResult r = cf.check("Player4", "Selling cheap items!!"); // 只差2个字符
|
||||
assertTrue(r.isBlocked(), "连续3条高度相似的消息应触发重复洪水检测");
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatFloodStatusContainsExpectedFields() {
|
||||
ChatFloodProtectionService cf = new ChatFloodProtectionService();
|
||||
cf.check("Player5", "Message one");
|
||||
cf.check("Player5", "Message two");
|
||||
|
||||
Map<String, Object> status = cf.getStatus();
|
||||
assertNotNull(status.get("totalMessages"));
|
||||
assertNotNull(status.get("mutedPlayers"));
|
||||
assertNotNull(status.get("floodEvents"));
|
||||
assertNotNull(status.get("activeTrackers"));
|
||||
assertTrue(status.get("enabled") instanceof Boolean);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// AntiAdvertisementService Tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void antiAdDetectsIpAddress() {
|
||||
AntiAdvertisementService ad = new AntiAdvertisementService();
|
||||
AntiAdvertisementService.CheckResult r = ad.check("Player1",
|
||||
"Join my server at 123.45.67.89:25565!");
|
||||
assertTrue(r.isBlocked(), "消息中包含IP地址应被检测为广告");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAdDetectsDomain() {
|
||||
AntiAdvertisementService ad = new AntiAdvertisementService();
|
||||
AntiAdvertisementService.CheckResult r = ad.check("Player2",
|
||||
"Check out myserver.com for great gameplay!");
|
||||
assertTrue(r.isBlocked(), "消息中包含.com域名应被检测为广告");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAdDetectsDiscordInvite() {
|
||||
AntiAdvertisementService ad = new AntiAdvertisementService();
|
||||
AntiAdvertisementService.CheckResult r = ad.check("Player3",
|
||||
"Join our discord.gg/abcdef for giveaways!");
|
||||
assertTrue(r.isBlocked(), "消息中包含Discord邀请应被检测为广告");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAdDetectsQQGroup() {
|
||||
AntiAdvertisementService ad = new AntiAdvertisementService();
|
||||
AntiAdvertisementService.CheckResult r = ad.check("Player4",
|
||||
"加群一起玩:12345678");
|
||||
assertTrue(r.isBlocked(), "消息中包含QQ群号应被检测为广告");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAdAllowsNormalMessage() {
|
||||
AntiAdvertisementService ad = new AntiAdvertisementService();
|
||||
AntiAdvertisementService.CheckResult r = ad.check("Player5",
|
||||
"Hey guys, how is everyone doing today?");
|
||||
assertTrue(r.isClean(), "正常聊天消息不应被拦截");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAdWhitelistIpPasses() {
|
||||
AntiAdvertisementService ad = new AntiAdvertisementService();
|
||||
ad.addWhitelistedIp("192.168.1.1");
|
||||
AntiAdvertisementService.CheckResult r = ad.check("Player6",
|
||||
"Server IP is 192.168.1.1:25565");
|
||||
assertTrue(r.isClean(), "白名单IP不应被拦截");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiAdStatusHasCounters() {
|
||||
AntiAdvertisementService ad = new AntiAdvertisementService();
|
||||
ad.check("Player7", "Join 10.0.0.1");
|
||||
Map<String, Object> status = ad.getStatus();
|
||||
assertTrue((Long) status.get("totalMessages") >= 1);
|
||||
assertNotNull(status.get("adsDetected"));
|
||||
assertNotNull(status.get("blockedMessages"));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// AntiPhishingLinkService Tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void antiPhishDetectsKnownPhishingDomain() {
|
||||
AntiPhishingLinkService ap = new AntiPhishingLinkService();
|
||||
AntiPhishingLinkService.CheckResult r = ap.check("Player1",
|
||||
"Verify your account at http://mc-login.com/verify");
|
||||
assertTrue(r.isBlocked(), "已知钓鱼域名应被检测");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiPhishDetectsFakeMojangDomain() {
|
||||
AntiPhishingLinkService ap = new AntiPhishingLinkService();
|
||||
AntiPhishingLinkService.CheckResult r = ap.check("Player2",
|
||||
"Free cape at http://minecraft-login.net/free");
|
||||
assertTrue(r.isBlocked(), "包含minecraft关键字的非官方域名应被检测");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiPhishDetectsShortUrl() {
|
||||
AntiPhishingLinkService ap = new AntiPhishingLinkService();
|
||||
AntiPhishingLinkService.CheckResult r = ap.check("Player3",
|
||||
"Claim your reward: http://bit.ly/freemc123");
|
||||
assertTrue(r.isBlocked(), "短链接应被标记");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiPhishDetectsFreeMcScam() {
|
||||
AntiPhishingLinkService ap = new AntiPhishingLinkService();
|
||||
AntiPhishingLinkService.CheckResult r = ap.check("Player4",
|
||||
"Get free minecraft account at http://steal-account.info");
|
||||
assertTrue(r.isBlocked(), "免费Minecraft骗局应被检测");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiPhishDetectsMismatchedText() {
|
||||
AntiPhishingLinkService ap = new AntiPhishingLinkService();
|
||||
AntiPhishingLinkService.CheckResult r = ap.check("Player5",
|
||||
"Mojang官方验证请访问: http://phish-site.com/login");
|
||||
assertTrue(r.isBlocked(), "声称官方但链接非官方应被检测为文本链接不匹配");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiPhishAllowsNormalMessage() {
|
||||
AntiPhishingLinkService ap = new AntiPhishingLinkService();
|
||||
AntiPhishingLinkService.CheckResult r = ap.check("Player6",
|
||||
"Hey everyone! Let's build something cool today.");
|
||||
assertTrue(r.isClean(), "不含URL的正常消息不应被拦截");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiPhishStatusHasCounters() {
|
||||
AntiPhishingLinkService ap = new AntiPhishingLinkService();
|
||||
ap.check("Player7", "Visit http://mc-login.com");
|
||||
Map<String, Object> status = ap.getStatus();
|
||||
assertNotNull(status.get("totalUrls"));
|
||||
assertNotNull(status.get("phishingDetected"));
|
||||
assertNotNull(status.get("shortUrlsResolved"));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// AntiCommandAbuseService Tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void antiCmdDetectsCommandFlood() {
|
||||
AntiCommandAbuseService ac = new AntiCommandAbuseService();
|
||||
// 10秒窗口内快速发送12条命令应触发洪水检测
|
||||
for (int i = 0; i < 12; i++) {
|
||||
ac.check("Player1", "/tp " + i, "tp");
|
||||
}
|
||||
// 第13条应被拦截
|
||||
AntiCommandAbuseService.CheckResult r = ac.check("Player1", "/tp 13", "tp");
|
||||
assertTrue(r.isBlocked(), "10秒内超过10条命令应触发洪水检测");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiCmdDetectsShellInjection() {
|
||||
AntiCommandAbuseService ac = new AntiCommandAbuseService();
|
||||
AntiCommandAbuseService.CheckResult r = ac.check("Player2",
|
||||
"/say hello; rm -rf /", "say", "; rm -rf /", true);
|
||||
assertTrue(r.isBlocked(), "包含Shell注入字符的命令应被检测");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiCmdDetectsPluginScanning() {
|
||||
AntiCommandAbuseService ac = new AntiCommandAbuseService();
|
||||
// 连续执行不存在的命令(模拟插件扫描)
|
||||
for (int i = 0; i < 6; i++) {
|
||||
ac.check("Player3", "/nonexistent" + i, "nonexistent" + i, "", false);
|
||||
}
|
||||
AntiCommandAbuseService.CheckResult r = ac.check("Player3",
|
||||
"/nonexistent6", "nonexistent6", "", false);
|
||||
assertTrue(r.isBlocked(), "连续执行不存在命令应被检测为插件扫描");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiCmdAllowsNormalCommands() {
|
||||
AntiCommandAbuseService ac = new AntiCommandAbuseService();
|
||||
AntiCommandAbuseService.CheckResult r = ac.check("Player4",
|
||||
"/home", "home", null, true);
|
||||
assertTrue(r.isClean(), "正常命令不应被拦截");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiCmdDetectsDollarSubCommandInjection() {
|
||||
AntiCommandAbuseService ac = new AntiCommandAbuseService();
|
||||
AntiCommandAbuseService.CheckResult r = ac.check("Player5",
|
||||
"/say $(cat /etc/passwd)", "say", "$(cat /etc/passwd)", true);
|
||||
assertTrue(r.isBlocked(), "$() Shell注入应被检测");
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiCmdStatusHasCommandStats() {
|
||||
AntiCommandAbuseService ac = new AntiCommandAbuseService();
|
||||
ac.check("Player6", "/home", "home");
|
||||
ac.check("Player6", "/home", "home");
|
||||
ac.check("Player6", "/spawn", "spawn");
|
||||
|
||||
Map<String, Object> status = ac.getStatus();
|
||||
assertNotNull(status.get("totalCommands"));
|
||||
assertNotNull(status.get("blockedCommands"));
|
||||
assertNotNull(status.get("commandStats"));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PlayerPrivacyService Tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void privacyAnonymizesIpAddress() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
String result = pp.anonymizeIp("Player connected from 192.168.1.100");
|
||||
assertTrue(result.contains("xxx.xxx"), "IP地址应被脱敏为 xxx.xxx");
|
||||
assertTrue(result.contains("192.168"), "IP前两位应保留");
|
||||
assertFalse(result.contains("1.100"), "IP后两位应被隐藏");
|
||||
}
|
||||
|
||||
@Test
|
||||
void privacyAnonymizesMultipleIps() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
String result = pp.anonymizeIp("Connections: 10.0.0.1 and 172.16.5.42");
|
||||
assertTrue(result.contains("10.0.xxx.xxx"), "第一个IP应被脱敏");
|
||||
assertTrue(result.contains("172.16.xxx.xxx"), "第二个IP应被脱敏");
|
||||
}
|
||||
|
||||
@Test
|
||||
void privacyRegionLevelRoundsCoordinates() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
pp.setCoordinateAnonymizationLevel(
|
||||
PlayerPrivacyService.CoordinateAnonymizationLevel.REGION);
|
||||
String result = pp.anonymizeCoordinates(128.5, 64.1, -256.8);
|
||||
// 128.5 -> 130, 64.1 -> 60, -256.8 -> -260
|
||||
assertTrue(result.contains("130"), "X坐标应四舍五入到10");
|
||||
assertTrue(result.contains("60"), "Y坐标应四舍五入到10");
|
||||
assertTrue(result.contains("-260"), "Z坐标应四舍五入到10");
|
||||
}
|
||||
|
||||
@Test
|
||||
void privacyHiddenLevelHidesCoordinates() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
pp.setCoordinateAnonymizationLevel(
|
||||
PlayerPrivacyService.CoordinateAnonymizationLevel.HIDDEN);
|
||||
String result = pp.anonymizeCoordinates(100, 64, 200);
|
||||
assertEquals("[***, ***, ***]", result, "HIDDEN级别应完全隐藏坐标");
|
||||
}
|
||||
|
||||
@Test
|
||||
void privacyDataExportRequest() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
String requestId = pp.requestDataExport("Player1", "admin");
|
||||
assertNotNull(requestId, "导出请求应返回有效ID");
|
||||
|
||||
List<PlayerPrivacyService.DataExportRequest> requests = pp.getPendingRequests();
|
||||
assertEquals(1, requests.size());
|
||||
assertEquals("Player1", requests.get(0).getPlayerName());
|
||||
assertEquals("EXPORT", requests.get(0).getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void privacyDataDeletionRequest() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
String requestId = pp.requestDataDeletion("Player2", "player");
|
||||
assertNotNull(requestId, "删除请求应返回有效ID");
|
||||
|
||||
Map<String, Object> status = pp.getStatus();
|
||||
assertTrue(((Number) status.get("pendingDataRequests")).intValue() >= 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void privacyAuditRecordsAccess() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
pp.auditAccess("dashboard-api", "Player3", "IP_ADDRESS");
|
||||
pp.auditAccess("dashboard-api", "Player3", "COORDINATES");
|
||||
|
||||
List<PlayerPrivacyService.AuditEntry> audits = pp.getAuditForPlayer("Player3");
|
||||
assertEquals(2, audits.size(), "应记录2条审计日志");
|
||||
assertEquals("IP_ADDRESS", audits.get(0).getDataType());
|
||||
assertEquals("COORDINATES", audits.get(1).getDataType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void privacyPrefersNonNullAudit() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
List<PlayerPrivacyService.AuditEntry> audits = pp.getAuditForPlayer("NonExistent");
|
||||
assertNotNull(audits, "不存在的玩家应返回空列表而非null");
|
||||
assertTrue(audits.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void privacyStatusHasExpectedFields() {
|
||||
PlayerPrivacyService pp = new PlayerPrivacyService();
|
||||
pp.anonymizeIp("Player from 1.2.3.4");
|
||||
|
||||
Map<String, Object> status = pp.getStatus();
|
||||
assertNotNull(status.get("anonymizedEntries"));
|
||||
assertNotNull(status.get("pendingDataRequests"));
|
||||
assertNotNull(status.get("privacyViolations"));
|
||||
assertNotNull(status.get("coordinateLevel"));
|
||||
assertNotNull(status.get("retentionDays"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
package com.aluer.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* V4.0 玩家行为安全模块测试 —— 每个模块至少 3 个测试(正常/异常/状态)。
|
||||
*
|
||||
* 覆盖的模块:
|
||||
* 1. AntiNukerService — 快速破坏检测
|
||||
* 2. AntiAutoClickerService — 自动点击检测
|
||||
* 3. AntiChestStealService — 自动偷箱检测
|
||||
* 4. AntiAutoFishService — 自动钓鱼检测
|
||||
* 5. AntiInventoryManipulationService — 背包操作异常检测
|
||||
* 6. AntiBaritoneService — AI 寻路机器人检测
|
||||
*/
|
||||
class V40PlayerBehaviorTest {
|
||||
|
||||
// ==================== AntiNukerService Tests ====================
|
||||
|
||||
/**
|
||||
* 正常玩家的方块破坏行为应返回 clean。
|
||||
* 正常挖掘速度约 0.3-0.4 秒/块,500ms 内破坏 1-2 个方块是合理的。
|
||||
*/
|
||||
@Test
|
||||
void antiNukerNormalBreakingReturnsClean() {
|
||||
AntiNukerService service = new AntiNukerService();
|
||||
|
||||
// 在 500ms 间隔内只破坏 1 个方块(正常挖矿节奏)
|
||||
AntiNukerService.DetectionResult r = service.detect(
|
||||
"NormalPlayer", "uuid-001", "STONE", 10, 64, 10, "world");
|
||||
|
||||
assertTrue(r.isClean(), "Normal breaking should be clean");
|
||||
assertFalse(r.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速破坏模式(Nuker)应被检测为异常。
|
||||
* 在 500ms 窗口内破坏超过 3 个方块属于 Nuker 行为。
|
||||
*/
|
||||
@Test
|
||||
void antiNukerRapidBreakingDetectsNuker() {
|
||||
AntiNukerService service = new AntiNukerService();
|
||||
|
||||
// 模拟 500ms 内破坏 6 个方块(Nuker 特征)
|
||||
for (int i = 0; i < 6; i++) {
|
||||
service.detect("NukerBot", "uuid-002", "STONE",
|
||||
i, 64, i, "world");
|
||||
}
|
||||
|
||||
long violations = service.getNukerViolations();
|
||||
assertTrue(violations > 0 || service.getTotalBreaks() >= 6,
|
||||
"Rapid breaking should be detected as nuker violation");
|
||||
}
|
||||
|
||||
/**
|
||||
* 同 tick 破坏多块(多目标同时破坏)应被标记为 flagged。
|
||||
* 这是 Nuker 的终极特征——只有发包修改才能在同一 tick 破坏多个方块。
|
||||
*/
|
||||
@Test
|
||||
void antiNukerMultiTargetSameTickIsFlagged() {
|
||||
AntiNukerService service = new AntiNukerService();
|
||||
|
||||
// 在极短时间内破坏 3 个方块,模拟同一 tick 多目标破坏
|
||||
service.detect("TickHacker", "uuid-003", "STONE", 0, 64, 0, "world");
|
||||
service.detect("TickHacker", "uuid-003", "STONE", 1, 64, 1, "world");
|
||||
service.detect("TickHacker", "uuid-003", "STONE", 2, 64, 2, "world");
|
||||
|
||||
AntiNukerService.DetectionResult r = service.detect(
|
||||
"TickHacker", "uuid-003", "STONE", 3, 64, 3, "world");
|
||||
assertTrue(r.isFlagged() || r.isSuspicious(),
|
||||
"Multiple blocks in same tick should be suspicious or flagged");
|
||||
}
|
||||
|
||||
/**
|
||||
* 分散破坏模式——大范围破坏应被检测。
|
||||
* Nuker 在大范围同时破坏,正常玩家挖掘范围小。
|
||||
*/
|
||||
@Test
|
||||
void antiNukerScatteredBreakingDetected() {
|
||||
AntiNukerService service = new AntiNukerService();
|
||||
|
||||
// 在大范围内快速破坏方块
|
||||
service.detect("ScatterBot", "uuid-004", "STONE", 0, 64, 0, "world");
|
||||
service.detect("ScatterBot", "uuid-004", "STONE", 10, 64, 0, "world");
|
||||
service.detect("ScatterBot", "uuid-004", "STONE", 0, 64, 10, "world");
|
||||
service.detect("ScatterBot", "uuid-004", "STONE", 10, 64, 10, "world");
|
||||
AntiNukerService.DetectionResult r = service.detect(
|
||||
"ScatterBot", "uuid-004", "STONE", 20, 64, 20, "world");
|
||||
|
||||
// 分散模式可能会触发 suspicious
|
||||
assertNotNull(r);
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 应返回包含关键指标的 LinkedHashMap。
|
||||
*/
|
||||
@Test
|
||||
void antiNukerStatusReturnsExpectedFields() {
|
||||
AntiNukerService service = new AntiNukerService();
|
||||
service.detect("StatusTest", "uuid-005", "STONE", 0, 64, 0, "world");
|
||||
service.detect("StatusTest", "uuid-005", "STONE", 1, 64, 1, "world");
|
||||
|
||||
Map<String, Object> status = service.getStatus();
|
||||
|
||||
assertNotNull(status, "Status should not be null");
|
||||
assertTrue(status.containsKey("totalBreaks"), "Should contain totalBreaks");
|
||||
assertTrue(status.containsKey("nukerViolations"), "Should contain nukerViolations");
|
||||
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
|
||||
assertTrue(status.containsKey("flaggedPlayers"), "Should contain flaggedPlayers");
|
||||
assertTrue(status.containsKey("enabled"), "Should contain enabled");
|
||||
assertTrue(status.get("totalBreaks") instanceof Long);
|
||||
assertTrue((Long) status.get("totalBreaks") >= 2);
|
||||
}
|
||||
|
||||
// ==================== AntiAutoClickerService Tests ====================
|
||||
|
||||
/**
|
||||
* 正常点击频率应返回 clean(CPS < 15)。
|
||||
*/
|
||||
@Test
|
||||
void antiAutoClickerNormalCpsReturnsClean() {
|
||||
AntiAutoClickerService service = new AntiAutoClickerService();
|
||||
|
||||
// 正常 CPS:在 1 秒内有 10 次点击(正常上限内)
|
||||
for (int i = 0; i < 10; i++) {
|
||||
AntiAutoClickerService.DetectionResult r = service.detect(
|
||||
"NormalPlayer", "uuid-010", "LEFT_CLICK");
|
||||
// 前几个点击可能因样本不足返回 clean
|
||||
if (i >= 9) {
|
||||
assertTrue(r.isClean() || r.isSuspicious());
|
||||
assertFalse(r.isFlagged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 超高 CPS(> 20)应被检测为异常。
|
||||
*/
|
||||
@Test
|
||||
void antiAutoClickerHighCpsDetected() {
|
||||
AntiAutoClickerService service = new AntiAutoClickerService();
|
||||
|
||||
// 在短时间内大量点击模拟高 CPS
|
||||
for (int i = 0; i < 30; i++) {
|
||||
service.detect("ClickerBot", "uuid-011", "LEFT_CLICK");
|
||||
}
|
||||
|
||||
long highCps = service.getHighCpsCount();
|
||||
assertTrue(highCps > 0 || service.getMacroSuspected() > 0,
|
||||
"High CPS should be detected");
|
||||
}
|
||||
|
||||
/**
|
||||
* 均匀点击间隔(低熵)应被检测为宏行为。
|
||||
*/
|
||||
@Test
|
||||
void antiAutoClickerUniformIntervalsDetectedAsMacro() {
|
||||
AntiAutoClickerService service = new AntiAutoClickerService();
|
||||
|
||||
// 模拟均匀间隔的点击(宏特征)
|
||||
for (int i = 0; i < 25; i++) {
|
||||
service.detect("MacroBot", "uuid-012", "LEFT_CLICK");
|
||||
}
|
||||
|
||||
// 至少应有高 CPS 检测或宏疑似
|
||||
assertTrue(service.getTotalPlayers() >= 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 应包含 totalPlayers、highCpsCount、macroSuspected。
|
||||
*/
|
||||
@Test
|
||||
void antiAutoClickerStatusReturnsExpectedFields() {
|
||||
AntiAutoClickerService service = new AntiAutoClickerService();
|
||||
service.detect("StatusPlayer", "uuid-013", "LEFT_CLICK");
|
||||
|
||||
Map<String, Object> status = service.getStatus();
|
||||
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalPlayers"), "Should contain totalPlayers");
|
||||
assertTrue(status.containsKey("highCpsCount"), "Should contain highCpsCount");
|
||||
assertTrue(status.containsKey("macroSuspected"), "Should contain macroSuspected");
|
||||
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
|
||||
}
|
||||
|
||||
// ==================== AntiChestStealService Tests ====================
|
||||
|
||||
/**
|
||||
* 正常打开箱子应返回 clean。
|
||||
*/
|
||||
@Test
|
||||
void antiChestStealNormalOpenReturnsClean() {
|
||||
AntiChestStealService service = new AntiChestStealService();
|
||||
|
||||
AntiChestStealService.DetectionResult r = service.detectOpen(
|
||||
"NormalPlayer", "uuid-020", "CHEST", 100, 64, 100);
|
||||
|
||||
assertTrue(r.isClean(), "Normal chest open should be clean");
|
||||
assertFalse(r.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* 瞬间取走大量物品应被检测为 Chest Stealer。
|
||||
*/
|
||||
@Test
|
||||
void antiChestStealInstantLootDetected() {
|
||||
AntiChestStealService service = new AntiChestStealService();
|
||||
|
||||
// 先记录打开事件
|
||||
service.detectOpen("Stealer", "uuid-021", "CHEST", 100, 64, 100);
|
||||
|
||||
// 在打开后立即取走大量物品
|
||||
for (int i = 0; i < 8; i++) {
|
||||
service.detectTakeItem("Stealer", "uuid-021",
|
||||
"DIAMOND", 1, "CHEST", i);
|
||||
}
|
||||
|
||||
long instantLoot = service.getInstantLootCount();
|
||||
long violations = service.getStealViolations();
|
||||
assertTrue(instantLoot > 0 || violations > 0,
|
||||
"Instant loot should be detected");
|
||||
}
|
||||
|
||||
/**
|
||||
* 高价值物品快速取走应被检测。
|
||||
*/
|
||||
@Test
|
||||
void antiChestStealHighValueFastTakeDetected() {
|
||||
AntiChestStealService service = new AntiChestStealService();
|
||||
|
||||
service.detectOpen("Valuesteal", "uuid-022", "CHEST", 100, 64, 100);
|
||||
|
||||
// 连续取走高价值物品
|
||||
AntiChestStealService.DetectionResult r1 = service.detectTakeItem(
|
||||
"Valuesteal", "uuid-022", "DIAMOND", 1, "CHEST", 0);
|
||||
AntiChestStealService.DetectionResult r2 = service.detectTakeItem(
|
||||
"Valuesteal", "uuid-022", "NETHERITE_INGOT", 1, "CHEST", 1);
|
||||
AntiChestStealService.DetectionResult r3 = service.detectTakeItem(
|
||||
"Valuesteal", "uuid-022", "EMERALD", 1, "CHEST", 2);
|
||||
AntiChestStealService.DetectionResult r4 = service.detectTakeItem(
|
||||
"Valuesteal", "uuid-022", "TOTEM_OF_UNDYING", 1, "CHEST", 3);
|
||||
AntiChestStealService.DetectionResult r5 = service.detectTakeItem(
|
||||
"Valuesteal", "uuid-022", "ELYTRA", 1, "CHEST", 4);
|
||||
|
||||
// 至少部分检测应返回 suspicious 或 flagged
|
||||
assertNotNull(r1);
|
||||
assertNotNull(r5);
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 应包含 totalChestOpens、stealViolations、instantLootCount。
|
||||
*/
|
||||
@Test
|
||||
void antiChestStealStatusReturnsExpectedFields() {
|
||||
AntiChestStealService service = new AntiChestStealService();
|
||||
service.detectOpen("StatusPlayer", "uuid-023", "CHEST", 0, 64, 0);
|
||||
|
||||
Map<String, Object> status = service.getStatus();
|
||||
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalChestOpens"), "Should contain totalChestOpens");
|
||||
assertTrue(status.containsKey("stealViolations"), "Should contain stealViolations");
|
||||
assertTrue(status.containsKey("instantLootCount"), "Should contain instantLootCount");
|
||||
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
|
||||
assertTrue((Long) status.get("totalChestOpens") >= 1);
|
||||
}
|
||||
|
||||
// ==================== AntiAutoFishService Tests ====================
|
||||
|
||||
/**
|
||||
* 正常钓鱼抛竿应被记录。
|
||||
*/
|
||||
@Test
|
||||
void antiAutoFishNormalCastRecorded() {
|
||||
AntiAutoFishService service = new AntiAutoFishService();
|
||||
|
||||
service.recordCast("NormalFisher", "uuid-030", 100, 64, 100);
|
||||
|
||||
assertTrue(service.getTotalCastings() >= 1,
|
||||
"Casting should be recorded");
|
||||
}
|
||||
|
||||
/**
|
||||
* 连续极快收竿应被检测为自动钓鱼。
|
||||
*/
|
||||
@Test
|
||||
void antiAutoFishConsecutiveFastReelDetected() {
|
||||
AntiAutoFishService service = new AntiAutoFishService();
|
||||
|
||||
// 模拟多次快速抛竿和收竿
|
||||
for (int i = 0; i < 5; i++) {
|
||||
service.recordCast("AutoFisher", "uuid-031", 100, 64, 100);
|
||||
service.detectReel("AutoFisher", "uuid-031", true, 0, 0);
|
||||
}
|
||||
|
||||
long violations = service.getAutoFishViolations();
|
||||
// 多次连续快速收竿可能被检测
|
||||
assertTrue(service.getTotalCastings() >= 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 正常玩家单次收竿返回 clean。
|
||||
*/
|
||||
@Test
|
||||
void antiAutoFishNormalReelReturnsClean() {
|
||||
AntiAutoFishService service = new AntiAutoFishService();
|
||||
|
||||
service.recordCast("CasualFisher", "uuid-032", 50, 64, 50);
|
||||
AntiAutoFishService.DetectionResult r = service.detectReel(
|
||||
"CasualFisher", "uuid-032", false, 0, 0);
|
||||
|
||||
// 单次收竿(首次)应返回 clean 或 suspicious(样本不足)
|
||||
assertNotNull(r);
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 应包含 totalCastings、autoFishViolations、reactionTimeStats。
|
||||
*/
|
||||
@Test
|
||||
void antiAutoFishStatusReturnsExpectedFields() {
|
||||
AntiAutoFishService service = new AntiAutoFishService();
|
||||
service.recordCast("StatusFisher", "uuid-033", 100, 64, 100);
|
||||
service.detectReel("StatusFisher", "uuid-033", true, 0, 0);
|
||||
|
||||
Map<String, Object> status = service.getStatus();
|
||||
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalCastings"), "Should contain totalCastings");
|
||||
assertTrue(status.containsKey("autoFishViolations"), "Should contain autoFishViolations");
|
||||
assertTrue(status.containsKey("reactionTimeStats"), "Should contain reactionTimeStats");
|
||||
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
|
||||
}
|
||||
|
||||
// ==================== AntiInventoryManipulationService Tests ====================
|
||||
|
||||
/**
|
||||
* 正常背包物品移动应返回 clean。
|
||||
*/
|
||||
@Test
|
||||
void antiInventoryNormalItemMoveReturnsClean() {
|
||||
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
|
||||
|
||||
AntiInventoryManipulationService.DetectionResult r = service.detectItemMove(
|
||||
"NormalPlayer", "uuid-040", 0, 1, "STONE", 64, -1);
|
||||
|
||||
assertTrue(r.isClean(), "Normal item move should be clean");
|
||||
assertFalse(r.isFlagged());
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问非法槽位(负索引或超范围)应被检测。
|
||||
*/
|
||||
@Test
|
||||
void antiInventoryInvalidSlotDetected() {
|
||||
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
|
||||
|
||||
// 访问负索引槽位
|
||||
AntiInventoryManipulationService.DetectionResult r = service.detectItemMove(
|
||||
"SlotHacker", "uuid-041", -1, 0, "DIAMOND", 1, -1);
|
||||
|
||||
assertTrue(r.isFlagged() || r.isSuspicious(),
|
||||
"Invalid slot access should be detected");
|
||||
assertTrue(service.getSlotViolations() >= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量丢弃(单 tick 丢弃多个物品)应被检测。
|
||||
* 注意:第 2 次同 tick 丢弃会触发 flagged 标记,后续丢弃被拦截不再计数。
|
||||
*/
|
||||
@Test
|
||||
void antiInventoryBatchDropDetected() {
|
||||
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
|
||||
|
||||
// 快速丢弃多个物品——前两次在同一个 tick,触发批量丢弃检测
|
||||
AntiInventoryManipulationService.DetectionResult r1 = service.detectItemDrop(
|
||||
"DropBot", "uuid-042", "COBBLESTONE", 1, 0);
|
||||
assertTrue(r1.isClean(), "Single drop should be clean");
|
||||
|
||||
AntiInventoryManipulationService.DetectionResult r2 = service.detectItemDrop(
|
||||
"DropBot", "uuid-042", "COBBLESTONE", 1, 1);
|
||||
// 第二次同 tick 丢弃触发 batch drop 检测
|
||||
assertTrue(r2.isFlagged() || r2.isSuspicious(),
|
||||
"Two drops in same tick should be detected as batch drop");
|
||||
|
||||
assertTrue(service.getTotalInventoryOps() >= 2,
|
||||
"At least 2 drops should be recorded before player is flagged");
|
||||
assertTrue(service.getManipulationViolations() >= 1,
|
||||
"Batch drop should trigger manipulation violation");
|
||||
}
|
||||
|
||||
/**
|
||||
* 超范围槽位访问应触发 slotViolations 计数器。
|
||||
*/
|
||||
@Test
|
||||
void antiInventoryOversizedSlotAccessDetected() {
|
||||
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
|
||||
|
||||
// 访问超出背包大小的槽位
|
||||
AntiInventoryManipulationService.DetectionResult r = service.detectItemMove(
|
||||
"RangeHacker", "uuid-043", 0, 99, "DIAMOND_BLOCK", 1, -1);
|
||||
|
||||
assertTrue(r.isFlagged() || r.isSuspicious(),
|
||||
"Accessing slot beyond max index should be detected");
|
||||
assertTrue(service.getSlotViolations() >= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 应包含 totalInventoryOps、manipulationViolations、slotViolations。
|
||||
*/
|
||||
@Test
|
||||
void antiInventoryStatusReturnsExpectedFields() {
|
||||
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
|
||||
service.detectItemMove("StatusPlayer", "uuid-044", 0, 1, "DIRT", 64, -1);
|
||||
|
||||
Map<String, Object> status = service.getStatus();
|
||||
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalInventoryOps"), "Should contain totalInventoryOps");
|
||||
assertTrue(status.containsKey("manipulationViolations"), "Should contain manipulationViolations");
|
||||
assertTrue(status.containsKey("slotViolations"), "Should contain slotViolations");
|
||||
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
|
||||
assertTrue((Long) status.get("totalInventoryOps") >= 1);
|
||||
}
|
||||
|
||||
// ==================== AntiBaritoneService Tests ====================
|
||||
|
||||
/**
|
||||
* 正常玩家移动应返回 clean。
|
||||
*/
|
||||
@Test
|
||||
void antiBaritoneNormalMovementReturnsClean() {
|
||||
AntiBaritoneService service = new AntiBaritoneService();
|
||||
|
||||
// 记录几次正常移动
|
||||
for (int i = 0; i < 5; i++) {
|
||||
AntiBaritoneService.DetectionResult r = service.detectMovement(
|
||||
"NormalPlayer", "uuid-050", 10 + i * 0.3, 64, 10 + i * 0.5,
|
||||
90.0f, false, false);
|
||||
// 前几个样本可能因数据不足返回 clean
|
||||
assertNotNull(r);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无社交交互的长时间在线应被检测为 Bot 行为。
|
||||
*/
|
||||
@Test
|
||||
void antiBaritoneLongSessionNoSocialDetected() {
|
||||
AntiBaritoneService service = new AntiBaritoneService();
|
||||
|
||||
// 模拟 Bot 长时间移动但无社交交互(需要足够样本触发检测)
|
||||
for (int i = 0; i < 50; i++) {
|
||||
service.detectMovement(
|
||||
"SilentBot", "uuid-051",
|
||||
100 + i * 0.5, 64, 100 + i * 0.5,
|
||||
(float) (i * 2), i % 2 == 0, false);
|
||||
}
|
||||
|
||||
assertTrue(service.getTotalTracked() >= 1,
|
||||
"Bot player should be tracked");
|
||||
}
|
||||
|
||||
/**
|
||||
* 机械式视角移动(恒定角速度)应被检测。
|
||||
*/
|
||||
@Test
|
||||
void antiBaritoneMechanicalLookDetected() {
|
||||
AntiBaritoneService service = new AntiBaritoneService();
|
||||
|
||||
// 模拟恒定角速度的视角移动(Baritone Lerp 特征)
|
||||
for (int i = 0; i < 35; i++) {
|
||||
float yaw = (float) (i * 2.0); // 恒定的 2 度增量
|
||||
float pitch = 0.0f;
|
||||
AntiBaritoneService.DetectionResult r = service.detectLook(
|
||||
"LookBot", "uuid-052", yaw, pitch);
|
||||
assertNotNull(r);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动聊天快速响应应被检测。
|
||||
*/
|
||||
@Test
|
||||
void antiBaritoneAutoChatFastResponseDetected() {
|
||||
AntiBaritoneService service = new AntiBaritoneService();
|
||||
|
||||
// 模拟 300ms 的极速响应(Baritone AutoChat 特征)
|
||||
AntiBaritoneService.DetectionResult r = service.detectChat(
|
||||
"ChatBot", "uuid-053", "Yes, I am mining here", true, 300);
|
||||
|
||||
assertTrue(r.isFlagged() || r.isSuspicious(),
|
||||
"Auto chat response under 500ms should be detected");
|
||||
}
|
||||
|
||||
/**
|
||||
* 正常聊天消息应返回 clean。
|
||||
*/
|
||||
@Test
|
||||
void antiBaritoneNormalChatReturnsClean() {
|
||||
AntiBaritoneService service = new AntiBaritoneService();
|
||||
|
||||
AntiBaritoneService.DetectionResult r = service.detectChat(
|
||||
"NormalChatPlayer", "uuid-054",
|
||||
"Hello everyone!", false, 0);
|
||||
|
||||
assertTrue(r.isClean(),
|
||||
"Normal chat message should be clean");
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatus() 应包含 totalTracked、baritoneSuspected、confidenceScores。
|
||||
*/
|
||||
@Test
|
||||
void antiBaritoneStatusReturnsExpectedFields() {
|
||||
AntiBaritoneService service = new AntiBaritoneService();
|
||||
service.detectMovement("StatusBot", "uuid-055",
|
||||
0, 64, 0, 0, false, false);
|
||||
service.detectLook("StatusBot", "uuid-055", 45.0f, 0.0f);
|
||||
service.detectChat("StatusBot", "uuid-055", "ok", false, 0);
|
||||
|
||||
Map<String, Object> status = service.getStatus();
|
||||
|
||||
assertNotNull(status);
|
||||
assertTrue(status.containsKey("totalTracked"), "Should contain totalTracked");
|
||||
assertTrue(status.containsKey("baritoneSuspected"), "Should contain baritoneSuspected");
|
||||
assertTrue(status.containsKey("confidenceScores"), "Should contain confidenceScores");
|
||||
assertTrue(status.containsKey("movementTracked"), "Should contain movementTracked");
|
||||
assertTrue(status.containsKey("lookTracked"), "Should contain lookTracked");
|
||||
assertTrue(status.containsKey("interactionTracked"), "Should contain interactionTracked");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
package com.aluer.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* V4.0 服务器保护模块测试类
|
||||
* 覆盖6个新增安全模块:PacketFloodProtection, AntiSignExploit, AntiBookBan,
|
||||
* AntiResourcePackExploit, AntiTabCompleteCrash, AntiOfflineModeSpoof
|
||||
*/
|
||||
class V40ServerProtectionTest {
|
||||
|
||||
// ==================== PacketFloodProtectionService ====================
|
||||
|
||||
@Test
|
||||
void packetFloodAllowsNormalTraffic() {
|
||||
PacketFloodProtectionService svc = new PacketFloodProtectionService();
|
||||
// 正常情况下几个数据包不影响
|
||||
assertTrue(svc.check("Player1", "ChatMessage", null).isClean());
|
||||
assertTrue(svc.check("Player1", "CustomPayload", "minecraft:register").isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void packetFloodDetectsChatMessageFlood() {
|
||||
PacketFloodProtectionService svc = new PacketFloodProtectionService();
|
||||
// 模拟聊天洪水:5秒内发送远超20/s的数据包
|
||||
for (int i = 0; i < 150; i++) {
|
||||
svc.check("Flooder1", "ChatMessage", null);
|
||||
}
|
||||
PacketFloodProtectionService.PacketFloodResult r = svc.check("Flooder1", "ChatMessage", null);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("ChatMessage")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void packetFloodDetectsBrandChannelFlood() {
|
||||
PacketFloodProtectionService svc = new PacketFloodProtectionService();
|
||||
// 品牌包在60秒内超过3次即为异常
|
||||
for (int i = 0; i < 5; i++) {
|
||||
svc.check("SuspiciousPlayer", "PluginMessage", "minecraft:brand");
|
||||
}
|
||||
PacketFloodProtectionService.PacketFloodResult r = svc.check(
|
||||
"SuspiciousPlayer", "PluginMessage", "minecraft:brand");
|
||||
assertTrue(r.isBlocked());
|
||||
}
|
||||
|
||||
@Test
|
||||
void packetFloodIncrementalPenaltyIncreases() {
|
||||
PacketFloodProtectionService svc = new PacketFloodProtectionService();
|
||||
// CustomPayload阈值=50/s即250/5s窗口,需发送>250个才触发
|
||||
for (int i = 0; i < 300; i++) {
|
||||
svc.check("Attacker1", "CustomPayload", "bad:channel");
|
||||
}
|
||||
PacketFloodProtectionService.PacketFloodResult r1 = svc.check("Attacker1", "CustomPayload", "bad:channel");
|
||||
assertTrue(r1.isBlocked());
|
||||
// 递增惩罚应该有起步延迟(首次违规100ms)
|
||||
assertTrue(r1.getPenaltyMs() >= 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void packetFloodStatusHasCounters() {
|
||||
PacketFloodProtectionService svc = new PacketFloodProtectionService();
|
||||
svc.check("P1", "ChatMessage", null);
|
||||
svc.check("P2", "CustomPayload", "test:chan");
|
||||
Map<String, Object> s = svc.getStatus();
|
||||
assertNotNull(s.get("totalPackets"));
|
||||
assertNotNull(s.get("blockedCount"));
|
||||
assertNotNull(s.get("packetTypeStats"));
|
||||
assertTrue(((Number) s.get("totalPackets")).longValue() >= 2);
|
||||
}
|
||||
|
||||
// ==================== AntiSignExploitService ====================
|
||||
|
||||
@Test
|
||||
void antiSignExploitAllowsNormalSign() {
|
||||
AntiSignExploitService svc = new AntiSignExploitService();
|
||||
String[] lines = {"Hello", "World", "", ""};
|
||||
AntiSignExploitService.SignCheckResult r = svc.check("Player1", "world", 0, 64, 0, lines, 0);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiSignExploitDetectsDeepNBTNesting() {
|
||||
AntiSignExploitService svc = new AntiSignExploitService();
|
||||
String[] lines = {"test", "", "", ""};
|
||||
// NBT深度超过8层
|
||||
AntiSignExploitService.SignCheckResult r = svc.check("Hacker", "world", 0, 64, 0, lines, 15);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("NBT_DEEP_NESTING")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiSignExploitDetectsOversizedText() {
|
||||
AntiSignExploitService svc = new AntiSignExploitService();
|
||||
// 超过10000字符的单行文本
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{\"text\":\"");
|
||||
for (int i = 0; i < 11000; i++) sb.append("A");
|
||||
sb.append("\"}");
|
||||
String[] lines = {sb.toString(), "", "", ""};
|
||||
AntiSignExploitService.SignCheckResult r = svc.check("Hacker", "world", 0, 64, 0, lines, 0);
|
||||
assertTrue(r.isBlocked());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiSignExploitDetectsClickCommandInjection() {
|
||||
AntiSignExploitService svc = new AntiSignExploitService();
|
||||
String[] lines = {"{\"text\":\"Click me\",\"clickEvent\":{\"action\":\"run_command\",\"value\":\"/op Hacker\"}}",
|
||||
"", "", ""};
|
||||
AntiSignExploitService.SignCheckResult r = svc.check("Hacker", "world", 0, 64, 0, lines, 0);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("CLICK_COMMAND")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiSignExploitDetectsDangerousPlaceholder() {
|
||||
AntiSignExploitService svc = new AntiSignExploitService();
|
||||
String[] lines = {"{\"text\":\"${system:exec:rm -rf /}\"}", "", "", ""};
|
||||
AntiSignExploitService.SignCheckResult r = svc.check("Hacker", "world", 0, 64, 0, lines, 0);
|
||||
assertTrue(r.isBlocked());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiSignExploitStatusHasCounters() {
|
||||
AntiSignExploitService svc = new AntiSignExploitService();
|
||||
svc.check("P1", "world", 0, 64, 0, new String[]{"Hello"}, 0);
|
||||
Map<String, Object> s = svc.getStatus();
|
||||
assertNotNull(s.get("totalSignsChecked"));
|
||||
assertNotNull(s.get("exploitBlocked"));
|
||||
assertNotNull(s.get("lastBlockedDetails"));
|
||||
}
|
||||
|
||||
// ==================== AntiBookBanService ====================
|
||||
|
||||
@Test
|
||||
void antiBookBanAllowsNormalBook() {
|
||||
AntiBookBanService svc = new AntiBookBanService();
|
||||
List<String> pages = List.of("{\"text\":\"Hello World\"}", "{\"text\":\"Page 2\"}");
|
||||
AntiBookBanService.BookCheckResult r = svc.check("Player1", "My Book", pages, "Player1", 0, 500);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiBookBanDetectsExcessivePages() {
|
||||
AntiBookBanService svc = new AntiBookBanService();
|
||||
// 超过100页
|
||||
List<String> pages = new ArrayList<>();
|
||||
for (int i = 0; i < 150; i++) {
|
||||
pages.add("{\"text\":\"Page " + i + "\"}");
|
||||
}
|
||||
AntiBookBanService.BookCheckResult r = svc.check("Hacker", "Bad Book", pages, "Hacker", 0, 5000);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("EXCESSIVE_PAGES")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiBookBanDetectsOversizedPage() {
|
||||
AntiBookBanService svc = new AntiBookBanService();
|
||||
// 单页超过32767字符
|
||||
StringBuilder sb = new StringBuilder("{\"text\":\"");
|
||||
for (int i = 0; i < 33000; i++) sb.append("B");
|
||||
sb.append("\"}");
|
||||
List<String> pages = List.of(sb.toString());
|
||||
AntiBookBanService.BookCheckResult r = svc.check("Hacker", "Overflow Book", pages, "Hacker", 0, 50000);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("OVERSIZED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiBookBanDetectsDangerousTranslate() {
|
||||
AntiBookBanService svc = new AntiBookBanService();
|
||||
// translate组件引用危险翻译键
|
||||
List<String> pages = List.of("{\"translate\":\"translation.test.none\",\"color\":\"red\"}");
|
||||
AntiBookBanService.BookCheckResult r = svc.check("Hacker", "Translate Exploit", pages, "Hacker", 0, 200);
|
||||
assertTrue(r.isFlagged() || r.isBlocked());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiBookBanDetectsDeepJsonNesting() {
|
||||
AntiBookBanService svc = new AntiBookBanService();
|
||||
// 构造深度超过15层的嵌套JSON
|
||||
StringBuilder sb = new StringBuilder("{\"text\":\"x\"");
|
||||
for (int i = 0; i < 20; i++) {
|
||||
sb = new StringBuilder("{\"extra\":[" + sb + "]}");
|
||||
}
|
||||
List<String> pages = List.of(sb.toString());
|
||||
AntiBookBanService.BookCheckResult r = svc.check("Hacker", "Deep Nest Book", pages, "Hacker", 0, 500);
|
||||
assertTrue(r.isFlagged() || r.isBlocked());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiBookBanStatusHasCounters() {
|
||||
AntiBookBanService svc = new AntiBookBanService();
|
||||
svc.check("P1", "Test", List.of("{\"text\":\"Hello\"}"), "P1", 0, 100);
|
||||
Map<String, Object> s = svc.getStatus();
|
||||
assertNotNull(s.get("totalBooksChecked"));
|
||||
assertNotNull(s.get("bannedBooks"));
|
||||
}
|
||||
|
||||
// ==================== AntiResourcePackExploitService ====================
|
||||
|
||||
@Test
|
||||
void antiResourcePackAllowsNormalPack() {
|
||||
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
|
||||
// SHA-1必须是40位十六进制字符,否则格式校验会拒绝
|
||||
AntiResourcePackExploitService.PackCheckResult r = svc.check(
|
||||
"https://example.com/resourcepack.zip",
|
||||
"a1b2c3d4e5f6789012345678901234567890abcd",
|
||||
"Admin", 9);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiResourcePackDetectsFileScheme() {
|
||||
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
|
||||
AntiResourcePackExploitService.PackCheckResult r = svc.check(
|
||||
"file:///etc/passwd",
|
||||
"a1b2c3d4e5f6789012345678901234567890abc",
|
||||
"Hacker", 9);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("SCHEME") || s.contains("BLOCKED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiResourcePackDetectsInternalNetwork() {
|
||||
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
|
||||
AntiResourcePackExploitService.PackCheckResult r = svc.check(
|
||||
"http://192.168.1.100/malware.zip",
|
||||
"a1b2c3d4e5f6789012345678901234567890abc",
|
||||
"Hacker", 9);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("PRIVATE") || s.contains("SSRF")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiResourcePackDetectsBlacklistedSha1() {
|
||||
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
|
||||
String maliciousSha1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||
svc.addToSha1Blacklist(maliciousSha1);
|
||||
AntiResourcePackExploitService.PackCheckResult r = svc.check(
|
||||
"https://cdn.example.com/pack.zip", maliciousSha1, "Admin", 9);
|
||||
assertTrue(r.isBlocked());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiResourcePackDetectsDangerousExtension() {
|
||||
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
|
||||
AntiResourcePackExploitService.PackCheckResult r = svc.check(
|
||||
"https://evil.com/trojan.jar",
|
||||
"a1b2c3d4e5f6789012345678901234567890abc",
|
||||
"Hacker", 9);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("DANGEROUS") || s.contains("EXTENSION")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiResourcePackStatusHasCounters() {
|
||||
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
|
||||
svc.check("https://safe.com/pack.zip", "a1b2c3d4e5f6789012345678901234567890abc", "Admin", 9);
|
||||
Map<String, Object> s = svc.getStatus();
|
||||
assertNotNull(s.get("totalPacksChecked"));
|
||||
assertNotNull(s.get("blockedPacks"));
|
||||
assertNotNull(s.get("urlBlacklistSize"));
|
||||
}
|
||||
|
||||
// ==================== AntiTabCompleteCrashService ====================
|
||||
|
||||
@Test
|
||||
void antiTabCompleteAllowsNormalRequest() {
|
||||
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
|
||||
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Player1", "gamemode c", 8, 1);
|
||||
assertTrue(r.isClean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiTabCompleteDetectsOversizedRequest() {
|
||||
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
|
||||
// 构造超过8192字符的请求
|
||||
StringBuilder sb = new StringBuilder("execute as @e[tag=");
|
||||
for (int i = 0; i < 10000; i++) sb.append("a");
|
||||
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Hacker", sb.toString(), 0, 1);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("OVERSIZED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiTabCompleteDetectsDeepNesting() {
|
||||
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
|
||||
// 构造深度超过50层的嵌套括号
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 60; i++) sb.append("(\"");
|
||||
sb.append("test");
|
||||
for (int i = 0; i < 60; i++) sb.append("\")");
|
||||
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Hacker", sb.toString(), 0, 1);
|
||||
assertTrue(r.isBlocked());
|
||||
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("NESTING")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiTabCompleteDetectsTabFlood() {
|
||||
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
|
||||
// 高频补全请求洪水
|
||||
for (int i = 0; i < 150; i++) {
|
||||
svc.check("Flooder", "command " + i, 8, i);
|
||||
}
|
||||
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Flooder", "command x", 8, 999);
|
||||
assertTrue(r.isBlocked() || r.isSuspicious());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antiTabCompleteDetectsInvalidUtf8() {
|
||||
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
|
||||
// 包含无效UTF-8字节序列的请求
|
||||
String badText = "test | ||||