Upload folder via GUI - src
This commit is contained in:
@@ -27,12 +27,18 @@ import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringReader;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import net.md_5.bungee.api.scheduler.ScheduledTask;
|
||||
|
||||
/**
|
||||
* StatusAPI - zentraler Bungee HTTP-Status- und Broadcast-Endpunkt
|
||||
@@ -40,8 +46,13 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class StatusAPI extends Plugin implements Runnable {
|
||||
|
||||
private Thread thread;
|
||||
private volatile Thread thread;
|
||||
private volatile ServerSocket serverSocket;
|
||||
private volatile boolean shuttingDown = false;
|
||||
private int port = 9191;
|
||||
private ScheduledTask httpWatchdogTask;
|
||||
private ExecutorService requestExecutor;
|
||||
private final AtomicLong lastHttpRequestAt = new AtomicLong(0L);
|
||||
|
||||
private ModuleManager moduleManager;
|
||||
private UpdateChecker updateChecker;
|
||||
@@ -92,8 +103,14 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
|
||||
// WebServer starten
|
||||
getLogger().info("Starte Web-Server auf Port " + port + "...");
|
||||
thread = new Thread(this, "StatusAPI-HTTP-Server");
|
||||
thread.start();
|
||||
shuttingDown = false;
|
||||
requestExecutor = Executors.newFixedThreadPool(4, r -> {
|
||||
Thread t = new Thread(r, "StatusAPI-HTTP-Worker");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
startHttpServerThread();
|
||||
httpWatchdogTask = ProxyServer.getInstance().getScheduler().schedule(this, this::ensureHttpServerAlive, 15, 15, TimeUnit.SECONDS);
|
||||
|
||||
// Update System
|
||||
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
||||
@@ -106,15 +123,68 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
shuttingDown = true;
|
||||
|
||||
getLogger().info("Stoppe Module...");
|
||||
if (moduleManager != null) {
|
||||
moduleManager.disableAll(this);
|
||||
}
|
||||
|
||||
getLogger().info("Stoppe Web-Server...");
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
try { thread.join(1000); } catch (InterruptedException ignored) {}
|
||||
if (httpWatchdogTask != null) {
|
||||
httpWatchdogTask.cancel();
|
||||
httpWatchdogTask = null;
|
||||
}
|
||||
|
||||
stopHttpServerThread();
|
||||
|
||||
if (requestExecutor != null) {
|
||||
requestExecutor.shutdownNow();
|
||||
requestExecutor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void startHttpServerThread() {
|
||||
if (thread != null && thread.isAlive()) {
|
||||
return;
|
||||
}
|
||||
thread = new Thread(this, "StatusAPI-HTTP-Server");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private synchronized void stopHttpServerThread() {
|
||||
Thread localThread = thread;
|
||||
if (localThread != null) {
|
||||
localThread.interrupt();
|
||||
}
|
||||
|
||||
ServerSocket localServerSocket = serverSocket;
|
||||
if (localServerSocket != null) {
|
||||
try {
|
||||
localServerSocket.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (localThread != null) {
|
||||
try {
|
||||
localThread.join(1500);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
thread = null;
|
||||
}
|
||||
|
||||
private void ensureHttpServerAlive() {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
Thread t = thread;
|
||||
if (t == null || !t.isAlive()) {
|
||||
getLogger().warning("HTTP-Server-Thread war gestoppt und wird neu gestartet.");
|
||||
startHttpServerThread();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,35 +297,117 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
// --- WebServer & JSON ---
|
||||
@Override
|
||||
public void run() {
|
||||
try (ServerSocket serverSocket = new ServerSocket(port)) {
|
||||
serverSocket.setSoTimeout(1000);
|
||||
while (!Thread.interrupted()) {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
handleConnection(clientSocket);
|
||||
} catch (java.net.SocketTimeoutException e) {}
|
||||
catch (IOException e) {
|
||||
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
||||
while (!shuttingDown && !Thread.currentThread().isInterrupted()) {
|
||||
try (ServerSocket localServerSocket = new ServerSocket(port)) {
|
||||
this.serverSocket = localServerSocket;
|
||||
localServerSocket.setReuseAddress(true);
|
||||
localServerSocket.setSoTimeout(1000);
|
||||
|
||||
while (!shuttingDown && !Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
Socket clientSocket = localServerSocket.accept();
|
||||
submitConnection(clientSocket);
|
||||
} catch (SocketTimeoutException ignored) {
|
||||
// Poll-Schleife fuer Interrupt/Shutdown.
|
||||
} catch (IOException e) {
|
||||
if (!shuttingDown) {
|
||||
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
if (!shuttingDown) {
|
||||
getLogger().severe("Unbehandelter Fehler im HTTP-Accept-Loop: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (!shuttingDown) {
|
||||
getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage());
|
||||
try {
|
||||
Thread.sleep(2000L);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
serverSocket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void submitConnection(Socket clientSocket) {
|
||||
if (clientSocket == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
clientSocket.setSoTimeout(5000);
|
||||
clientSocket.setTcpNoDelay(true);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
ExecutorService executor = requestExecutor;
|
||||
if (executor == null || executor.isShutdown()) {
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
handleConnection(clientSocket);
|
||||
} finally {
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException ex) {
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
} catch (IOException e) {
|
||||
getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnection(Socket clientSocket) {
|
||||
|
||||
// (doppelter/fehlerhafter Block entfernt)
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
|
||||
OutputStream out = clientSocket.getOutputStream()) {
|
||||
|
||||
String inputLine = in.readLine();
|
||||
if (inputLine == null) return;
|
||||
lastHttpRequestAt.set(System.currentTimeMillis());
|
||||
|
||||
String[] reqParts = inputLine.split(" ");
|
||||
if (reqParts.length < 2) return;
|
||||
String method = reqParts[0].trim();
|
||||
String path = reqParts[1].trim();
|
||||
String pathOnly = path;
|
||||
int queryIndex = path.indexOf('?');
|
||||
if (queryIndex >= 0) {
|
||||
pathOnly = path.substring(0, queryIndex);
|
||||
}
|
||||
|
||||
if ("GET".equalsIgnoreCase(method) && "/health".equalsIgnoreCase(path)) {
|
||||
long lastMs = lastHttpRequestAt.get();
|
||||
long age = lastMs <= 0L ? -1L : (System.currentTimeMillis() - lastMs);
|
||||
String json = "{\"success\":true,\"online\":true,\"last_request_age_ms\":" + age + "}";
|
||||
sendHttpResponse(out, json, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("GET".equalsIgnoreCase(method) && "/antibot/security-log".equalsIgnoreCase(pathOnly)) {
|
||||
Map<String, Object> payload = new LinkedHashMap<String, Object>();
|
||||
payload.put("success", true);
|
||||
payload.put("events", loadAntiBotSecurityEvents(250));
|
||||
sendHttpResponse(out, buildJsonString(payload), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
String line;
|
||||
@@ -609,7 +761,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
response.append("Access-Control-Allow-Origin: *\r\n");
|
||||
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
|
||||
response.append("Connection: close\r\n\r\n");
|
||||
out.write(response.toString().getBytes("UTF-8"));
|
||||
out.write(response.toString().getBytes(StandardCharsets.UTF_8));
|
||||
out.write(jsonBytes);
|
||||
out.flush();
|
||||
}
|
||||
@@ -741,4 +893,84 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> loadAntiBotSecurityEvents(int maxEntries) {
|
||||
List<Map<String, Object>> out = new ArrayList<Map<String, Object>>();
|
||||
File logFile = new File(getDataFolder(), "antibot-security.log");
|
||||
if (!logFile.exists() || maxEntries <= 0) {
|
||||
return out;
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(logFile.toPath(), StandardCharsets.UTF_8);
|
||||
for (int i = lines.size() - 1; i >= 0 && out.size() < maxEntries; i--) {
|
||||
String line = lines.get(i);
|
||||
if (line == null || line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, String> parsed = parseSecurityLogLine(line);
|
||||
String event = parsed.get("event");
|
||||
if (!isAttackSecurityEvent(event)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String player = parsed.get("player");
|
||||
String uuid = parsed.get("uuid");
|
||||
if (player == null || player.trim().isEmpty() || "-".equals(player) || "unknown".equalsIgnoreCase(player)) {
|
||||
continue;
|
||||
}
|
||||
if (uuid == null || uuid.trim().isEmpty()) {
|
||||
uuid = "-";
|
||||
}
|
||||
|
||||
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
||||
row.put("datetime", parsed.getOrDefault("datetime", ""));
|
||||
row.put("player", player);
|
||||
row.put("uuid", uuid);
|
||||
row.put("ip", parsed.getOrDefault("ip", "-"));
|
||||
out.add(row);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Konnte antibot-security.log nicht lesen: " + e.getMessage());
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<String, String> parseSecurityLogLine(String line) {
|
||||
Map<String, String> map = new LinkedHashMap<String, String>();
|
||||
if (line == null) {
|
||||
return map;
|
||||
}
|
||||
|
||||
String[] segments = line.split("\\\\s*\\\\|\\\\s*");
|
||||
if (segments.length > 0) {
|
||||
map.put("datetime", segments[0].trim());
|
||||
}
|
||||
|
||||
for (int i = 1; i < segments.length; i++) {
|
||||
String seg = segments[i];
|
||||
int idx = seg.indexOf('=');
|
||||
if (idx <= 0) {
|
||||
continue;
|
||||
}
|
||||
String key = seg.substring(0, idx).trim().toLowerCase(Locale.ROOT);
|
||||
String value = seg.substring(idx + 1).trim();
|
||||
map.put(key, value);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private boolean isAttackSecurityEvent(String event) {
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
String e = event.trim().toLowerCase(Locale.ROOT);
|
||||
return e.contains("ip_rate")
|
||||
|| e.contains("vpn")
|
||||
|| e.contains("learning_threshold_block")
|
||||
|| e.contains("block");
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ 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;
|
||||
@@ -14,9 +16,11 @@ 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;
|
||||
@@ -24,13 +28,18 @@ 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;
|
||||
@@ -60,6 +69,23 @@ public class AntiBotModule implements Module, Listener {
|
||||
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;
|
||||
@@ -75,6 +101,9 @@ public class AntiBotModule implements Module, Listener {
|
||||
private final Map<String, IpWindow> perIpWindows = new ConcurrentHashMap<String, IpWindow>();
|
||||
private final Map<String, Long> blockedIpsUntil = new ConcurrentHashMap<String, Long>();
|
||||
private final Map<String, VpnCacheEntry> vpnCache = new ConcurrentHashMap<String, VpnCacheEntry>();
|
||||
private final Map<String, RecentPlayerIdentity> recentIdentityByIp = new ConcurrentHashMap<String, RecentPlayerIdentity>();
|
||||
private final Map<String, LearningProfile> learningProfiles = new ConcurrentHashMap<String, LearningProfile>();
|
||||
private final Deque<String> learningRecentEvents = new ArrayDeque<String>();
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -89,6 +118,7 @@ public class AntiBotModule implements Module, Listener {
|
||||
this.plugin = (StatusAPI) plugin;
|
||||
ensureModuleConfigExists();
|
||||
loadConfig();
|
||||
ensureSecurityLogFile();
|
||||
|
||||
if (!enabled) {
|
||||
this.plugin.getLogger().info("[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME + " (antibot.enabled=false)");
|
||||
@@ -107,6 +137,10 @@ public class AntiBotModule implements Module, Listener {
|
||||
perIpWindows.clear();
|
||||
blockedIpsUntil.clear();
|
||||
vpnCache.clear();
|
||||
learningProfiles.clear();
|
||||
synchronized (learningRecentEvents) {
|
||||
learningRecentEvents.clear();
|
||||
}
|
||||
blockedIpsCurrentAttack.clear();
|
||||
attackMode = false;
|
||||
}
|
||||
@@ -119,6 +153,10 @@ public class AntiBotModule implements Module, Listener {
|
||||
perIpWindows.clear();
|
||||
blockedIpsUntil.clear();
|
||||
vpnCache.clear();
|
||||
learningProfiles.clear();
|
||||
synchronized (learningRecentEvents) {
|
||||
learningRecentEvents.clear();
|
||||
}
|
||||
blockedIpsCurrentAttack.clear();
|
||||
attackMode = false;
|
||||
attackCalmSince = 0L;
|
||||
@@ -127,6 +165,7 @@ public class AntiBotModule implements Module, Listener {
|
||||
lastCps = 0;
|
||||
peakCps.set(0);
|
||||
loadConfig();
|
||||
ensureSecurityLogFile();
|
||||
}
|
||||
|
||||
public Map<String, Object> buildSnapshot() {
|
||||
@@ -145,6 +184,8 @@ public class AntiBotModule implements Module, Listener {
|
||||
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;
|
||||
}
|
||||
@@ -157,6 +198,8 @@ public class AntiBotModule implements Module, Listener {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -171,6 +214,8 @@ public class AntiBotModule implements Module, Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
cacheRecentIdentity(ip, event.getConnection(), System.currentTimeMillis());
|
||||
|
||||
recordConnection();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
@@ -178,14 +223,36 @@ public class AntiBotModule implements Module, Listener {
|
||||
|
||||
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 (isIpRateExceeded(ip, now)) {
|
||||
blockIp(ip, now);
|
||||
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) {
|
||||
@@ -193,13 +260,47 @@ public class AntiBotModule implements Module, Listener {
|
||||
if (info != null) {
|
||||
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
|
||||
if (shouldBlock) {
|
||||
blockIp(ip, now);
|
||||
blockEvent(event);
|
||||
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);
|
||||
}
|
||||
@@ -218,6 +319,20 @@ public class AntiBotModule implements Module, Listener {
|
||||
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();
|
||||
if (sa.getAddress() != null) {
|
||||
return sa.getAddress().getHostAddress();
|
||||
}
|
||||
return sa.getHostString();
|
||||
}
|
||||
return String.valueOf(player.getAddress());
|
||||
}
|
||||
|
||||
private void recordConnection() {
|
||||
long sec = System.currentTimeMillis() / 1000L;
|
||||
if (sec != currentSecond) {
|
||||
@@ -266,6 +381,27 @@ public class AntiBotModule implements Module, Listener {
|
||||
vpnCache.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, RecentPlayerIdentity> entry : recentIdentityByIp.entrySet()) {
|
||||
RecentPlayerIdentity id = entry.getValue();
|
||||
if (id == null || (now - id.updatedAtMs) > 600_000L) {
|
||||
recentIdentityByIp.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (learningModeEnabled) {
|
||||
long staleAfter = Math.max(60, learningStateWindowSeconds) * 1000L;
|
||||
for (Map.Entry<String, LearningProfile> entry : learningProfiles.entrySet()) {
|
||||
LearningProfile lp = entry.getValue();
|
||||
if (lp == null) {
|
||||
learningProfiles.remove(entry.getKey());
|
||||
continue;
|
||||
}
|
||||
if ((now - lp.lastSeenAt) > staleAfter && lp.score <= 0) {
|
||||
learningProfiles.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
@@ -356,6 +492,24 @@ public class AntiBotModule implements Module, Listener {
|
||||
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());
|
||||
}
|
||||
@@ -496,6 +650,266 @@ public class AntiBotModule implements Module, Listener {
|
||||
}
|
||||
}
|
||||
|
||||
private void evaluateLearningBaseline(String ip, long now) {
|
||||
LearningProfile profile = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
|
||||
|
||||
synchronized (profile) {
|
||||
decayLearningProfile(profile, now);
|
||||
|
||||
long delta = now - profile.lastConnectionAt;
|
||||
if (profile.lastConnectionAt > 0 && delta <= Math.max(250L, learningRapidWindowMs)) {
|
||||
profile.rapidStreak++;
|
||||
int points = learningRapidPoints + Math.min(profile.rapidStreak, 5);
|
||||
profile.score += Math.max(1, points);
|
||||
recordLearningEvent("IP=" + ip + " +" + points + " rapid-connect score=" + profile.score);
|
||||
} else {
|
||||
profile.rapidStreak = 0;
|
||||
}
|
||||
|
||||
if (attackMode) {
|
||||
profile.score += Math.max(1, learningAttackModePoints);
|
||||
recordLearningEvent("IP=" + ip + " +" + learningAttackModePoints + " attack-mode score=" + profile.score);
|
||||
}
|
||||
|
||||
if (lastCps >= Math.max(1, maxCps)) {
|
||||
profile.score += Math.max(1, learningHighCpsPoints);
|
||||
recordLearningEvent("IP=" + ip + " +" + learningHighCpsPoints + " high-cps score=" + profile.score);
|
||||
}
|
||||
|
||||
profile.lastConnectionAt = now;
|
||||
profile.lastSeenAt = now;
|
||||
|
||||
if (profile.score >= learningScoreThreshold) {
|
||||
blockIp(ip, now);
|
||||
recordLearningEvent("BLOCK " + ip + " reason=learning-threshold score=" + profile.score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int addLearningScore(String ip, long now, int points, String reason, boolean checkThreshold) {
|
||||
LearningProfile profile = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
|
||||
synchronized (profile) {
|
||||
decayLearningProfile(profile, now);
|
||||
int add = Math.max(1, points);
|
||||
profile.score += add;
|
||||
profile.lastSeenAt = now;
|
||||
recordLearningEvent("IP=" + ip + " +" + add + " " + reason + " score=" + profile.score);
|
||||
|
||||
if (checkThreshold && profile.score >= learningScoreThreshold) {
|
||||
blockIp(ip, now);
|
||||
recordLearningEvent("BLOCK " + ip + " reason=" + reason + " score=" + profile.score);
|
||||
}
|
||||
|
||||
return profile.score;
|
||||
}
|
||||
}
|
||||
|
||||
private int getLearningScore(String ip, long now) {
|
||||
LearningProfile profile = learningProfiles.get(ip);
|
||||
if (profile == null) {
|
||||
return 0;
|
||||
}
|
||||
synchronized (profile) {
|
||||
decayLearningProfile(profile, now);
|
||||
return profile.score;
|
||||
}
|
||||
}
|
||||
|
||||
private void decayLearningProfile(LearningProfile profile, long now) {
|
||||
long elapsedMs = Math.max(0L, now - profile.lastScoreUpdateAt);
|
||||
if (elapsedMs > 0L) {
|
||||
long decay = (elapsedMs / 1000L) * Math.max(0, learningDecayPerSecond);
|
||||
if (decay > 0L) {
|
||||
profile.score = (int) Math.max(0L, profile.score - decay);
|
||||
}
|
||||
profile.lastScoreUpdateAt = now;
|
||||
}
|
||||
|
||||
long resetAfter = Math.max(30, learningStateWindowSeconds) * 1000L;
|
||||
if (profile.lastSeenAt > 0L && now - profile.lastSeenAt > resetAfter) {
|
||||
profile.score = 0;
|
||||
profile.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() || "unknown".equalsIgnoreCase(uuid))
|
||||
&& 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()) {
|
||||
UUID offlineUuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.trim()).getBytes(StandardCharsets.UTF_8));
|
||||
return offlineUuid.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<String>();
|
||||
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());
|
||||
@@ -586,6 +1000,28 @@ public class AntiBotModule implements Module, Listener {
|
||||
boolean hosting;
|
||||
}
|
||||
|
||||
private static class LearningProfile {
|
||||
long lastConnectionAt;
|
||||
long lastScoreUpdateAt;
|
||||
long lastSeenAt;
|
||||
int rapidStreak;
|
||||
int score;
|
||||
|
||||
LearningProfile(long now) {
|
||||
this.lastConnectionAt = now;
|
||||
this.lastScoreUpdateAt = now;
|
||||
this.lastSeenAt = now;
|
||||
this.rapidStreak = 0;
|
||||
this.score = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecentPlayerIdentity {
|
||||
String playerName;
|
||||
String playerUuid;
|
||||
long updatedAtMs;
|
||||
}
|
||||
|
||||
private class AntiBotCommand extends Command {
|
||||
AntiBotCommand() {
|
||||
super("antibot", "statusapi.antibot");
|
||||
@@ -611,6 +1047,16 @@ public class AntiBotModule implements Module, Listener {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ public class ChatConfig {
|
||||
private String pmFormatReceiver;
|
||||
private String pmFormatSpy;
|
||||
private String pmSpyPermission;
|
||||
private boolean pmRateLimitEnabled;
|
||||
private long pmRateLimitWindowMs;
|
||||
private int pmRateLimitMaxActions;
|
||||
private long pmRateLimitBlockMs;
|
||||
private String pmRateLimitMessage;
|
||||
|
||||
// Mute
|
||||
private int defaultMuteDuration;
|
||||
@@ -294,6 +299,9 @@ public class ChatConfig {
|
||||
filterConfig.spamCooldownMs = spam.getInt("cooldown-ms", 1500);
|
||||
filterConfig.spamMaxMessages = spam.getInt("max-messages", 3);
|
||||
filterConfig.spamMessage = spam.getString("message", "&cNicht so schnell!");
|
||||
filterConfig.globalRateLimitWindowMs = Math.max(500L, filterConfig.spamCooldownMs);
|
||||
filterConfig.globalRateLimitMaxActions = Math.max(1, filterConfig.spamMaxMessages);
|
||||
filterConfig.globalRateLimitBlockMs = Math.max(2000L, filterConfig.spamCooldownMs * 2L);
|
||||
}
|
||||
Configuration dup = cf.getSection("duplicate-check");
|
||||
if (dup != null) {
|
||||
@@ -324,6 +332,34 @@ public class ChatConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Globales Rate-Limit-Framework ---
|
||||
pmRateLimitEnabled = true;
|
||||
pmRateLimitWindowMs = 5000L;
|
||||
pmRateLimitMaxActions = 4;
|
||||
pmRateLimitBlockMs = 10000L;
|
||||
pmRateLimitMessage = "&cDu sendest zu viele private Nachrichten. Bitte warte kurz.";
|
||||
|
||||
Configuration rl = config.getSection("rate-limit");
|
||||
if (rl != null) {
|
||||
Configuration rlChat = rl.getSection("chat");
|
||||
if (rlChat != null) {
|
||||
filterConfig.globalRateLimitEnabled = rlChat.getBoolean("enabled", true);
|
||||
filterConfig.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs);
|
||||
filterConfig.globalRateLimitMaxActions = rlChat.getInt("max-actions", filterConfig.globalRateLimitMaxActions);
|
||||
filterConfig.globalRateLimitBlockMs = rlChat.getLong("block-ms", filterConfig.globalRateLimitBlockMs);
|
||||
filterConfig.spamMessage = rlChat.getString("message", filterConfig.spamMessage);
|
||||
}
|
||||
|
||||
Configuration rlPm = rl.getSection("private-messages");
|
||||
if (rlPm != null) {
|
||||
pmRateLimitEnabled = rlPm.getBoolean("enabled", pmRateLimitEnabled);
|
||||
pmRateLimitWindowMs = rlPm.getLong("window-ms", pmRateLimitWindowMs);
|
||||
pmRateLimitMaxActions = rlPm.getInt("max-actions", pmRateLimitMaxActions);
|
||||
pmRateLimitBlockMs = rlPm.getLong("block-ms", pmRateLimitBlockMs);
|
||||
pmRateLimitMessage = rlPm.getString("message", pmRateLimitMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Mentions ---
|
||||
Configuration mn = config.getSection("mentions");
|
||||
if (mn != null) {
|
||||
@@ -445,6 +481,11 @@ public class ChatConfig {
|
||||
public String getPmFormatReceiver() { return pmFormatReceiver; }
|
||||
public String getPmFormatSpy() { return pmFormatSpy; }
|
||||
public String getPmSpyPermission() { return pmSpyPermission; }
|
||||
public boolean isPmRateLimitEnabled() { return pmRateLimitEnabled; }
|
||||
public long getPmRateLimitWindowMs() { return pmRateLimitWindowMs; }
|
||||
public int getPmRateLimitMaxActions() { return pmRateLimitMaxActions; }
|
||||
public long getPmRateLimitBlockMs() { return pmRateLimitBlockMs; }
|
||||
public String getPmRateLimitMessage() { return pmRateLimitMessage; }
|
||||
|
||||
public int getDefaultMuteDuration() { return defaultMuteDuration; }
|
||||
public String getMutedMessage() { return mutedMessage; }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.viper.status.modules.chat;
|
||||
|
||||
import net.viper.status.ratelimit.GlobalRateLimitFramework;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -17,13 +19,10 @@ import java.util.regex.Pattern;
|
||||
public class ChatFilter {
|
||||
|
||||
private final ChatFilterConfig cfg;
|
||||
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
||||
|
||||
// UUID → letzter Nachricht-Zeitstempel (ms)
|
||||
private final Map<UUID, Long> lastMessageTime = new ConcurrentHashMap<>();
|
||||
// UUID → letzte Nachricht (für Duplikat-Check)
|
||||
private final Map<UUID, String> lastMessageText = new ConcurrentHashMap<>();
|
||||
// UUID → Spam-Zähler (aufeinanderfolgende schnelle Nachrichten)
|
||||
private final Map<UUID, Integer> spamCount = new ConcurrentHashMap<>();
|
||||
|
||||
// Kompilierte Regex-Pattern für Blacklist-Wörter
|
||||
private final List<Pattern> blacklistPatterns = new ArrayList<>();
|
||||
@@ -79,17 +78,21 @@ public class ChatFilter {
|
||||
|
||||
// ── 1. Spam-Cooldown ──
|
||||
if (cfg.antiSpamEnabled && !isAdmin) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastMessageTime.get(uuid);
|
||||
if (last != null && (now - last) < cfg.spamCooldownMs) {
|
||||
int count = spamCount.merge(uuid, 1, Integer::sum);
|
||||
if (count >= cfg.spamMaxMessages) {
|
||||
if (cfg.globalRateLimitEnabled) {
|
||||
GlobalRateLimitFramework.Result rl = rateLimiter.check(
|
||||
"chat.message",
|
||||
uuid.toString(),
|
||||
new GlobalRateLimitFramework.Rule(
|
||||
true,
|
||||
cfg.globalRateLimitWindowMs,
|
||||
cfg.globalRateLimitMaxActions,
|
||||
cfg.globalRateLimitBlockMs
|
||||
)
|
||||
);
|
||||
if (rl.isBlocked()) {
|
||||
return new FilterResponse(FilterResult.BLOCKED, message, cfg.spamMessage);
|
||||
}
|
||||
} else {
|
||||
spamCount.put(uuid, 0);
|
||||
}
|
||||
lastMessageTime.put(uuid, now);
|
||||
}
|
||||
|
||||
// ── 2. Duplikat-Check ──
|
||||
@@ -201,9 +204,8 @@ public class ChatFilter {
|
||||
// ===== Cleanup beim Logout =====
|
||||
|
||||
public void cleanup(UUID uuid) {
|
||||
lastMessageTime.remove(uuid);
|
||||
lastMessageText.remove(uuid);
|
||||
spamCount.remove(uuid);
|
||||
rateLimiter.clearActor(uuid.toString());
|
||||
}
|
||||
|
||||
// ===== Konfigurationsklasse =====
|
||||
@@ -211,10 +213,16 @@ public class ChatFilter {
|
||||
public static class ChatFilterConfig {
|
||||
// Anti-Spam
|
||||
public boolean antiSpamEnabled = true;
|
||||
public long spamCooldownMs = 1500; // ms zwischen Nachrichten
|
||||
public int spamMaxMessages = 3; // max. Nachrichten innerhalb Cooldown
|
||||
public long spamCooldownMs = 1500; // Legacy-Feld fuer Kompatibilitaet
|
||||
public int spamMaxMessages = 3; // Legacy-Feld fuer Kompatibilitaet
|
||||
public String spamMessage = "&cBitte nicht so schnell schreiben!";
|
||||
|
||||
// Globales Rate-Limit-Framework
|
||||
public boolean globalRateLimitEnabled = true;
|
||||
public long globalRateLimitWindowMs = 2500;
|
||||
public int globalRateLimitMaxActions = 3;
|
||||
public long globalRateLimitBlockMs = 6000;
|
||||
|
||||
// Duplikat
|
||||
public boolean duplicateCheckEnabled = true;
|
||||
public String duplicateMessage = "&cBitte keine identischen Nachrichten senden.";
|
||||
|
||||
@@ -4,6 +4,7 @@ import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.viper.status.ratelimit.GlobalRateLimitFramework;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -15,6 +16,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public class PrivateMsgManager {
|
||||
|
||||
private final BlockManager blockManager;
|
||||
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
||||
|
||||
// UUID → letzte PM-Gesprächspartner UUID (für /r)
|
||||
private final Map<UUID, UUID> lastPartner = new ConcurrentHashMap<>();
|
||||
@@ -55,6 +57,24 @@ public class PrivateMsgManager {
|
||||
sender.sendMessage(color("&cDieser Spieler hat dich blockiert oder du hast ihn blockiert."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.isPmRateLimitEnabled()) {
|
||||
GlobalRateLimitFramework.Result result = rateLimiter.check(
|
||||
"chat.pm",
|
||||
sender.getUniqueId().toString(),
|
||||
new GlobalRateLimitFramework.Rule(
|
||||
true,
|
||||
config.getPmRateLimitWindowMs(),
|
||||
config.getPmRateLimitMaxActions(),
|
||||
config.getPmRateLimitBlockMs()
|
||||
)
|
||||
);
|
||||
|
||||
if (result.isBlocked()) {
|
||||
sender.sendMessage(color(config.getPmRateLimitMessage()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Formatierung
|
||||
|
||||
@@ -10,6 +10,7 @@ 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.ratelimit.GlobalRateLimitFramework;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -27,6 +28,13 @@ public class CommandBlockerModule implements Module, Listener {
|
||||
|
||||
private File file;
|
||||
private Set<String> blocked = new HashSet<>();
|
||||
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
||||
|
||||
private boolean commandRateLimitEnabled = true;
|
||||
private long commandRateLimitWindowMs = 3000L;
|
||||
private int commandRateLimitMaxActions = 8;
|
||||
private long commandRateLimitBlockMs = 6000L;
|
||||
private String commandRateLimitMessage = "&cZu viele Befehle in kurzer Zeit. Bitte warte kurz.";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -75,6 +83,25 @@ public class CommandBlockerModule implements Module, Listener {
|
||||
String msg = event.getMessage();
|
||||
if (msg == null || msg.length() <= 1) return;
|
||||
|
||||
if (commandRateLimitEnabled) {
|
||||
GlobalRateLimitFramework.Result result = rateLimiter.check(
|
||||
"chat.command",
|
||||
player.getUniqueId().toString(),
|
||||
new GlobalRateLimitFramework.Rule(
|
||||
true,
|
||||
commandRateLimitWindowMs,
|
||||
commandRateLimitMaxActions,
|
||||
commandRateLimitBlockMs
|
||||
)
|
||||
);
|
||||
|
||||
if (result.isBlocked()) {
|
||||
event.setCancelled(true);
|
||||
player.sendMessage(ChatColor.translateAlternateColorCodes('&', commandRateLimitMessage));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String cmd = msg.substring(1).toLowerCase(Locale.ROOT);
|
||||
String base = cmd.split(" ")[0];
|
||||
|
||||
@@ -154,6 +181,21 @@ public class CommandBlockerModule implements Module, Listener {
|
||||
}
|
||||
}
|
||||
|
||||
if (data != null && data.containsKey("rate-limit")) {
|
||||
Object rlObj = data.get("rate-limit");
|
||||
if (rlObj instanceof Map) {
|
||||
Map rl = (Map) rlObj;
|
||||
commandRateLimitEnabled = parseBoolean(rl.get("enabled"), commandRateLimitEnabled);
|
||||
commandRateLimitWindowMs = parseLong(rl.get("window-ms"), commandRateLimitWindowMs);
|
||||
commandRateLimitMaxActions = (int) parseLong(rl.get("max-actions"), commandRateLimitMaxActions);
|
||||
commandRateLimitBlockMs = parseLong(rl.get("block-ms"), commandRateLimitBlockMs);
|
||||
Object msgObj = rl.get("message");
|
||||
if (msgObj != null) {
|
||||
commandRateLimitMessage = String.valueOf(msgObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
if (plugin != null) plugin.getLogger().severe("[CommandBlocker] Fehler beim Laden: " + e.getMessage());
|
||||
else System.err.println("[CommandBlocker] Fehler beim Laden: " + e.getMessage());
|
||||
@@ -165,6 +207,15 @@ public class CommandBlockerModule implements Module, Listener {
|
||||
Yaml yaml = new Yaml();
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("blocked", new ArrayList<>(blocked));
|
||||
|
||||
Map<String, Object> rl = new LinkedHashMap<>();
|
||||
rl.put("enabled", commandRateLimitEnabled);
|
||||
rl.put("window-ms", commandRateLimitWindowMs);
|
||||
rl.put("max-actions", commandRateLimitMaxActions);
|
||||
rl.put("block-ms", commandRateLimitBlockMs);
|
||||
rl.put("message", commandRateLimitMessage);
|
||||
out.put("rate-limit", rl);
|
||||
|
||||
FileWriter fw = null;
|
||||
try {
|
||||
fw = new FileWriter(file);
|
||||
@@ -177,4 +228,18 @@ public class CommandBlockerModule implements Module, Listener {
|
||||
else System.err.println("[CommandBlocker] Fehler beim Speichern: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseBoolean(Object obj, boolean fallback) {
|
||||
if (obj == null) return fallback;
|
||||
return Boolean.parseBoolean(String.valueOf(obj));
|
||||
}
|
||||
|
||||
private long parseLong(Object obj, long fallback) {
|
||||
if (obj == null) return fallback;
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(obj));
|
||||
} catch (Exception ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.api.scheduler.ScheduledTask;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
import java.io.File;
|
||||
@@ -55,11 +56,18 @@ public class NetworkInfoModule implements Module {
|
||||
private int alertMemoryPercent = 90;
|
||||
private int alertPlayerPercent = 95;
|
||||
private int alertCooldownSeconds = 300;
|
||||
private boolean alertTpsEnabled = true;
|
||||
private double alertTpsThreshold = 18.0D;
|
||||
private boolean attackNotificationsEnabled = true;
|
||||
private String attackApiKey = "";
|
||||
private String attackDefaultSource = "BetterBungee";
|
||||
private long lastMemoryAlertAt = 0L;
|
||||
private long lastPlayerAlertAt = 0L;
|
||||
private long lastTpsAlertAt = 0L;
|
||||
private volatile double currentProxyTps = 20.0D;
|
||||
private long lastTpsSampleAtMs = 0L;
|
||||
private ScheduledTask alertTask;
|
||||
private ScheduledTask tpsSamplerTask;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -82,6 +90,8 @@ public class NetworkInfoModule implements Module {
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new NetInfoCommand());
|
||||
}
|
||||
|
||||
tpsSamplerTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::sampleProxyTps, 1, 1, TimeUnit.SECONDS);
|
||||
|
||||
if (webhookEnabled && !webhookUrl.isEmpty()) {
|
||||
if (webhookNotifyStartStop) {
|
||||
boolean delivered = sendLifecycleStartNotification();
|
||||
@@ -96,7 +106,7 @@ public class NetworkInfoModule implements Module {
|
||||
}
|
||||
}
|
||||
int interval = Math.max(10, webhookCheckSeconds);
|
||||
ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS);
|
||||
alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[NetworkInfoModule] aktiviert. commandEnabled=" + commandEnabled + ", includePlayerNames=" + includePlayerNames + ", webhookEnabled=" + webhookEnabled + ", notifyStartStop=" + webhookNotifyStartStop + ", webhookUrlPresent=" + !webhookUrl.isEmpty());
|
||||
@@ -104,6 +114,15 @@ public class NetworkInfoModule implements Module {
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
if (alertTask != null) {
|
||||
alertTask.cancel();
|
||||
alertTask = null;
|
||||
}
|
||||
if (tpsSamplerTask != null) {
|
||||
tpsSamplerTask.cancel();
|
||||
tpsSamplerTask = null;
|
||||
}
|
||||
|
||||
if (enabled && webhookEnabled && webhookNotifyStartStop && webhookUrl != null && !webhookUrl.isEmpty()) {
|
||||
boolean delivered = sendLifecycleStopNotification();
|
||||
if (!delivered) {
|
||||
@@ -262,6 +281,7 @@ public class NetworkInfoModule implements Module {
|
||||
system.put("os_arch", System.getProperty("os.arch"));
|
||||
system.put("available_processors", Runtime.getRuntime().availableProcessors());
|
||||
system.put("system_load_percent", getSystemLoadPercent());
|
||||
system.put("proxy_tps", roundDouble(currentProxyTps, 2));
|
||||
|
||||
out.put("enabled", true);
|
||||
out.put("timestamp_unix", now / 1000L);
|
||||
@@ -405,6 +425,8 @@ public class NetworkInfoModule implements Module {
|
||||
alertMemoryPercent = 90;
|
||||
alertPlayerPercent = 95;
|
||||
alertCooldownSeconds = 300;
|
||||
alertTpsEnabled = true;
|
||||
alertTpsThreshold = 18.0D;
|
||||
attackNotificationsEnabled = true;
|
||||
attackApiKey = "";
|
||||
attackDefaultSource = "BetterBungee";
|
||||
@@ -428,6 +450,8 @@ public class NetworkInfoModule implements Module {
|
||||
alertMemoryPercent = parseInt(props.getProperty("networkinfo.alert.memory_percent", "90"), 90);
|
||||
alertPlayerPercent = parseInt(props.getProperty("networkinfo.alert.player_percent", "95"), 95);
|
||||
alertCooldownSeconds = parseInt(props.getProperty("networkinfo.alert.cooldown_seconds", "300"), 300);
|
||||
alertTpsEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.alert.tps_enabled", "true"));
|
||||
alertTpsThreshold = parseDouble(props.getProperty("networkinfo.alert.tps_threshold", "18.0"), 18.0D);
|
||||
attackNotificationsEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.attack.enabled", "true"));
|
||||
attackApiKey = props.getProperty("networkinfo.attack.api_key", "").trim();
|
||||
attackDefaultSource = props.getProperty("networkinfo.attack.source", "BetterBungee").trim();
|
||||
@@ -446,6 +470,8 @@ public class NetworkInfoModule implements Module {
|
||||
alertMemoryPercent = 90;
|
||||
alertPlayerPercent = 95;
|
||||
alertCooldownSeconds = 300;
|
||||
alertTpsEnabled = true;
|
||||
alertTpsThreshold = 18.0D;
|
||||
attackNotificationsEnabled = true;
|
||||
attackApiKey = "";
|
||||
attackDefaultSource = "BetterBungee";
|
||||
@@ -494,6 +520,7 @@ public class NetworkInfoModule implements Module {
|
||||
|
||||
int memoryPercent = toInt(memory.get("usage_percent"));
|
||||
int playerPercent = toInt(players.get("occupancy_percent"));
|
||||
double proxyTps = currentProxyTps;
|
||||
|
||||
if (memoryPercent >= Math.max(1, alertMemoryPercent) && canSend(lastMemoryAlertAt, now)) {
|
||||
lastMemoryAlertAt = now;
|
||||
@@ -542,6 +569,53 @@ public class NetworkInfoModule implements Module {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (alertTpsEnabled && proxyTps > 0D && proxyTps < Math.max(1D, alertTpsThreshold) && canSend(lastTpsAlertAt, now)) {
|
||||
lastTpsAlertAt = now;
|
||||
String tpsText = String.format(Locale.ROOT, "%.2f", proxyTps);
|
||||
String thresholdText = String.format(Locale.ROOT, "%.2f", Math.max(1D, alertTpsThreshold));
|
||||
|
||||
if (isCompactEmbedMode()) {
|
||||
sendWebhookEmbed(
|
||||
webhookUrl,
|
||||
"🟥 Niedrige Proxy-TPS",
|
||||
"Aktuell: **" + tpsText + " TPS**\nSchwelle: **" + thresholdText + " TPS**",
|
||||
0xE74C3C
|
||||
);
|
||||
} else {
|
||||
StringBuilder fields = new StringBuilder();
|
||||
appendEmbedField(fields, "Proxy TPS", tpsText, true);
|
||||
appendEmbedField(fields, "Schwelle", thresholdText, true);
|
||||
appendEmbedField(fields, "Check-Intervall", Math.max(10, webhookCheckSeconds) + "s", true);
|
||||
sendWebhookEmbed(
|
||||
webhookUrl,
|
||||
"🟥 Niedrige Proxy-TPS",
|
||||
"Die gemessene Proxy-TPS liegt unter der konfigurierten Schwelle.",
|
||||
0xE74C3C,
|
||||
fields.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sampleProxyTps() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (lastTpsSampleAtMs <= 0L) {
|
||||
lastTpsSampleAtMs = now;
|
||||
currentProxyTps = 20.0D;
|
||||
return;
|
||||
}
|
||||
|
||||
long deltaMs = now - lastTpsSampleAtMs;
|
||||
lastTpsSampleAtMs = now;
|
||||
if (deltaMs <= 0L) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1s Scheduler-Tick sollte etwa 20 TPS entsprechen. Abweichung zeigt Main-Thread-Lag.
|
||||
double instantTps = (1000.0D / (double) deltaMs) * 20.0D;
|
||||
instantTps = Math.max(0.1D, Math.min(20.0D, instantTps));
|
||||
currentProxyTps = (currentProxyTps * 0.7D) + (instantTps * 0.3D);
|
||||
}
|
||||
|
||||
private boolean isCompactEmbedMode() {
|
||||
@@ -561,6 +635,14 @@ public class NetworkInfoModule implements Module {
|
||||
}
|
||||
}
|
||||
|
||||
private double parseDouble(String s, double fallback) {
|
||||
try {
|
||||
return Double.parseDouble(s == null ? "" : s.trim());
|
||||
} catch (Exception ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private int toInt(Object o) {
|
||||
if (o instanceof Number) {
|
||||
return ((Number) o).intValue();
|
||||
@@ -572,6 +654,11 @@ public class NetworkInfoModule implements Module {
|
||||
}
|
||||
}
|
||||
|
||||
private double roundDouble(double value, int digits) {
|
||||
double factor = Math.pow(10D, Math.max(0, digits));
|
||||
return Math.round(value * factor) / factor;
|
||||
}
|
||||
|
||||
private boolean sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color) {
|
||||
return sendWebhookEmbed(targetWebhookUrl, title, description, color, null, true);
|
||||
}
|
||||
@@ -733,6 +820,8 @@ public class NetworkInfoModule implements Module {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> memory = (Map<String, Object>) snapshot.get("memory");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> system = (Map<String, Object>) snapshot.get("system");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> ping = (Map<String, Object>) players.get("ping");
|
||||
|
||||
sender.sendMessage(ChatColor.GOLD + "----- StatusAPI NetworkInfo -----");
|
||||
@@ -740,6 +829,7 @@ public class NetworkInfoModule implements Module {
|
||||
sender.sendMessage(ChatColor.YELLOW + "Spieler: " + ChatColor.WHITE + players.get("online") + "/" + players.get("max") + ChatColor.GRAY + " (Bedrock: " + players.get("bedrock_online") + ")");
|
||||
sender.sendMessage(ChatColor.YELLOW + "Ping: " + ChatColor.WHITE + "avg " + ping.get("avg_ms") + "ms, min " + ping.get("min_ms") + "ms, max " + ping.get("max_ms") + "ms");
|
||||
sender.sendMessage(ChatColor.YELLOW + "RAM: " + ChatColor.WHITE + memory.get("used_mb") + "MB / " + memory.get("max_mb") + "MB" + ChatColor.GRAY + " (" + memory.get("usage_percent") + "%)");
|
||||
sender.sendMessage(ChatColor.YELLOW + "Proxy TPS: " + ChatColor.WHITE + system.get("proxy_tps") + ChatColor.GRAY + " (Alert < " + String.format(Locale.ROOT, "%.2f", alertTpsThreshold) + ")");
|
||||
sender.sendMessage(ChatColor.YELLOW + "Backends: " + ChatColor.WHITE + ((List<?>) snapshot.get("backend_servers")).size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package net.viper.status.ratelimit;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Gemeinsames Rate-Limit-Framework fuer mehrere Module.
|
||||
*/
|
||||
public final class GlobalRateLimitFramework {
|
||||
|
||||
private static final GlobalRateLimitFramework INSTANCE = new GlobalRateLimitFramework();
|
||||
|
||||
private final Map<String, Bucket> buckets = new ConcurrentHashMap<String, Bucket>();
|
||||
|
||||
private GlobalRateLimitFramework() {
|
||||
}
|
||||
|
||||
public static GlobalRateLimitFramework getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public Result check(String scope, String actorId, Rule rule) {
|
||||
return check(scope, actorId, rule, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public Result check(String scope, String actorId, Rule rule, long now) {
|
||||
if (rule == null || !rule.enabled || scope == null || scope.isEmpty() || actorId == null || actorId.isEmpty()) {
|
||||
return Result.allowed(0, 0L);
|
||||
}
|
||||
|
||||
String key = scope + ":" + actorId;
|
||||
Bucket bucket = buckets.computeIfAbsent(key, k -> new Bucket());
|
||||
|
||||
synchronized (bucket) {
|
||||
if (bucket.blockedUntil > now) {
|
||||
return Result.blocked(Math.max(0L, bucket.blockedUntil - now), bucket.hits.size());
|
||||
}
|
||||
|
||||
long minTs = now - Math.max(1L, rule.windowMs);
|
||||
while (!bucket.hits.isEmpty() && bucket.hits.peekFirst() < minTs) {
|
||||
bucket.hits.pollFirst();
|
||||
}
|
||||
|
||||
bucket.hits.addLast(now);
|
||||
|
||||
if (bucket.hits.size() > Math.max(1, rule.maxActions)) {
|
||||
long blockMs = Math.max(1L, rule.blockMs);
|
||||
bucket.blockedUntil = now + blockMs;
|
||||
return Result.blocked(blockMs, bucket.hits.size());
|
||||
}
|
||||
|
||||
return Result.allowed(bucket.hits.size(), 0L);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearActor(String actorId) {
|
||||
if (actorId == null || actorId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (String key : buckets.keySet()) {
|
||||
if (key.endsWith(":" + actorId)) {
|
||||
buckets.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Rule {
|
||||
public final boolean enabled;
|
||||
public final long windowMs;
|
||||
public final int maxActions;
|
||||
public final long blockMs;
|
||||
|
||||
public Rule(boolean enabled, long windowMs, int maxActions, long blockMs) {
|
||||
this.enabled = enabled;
|
||||
this.windowMs = Math.max(1L, windowMs);
|
||||
this.maxActions = Math.max(1, maxActions);
|
||||
this.blockMs = Math.max(1L, blockMs);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Result {
|
||||
private final boolean blocked;
|
||||
private final int currentHits;
|
||||
private final long remainingBlockMs;
|
||||
|
||||
private Result(boolean blocked, int currentHits, long remainingBlockMs) {
|
||||
this.blocked = blocked;
|
||||
this.currentHits = Math.max(0, currentHits);
|
||||
this.remainingBlockMs = Math.max(0L, remainingBlockMs);
|
||||
}
|
||||
|
||||
public static Result blocked(long remainingBlockMs, int currentHits) {
|
||||
return new Result(true, currentHits, remainingBlockMs);
|
||||
}
|
||||
|
||||
public static Result allowed(int currentHits, long remainingBlockMs) {
|
||||
return new Result(false, currentHits, remainingBlockMs);
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return blocked;
|
||||
}
|
||||
|
||||
public int getCurrentHits() {
|
||||
return currentHits;
|
||||
}
|
||||
|
||||
public long getRemainingBlockMs() {
|
||||
return remainingBlockMs;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Bucket {
|
||||
final Deque<Long> hits = new ArrayDeque<Long>();
|
||||
long blockedUntil = 0L;
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,25 @@ private-messages:
|
||||
format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}"
|
||||
social-spy-permission: "chat.socialspy"
|
||||
|
||||
# ============================================================
|
||||
# GLOBALES RATE-LIMIT-FRAMEWORK
|
||||
# Zentraler Schutz für Chat/PM/Command-Flood.
|
||||
# ============================================================
|
||||
rate-limit:
|
||||
chat:
|
||||
enabled: true
|
||||
window-ms: 2500
|
||||
max-actions: 3
|
||||
block-ms: 6000
|
||||
message: "&cBitte nicht so schnell schreiben!"
|
||||
|
||||
private-messages:
|
||||
enabled: true
|
||||
window-ms: 5000
|
||||
max-actions: 4
|
||||
block-ms: 10000
|
||||
message: "&cDu sendest zu viele private Nachrichten. Bitte warte kurz."
|
||||
|
||||
# ============================================================
|
||||
# MUTE
|
||||
# ============================================================
|
||||
|
||||
@@ -8,7 +8,7 @@ networkinfo.include_player_names=false
|
||||
|
||||
# Discord Webhook fuer Status-, Warn- und Attack-Meldungen
|
||||
networkinfo.webhook.enabled=true
|
||||
networkinfo.webhook.url=
|
||||
networkinfo.webhook.url=https://discord.com/api/webhooks/1488630083164831844/o7L5Mhy5P_xE_n-2Dq9usIVX40o7fCpPHgaGQOVIQHjfK7SDrVJbdeZM-G6vVRVhvzT9
|
||||
networkinfo.webhook.username=StatusAPI
|
||||
networkinfo.webhook.thumbnail_url=
|
||||
networkinfo.webhook.notify_start_stop=true
|
||||
@@ -20,6 +20,9 @@ networkinfo.webhook.check_seconds=30
|
||||
networkinfo.alert.memory_percent=90
|
||||
networkinfo.alert.player_percent=95
|
||||
networkinfo.alert.cooldown_seconds=300
|
||||
# Proxy-TPS Alert (20.0 = perfekt, Werte < 20 zeigen Main-Thread-Lag am Proxy)
|
||||
networkinfo.alert.tps_enabled=true
|
||||
networkinfo.alert.tps_threshold=18.0
|
||||
|
||||
# Attack Meldungen (Detected/Stopped)
|
||||
networkinfo.attack.enabled=true
|
||||
@@ -61,6 +64,26 @@ antibot.vpn_check.block_hosting=true
|
||||
antibot.vpn_check.cache_minutes=30
|
||||
antibot.vpn_check.timeout_ms=2500
|
||||
|
||||
# Sicherheitslog fuer Angreifer/VPN/Proxy-Events (mit Name/UUID falls verfuegbar)
|
||||
antibot.security_log.enabled=true
|
||||
antibot.security_log.file=antibot-security.log
|
||||
|
||||
# Lernmodus: Muster mitschreiben, Score bilden und erst ab Schwellwert blockieren.
|
||||
antibot.learning.enabled=true
|
||||
antibot.learning.score_threshold=100
|
||||
antibot.learning.decay_per_second=2
|
||||
antibot.learning.state_window_seconds=120
|
||||
|
||||
# Punktelogik pro Muster
|
||||
antibot.learning.rapid.window_ms=1500
|
||||
antibot.learning.rapid.points=12
|
||||
antibot.learning.ip_rate_exceeded.points=30
|
||||
antibot.learning.vpn_proxy.points=40
|
||||
antibot.learning.vpn_hosting.points=30
|
||||
antibot.learning.attack_mode.points=12
|
||||
antibot.learning.high_cps.points=10
|
||||
antibot.learning.recent_events.limit=30
|
||||
|
||||
# ===========================
|
||||
# BACKEND JOIN GUARD SYNC (optional)
|
||||
# ===========================
|
||||
|
||||
Reference in New Issue
Block a user