From a27feca901c7cd489eaf6e230da547af69b202d0 Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Thu, 2 Apr 2026 09:03:02 +0200 Subject: [PATCH] Upload folder via GUI - src --- .../backendguard/BackendJoinGuardPlugin.java | 509 ++++++++++++++++++ src/main/resources/config.yml | 29 + src/main/resources/plugin.yml | 14 + 3 files changed, 552 insertions(+) create mode 100644 src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java b/src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java new file mode 100644 index 0000000..f4d9c3f --- /dev/null +++ b/src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java @@ -0,0 +1,509 @@ +package net.viper.backendguard; + +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class BackendJoinGuardPlugin extends JavaPlugin implements Listener { + + private boolean enforcementEnabled; + private boolean logBlockedAttempts; + private String kickMessage; + private final Set allowedExactIps = new HashSet(); + private final List allowedCidrs = new ArrayList(); + private boolean statusApiSyncEnabled; + private String statusApiBaseUrl; + private String statusApiEndpointPath; + private String statusApiApiKey; + private int statusApiIntervalSeconds; + private boolean statusApiLogSyncErrors; + private BukkitTask syncTask; + + @Override + public void onEnable() { + saveDefaultConfig(); + reloadGuardConfig(); + getServer().getPluginManager().registerEvents(this, this); + getLogger().info("BackendJoinGuard aktiviert. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size() + ", StatusAPI-Sync=" + statusApiSyncEnabled); + } + + @Override + public void onDisable() { + cancelSyncTask(); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) { + if (!enforcementEnabled) { + return; + } + + InetAddress address = event.getAddress(); + if (address == null) { + return; + } + + if (isAllowed(address)) { + return; + } + + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, colorize(kickMessage)); + if (logBlockedAttempts) { + getLogger().warning("Direktjoin blockiert: player=" + event.getName() + ", ip=" + address.getHostAddress()); + } + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!"backendguard".equalsIgnoreCase(command.getName())) { + return false; + } + + if (!sender.hasPermission("backendguard.admin")) { + sender.sendMessage(colorize("&cDafuer hast du keine Rechte.")); + return true; + } + + if (args.length == 1 && "reload".equalsIgnoreCase(args[0])) { + reloadConfig(); + reloadGuardConfig(); + if (statusApiSyncEnabled) { + syncFromStatusApi(true); + sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. StatusAPI-Sync aktiv.")); + } else { + sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. Standalone-Modus aktiv.")); + } + return true; + } + + sender.sendMessage(colorize("&e/backendguard reload")); + return true; + } + + private void reloadGuardConfig() { + FileConfiguration config = getConfig(); + enforcementEnabled = config.getBoolean("enforcement-enabled", true); + logBlockedAttempts = config.getBoolean("log-blocked-attempts", true); + kickMessage = config.getString("kick-message", "&cBitte verbinde dich nur ueber den Proxy."); + + allowedExactIps.clear(); + allowedCidrs.clear(); + + for (String entry : config.getStringList("allowed-proxy-ips")) { + String normalized = normalizeIp(entry); + if (normalized != null) { + allowedExactIps.add(normalized); + } + } + + for (String entry : config.getStringList("allowed-proxy-cidrs")) { + SubnetRule rule = parseSubnet(entry); + if (rule != null) { + allowedCidrs.add(rule); + } + } + + statusApiSyncEnabled = config.getBoolean("statusapi-sync.enabled", false); + statusApiBaseUrl = config.getString("statusapi-sync.base-url", "http://127.0.0.1:9191"); + statusApiEndpointPath = config.getString("statusapi-sync.endpoint-path", "/network/backendguard/config"); + statusApiApiKey = config.getString("statusapi-sync.api-key", ""); + statusApiIntervalSeconds = Math.max(10, config.getInt("statusapi-sync.interval-seconds", 60)); + statusApiLogSyncErrors = config.getBoolean("statusapi-sync.log-sync-errors", true); + + cancelSyncTask(); + if (statusApiSyncEnabled) { + // Erst lokale Werte laden, dann zentral ueberschreiben falls Sync klappt. + syncFromStatusApi(false); + syncTask = getServer().getScheduler().runTaskTimerAsynchronously( + this, + () -> syncFromStatusApi(false), + statusApiIntervalSeconds * 20L, + statusApiIntervalSeconds * 20L + ); + } + } + + private void cancelSyncTask() { + if (syncTask != null) { + syncTask.cancel(); + syncTask = null; + } + } + + private void syncFromStatusApi(boolean manualTrigger) { + if (!statusApiSyncEnabled) { + return; + } + + HttpURLConnection conn = null; + try { + URL url = new URL(buildSyncUrl()); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(4000); + conn.setReadTimeout(6000); + conn.setRequestProperty("Accept", "application/json"); + if (statusApiApiKey != null && !statusApiApiKey.trim().isEmpty()) { + conn.setRequestProperty("x-api-key", statusApiApiKey.trim()); + } + + int code = conn.getResponseCode(); + if (code < 200 || code >= 300) { + if (statusApiLogSyncErrors || manualTrigger) { + getLogger().warning("StatusAPI-Sync fehlgeschlagen (HTTP " + code + "). Nutze lokale Guard-Werte."); + } + return; + } + + StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + sb.append(line); + } + } + + String json = sb.toString(); + Boolean remoteEnforcement = extractJsonBoolean(json, "enforcement_enabled"); + Boolean remoteLogBlocked = extractJsonBoolean(json, "log_blocked_attempts"); + String remoteKickMessage = extractJsonString(json, "kick_message"); + List remoteIps = extractJsonStringArray(json, "allowed_proxy_ips"); + List remoteCidrs = extractJsonStringArray(json, "allowed_proxy_cidrs"); + + if (remoteEnforcement != null) { + enforcementEnabled = remoteEnforcement; + } + if (remoteLogBlocked != null) { + logBlockedAttempts = remoteLogBlocked; + } + if (remoteKickMessage != null && !remoteKickMessage.trim().isEmpty()) { + kickMessage = remoteKickMessage; + } + + if (remoteIps != null) { + allowedExactIps.clear(); + for (String ip : remoteIps) { + String normalized = normalizeIp(ip); + if (normalized != null) { + allowedExactIps.add(normalized); + } + } + } + + if (remoteCidrs != null) { + allowedCidrs.clear(); + for (String cidr : remoteCidrs) { + SubnetRule rule = parseSubnet(cidr); + if (rule != null) { + allowedCidrs.add(rule); + } + } + } + + if (manualTrigger) { + getLogger().info("StatusAPI-Sync erfolgreich. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size()); + } + } catch (Exception e) { + if (statusApiLogSyncErrors || manualTrigger) { + getLogger().warning("StatusAPI-Sync Fehler: " + e.getMessage() + ". Nutze lokale Guard-Werte."); + } + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + private String buildSyncUrl() { + String base = statusApiBaseUrl == null ? "" : statusApiBaseUrl.trim(); + String path = statusApiEndpointPath == null ? "" : statusApiEndpointPath.trim(); + + if (base.endsWith("/") && path.startsWith("/")) { + return base.substring(0, base.length() - 1) + path; + } + if (!base.endsWith("/") && !path.startsWith("/")) { + return base + "/" + path; + } + return base + path; + } + + private Boolean extractJsonBoolean(String json, String key) { + String token = extractJsonToken(json, key); + if (token == null) { + return null; + } + if ("true".equalsIgnoreCase(token)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(token)) { + return Boolean.FALSE; + } + return null; + } + + private String extractJsonString(String json, String key) { + if (json == null || key == null) { + return null; + } + String search = "\"" + key + "\""; + int idx = json.indexOf(search); + if (idx < 0) { + return null; + } + int colon = json.indexOf(':', idx + search.length()); + if (colon < 0) { + return null; + } + int i = colon + 1; + while (i < json.length() && Character.isWhitespace(json.charAt(i))) { + i++; + } + if (i >= json.length() || json.charAt(i) != '"') { + return null; + } + + i++; + StringBuilder sb = new StringBuilder(); + boolean escape = false; + while (i < json.length()) { + char ch = json.charAt(i++); + if (escape) { + if (ch == 'n') { + sb.append('\n'); + } else if (ch == 'r') { + sb.append('\r'); + } else { + sb.append(ch); + } + escape = false; + continue; + } + if (ch == '\\') { + escape = true; + continue; + } + if (ch == '"') { + break; + } + sb.append(ch); + } + return sb.toString(); + } + + private String extractJsonToken(String json, String key) { + if (json == null || key == null) { + return null; + } + String search = "\"" + key + "\""; + int idx = json.indexOf(search); + if (idx < 0) { + return null; + } + int colon = json.indexOf(':', idx + search.length()); + if (colon < 0) { + return null; + } + int i = colon + 1; + while (i < json.length() && Character.isWhitespace(json.charAt(i))) { + i++; + } + if (i >= json.length()) { + return null; + } + + StringBuilder sb = new StringBuilder(); + while (i < json.length()) { + char ch = json.charAt(i); + if (ch == ',' || ch == '}' || ch == ']' || Character.isWhitespace(ch)) { + break; + } + sb.append(ch); + i++; + } + return sb.toString().trim(); + } + + private List extractJsonStringArray(String json, String key) { + if (json == null || key == null) { + return null; + } + + String search = "\"" + key + "\""; + int idx = json.indexOf(search); + if (idx < 0) { + return null; + } + + int colon = json.indexOf(':', idx + search.length()); + if (colon < 0) { + return null; + } + + int start = json.indexOf('[', colon + 1); + if (start < 0) { + return null; + } + + int depth = 0; + int end = -1; + for (int i = start; i < json.length(); i++) { + char ch = json.charAt(i); + if (ch == '[') { + depth++; + } else if (ch == ']') { + depth--; + if (depth == 0) { + end = i; + break; + } + } + } + + if (end < 0 || end <= start) { + return null; + } + + String raw = json.substring(start + 1, end); + List out = new ArrayList(); + boolean inString = false; + boolean escape = false; + StringBuilder current = new StringBuilder(); + + for (int i = 0; i < raw.length(); i++) { + char ch = raw.charAt(i); + if (!inString) { + if (ch == '"') { + inString = true; + current.setLength(0); + } + continue; + } + + if (escape) { + if (ch == 'n') { + current.append('\n'); + } else if (ch == 'r') { + current.append('\r'); + } else { + current.append(ch); + } + escape = false; + continue; + } + + if (ch == '\\') { + escape = true; + continue; + } + + if (ch == '"') { + inString = false; + out.add(current.toString()); + continue; + } + + current.append(ch); + } + + return out; + } + + private boolean isAllowed(InetAddress address) { + String normalized = normalizeIp(address.getHostAddress()); + if (normalized != null && allowedExactIps.contains(normalized)) { + return true; + } + + for (SubnetRule rule : allowedCidrs) { + if (rule.matches(address)) { + return true; + } + } + return false; + } + + private String normalizeIp(String raw) { + if (raw == null || raw.trim().isEmpty()) { + return null; + } + try { + return InetAddress.getByName(raw.trim()).getHostAddress().toLowerCase(Locale.ROOT); + } catch (UnknownHostException e) { + getLogger().warning("Ungueltige IP in Config ignoriert: " + raw); + return null; + } + } + + private SubnetRule parseSubnet(String raw) { + if (raw == null || raw.trim().isEmpty() || !raw.contains("/")) { + return null; + } + + String[] parts = raw.trim().split("/", 2); + try { + InetAddress network = InetAddress.getByName(parts[0]); + int prefix = Integer.parseInt(parts[1]); + return new SubnetRule(network, prefix); + } catch (Exception e) { + getLogger().warning("Ungueltige CIDR in Config ignoriert: " + raw); + return null; + } + } + + private String colorize(String text) { + return ChatColor.translateAlternateColorCodes('&', text == null ? "" : text); + } + + private static final class SubnetRule { + private final byte[] networkBytes; + private final int prefixLength; + + private SubnetRule(InetAddress network, int prefixLength) { + this.networkBytes = network.getAddress(); + this.prefixLength = prefixLength; + } + + private boolean matches(InetAddress address) { + byte[] candidate = address.getAddress(); + if (candidate.length != networkBytes.length) { + return false; + } + + int fullBytes = prefixLength / 8; + int remainderBits = prefixLength % 8; + + for (int i = 0; i < fullBytes; i++) { + if (candidate[i] != networkBytes[i]) { + return false; + } + } + + if (remainderBits == 0) { + return true; + } + + int mask = (-1) << (8 - remainderBits); + return (candidate[fullBytes] & mask) == (networkBytes[fullBytes] & mask); + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..076d370 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,29 @@ +# BackendJoinGuard +# Dieses Plugin kommt auf JEDEN Unterserver (Paper/Spigot), nicht auf den Proxy. +# Nur Verbindungen von diesen Proxy-IPs oder Netzbereichen duerfen joinen. + +enforcement-enabled: true +log-blocked-attempts: true +kick-message: "&cBitte verbinde dich nur ueber den Proxy-Server. Direkte Unterserver-Joins sind blockiert." + +# Exakte Proxy-IPs +allowed-proxy-ips: + - "127.0.0.1" + - "::1" + +# Optional ganze Netze im CIDR-Format +allowed-proxy-cidrs: [] +# Beispiel: +# allowed-proxy-cidrs: +# - "10.0.0.0/24" +# - "192.168.178.10/32" + +# Optional: zentrale Steuerung ueber StatusAPI +# Standard bleibt Standalone (enabled=false), damit BackendJoinGuard auch ohne StatusAPI nutzbar ist. +statusapi-sync: + enabled: true + base-url: "http://127.0.0.1:9191" + endpoint-path: "/network/backendguard/config" + api-key: "bgSync_7Rk9pQ2nLm5xV8cH4tW1yZ6" + interval-seconds: 60 + log-sync-errors: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..b547e07 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,14 @@ +name: BackendJoinGuard +main: net.viper.backendguard.BackendJoinGuardPlugin +version: 1.0.0 +author: M_Viper +description: Blockiert direkte Joins auf Backendserver und erlaubt nur Proxy-IPs. +api-version: '1.20' +commands: + backendguard: + description: BackendJoinGuard neu laden + usage: /backendguard reload +permissions: + backendguard.admin: + description: Darf BackendJoinGuard verwalten + default: op