安全模块大规模重构:151文件从com.aluer.security分包到8个子包
新包结构: - com.aluer.anticheat.combat (16) — KillAura/Reach/Criticals/CrystalAura/Totem等战斗反作弊 - com.aluer.anticheat.movement (18) — Fly/Speed/Jesus/NoFall/Timer/Phase等移动反作弊 - com.aluer.anticheat.world (14) — Nuker/Xray/AutoMine/VeinMiner等世界反作弊 - com.aluer.anticheat.player (8) — FakePlayer/AltAccount/NameSpoof等玩家反作弊 - com.aluer.network (32) — DDoS/Protocol/TokenBucket/Firewall等网络安全 - com.aluer.server (10) — ChunkLoad/EntityCount/Redstone/CrashExploit等服务器保护 - com.aluer.chat (5) — ChatFlood/Advertisement/Phishing/CommandAbuse等聊天安全 - com.aluer.defense (50) — WAF/IDS/IPS/Honeypot/EDR/Forensics等纵深防御 所有package声明+跨包import已更新,323测试全绿
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
package com.aluer.anticheat.player;
|
||||
|
||||
import com.aluer.config.ServerGuardConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.Normalizer;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 玩家名称冒充检测 — V4.0 访问控制模块
|
||||
*
|
||||
* 检测原理:
|
||||
* 1. 检测新加入玩家的名称是否通过Unicode同形字符(homoglyph)冒充现有玩家或管理员
|
||||
* - 例如用 ᴀ (U+1D00) 替换 A, 用 ο (Greek omicron U+03BF) 替换 o
|
||||
* - 用 і (Cyrillic U+0456) 替换 i, 用 а (Cyrillic U+0430) 替换 a
|
||||
* 2. 检测包含不可见/零宽字符的玩家名(U+200B零宽空格、U+200C零宽非连接符等)
|
||||
* 这些字符在视觉上不可见但会使玩家名在系统中识别为不同名称
|
||||
* 3. 大小写欺骗检测:Admin → admin, ADMIN, Admín 等变体
|
||||
* Minecraft用户名不区分大小写,但利用相似Unicode字符可绕过展示层检查
|
||||
* 4. 维护已知冒充目标名单(所有在线OP玩家名 + 知名Minecraft人物名 + 服务器自定义名单)
|
||||
* 新玩家加入时计算与该名单中每一项的相似度
|
||||
* 5. 使用 Levenshtein 编辑距离算法计算名称相似度,并配合 Unicode NFKC 标准化后比对
|
||||
* 相似度阈值可配置,超过阈值的名称标记为冒充尝试
|
||||
* 6. 检测名称中包含的空格/特殊前缀后缀(如 [Admin] PlayerName 格式混淆)
|
||||
*
|
||||
* 配置开关:serverguard.security.super-evolution.anti-name-spoof
|
||||
*/
|
||||
@Service
|
||||
public class AntiNameSpoofService {
|
||||
|
||||
private final ServerGuardConfig config;
|
||||
|
||||
/** 需保护的名称集合:OP名、管理员名、知名人物名 */
|
||||
private final Set<String> protectedNames = ConcurrentHashMap.newKeySet();
|
||||
/** 自定义虚假目标(特殊名单) */
|
||||
private final Set<String> customSpoofTargets = ConcurrentHashMap.newKeySet();
|
||||
/** 已检测到的冒充尝试 */
|
||||
private final Map<String, List<SpoofAttempt>> spoofHistory = new ConcurrentHashMap<>();
|
||||
/** 已被阻止的冒充名称集合 */
|
||||
private final Set<String> blockedNames = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final AtomicLong totalNameChecks = new AtomicLong(0);
|
||||
private final AtomicLong spoofAttempts = new AtomicLong(0);
|
||||
|
||||
/**
|
||||
* Unicode同形字符映射表:视觉相似但Unicode码点不同的字符
|
||||
* key = 标准ASCII字母,value = 其Unicode同形字符列表
|
||||
*/
|
||||
private static final Map<Character, List<Character>> HOMOGLYPH_MAP = Map.ofEntries(
|
||||
Map.entry('A', List.of('A', 'A', 'Á', 'À', 'Â', 'Ã', 'Ä', 'Å', 'Ā', 'Ă', 'Ą')),
|
||||
Map.entry('a', List.of('a', 'á', 'à', 'â', 'ã', 'ä', 'å', 'ā', 'ă', 'ą', '@')),
|
||||
Map.entry('B', List.of('B', 'ß', 'Þ')),
|
||||
Map.entry('b', List.of('b', 'þ')),
|
||||
Map.entry('C', List.of('C', 'Ç', 'Ć', 'Č')),
|
||||
Map.entry('c', List.of('c', 'ç', 'ć', 'č')),
|
||||
Map.entry('E', List.of('E', 'È', 'É', 'Ê', 'Ë', 'Ē', 'Ĕ', 'Ė', 'Ę', 'Ě')),
|
||||
Map.entry('e', List.of('e', 'è', 'é', 'ê', 'ë', 'ē', 'ĕ', 'ė', 'ę', 'ě')),
|
||||
Map.entry('H', List.of('H', 'Ĥ', 'Ħ')),
|
||||
Map.entry('h', List.of('h', 'ĥ', 'ħ')),
|
||||
Map.entry('I', List.of('I', 'Ì', 'Í', 'Î', 'Ï', 'Ĩ', 'Ī', 'Ĭ', 'Į', 'İ')),
|
||||
Map.entry('i', List.of('i', 'ì', 'í', 'î', 'ï', 'ĩ', 'ī', 'ĭ', 'į', 'ı', '¡')),
|
||||
Map.entry('K', List.of('K', 'Ķ', 'Œ')),
|
||||
Map.entry('k', List.of('k', 'ķ', 'ĸ')),
|
||||
Map.entry('l', List.of('l', 'ł', 'ŀ', 'ĺ', 'ļ', 'ľ')),
|
||||
Map.entry('N', List.of('N', 'Ñ', 'Ń', 'Ņ', 'Ň')),
|
||||
Map.entry('n', List.of('n', 'ñ', 'ń', 'ņ', 'ň', 'ʼn')),
|
||||
Map.entry('O', List.of('O', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø', 'Ō', 'Ŏ', 'Ő')),
|
||||
Map.entry('o', List.of('o', 'ò', 'ó', 'ô', 'õ', 'ö', 'ø', 'ō', 'ŏ', 'ő', 'ο', 'о')),
|
||||
Map.entry('P', List.of('P', 'Þ')),
|
||||
Map.entry('p', List.of('p', 'þ')),
|
||||
Map.entry('S', List.of('S', 'Ś', 'Ŝ', 'Ş', 'Š')),
|
||||
Map.entry('s', List.of('s', 'ś', 'ŝ', 'ş', 'š')),
|
||||
Map.entry('T', List.of('T', 'Ţ', 'Ť', 'Ŧ')),
|
||||
Map.entry('t', List.of('t', 'ţ', 'ť', 'ŧ')),
|
||||
Map.entry('U', List.of('U', 'Ù', 'Ú', 'Û', 'Ü', 'Ũ', 'Ū', 'Ŭ', 'Ů', 'Ű', 'Ų')),
|
||||
Map.entry('u', List.of('u', 'ù', 'ú', 'û', 'ü', 'ũ', 'ū', 'ŭ', 'ů', 'ű', 'ų', 'µ')),
|
||||
Map.entry('Y', List.of('Y', 'Ý', 'Ŷ', 'Ÿ')),
|
||||
Map.entry('y', List.of('y', 'ý', 'ÿ', 'ŷ')),
|
||||
Map.entry('Z', List.of('Z', 'Ź', 'Ż', 'Ž')),
|
||||
Map.entry('z', List.of('z', 'ź', 'ż', 'ž'))
|
||||
);
|
||||
|
||||
/** 零宽/不可见Unicode字符 */
|
||||
private static final Set<Character> INVISIBLE_CHARACTERS = Set.of(
|
||||
'', // 零宽空格 Zero Width Space
|
||||
'', // 零宽非连接符 Zero Width Non-Joiner
|
||||
'', // 零宽连接符 Zero Width Joiner
|
||||
'', // 左到右标记 Left-to-Right Mark
|
||||
'', // 右到左标记 Right-to-Left Mark
|
||||
'', // 词连接符 Word Joiner
|
||||
'', // 函数应用 Function Application
|
||||
'', // 不可见乘号 Invisible Times
|
||||
'', // 不可见分隔符 Invisible Separator
|
||||
'', // 不可见加号 Invisible Plus
|
||||
'', // 零宽不中断空格 Zero Width No-Break Space / BOM
|
||||
'', // 软连接符 Soft Hyphen
|
||||
'͏', // 组合用字素连接符 Combining Grapheme Joiner
|
||||
'', // 阿拉伯字母格式标记 Arabic Letter Mark
|
||||
'', // 蒙古语元音分隔符(已在Unicode 6.3弃用但仍可能被滥用)
|
||||
'', // 左到右隔离 Left-to-Right Isolate
|
||||
'', // 右到左隔离 Right-to-Left Isolate
|
||||
'', // 第一个强隔离 First Strong Isolate
|
||||
'' // 弹出方向隔离 Pop Directional Isolate
|
||||
);
|
||||
|
||||
/** Minecraft知名人物名(常被冒充目标) */
|
||||
private static final Set<String> WELL_KNOWN_NAMES = Set.of(
|
||||
"Notch", "jeb_", "Dinnerbone", "Grumm",
|
||||
"Dream", "Technoblade", "TommyInnit", "Philza",
|
||||
"MumboJumbo", "Grian", "Iskall85", "Scar",
|
||||
"CaptainSparklez", "DanTDM", "PrestonPlayz",
|
||||
"Hypixel", "Spigot", "Paper", "Mojang", "Microsoft",
|
||||
"Administrator", "Owner", "Admin"
|
||||
);
|
||||
|
||||
/** 相似度阈值(Levenshtein距离占名称长度的比例,0-1,越小越相似) */
|
||||
private static final double LEVENSHTEIN_SIMILARITY_THRESHOLD = 0.3;
|
||||
/** Minecraft玩家名最大长度 */
|
||||
private static final int MAX_PLAYERNAME_LENGTH = 16;
|
||||
|
||||
/** 无参构造函数,使用默认配置 */
|
||||
public AntiNameSpoofService() {
|
||||
this(new ServerGuardConfig());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public AntiNameSpoofService(ServerGuardConfig config) {
|
||||
this.config = config;
|
||||
// 初始化默认保护名单:知名Minecraft人物名
|
||||
protectedNames.addAll(WELL_KNOWN_NAMES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查新加入玩家的名称是否在冒充其他玩家/管理员
|
||||
*
|
||||
* @param playerName 待检查的玩家名
|
||||
* @param ip 玩家IP(用于日志记录)
|
||||
* @return 检测结果,包含是否检测到冒充及原因
|
||||
*/
|
||||
public NameSpoofResult checkPlayerName(String playerName, String ip) {
|
||||
totalNameChecks.incrementAndGet();
|
||||
|
||||
// 配置开关检查
|
||||
if (!config.getSecurity().getSuperEvolution().isAntiNameSpoof()) {
|
||||
return NameSpoofResult.clean();
|
||||
}
|
||||
|
||||
if (playerName == null || playerName.isBlank()) {
|
||||
return NameSpoofResult.clean();
|
||||
}
|
||||
|
||||
// Minecraft玩家名不应超过16字符
|
||||
if (playerName.length() > MAX_PLAYERNAME_LENGTH) {
|
||||
return NameSpoofResult.clean(); // 长度异常在其他模块处理
|
||||
}
|
||||
|
||||
List<String> reasons = new ArrayList<>();
|
||||
boolean spoofDetected = false;
|
||||
Instant now = Instant.now();
|
||||
|
||||
// 检查1:零宽/不可见字符检测
|
||||
String visibleName = removeInvisibleCharacters(playerName);
|
||||
if (!visibleName.equals(playerName)) {
|
||||
spoofDetected = true;
|
||||
spoofAttempts.incrementAndGet();
|
||||
reasons.add("INVISIBLE_CHARS: Player name \"" + escapeName(playerName)
|
||||
+ "\" contains invisible/zero-width Unicode characters ("
|
||||
+ countInvisibleChars(playerName) + " chars detected) — "
|
||||
+ "can be used to impersonate \"" + visibleName + "\"");
|
||||
|
||||
blockedNames.add(playerName);
|
||||
recordSpoofAttempt(playerName, visibleName, "INVISIBLE_CHARS", ip, now);
|
||||
}
|
||||
|
||||
// 检查2:Unicode标准化后的差异对比
|
||||
String normalized = normalizeForComparison(playerName);
|
||||
if (!spoofDetected) {
|
||||
for (String protectedName : protectedNames) {
|
||||
String normalizedProtected = normalizeForComparison(protectedName);
|
||||
if (normalized.equalsIgnoreCase(normalizedProtected)) {
|
||||
spoofDetected = true;
|
||||
spoofAttempts.incrementAndGet();
|
||||
reasons.add("UNICODE_SPOOF: Player name \"" + playerName
|
||||
+ "\" uses Unicode characters to visually mimic \"" + protectedName
|
||||
+ "\" (normalized: " + normalized + " == " + normalizedProtected + ")");
|
||||
|
||||
blockedNames.add(playerName);
|
||||
recordSpoofAttempt(playerName, protectedName, "UNICODE_SPOOF", ip, now);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查3:大小写欺骗(不区分大小写比对后,名称匹配但大小写不同)
|
||||
if (!spoofDetected) {
|
||||
String lowerPlayer = playerName.toLowerCase(Locale.ROOT);
|
||||
for (String protectedName : protectedNames) {
|
||||
String lowerProtected = protectedName.toLowerCase(Locale.ROOT);
|
||||
if (lowerPlayer.equals(lowerProtected) && !playerName.equals(protectedName)) {
|
||||
// 确认不是同一个名字(大小写差异的另一个实现方式)
|
||||
spoofDetected = true;
|
||||
spoofAttempts.incrementAndGet();
|
||||
reasons.add("CASE_SPOOF: Player name \"" + playerName
|
||||
+ "\" uses different case to impersonate \"" + protectedName + "\"");
|
||||
|
||||
blockedNames.add(playerName);
|
||||
recordSpoofAttempt(playerName, protectedName, "CASE_SPOOF", ip, now);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查4:Levenshtein编辑距离相似度检测
|
||||
if (!spoofDetected) {
|
||||
for (String protectedName : protectedNames) {
|
||||
double similarity = levenshteinSimilarity(playerName.toLowerCase(Locale.ROOT),
|
||||
protectedName.toLowerCase(Locale.ROOT));
|
||||
if (similarity < LEVENSHTEIN_SIMILARITY_THRESHOLD
|
||||
&& playerName.length() >= 3 && protectedName.length() >= 3) {
|
||||
// 排除完全不同的短名称误报
|
||||
if (Math.abs(playerName.length() - protectedName.length()) <= 2) {
|
||||
spoofDetected = true;
|
||||
spoofAttempts.incrementAndGet();
|
||||
reasons.add("SIMILARITY_SPOOF: Player name \"" + playerName
|
||||
+ "\" is highly similar to protected name \"" + protectedName
|
||||
+ "\" (Levenshtein distance: " + String.format("%.2f", similarity) + ")");
|
||||
|
||||
blockedNames.add(playerName);
|
||||
recordSpoofAttempt(playerName, protectedName, "SIMILARITY_SPOOF", ip, now);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spoofDetected) {
|
||||
return NameSpoofResult.spoofed(reasons);
|
||||
}
|
||||
return NameSpoofResult.clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个需要保护的名称(OP玩家名、管理员名等)
|
||||
*
|
||||
* @param name 需保护的名称
|
||||
*/
|
||||
public void addProtectedName(String name) {
|
||||
if (name != null && !name.isBlank()) {
|
||||
protectedNames.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加受保护的名称
|
||||
*
|
||||
* @param names 需保护的名称列表
|
||||
*/
|
||||
public void addProtectedNames(Collection<String> names) {
|
||||
if (names != null) {
|
||||
protectedNames.addAll(names);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从受保护名单中移除名称
|
||||
*
|
||||
* @param name 名称
|
||||
*/
|
||||
public void removeProtectedName(String name) {
|
||||
if (name != null) {
|
||||
protectedNames.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有受保护的名称
|
||||
*
|
||||
* @return 受保护名称集合
|
||||
*/
|
||||
public Set<String> getProtectedNames() {
|
||||
return new HashSet<>(protectedNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块运行状态
|
||||
*
|
||||
* @return 状态Map,包含totalNameChecks/spoofAttempts/blockedNames
|
||||
*/
|
||||
public Map<String, Object> getStatus() {
|
||||
Map<String, Object> s = new LinkedHashMap<>();
|
||||
s.put("enabled", config.getSecurity().getSuperEvolution().isAntiNameSpoof());
|
||||
s.put("totalNameChecks", totalNameChecks.get());
|
||||
s.put("spoofAttempts", spoofAttempts.get());
|
||||
s.put("blockedNames", blockedNames.size());
|
||||
s.put("protectedNames", protectedNames.size());
|
||||
s.put("blockedNameList", new ArrayList<>(blockedNames));
|
||||
s.put("protectedNameList", new ArrayList<>(protectedNames));
|
||||
|
||||
// 最近10次冒充尝试
|
||||
List<Map<String, Object>> recent = new ArrayList<>();
|
||||
int total = 0;
|
||||
for (List<SpoofAttempt> attempts : spoofHistory.values()) {
|
||||
for (SpoofAttempt a : attempts) {
|
||||
if (total < 10) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("time", a.time.toString());
|
||||
m.put("playerName", a.playerName);
|
||||
m.put("target", a.targetName);
|
||||
m.put("type", a.spoofType);
|
||||
m.put("ip", a.ip);
|
||||
recent.add(m);
|
||||
}
|
||||
total++;
|
||||
}
|
||||
}
|
||||
s.put("recentSpoofAttempts", recent);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ==================== 内部辅助方法 ====================
|
||||
|
||||
/**
|
||||
* 移除字符串中的所有零宽/不可见Unicode字符
|
||||
* 用于显示清洗后的可见名称,以及底层对比
|
||||
*/
|
||||
private String removeInvisibleCharacters(String input) {
|
||||
if (input == null) return null;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
char c = input.charAt(i);
|
||||
if (!INVISIBLE_CHARACTERS.contains(c)) {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 统计不可见字符数量 */
|
||||
private int countInvisibleChars(String input) {
|
||||
if (input == null) return 0;
|
||||
int count = 0;
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
if (INVISIBLE_CHARACTERS.contains(input.charAt(i))) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unicode NFKC标准化后转为小写,用于名称的公平比对
|
||||
* NFKC会将兼容性字符分解并重新组合(例如将全角字母转为半角)
|
||||
*/
|
||||
private String normalizeForComparison(String input) {
|
||||
if (input == null) return "";
|
||||
// 先移除不可见字符,再NFKC标准化,再转小写
|
||||
String cleaned = removeInvisibleCharacters(input);
|
||||
return Normalizer.normalize(cleaned, Normalizer.Form.NFKC).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个字符串之间的Levenshtein编辑距离相似度
|
||||
* 返回值 = 编辑距离 / 最大字符串长度(结果越小越相似)
|
||||
*
|
||||
* @param s1 字符串1
|
||||
* @param s2 字符串2
|
||||
* @return 标准化相似度(0=完全相同,1=完全不同)
|
||||
*/
|
||||
private double levenshteinSimilarity(String s1, String s2) {
|
||||
if (s1 == null || s2 == null) return 1.0;
|
||||
if (s1.equals(s2)) return 0.0;
|
||||
|
||||
int[][] dp = new int[s1.length() + 1][s2.length() + 1];
|
||||
for (int i = 0; i <= s1.length(); i++) dp[i][0] = i;
|
||||
for (int j = 0; j <= s2.length(); j++) dp[0][j] = j;
|
||||
|
||||
for (int i = 1; i <= s1.length(); i++) {
|
||||
for (int j = 1; j <= s2.length(); j++) {
|
||||
int cost = s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1;
|
||||
dp[i][j] = Math.min(Math.min(
|
||||
dp[i - 1][j] + 1, // 删除
|
||||
dp[i][j - 1] + 1), // 插入
|
||||
dp[i - 1][j - 1] + cost); // 替换
|
||||
}
|
||||
}
|
||||
|
||||
int maxLen = Math.max(s1.length(), s2.length());
|
||||
if (maxLen == 0) return 0.0;
|
||||
return (double) dp[s1.length()][s2.length()] / maxLen;
|
||||
}
|
||||
|
||||
/** 将名称中的不可见字符转换为Unicode转义序列用于安全日志输出 */
|
||||
private String escapeName(String name) {
|
||||
if (name == null) return "null";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char c = name.charAt(i);
|
||||
if (INVISIBLE_CHARACTERS.contains(c)) {
|
||||
sb.append(String.format("\\u%04X", (int) c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 记录一次冒充尝试 */
|
||||
private void recordSpoofAttempt(String playerName, String targetName, String spoofType, String ip, Instant time) {
|
||||
SpoofAttempt attempt = new SpoofAttempt(time, playerName, targetName, spoofType, ip);
|
||||
spoofHistory.computeIfAbsent(playerName, k -> Collections.synchronizedList(new ArrayList<>())).add(attempt);
|
||||
}
|
||||
|
||||
// ==================== 内部数据类 ====================
|
||||
|
||||
/** 冒充尝试记录 */
|
||||
private static class SpoofAttempt {
|
||||
final Instant time;
|
||||
final String playerName;
|
||||
final String targetName;
|
||||
final String spoofType;
|
||||
final String ip;
|
||||
|
||||
SpoofAttempt(Instant time, String playerName, String targetName, String spoofType, String ip) {
|
||||
this.time = time;
|
||||
this.playerName = playerName;
|
||||
this.targetName = targetName;
|
||||
this.spoofType = spoofType;
|
||||
this.ip = ip;
|
||||
}
|
||||
}
|
||||
|
||||
/** 名称冒充检测结果 */
|
||||
public static class NameSpoofResult {
|
||||
private final boolean spoofed;
|
||||
private final List<String> reasons;
|
||||
|
||||
private NameSpoofResult(boolean spoofed, List<String> reasons) {
|
||||
this.spoofed = spoofed;
|
||||
this.reasons = reasons;
|
||||
}
|
||||
|
||||
public static NameSpoofResult clean() {
|
||||
return new NameSpoofResult(false, List.of());
|
||||
}
|
||||
|
||||
public static NameSpoofResult spoofed(List<String> reasons) {
|
||||
return new NameSpoofResult(true, reasons);
|
||||
}
|
||||
|
||||
public boolean isSpoofed() { return spoofed; }
|
||||
public boolean isClean() { return !spoofed; }
|
||||
public List<String> getReasons() { return reasons; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user