Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-04-02 20:56:43 +02:00
parent 802d1dfb33
commit 44951c4001
10 changed files with 1106 additions and 43 deletions

View File

@@ -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()) {
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 = serverSocket.accept();
handleConnection(clientSocket);
} catch (java.net.SocketTimeoutException e) {}
catch (IOException e) {
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) {
}
}
}
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");
}
}

View File

@@ -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,27 +223,83 @@ 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);
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);
@@ -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;
}

View File

@@ -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; }

View File

@@ -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.";

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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
# ============================================================

View File

@@ -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)
# ===========================