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