Upload folder via GUI - src
This commit is contained in:
509
src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java
Normal file
509
src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java
Normal file
@@ -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<String> allowedExactIps = new HashSet<String>();
|
||||
private final List<SubnetRule> allowedCidrs = new ArrayList<SubnetRule>();
|
||||
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<String> remoteIps = extractJsonStringArray(json, "allowed_proxy_ips");
|
||||
List<String> 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<String> 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<String> out = new ArrayList<String>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/main/resources/config.yml
Normal file
29
src/main/resources/config.yml
Normal file
@@ -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
|
||||
14
src/main/resources/plugin.yml
Normal file
14
src/main/resources/plugin.yml
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user