Delete src/main/java/net/viper/status/modules/antibot/AntiBotModule.java via Git Manager GUI
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user