Delete src/main/java/net/viper/status/modules/antibot/AntiBotModule.java via Git Manager GUI

This commit is contained in:
2026-05-22 17:25:16 +00:00
parent 783c15b82f
commit 82185c4376

View File

@@ -1,853 +0,0 @@
package net.viper.status.modules.antibot;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import net.viper.status.StatusAPI;
import net.viper.status.module.Module;
import net.viper.status.modules.network.NetworkInfoModule;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.file.Files;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Date;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* Eigenständiger AntiBot/Attack-Guard.
*
* Fixes:
* - cleanupExpired() nutzt jetzt removeIf() statt Iteration + remove() (Bug #3)
* - applyProfileDefaults() setzt korrekten attackDefaultSource aus Config
*/
public class AntiBotModule implements Module, Listener {
private static final String CONFIG_FILE_NAME = "network-guard.properties";
private StatusAPI plugin;
private boolean enabled = true;
private String profile = "high-traffic";
private int maxCps = 120;
private int attackStartCps = 220;
private int attackStopCps = 120;
private int attackCalmSeconds = 20;
private int ipConnectionsPerMinute = 18;
private int ipBlockSeconds = 600;
private String kickMessage = "Zu viele Verbindungen von deiner IP. Bitte warte kurz.";
private boolean vpnCheckEnabled = false;
private boolean vpnBlockProxy = true;
private boolean vpnBlockHosting = true;
private int vpnCacheMinutes = 30;
private int vpnTimeoutMs = 2500;
private boolean securityLogEnabled = true;
private String securityLogFileName = "antibot-security.log";
private File securityLogFile;
private final Object securityLogLock = new Object();
private boolean learningModeEnabled = true;
private int learningScoreThreshold = 100;
private int learningDecayPerSecond = 2;
private int learningStateWindowSeconds = 120;
private int learningRapidWindowMs = 1500;
private int learningRapidPoints = 12;
private int learningIpRateExceededPoints = 30;
private int learningVpnProxyPoints = 40;
private int learningVpnHostingPoints = 30;
private int learningAttackModePoints = 12;
private int learningHighCpsPoints = 10;
private int learningRecentEventLimit = 30;
private final AtomicInteger currentSecondConnections = new AtomicInteger(0);
private volatile long currentSecond = System.currentTimeMillis() / 1000L;
private volatile int lastCps = 0;
private final AtomicInteger peakCps = new AtomicInteger(0);
private volatile boolean attackMode = false;
private volatile long attackCalmSince = 0L;
private final AtomicLong blockedConnectionsTotal = new AtomicLong(0L);
private final AtomicLong blockedConnectionsCurrentAttack = new AtomicLong(0L);
private final Set<String> blockedIpsCurrentAttack = ConcurrentHashMap.newKeySet();
private final Map<String, IpWindow> perIpWindows = new ConcurrentHashMap<>();
private final Map<String, Long> blockedIpsUntil = new ConcurrentHashMap<>();
private final Map<String, VpnCacheEntry> vpnCache = new ConcurrentHashMap<>();
private final Map<String, RecentPlayerIdentity> recentIdentityByIp = new ConcurrentHashMap<>();
private final Map<String, LearningProfile> learningProfiles = new ConcurrentHashMap<>();
private final Deque<String> learningRecentEvents = new ArrayDeque<>();
@Override
public String getName() { return "AntiBotModule"; }
@Override
public void onEnable(Plugin plugin) {
if (!(plugin instanceof StatusAPI)) return;
this.plugin = (StatusAPI) plugin;
ensureModuleConfigExists();
loadConfig();
ensureSecurityLogFile();
if (!enabled) {
StatusAPI.debugLog(this.plugin, "[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME);
return;
}
ProxyServer.getInstance().getPluginManager().registerListener(this.plugin, this);
ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin, new AntiBotCommand());
ProxyServer.getInstance().getScheduler().schedule(this.plugin, this::tick, 1, 1, TimeUnit.SECONDS);
this.plugin.getLogger().fine("[AntiBotModule] aktiviert. maxCps=" + maxCps
+ ", attackStartCps=" + attackStartCps + ", ip/min=" + ipConnectionsPerMinute);
}
@Override
public void onDisable(Plugin plugin) {
perIpWindows.clear();
blockedIpsUntil.clear();
vpnCache.clear();
learningProfiles.clear();
synchronized (learningRecentEvents) { learningRecentEvents.clear(); }
blockedIpsCurrentAttack.clear();
attackMode = false;
}
public boolean isEnabled() { return enabled; }
private void reloadRuntimeState() {
perIpWindows.clear();
blockedIpsUntil.clear();
vpnCache.clear();
learningProfiles.clear();
synchronized (learningRecentEvents) { learningRecentEvents.clear(); }
blockedIpsCurrentAttack.clear();
attackMode = false;
attackCalmSince = 0L;
blockedConnectionsCurrentAttack.set(0L);
currentSecondConnections.set(0);
lastCps = 0;
peakCps.set(0);
loadConfig();
ensureSecurityLogFile();
}
public Map<String, Object> buildSnapshot() {
Map<String, Object> out = new LinkedHashMap<>();
out.put("enabled", enabled);
out.put("profile", profile);
out.put("attack_mode", attackMode);
out.put("protection_enabled", enabled);
out.put("attack_mode_status", attackMode ? "active" : "normal");
out.put("attack_mode_display", attackMode ? "Angriff erkannt" : "Normalbetrieb");
out.put("status_message", enabled
? (attackMode ? "AntiBot aktiv: Angriff erkannt" : "AntiBot aktiv: kein Angriff erkannt")
: "AntiBot deaktiviert");
out.put("last_cps", lastCps);
out.put("peak_cps", peakCps.get());
out.put("blocked_ips_active", blockedIpsUntil.size());
out.put("blocked_connections_total", blockedConnectionsTotal.get());
out.put("vpn_check_enabled", vpnCheckEnabled);
out.put("learning_mode_enabled", learningModeEnabled);
out.put("learning_profiles", learningProfiles.size());
out.put("thresholds", buildThresholds());
return out;
}
private Map<String, Object> buildThresholds() {
Map<String, Object> m = new LinkedHashMap<>();
m.put("max_cps", maxCps);
m.put("attack_start_cps", attackStartCps);
m.put("attack_stop_cps", attackStopCps);
m.put("attack_calm_seconds", attackCalmSeconds);
m.put("ip_connections_per_minute", ipConnectionsPerMinute);
m.put("ip_block_seconds", ipBlockSeconds);
m.put("learning_score_threshold", learningScoreThreshold);
m.put("learning_decay_per_second", learningDecayPerSecond);
return m;
}
@EventHandler
public void onPreLogin(PreLoginEvent event) {
if (!enabled) return;
String ip = extractIp(event.getConnection());
if (ip == null || ip.isEmpty()) return;
cacheRecentIdentity(ip, event.getConnection(), System.currentTimeMillis());
recordConnection();
long now = System.currentTimeMillis();
// FIX #3: cleanupExpired verwendet removeIf statt Iteration+remove
cleanupExpired(now);
Long blockedUntil = blockedIpsUntil.get(ip);
if (blockedUntil != null && blockedUntil > now) {
logSecurityEvent("ip_block_active", ip, event.getConnection(), "blocked_until_ms=" + blockedUntil);
blockEvent(event);
return;
}
if (learningModeEnabled) {
evaluateLearningBaseline(ip, now);
Long learningBlock = blockedIpsUntil.get(ip);
if (learningBlock != null && learningBlock > now) {
blockEvent(event);
return;
}
}
boolean ipRateExceeded = isIpRateExceeded(ip, now);
if (ipRateExceeded) {
if (learningModeEnabled) {
int score = addLearningScore(ip, now, learningIpRateExceededPoints, "ip-rate-exceeded", true);
logSecurityEvent("ip_rate_exceeded_scored", ip, event.getConnection(), "score=" + score + ", threshold=" + learningScoreThreshold);
if (score >= learningScoreThreshold) {
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=ip-rate-exceeded, score=" + score);
blockEvent(event);
return;
}
} else {
blockIp(ip, now);
logSecurityEvent("ip_rate_limit_block", ip, event.getConnection(), "mode=direct");
blockEvent(event);
return;
}
}
if (vpnCheckEnabled) {
VpnCheckResult info = getVpnInfo(ip, now);
if (info != null) {
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
if (shouldBlock) {
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
if (learningModeEnabled) {
if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false);
if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
int current = getLearningScore(ip, now);
if (current >= learningScoreThreshold) {
blockIp(ip, now);
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
blockEvent(event);
}
} else {
blockIp(ip, now);
logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
blockEvent(event);
}
}
}
}
}
@EventHandler
public void onPostLogin(PostLoginEvent event) {
if (!enabled || event == null || event.getPlayer() == null) return;
ProxiedPlayer player = event.getPlayer();
String ip = extractIpFromPlayer(player);
if (ip == null || ip.isEmpty()) return;
cacheRecentIdentityDirect(ip, player.getName(), player.getUniqueId(), System.currentTimeMillis());
}
private void blockEvent(PreLoginEvent event) {
event.setCancelled(true);
}
private String extractIp(PendingConnection conn) {
if (conn == null || conn.getAddress() == null) return null;
if (conn.getAddress() instanceof InetSocketAddress) {
InetSocketAddress sa = (InetSocketAddress) conn.getAddress();
return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString();
}
return String.valueOf(conn.getAddress());
}
private String extractIpFromPlayer(ProxiedPlayer player) {
if (player == null || player.getAddress() == null) return null;
if (player.getAddress() instanceof InetSocketAddress) {
InetSocketAddress sa = (InetSocketAddress) player.getAddress();
return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString();
}
return String.valueOf(player.getAddress());
}
private void recordConnection() {
long sec = System.currentTimeMillis() / 1000L;
if (sec != currentSecond) {
synchronized (this) {
if (sec != currentSecond) {
currentSecond = sec;
currentSecondConnections.set(0);
}
}
}
currentSecondConnections.incrementAndGet();
}
private boolean isIpRateExceeded(String ip, long now) {
IpWindow window = perIpWindows.computeIfAbsent(ip, k -> new IpWindow(now));
synchronized (window) {
long diff = now - window.windowStart;
if (diff > 60_000L) {
window.windowStart = now;
window.count = 0;
}
window.count++;
return window.count > Math.max(1, ipConnectionsPerMinute);
}
}
/**
* Sperrt eine IP für die konfigurierte Block-Dauer (antibot.ip.block_seconds).
* Kann von anderen Modulen aufgerufen werden (z. B. MultiAccountGuard).
* @param ip Die zu sperrende IP-Adresse
* @param durationSeconds Sperrdauer in Sekunden (0 = antibot-Standard verwenden)
*/
public void blockIpExternal(String ip, int durationSeconds) {
long now = System.currentTimeMillis();
long duration = durationSeconds > 0 ? durationSeconds : Math.max(1, ipBlockSeconds);
blockedIpsUntil.put(ip, now + duration * 1000L);
blockedConnectionsTotal.incrementAndGet();
}
private void blockIp(String ip, long now) {
blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L);
blockedConnectionsTotal.incrementAndGet();
if (attackMode) {
blockedConnectionsCurrentAttack.incrementAndGet();
blockedIpsCurrentAttack.add(ip);
}
}
/**
* FIX #3: Verwendet removeIf() statt for-each + remove() um ConcurrentModificationException zu vermeiden.
*/
private void cleanupExpired(long now) {
blockedIpsUntil.entrySet().removeIf(e -> e.getValue() <= now);
vpnCache.entrySet().removeIf(e -> e.getValue().expiresAt <= now);
recentIdentityByIp.entrySet().removeIf(e -> {
RecentPlayerIdentity id = e.getValue();
return id == null || (now - id.updatedAtMs) > 600_000L;
});
if (learningModeEnabled) {
long staleAfter = Math.max(60, learningStateWindowSeconds) * 1000L;
learningProfiles.entrySet().removeIf(e -> {
LearningProfile lp = e.getValue();
return lp == null || ((now - lp.lastSeenAt) > staleAfter && lp.score <= 0);
});
}
}
private void tick() {
if (!enabled) return;
int cps = currentSecondConnections.getAndSet(0);
lastCps = cps;
if (cps > peakCps.get()) peakCps.set(cps);
long now = System.currentTimeMillis();
if (!attackMode && cps >= Math.max(1, attackStartCps)) {
attackMode = true;
attackCalmSince = 0L;
blockedConnectionsCurrentAttack.set(0L);
blockedIpsCurrentAttack.clear();
sendAttackToWebhook("detected", cps, null, null, "StatusAPI AntiBot");
plugin.getLogger().warning("[AntiBotModule] Attack erkannt. CPS=" + cps);
return;
}
if (attackMode) {
if (cps <= Math.max(1, attackStopCps)) {
if (attackCalmSince == 0L) attackCalmSince = now;
long calmFor = now - attackCalmSince;
if (calmFor >= Math.max(1, attackCalmSeconds) * 1000L) {
attackMode = false;
attackCalmSince = 0L;
int blockedIps = blockedIpsCurrentAttack.size();
long blockedConns = blockedConnectionsCurrentAttack.get();
sendAttackToWebhook("stopped", cps, blockedIps, blockedConns, "StatusAPI AntiBot");
plugin.getLogger().warning("[AntiBotModule] Attack beendet. blockedIps=" + blockedIps + ", blockedConnections=" + blockedConns);
}
} else {
attackCalmSince = 0L;
}
}
}
private void sendAttackToWebhook(String type, Integer cps, Integer blockedIps, Long blockedConnections, String source) {
NetworkInfoModule networkInfoModule = getNetworkInfoModule();
if (networkInfoModule == null) return;
networkInfoModule.sendAttackNotification(type, cps, blockedIps, blockedConnections, source);
}
private NetworkInfoModule getNetworkInfoModule() {
if (plugin == null || plugin.getModuleManager() == null) return null;
return (NetworkInfoModule) plugin.getModuleManager().getModule("NetworkInfoModule");
}
private void loadConfig() {
File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
if (!file.exists()) return;
Properties props = new Properties();
try (FileInputStream in = new FileInputStream(file)) {
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
enabled = parseBoolean(props.getProperty("antibot.enabled"), true);
profile = normalizeProfile(props.getProperty("antibot.profile", "high-traffic"));
applyProfileDefaults(profile);
maxCps = parseInt(props.getProperty("antibot.max_cps"), maxCps);
attackStartCps = parseInt(props.getProperty("antibot.attack.start_cps"), attackStartCps);
attackStopCps = parseInt(props.getProperty("antibot.attack.stop_cps"), attackStopCps);
attackCalmSeconds = parseInt(props.getProperty("antibot.attack.stop_grace_seconds"), attackCalmSeconds);
ipConnectionsPerMinute = parseInt(props.getProperty("antibot.ip.max_connections_per_minute"), ipConnectionsPerMinute);
ipBlockSeconds = parseInt(props.getProperty("antibot.ip.block_seconds"), ipBlockSeconds);
kickMessage = props.getProperty("antibot.kick_message", kickMessage);
vpnCheckEnabled = parseBoolean(props.getProperty("antibot.vpn_check.enabled"), vpnCheckEnabled);
vpnBlockProxy = parseBoolean(props.getProperty("antibot.vpn_check.block_proxy"), vpnBlockProxy);
vpnBlockHosting = parseBoolean(props.getProperty("antibot.vpn_check.block_hosting"), vpnBlockHosting);
vpnCacheMinutes = parseInt(props.getProperty("antibot.vpn_check.cache_minutes"), vpnCacheMinutes);
vpnTimeoutMs = parseInt(props.getProperty("antibot.vpn_check.timeout_ms"), vpnTimeoutMs);
securityLogEnabled = parseBoolean(props.getProperty("antibot.security_log.enabled"), securityLogEnabled);
securityLogFileName = props.getProperty("antibot.security_log.file", securityLogFileName).trim();
if (securityLogFileName.isEmpty()) securityLogFileName = "antibot-security.log";
learningModeEnabled = parseBoolean(props.getProperty("antibot.learning.enabled"), learningModeEnabled);
learningScoreThreshold = parseInt(props.getProperty("antibot.learning.score_threshold"), learningScoreThreshold);
learningDecayPerSecond = parseInt(props.getProperty("antibot.learning.decay_per_second"), learningDecayPerSecond);
learningStateWindowSeconds = parseInt(props.getProperty("antibot.learning.state_window_seconds"), learningStateWindowSeconds);
learningRapidWindowMs = parseInt(props.getProperty("antibot.learning.rapid.window_ms"), learningRapidWindowMs);
learningRapidPoints = parseInt(props.getProperty("antibot.learning.rapid.points"), learningRapidPoints);
learningIpRateExceededPoints = parseInt(props.getProperty("antibot.learning.ip_rate_exceeded.points"), learningIpRateExceededPoints);
learningVpnProxyPoints = parseInt(props.getProperty("antibot.learning.vpn_proxy.points"), learningVpnProxyPoints);
learningVpnHostingPoints = parseInt(props.getProperty("antibot.learning.vpn_hosting.points"), learningVpnHostingPoints);
learningAttackModePoints = parseInt(props.getProperty("antibot.learning.attack_mode.points"), learningAttackModePoints);
learningHighCpsPoints = parseInt(props.getProperty("antibot.learning.high_cps.points"), learningHighCpsPoints);
learningRecentEventLimit = parseInt(props.getProperty("antibot.learning.recent_events.limit"), learningRecentEventLimit);
} catch (Exception e) {
plugin.getLogger().warning("[AntiBotModule] Fehler beim Laden von " + CONFIG_FILE_NAME + ": " + e.getMessage());
}
}
private String normalizeProfile(String raw) {
if (raw == null) return "high-traffic";
String v = raw.trim().toLowerCase(Locale.ROOT);
return "strict".equals(v) ? "strict" : "high-traffic";
}
private void applyProfileDefaults(String profileName) {
if ("strict".equals(profileName)) {
maxCps = 120; attackStartCps = 220; attackStopCps = 120; attackCalmSeconds = 20;
ipConnectionsPerMinute = 18; ipBlockSeconds = 900;
vpnCheckEnabled = true; vpnBlockProxy = true; vpnBlockHosting = true;
vpnCacheMinutes = 30; vpnTimeoutMs = 2500;
} else {
maxCps = 180; attackStartCps = 300; attackStopCps = 170; attackCalmSeconds = 25;
ipConnectionsPerMinute = 24; ipBlockSeconds = 600;
vpnCheckEnabled = false; vpnBlockProxy = true; vpnBlockHosting = true;
vpnCacheMinutes = 30; vpnTimeoutMs = 2500;
}
}
private boolean isSupportedProfile(String raw) {
if (raw == null) return false;
String v = raw.trim().toLowerCase(Locale.ROOT);
return "strict".equals(v) || "high-traffic".equals(v);
}
private boolean applyProfileAndPersist(String requestedProfile) {
if (!isSupportedProfile(requestedProfile)) return false;
String normalized = normalizeProfile(requestedProfile);
profile = normalized;
applyProfileDefaults(normalized);
Map<String, String> values = new LinkedHashMap<>();
values.put("antibot.profile", normalized);
values.put("antibot.max_cps", String.valueOf(maxCps));
values.put("antibot.attack.start_cps", String.valueOf(attackStartCps));
values.put("antibot.attack.stop_cps", String.valueOf(attackStopCps));
values.put("antibot.attack.stop_grace_seconds", String.valueOf(attackCalmSeconds));
values.put("antibot.ip.max_connections_per_minute", String.valueOf(ipConnectionsPerMinute));
values.put("antibot.ip.block_seconds", String.valueOf(ipBlockSeconds));
values.put("antibot.vpn_check.enabled", String.valueOf(vpnCheckEnabled));
values.put("antibot.vpn_check.block_proxy", String.valueOf(vpnBlockProxy));
values.put("antibot.vpn_check.block_hosting", String.valueOf(vpnBlockHosting));
values.put("antibot.vpn_check.cache_minutes", String.valueOf(vpnCacheMinutes));
values.put("antibot.vpn_check.timeout_ms", String.valueOf(vpnTimeoutMs));
try { updateConfigValues(values); return true; }
catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Profil nicht speichern: " + e.getMessage()); return false; }
}
private synchronized void updateConfigValues(Map<String, String> keyValues) throws Exception {
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
List<String> lines = target.exists()
? Files.readAllLines(target.toPath(), StandardCharsets.UTF_8)
: new ArrayList<>();
for (Map.Entry<String, String> entry : keyValues.entrySet()) {
String key = entry.getKey();
String newLine = key + "=" + entry.getValue();
boolean replaced = false;
for (int i = 0; i < lines.size(); i++) {
if (lines.get(i).trim().startsWith(key + "=")) {
lines.set(i, newLine);
replaced = true;
break;
}
}
if (!replaced) lines.add(newLine);
}
Files.write(target.toPath(), lines, StandardCharsets.UTF_8);
}
private void ensureModuleConfigExists() {
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
if (target.exists()) return;
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME);
FileOutputStream out = new FileOutputStream(target)) {
if (in == null) { plugin.getLogger().warning("[AntiBotModule] Standarddatei nicht im JAR."); return; }
byte[] buffer = new byte[4096]; int read;
while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read);
} catch (Exception e) {
plugin.getLogger().warning("[AntiBotModule] Konnte Config nicht erstellen: " + e.getMessage());
}
}
private void evaluateLearningBaseline(String ip, long now) {
LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
synchronized (lp) {
decayLearningProfile(lp, now);
long delta = now - lp.lastConnectionAt;
if (lp.lastConnectionAt > 0 && delta <= Math.max(250L, learningRapidWindowMs)) {
lp.rapidStreak++;
int points = learningRapidPoints + Math.min(lp.rapidStreak, 5);
lp.score += Math.max(1, points);
recordLearningEvent("IP=" + ip + " +" + points + " rapid-connect score=" + lp.score);
} else {
lp.rapidStreak = 0;
}
if (attackMode) { lp.score += Math.max(1, learningAttackModePoints); recordLearningEvent("IP=" + ip + " +" + learningAttackModePoints + " attack-mode score=" + lp.score); }
if (lastCps >= Math.max(1, maxCps)) { lp.score += Math.max(1, learningHighCpsPoints); recordLearningEvent("IP=" + ip + " +" + learningHighCpsPoints + " high-cps score=" + lp.score); }
lp.lastConnectionAt = now;
lp.lastSeenAt = now;
if (lp.score >= learningScoreThreshold) {
blockIp(ip, now);
recordLearningEvent("BLOCK " + ip + " reason=learning-threshold score=" + lp.score);
}
}
}
private int addLearningScore(String ip, long now, int points, String reason, boolean checkThreshold) {
LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
synchronized (lp) {
decayLearningProfile(lp, now);
int add = Math.max(1, points);
lp.score += add;
lp.lastSeenAt = now;
recordLearningEvent("IP=" + ip + " +" + add + " " + reason + " score=" + lp.score);
if (checkThreshold && lp.score >= learningScoreThreshold) {
blockIp(ip, now);
recordLearningEvent("BLOCK " + ip + " reason=" + reason + " score=" + lp.score);
}
return lp.score;
}
}
private int getLearningScore(String ip, long now) {
LearningProfile lp = learningProfiles.get(ip);
if (lp == null) return 0;
synchronized (lp) { decayLearningProfile(lp, now); return lp.score; }
}
private void decayLearningProfile(LearningProfile lp, long now) {
long elapsedMs = Math.max(0L, now - lp.lastScoreUpdateAt);
if (elapsedMs > 0L) {
long decay = (elapsedMs / 1000L) * Math.max(0, learningDecayPerSecond);
if (decay > 0L) lp.score = (int) Math.max(0L, lp.score - decay);
lp.lastScoreUpdateAt = now;
}
long resetAfter = Math.max(30, learningStateWindowSeconds) * 1000L;
if (lp.lastSeenAt > 0L && now - lp.lastSeenAt > resetAfter) {
lp.score = 0;
lp.rapidStreak = 0;
}
}
private void recordLearningEvent(String event) {
String line = new SimpleDateFormat("HH:mm:ss").format(new Date()) + " " + event;
synchronized (learningRecentEvents) {
learningRecentEvents.addLast(line);
while (learningRecentEvents.size() > Math.max(5, learningRecentEventLimit)) learningRecentEvents.pollFirst();
}
}
private void ensureSecurityLogFile() {
if (plugin == null) return;
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
securityLogFile = new File(plugin.getDataFolder(), securityLogFileName);
try { if (!securityLogFile.exists()) securityLogFile.createNewFile(); }
catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Sicherheitslog nicht erstellen: " + e.getMessage()); }
}
private void logSecurityEvent(String eventType, String ip, PendingConnection conn, String details) {
if (!securityLogEnabled || plugin == null) return;
if (securityLogFile == null) { ensureSecurityLogFile(); if (securityLogFile == null) return; }
String name = extractPlayerName(conn);
String uuid = extractPlayerUuid(conn, name);
if ((name == null || name.isEmpty() || "unknown".equalsIgnoreCase(name)) && ip != null) {
RecentPlayerIdentity cached = recentIdentityByIp.get(ip);
if (cached != null) {
if (cached.playerName != null && !cached.playerName.trim().isEmpty()) name = cached.playerName;
if ((uuid == null || uuid.isEmpty()) && cached.playerUuid != null && !cached.playerUuid.trim().isEmpty()) uuid = cached.playerUuid;
}
}
if (name == null || name.trim().isEmpty()) name = "unknown";
if (uuid == null || uuid.trim().isEmpty()) uuid = "unknown";
String line = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
+ " | event=" + safeLog(eventType)
+ " | ip=" + safeLog(ip)
+ " | player=" + safeLog(name)
+ " | uuid=" + safeLog(uuid)
+ " | details=" + safeLog(details);
synchronized (securityLogLock) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(securityLogFile, true))) {
bw.write(line); bw.newLine();
} catch (Exception e) {
plugin.getLogger().warning("[AntiBotModule] Sicherheitslog-Schreibfehler: " + e.getMessage());
}
}
}
private void cacheRecentIdentity(String ip, PendingConnection conn, long now) {
if (ip == null || ip.isEmpty() || conn == null) return;
String name = extractPlayerName(conn);
String uuid = extractPlayerUuid(conn, name);
if ((name == null || name.isEmpty()) && (uuid == null || uuid.isEmpty())) return;
RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity());
synchronized (identity) {
if (name != null && !name.trim().isEmpty()) identity.playerName = name.trim();
if (uuid != null && !uuid.trim().isEmpty()) identity.playerUuid = uuid.trim();
identity.updatedAtMs = now;
}
}
private void cacheRecentIdentityDirect(String ip, String playerName, UUID playerUuid, long now) {
if (ip == null || ip.isEmpty()) return;
String name = playerName == null ? "" : playerName.trim();
String uuid = playerUuid == null ? "" : playerUuid.toString();
if (name.isEmpty() && uuid.isEmpty()) return;
RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity());
synchronized (identity) {
if (!name.isEmpty()) identity.playerName = name;
if (!uuid.isEmpty()) identity.playerUuid = uuid;
identity.updatedAtMs = now;
}
}
private String extractPlayerName(PendingConnection conn) {
if (conn == null) return "";
try { String raw = conn.getName(); return raw == null ? "" : raw.trim(); } catch (Exception ignored) { return ""; }
}
private String extractPlayerUuid(PendingConnection conn, String playerName) {
if (conn != null) {
try { UUID uuid = conn.getUniqueId(); if (uuid != null) return uuid.toString(); } catch (Exception ignored) {}
}
if (playerName != null && !playerName.trim().isEmpty()) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.trim()).getBytes(StandardCharsets.UTF_8)).toString();
}
return "";
}
private String safeLog(String input) {
if (input == null || input.isEmpty()) return "-";
return input.replace("\n", " ").replace("\r", " ").trim();
}
private List<String> getRecentLearningEvents(int max) {
List<String> out = new ArrayList<>();
synchronized (learningRecentEvents) {
int skip = Math.max(0, learningRecentEvents.size() - Math.max(1, max));
int idx = 0;
for (String line : learningRecentEvents) {
if (idx++ < skip) continue;
out.add(line);
}
}
return out;
}
private boolean parseBoolean(String s, boolean fallback) {
if (s == null) return fallback;
return Boolean.parseBoolean(s.trim());
}
private int parseInt(String s, int fallback) {
try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception ignored) { return fallback; }
}
private VpnCheckResult getVpnInfo(String ip, long now) {
VpnCacheEntry cached = vpnCache.get(ip);
if (cached != null && cached.expiresAt > now) return cached.result;
VpnCheckResult fresh = requestIpApi(ip);
if (fresh != null) {
VpnCacheEntry entry = new VpnCacheEntry();
entry.result = fresh;
entry.expiresAt = now + Math.max(1, vpnCacheMinutes) * 60_000L;
vpnCache.put(ip, entry);
}
return fresh;
}
private VpnCheckResult requestIpApi(String ip) {
HttpURLConnection conn = null;
try {
String url = "http://ip-api.com/json/" + ip + "?fields=status,proxy,hosting";
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(vpnTimeoutMs);
conn.setReadTimeout(vpnTimeoutMs);
conn.setRequestProperty("User-Agent", "StatusAPI-AntiBot/1.0");
if (conn.getResponseCode() < 200 || conn.getResponseCode() >= 300) return null;
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line; while ((line = br.readLine()) != null) sb.append(line);
}
String json = sb.toString();
if (json.isEmpty() || !json.contains("\"status\":\"success\"")) return null;
VpnCheckResult result = new VpnCheckResult();
result.proxy = json.contains("\"proxy\":true");
result.hosting = json.contains("\"hosting\":true");
return result;
} catch (Exception ignored) { return null; }
finally { if (conn != null) conn.disconnect(); }
}
// --- Interne Klassen ---
private static class IpWindow {
long windowStart; int count;
IpWindow(long now) { this.windowStart = now; this.count = 0; }
}
private static class VpnCacheEntry { VpnCheckResult result; long expiresAt; }
private static class VpnCheckResult { boolean proxy; boolean hosting; }
private static class LearningProfile {
long lastConnectionAt, lastScoreUpdateAt, lastSeenAt;
int rapidStreak, score;
LearningProfile(long now) { lastConnectionAt = lastScoreUpdateAt = lastSeenAt = now; }
}
private static class RecentPlayerIdentity { String playerName; String playerUuid; long updatedAtMs; }
// --- Command ---
private class AntiBotCommand extends Command {
AntiBotCommand() { super("antibot", "statusapi.antibot"); }
@Override
public void execute(CommandSender sender, String[] args) {
if (!enabled) { sender.sendMessage(ChatColor.RED + "AntiBotModule ist deaktiviert."); return; }
if (args.length == 0 || "status".equalsIgnoreCase(args[0])) {
sender.sendMessage(ChatColor.GOLD + "----- AntiBot Status -----");
sender.sendMessage(ChatColor.YELLOW + "Schutz aktiv: " + ChatColor.WHITE + enabled);
sender.sendMessage(ChatColor.YELLOW + "Profil: " + ChatColor.WHITE + profile);
if (attackMode) sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.RED + "AKTIV");
else sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.GREEN + "Normal");
sender.sendMessage(ChatColor.YELLOW + "CPS: " + ChatColor.WHITE + lastCps + ChatColor.GRAY + " (Peak " + peakCps.get() + ")");
sender.sendMessage(ChatColor.YELLOW + "Schwellen: " + ChatColor.WHITE + "start " + attackStartCps + ChatColor.GRAY + " / " + ChatColor.WHITE + "stop " + attackStopCps + ChatColor.GRAY + " CPS");
sender.sendMessage(ChatColor.YELLOW + "Active IP Blocks: " + ChatColor.WHITE + blockedIpsUntil.size());
sender.sendMessage(ChatColor.YELLOW + "Total blocked connections: " + ChatColor.WHITE + blockedConnectionsTotal.get());
sender.sendMessage(ChatColor.YELLOW + "VPN Check: " + ChatColor.WHITE + vpnCheckEnabled);
sender.sendMessage(ChatColor.YELLOW + "Learning Mode: " + ChatColor.WHITE + learningModeEnabled
+ ChatColor.GRAY + " (threshold=" + learningScoreThreshold + ")");
List<String> recent = getRecentLearningEvents(3);
if (!recent.isEmpty()) {
sender.sendMessage(ChatColor.YELLOW + "Learning Events:");
for (String line : recent) sender.sendMessage(ChatColor.GRAY + "- " + line);
}
return;
}
if ("clearblocks".equalsIgnoreCase(args[0])) {
blockedIpsUntil.clear();
sender.sendMessage(ChatColor.GREEN + "Alle IP-Blocks wurden entfernt.");
return;
}
if ("unblock".equalsIgnoreCase(args[0]) && args.length >= 2) {
String ip = args[1].trim();
if (blockedIpsUntil.remove(ip) != null) sender.sendMessage(ChatColor.GREEN + "IP entblockt: " + ip);
else sender.sendMessage(ChatColor.RED + "IP war nicht geblockt: " + ip);
return;
}
if ("profile".equalsIgnoreCase(args[0])) {
if (args.length < 2) {
sender.sendMessage(ChatColor.YELLOW + "Aktuelles Profil: " + ChatColor.WHITE + profile);
sender.sendMessage(ChatColor.YELLOW + "Benutzung: /antibot profile <strict|high-traffic>");
return;
}
String requested = args[1].trim().toLowerCase(Locale.ROOT);
if (!isSupportedProfile(requested)) { sender.sendMessage(ChatColor.RED + "Unbekanntes Profil. Erlaubt: strict, high-traffic"); return; }
boolean ok = applyProfileAndPersist(requested);
if (!ok) { sender.sendMessage(ChatColor.RED + "Profil konnte nicht gespeichert werden."); return; }
sender.sendMessage(ChatColor.GREEN + "AntiBot-Profil umgestellt auf: " + requested);
sender.sendMessage(ChatColor.GRAY + "Werte wurden in " + CONFIG_FILE_NAME + " gespeichert.");
return;
}
if ("reload".equalsIgnoreCase(args[0])) {
reloadRuntimeState();
sender.sendMessage(ChatColor.GREEN + "AntiBot-Konfiguration neu geladen.");
sender.sendMessage(ChatColor.GRAY + "Aktives Profil: " + profile);
return;
}
sender.sendMessage(ChatColor.YELLOW + "/antibot status");
sender.sendMessage(ChatColor.YELLOW + "/antibot clearblocks");
sender.sendMessage(ChatColor.YELLOW + "/antibot unblock <ip>");
sender.sendMessage(ChatColor.YELLOW + "/antibot profile <strict|high-traffic>");
sender.sendMessage(ChatColor.YELLOW + "/antibot reload");
}
}
}