From 4bf580ae2c5de54a65c079646c414d5321145570 Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Thu, 21 May 2026 23:28:44 +0200 Subject: [PATCH] Upload folder via GUI - src --- .../main/java/net/viper/status/StatusAPI.java | 12 +- .../status/modules/antibot/AntiBotModule.java | 15 +- .../viper/status/modules/help/HelpModule.java | 252 ++++++++++ .../modules/network/MultiAccountGuard.java | 445 ++++++++++++++++++ .../main/resources/network-guard.properties | 43 +- StatusAPI/src/main/resources/plugin.yml | 21 +- .../src/main/resources/verify.properties | 11 + 7 files changed, 790 insertions(+), 9 deletions(-) create mode 100644 StatusAPI/src/main/java/net/viper/status/modules/help/HelpModule.java create mode 100644 StatusAPI/src/main/java/net/viper/status/modules/network/MultiAccountGuard.java diff --git a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java index 0a9782a..32c9b07 100644 --- a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java +++ b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java @@ -10,6 +10,7 @@ import net.viper.status.modules.tablist.TablistModule; import net.viper.status.modules.scoreboard.ScoreboardModule; import net.viper.status.modules.antibot.AntiBotModule; import net.viper.status.modules.network.NetworkInfoModule; +import net.viper.status.modules.network.MultiAccountGuard; import net.viper.status.modules.AutoMessage.AutoMessageModule; import net.viper.status.modules.customcommands.CustomCommandModule; import net.viper.status.modules.serverswitcher.ServerSwitcherModule; @@ -20,6 +21,7 @@ import net.viper.status.modules.commandblocker.CommandBlockerModule; import net.viper.status.modules.broadcast.BroadcastModule; import net.viper.status.modules.chat.ChatModule; import net.viper.status.modules.vanish.VanishModule; +import net.viper.status.modules.help.HelpModule; import java.io.BufferedReader; import java.io.File; @@ -106,6 +108,7 @@ public class StatusAPI extends Plugin implements Runnable { // Module in korrekter Reihenfolge registrieren // VanishModule MUSS vor ChatModule registriert werden (VanishProvider-Abhängigkeit) moduleManager.registerModule(new StatsModule()); + moduleManager.registerModule(new HelpModule()); moduleManager.registerModule(new VerifyModule()); moduleManager.registerModule(new BroadcastModule()); moduleManager.registerModule(new CommandBlockerModule()); @@ -113,6 +116,7 @@ public class StatusAPI extends Plugin implements Runnable { moduleManager.registerModule(new ChatModule()); moduleManager.registerModule(new AntiBotModule()); moduleManager.registerModule(new NetworkInfoModule()); + moduleManager.registerModule(new MultiAccountGuard()); moduleManager.registerModule(new AutoMessageModule()); moduleManager.registerModule(new CustomCommandModule()); moduleManager.registerModule(new ServerSwitcherModule()); @@ -1400,9 +1404,15 @@ public class StatusAPI extends Plugin implements Runnable { @Override public void execute(net.md_5.bungee.api.CommandSender sender, String[] args) { if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + boolean isAdmin = sender.hasPermission("statusapi.admin") + || !(sender instanceof net.md_5.bungee.api.connection.ProxiedPlayer); send(sender, "&8&m──────────────────────────────────────────"); send(sender, "&6&lStatusAPI &7| Befehle"); - send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden"); + if (isAdmin) { + send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden"); + } else { + send(sender, "&7Keine weiteren Unterbefehle verfügbar."); + } send(sender, "&8&m──────────────────────────────────────────"); return; } diff --git a/StatusAPI/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java b/StatusAPI/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java index c17ee3d..1a15bd6 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java @@ -329,6 +329,19 @@ public class AntiBotModule implements Module, Listener { } } + /** + * Sperrt eine IP für die konfigurierte Block-Dauer (antibot.ip.block_seconds). + * Kann von anderen Modulen aufgerufen werden (z. B. MultiAccountGuard). + * @param ip Die zu sperrende IP-Adresse + * @param durationSeconds Sperrdauer in Sekunden (0 = antibot-Standard verwenden) + */ + public void blockIpExternal(String ip, int durationSeconds) { + long now = System.currentTimeMillis(); + long duration = durationSeconds > 0 ? durationSeconds : Math.max(1, ipBlockSeconds); + blockedIpsUntil.put(ip, now + duration * 1000L); + blockedConnectionsTotal.incrementAndGet(); + } + private void blockIp(String ip, long now) { blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L); blockedConnectionsTotal.incrementAndGet(); @@ -837,4 +850,4 @@ public class AntiBotModule implements Module, Listener { sender.sendMessage(ChatColor.YELLOW + "/antibot reload"); } } -} +} \ No newline at end of file diff --git a/StatusAPI/src/main/java/net/viper/status/modules/help/HelpModule.java b/StatusAPI/src/main/java/net/viper/status/modules/help/HelpModule.java new file mode 100644 index 0000000..0edb0ea --- /dev/null +++ b/StatusAPI/src/main/java/net/viper/status/modules/help/HelpModule.java @@ -0,0 +1,252 @@ +package net.viper.status.modules.help; + +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.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.ComponentBuilder; +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.viper.status.StatusAPI; +import net.viper.status.module.Module; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * HelpModule – seitenbasierte Ingame-Hilfe. + * + * verify.properties: + * statusapi.help=vn → /vn help [seite] + * statusapi.help.permission=statusapi.admin + */ +public class HelpModule implements Module { + + private String commandName = "help"; + private String adminPermission = "statusapi.admin"; + + @Override + public String getName() { return "HelpModule"; } + + @Override + public void onEnable(Plugin plugin) { + Properties props = ((StatusAPI) plugin).getVerifyProperties(); + if (props != null) { + String cn = props.getProperty("statusapi.help", "help").trim(); + if (!cn.isEmpty()) commandName = cn; + String ap = props.getProperty("statusapi.help.permission", "statusapi.admin").trim(); + if (!ap.isEmpty()) adminPermission = ap; + } + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, + new HelpCommand(commandName, adminPermission)); + plugin.getLogger().info("[HelpModule] /" + commandName + " help registriert (Admin-Permission: " + adminPermission + ")"); + } + + @Override + public void onDisable(Plugin plugin) {} + + // ───────────────────────────────────────────────────────────────────────── + + private static class HelpCommand extends Command { + + private final String adminPerm; + + // Jede Seite ist eine Liste von Zeilen + // Seiten werden zur Laufzeit je nach Berechtigung zusammengebaut + HelpCommand(String name, String adminPerm) { + super(name, null); + this.adminPerm = adminPerm; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length == 0) { + send(sender, "&7Nutze &e/" + getName() + " help &7für eine Befehlsübersicht."); + return; + } + if (!args[0].equalsIgnoreCase("help")) { + send(sender, "&cUnbekannter Unterbefehl. Nutze &e/" + getName() + " help&c."); + return; + } + + boolean isAdmin = !(sender instanceof ProxiedPlayer) + || sender.hasPermission(adminPerm) + || sender.hasPermission("statusapi.admin"); + + // Seiten aufbauen + List> pages = buildPages(isAdmin); + + int totalPages = pages.size(); + int page = 1; + + if (args.length >= 2) { + try { + page = Integer.parseInt(args[1].trim()); + } catch (NumberFormatException e) { + send(sender, "&cUngültige Seitenzahl. Nutze &e/" + getName() + " help <1-" + totalPages + ">&c."); + return; + } + } + + if (page < 1 || page > totalPages) { + send(sender, "&cSeite &e" + page + " &cexistiert nicht. Verfügbar: &e1&c-&e" + totalPages + "&c."); + return; + } + + // Header + send(sender, ""); + send(sender, "&8&m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + send(sender, " &6&lStatusAPI &8| &7Hilfe &8– &7Seite &e" + page + "&8/&e" + totalPages); + send(sender, "&8&m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + send(sender, ""); + + // Seiteninhalte + for (String line : pages.get(page - 1)) { + send(sender, line); + } + + send(sender, ""); + + // Navigation + sendNavigation(sender, getName(), page, totalPages); + send(sender, ""); + } + + /** Baut alle Seiten zusammen. Admins bekommen zusätzliche Seiten. */ + private List> buildPages(boolean isAdmin) { + List> pages = new ArrayList<>(); + + // ── Seite 1: Allgemein & Chat ───────────────────────────────────── + List p1 = new ArrayList<>(); + p1.add(" &e&lAllgemein"); + p1.add(" &a/verify &8– &7Account verifizieren"); + p1.add(" &a/forumlink &8(&7/fl&8) &8– &7Forum-Account verknüpfen"); + p1.add(" &a/forum &8– &7Forum-Benachrichtigungen"); + p1.add(" &a/go [server] &8(&7/wechsel, /switch&8) &8– &7Serverwechsel"); + p1.add(" &a/scoreboard &8(&7/sb&8) [hide|show] &8– &7Scoreboard umschalten"); + p1.add(""); + p1.add(" &e&lChat"); + p1.add(" &a/msg &8(&7/w, /tell&8) &8– &7Private Nachricht"); + p1.add(" &a/r &8(&7/reply, /antwort&8) &8– &7Auf PN antworten"); + p1.add(" &a/ignore &8(&7/block&8) &8– &7Spieler ignorieren"); + p1.add(" &a/unignore &8(&7/unblock&8) &8– &7Ignorierung aufheben"); + p1.add(" &a/channel [kanal] &8(&7/ch, /kanal&8) &8– &7Kanal wechseln"); + p1.add(" &a/chataus &8(&7/togglechat&8) &8– &7Chat-Empfang umschalten"); + pages.add(p1); + + // ── Seite 2: Chat (weiter) & Account-Verknüpfungen ─────────────── + List p2 = new ArrayList<>(); + p2.add(" &e&lChat (Fortsetzung)"); + p2.add(" &a/emoji &8(&7/emojis&8) &8– &7Alle Emojis anzeigen"); + p2.add(" &a/mentions &8(&7/mention&8) &8– &7Mention-Benachrichtigungen"); + p2.add(" &a/helpop &8– &7Team um Hilfe bitten"); + p2.add(" &a/report &8– &7Spieler melden"); + p2.add(" &a/chatbypass &8(&7/cbp&8) &8– &7ChatModule überspringen"); + p2.add(""); + p2.add(" &e&lAccount-Verknüpfungen"); + p2.add(" &a/discordlink &8(&7/dlink&8) &8– &7Discord verknüpfen"); + p2.add(" &a/telegramlink &8(&7/tlink&8) &8– &7Telegram verknüpfen"); + p2.add(" &a/unlink &8– &7Verknüpfung aufheben"); + pages.add(p2); + + // ── Admin-Seiten nur für Berechtigte ────────────────────────────── + if (isAdmin) { + // ── Seite 3: StatusAPI, AntiBot, Vanish ─────────────────────── + List p3 = new ArrayList<>(); + p3.add(" &c&lAdmin &8– &eStatusAPI & AntiBot"); + p3.add(" &c/statusapi reload &8(&7/sapi reload&8) &8– &7Scoreboard & Tablist neu laden"); + p3.add(" &c/netinfo &8– &7Proxy- & Systeminfos"); + p3.add(""); + p3.add(" &c/antibot status &8– &7AntiBot-Status anzeigen"); + p3.add(" &c/antibot clearblocks &8– &7IP-Blockliste leeren"); + p3.add(" &c/antibot unblock &8– &7IP entsperren"); + p3.add(" &c/antibot profile &8– &7Schutzprofil wechseln"); + p3.add(" &c/antibot reload &8– &7AntiBot neu laden"); + p3.add(""); + p3.add(" &c&lAdmin &8– &eVanish"); + p3.add(" &c/vanish [Spieler] &8(&7/v&8) &8– &7Unsichtbar schalten"); + p3.add(" &c/vanishlist &8(&7/vlist&8) &8– &7Unsichtbare Spieler anzeigen"); + pages.add(p3); + + // ── Seite 4: Chat-Admin, Reports, sonstige ──────────────────── + List p4 = new ArrayList<>(); + p4.add(" &c&lAdmin &8– &eChat-Administration"); + p4.add(" &c/broadcast &8(&7/bc, /alert&8) &8– &7Broadcast an alle"); + p4.add(" &c/chatmute [Min.] &8(&7/gmute&8) &8– &7Spieler muten"); + p4.add(" &c/chatunmute &8(&7/gunmute&8) &8– &7Mute aufheben"); + p4.add(" &c/socialspy &8(&7/spy&8) &8– &7Private Nachrichten mitlesen"); + p4.add(" &c/chatinfo &8– &7Chat-Info eines Spielers"); + p4.add(" &c/chathist [Spieler] [n] &8– &7Chat-Verlauf anzeigen"); + p4.add(" &c/chatreload &8– &7Chat-Konfiguration neu laden"); + p4.add(""); + p4.add(" &c&lAdmin &8– &eReports, Tools"); + p4.add(" &c/reports [all] &8– &7Offene Reports anzeigen"); + p4.add(" &c/reportclose &8– &7Report schließen"); + p4.add(" &c/automessage reload &8– &7AutoMessage neu laden"); + p4.add(" &c/bcmds reload &8– &7Custom-Commands neu laden"); + p4.add(" &c/cb &8– &7Command-Blocker verwalten"); + p4.add(" &c/scoreboard admin|player &8– &7Admin/Spieler-Ansicht wechseln"); + pages.add(p4); + } + + return pages; + } + + /** Sendet eine klickbare Navigationszeile mit ◀ Seite X/Y ▶ */ + private void sendNavigation(CommandSender sender, String cmd, int page, int total) { + // Für Konsole: einfacher Text + if (!(sender instanceof ProxiedPlayer)) { + String nav = " "; + if (page > 1) nav += "&7[&e◀&7] "; + nav += "&8Seite &e" + page + "&8/&e" + total; + if (page < total) nav += " &7[&e▶&7]"; + send(sender, nav); + return; + } + + // Für Spieler: klickbare Buttons + TextComponent line = new TextComponent(" "); + + // ◀ zurück + if (page > 1) { + TextComponent prev = new TextComponent( + ChatColor.translateAlternateColorCodes('&', "&7[&e◀&7] ")); + prev.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, + "/" + cmd + " help " + (page - 1))); + prev.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder(ChatColor.translateAlternateColorCodes('&', + "&7Seite &e" + (page - 1) + " &7anzeigen")).create())); + line.addExtra(prev); + } + + // Seitenanzeige + TextComponent mid = new TextComponent( + ChatColor.translateAlternateColorCodes('&', + "&8Seite &e" + page + "&8/&e" + total)); + line.addExtra(mid); + + // ▶ vor + if (page < total) { + TextComponent next = new TextComponent( + ChatColor.translateAlternateColorCodes('&', " &7[&e▶&7]")); + next.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, + "/" + cmd + " help " + (page + 1))); + next.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder(ChatColor.translateAlternateColorCodes('&', + "&7Seite &e" + (page + 1) + " &7anzeigen")).create())); + line.addExtra(next); + } + + sender.sendMessage(line); + } + + private static void send(CommandSender s, String text) { + s.sendMessage(new TextComponent( + ChatColor.translateAlternateColorCodes('&', text))); + } + } +} diff --git a/StatusAPI/src/main/java/net/viper/status/modules/network/MultiAccountGuard.java b/StatusAPI/src/main/java/net/viper/status/modules/network/MultiAccountGuard.java new file mode 100644 index 0000000..6464d2c --- /dev/null +++ b/StatusAPI/src/main/java/net/viper/status/modules/network/MultiAccountGuard.java @@ -0,0 +1,445 @@ +package net.viper.status.modules.network; + +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.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; +import net.md_5.bungee.event.EventPriority; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.PermissionNode; +import net.viper.status.StatusAPI; +import net.viper.status.module.Module; +import net.viper.status.modules.antibot.AntiBotModule; + +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +/** + * MultiAccountGuard + * + * Features: + * - IP-Check: blockiert zweiten Account von gleicher IP + * - Bypass NUR über LuckPerms (OP zählt nicht) + * - Persistentes Log in multiaccountguard.log + * - Staff-Benachrichtigung ingame (Permission: statusapi.staff.notify) + * - Temporärer IP-Bann nach X Versuchen (Integration mit AntiBotModule) + * - Discord-Webhook bei Konflikt + */ +public class MultiAccountGuard implements Module, Listener { + + private static final String CONFIG_FILE = "network-guard.properties"; + private static final String LOG_FILE = "multiaccountguard.log"; + public static final String BYPASS_PERM = "statusapi.multiaccountguard.bypass"; + public static final String STAFF_PERM = "statusapi.staff.notify"; + + private static final DateTimeFormatter LOG_FMT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); + + private Plugin plugin; + private Logger log; + private File logFile; + + // Config + private boolean enabled = true; + private boolean checkIp = true; + private boolean kickExisting = false; + private String kickMessage = "&cDu bist bereits mit einem anderen Account online!\n&7Bitte trenne deinen anderen Account zuerst."; + + // Staff-Benachrichtigung + private boolean staffNotifyEnabled = true; + private String staffNotifyFormat = "&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip}"; + + // Temporärer IP-Bann + private boolean tempBanEnabled = true; + private int tempBanMaxAttempts = 3; + private int tempBanDurationSecs = 300; + /** IP → Anzahl Konflikte seit letztem Reset */ + private final Map attemptsByIp = new ConcurrentHashMap<>(); + + // Webhook + private boolean webhookEnabled = true; + private String webhookUrl = ""; + private String webhookUsername = "StatusAPI"; + private String webhookThumbnailUrl = ""; + + @Override public String getName() { return "MultiAccountGuard"; } + + // ------------------------------------------------------------------------- + // Enable / Disable + // ------------------------------------------------------------------------- + + @Override + public void onEnable(Plugin plugin) { + this.plugin = plugin; + this.log = plugin.getLogger(); + this.logFile = new File(plugin.getDataFolder(), LOG_FILE); + + loadConfig(); + + if (!enabled) { + log.info("[MultiAccountGuard] Deaktiviert."); + return; + } + + ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); + + log.info("[MultiAccountGuard] Aktiv | IP-Check=" + checkIp + + " | kickExisting=" + kickExisting + + " | staffNotify=" + staffNotifyEnabled + + " | tempBan=" + tempBanEnabled + "(max=" + tempBanMaxAttempts + ", " + tempBanDurationSecs + "s)" + + " | Webhook=" + (webhookEnabled && !webhookUrl.isEmpty())); + log.info("[MultiAccountGuard] Bypass NUR via LuckPerms: /lp user permission set " + BYPASS_PERM + " true"); + log.info("[MultiAccountGuard] Log-Datei: " + logFile.getAbsolutePath()); + } + + @Override + public void onDisable(Plugin plugin) {} + + // ------------------------------------------------------------------------- + // Event + // ------------------------------------------------------------------------- + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPostLogin(PostLoginEvent event) { + if (!enabled) return; + + ProxiedPlayer joining = event.getPlayer(); + + if (hasBypass(joining)) { + log.info("[MultiAccountGuard] " + joining.getName() + " hat Bypass (LuckPerms) – übersprungen."); + return; + } + + UUID joiningUuid = joining.getUniqueId(); + String joiningIp = extractIp(joining.getSocketAddress()); + + if (joiningIp == null) { + log.warning("[MultiAccountGuard] Konnte IP von " + joining.getName() + " nicht lesen – übersprungen."); + return; + } + + log.info("[MultiAccountGuard] Login-Check: " + joining.getName() + + " | UUID=" + joiningUuid + " | IP=" + joiningIp); + + // Alle anderen Spieler (sich selbst per UUID ausschließen) + List others = new ArrayList<>(); + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + if (p.getUniqueId().equals(joiningUuid)) continue; + others.add(p); + } + + for (ProxiedPlayer online : others) { + if (hasBypass(online)) continue; + + String onlineIp = extractIp(online.getSocketAddress()); + if (onlineIp == null) continue; + + if (checkIp && joiningIp.equals(onlineIp)) { + log.warning("[MultiAccountGuard] KONFLIKT: " + + joining.getName() + " (" + joiningUuid + ")" + + " <-> " + online.getName() + " (" + online.getUniqueId() + ")" + + " IP=" + joiningIp); + handleConflict(joining, online, joiningIp); + return; + } + } + + log.info("[MultiAccountGuard] " + joining.getName() + " – kein Konflikt."); + } + + // ------------------------------------------------------------------------- + // Konflikt behandeln + // ------------------------------------------------------------------------- + + private void handleConflict(ProxiedPlayer joining, ProxiedPlayer existing, String ip) { + + TextComponent msg = new TextComponent( + ChatColor.translateAlternateColorCodes('&', kickMessage)); + + final String blockedName, allowedName; + final UUID blockedUuid, allowedUuid; + + if (kickExisting) { + existing.disconnect(msg); + blockedName = existing.getName(); blockedUuid = existing.getUniqueId(); + allowedName = joining.getName(); allowedUuid = joining.getUniqueId(); + log.warning("[MultiAccountGuard] Bestehender Account " + existing.getName() + " getrennt."); + } else { + joining.disconnect(msg); + blockedName = joining.getName(); blockedUuid = joining.getUniqueId(); + allowedName = existing.getName(); allowedUuid = existing.getUniqueId(); + log.warning("[MultiAccountGuard] Neuer Account " + joining.getName() + " blockiert."); + } + + // 1. Persistentes Log + writeLog(blockedName, blockedUuid, allowedName, allowedUuid, ip); + + // 2. Staff-Benachrichtigung + if (staffNotifyEnabled) { + notifyStaff(blockedName, allowedName, ip); + } + + // 3. Temporärer IP-Bann + if (tempBanEnabled) { + int attempts = attemptsByIp.merge(ip, 1, Integer::sum); + log.info("[MultiAccountGuard] IP " + ip + " hat " + attempts + "/" + tempBanMaxAttempts + " Versuche."); + if (attempts >= tempBanMaxAttempts) { + attemptsByIp.remove(ip); + banIp(ip); + } + } + + // 4. Discord-Webhook (async) + if (webhookEnabled && webhookUrl != null && !webhookUrl.isEmpty()) { + final String bn = blockedName, an = allowedName; + final UUID bu = blockedUuid, au = allowedUuid; + final int att = attemptsByIp.getOrDefault(ip, tempBanMaxAttempts); + ProxyServer.getInstance().getScheduler().runAsync(plugin, + () -> sendWebhook(bn, bu, an, au, ip, att)); + } + } + + // ------------------------------------------------------------------------- + // 1. Persistentes Log + // ------------------------------------------------------------------------- + + private void writeLog(String blockedName, UUID blockedUuid, + String allowedName, UUID allowedUuid, String ip) { + try { + if (!logFile.getParentFile().exists()) logFile.getParentFile().mkdirs(); + + String line = String.format("[%s] KONFLIKT | Geblockt: %s (%s) | Online: %s (%s) | IP: %s%n", + LOG_FMT.format(Instant.now()), + blockedName, blockedUuid, + allowedName, allowedUuid, + ip); + + Files.write(logFile.toPath(), line.getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + + } catch (Exception e) { + log.warning("[MultiAccountGuard] Log-Fehler: " + e.getMessage()); + } + } + + // ------------------------------------------------------------------------- + // 2. Staff-Benachrichtigung + // ------------------------------------------------------------------------- + + private void notifyStaff(String blockedName, String existingName, String ip) { + String raw = staffNotifyFormat + .replace("{blocked}", blockedName) + .replace("{existing}", existingName) + .replace("{ip}", ip); + String formatted = ChatColor.translateAlternateColorCodes('&', raw); + TextComponent msg = new TextComponent(formatted); + + int notified = 0; + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + if (p.hasPermission(STAFF_PERM)) { + p.sendMessage(msg); + notified++; + } + } + log.info("[MultiAccountGuard] Staff-Benachrichtigung gesendet an " + notified + " Spieler."); + } + + // ------------------------------------------------------------------------- + // 3. Temporärer IP-Bann via AntiBotModule + // ------------------------------------------------------------------------- + + private void banIp(String ip) { + try { + StatusAPI statusApi = (StatusAPI) ProxyServer.getInstance() + .getPluginManager().getPlugin("StatusAPI"); + if (statusApi == null) { + log.warning("[MultiAccountGuard] StatusAPI nicht gefunden – IP-Bann nicht möglich."); + return; + } + AntiBotModule antiBot = statusApi.getModuleManager().getModule(AntiBotModule.class); + if (antiBot == null) { + log.warning("[MultiAccountGuard] AntiBotModule nicht gefunden – IP-Bann nicht möglich."); + return; + } + antiBot.blockIpExternal(ip, tempBanDurationSecs); + log.warning("[MultiAccountGuard] IP " + ip + " für " + tempBanDurationSecs + "s gebannt (zu viele Multi-Account-Versuche)."); + + // Staff über den Bann informieren + String banMsg = ChatColor.translateAlternateColorCodes('&', + "&8[&cMAG&8] &7IP &f" + ip + " &7wurde für &c" + tempBanDurationSecs + "s &7gebannt &8(zu viele Versuche)."); + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + if (p.hasPermission(STAFF_PERM)) { + p.sendMessage(new TextComponent(banMsg)); + } + } + + // In Log schreiben + try { + String line = String.format("[%s] IP-BANN | IP: %s | Dauer: %ds%n", + LOG_FMT.format(Instant.now()), ip, tempBanDurationSecs); + Files.write(logFile.toPath(), line.getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } catch (Exception ignored) {} + + } catch (Exception e) { + log.warning("[MultiAccountGuard] IP-Bann Fehler: " + e.getMessage()); + } + } + + // ------------------------------------------------------------------------- + // 4. Discord-Webhook + // ------------------------------------------------------------------------- + + private void sendWebhook(String blockedName, UUID blockedUuid, + String allowedName, UUID allowedUuid, + String ip, int attempts) { + StringBuilder fields = new StringBuilder(); + appendField(fields, "\uD83D\uDEAB Geblockter Account", + blockedName + "\n`" + blockedUuid + "`", false); + appendField(fields, "\u2705 Verbundener Account", + allowedName + "\n`" + allowedUuid + "`", false); + appendField(fields, "\uD83C\uDF10 IP", "`" + ip + "`", true); + appendField(fields, "Aktion", + kickExisting ? "Alter Account getrennt" : "Neuer Account blockiert", true); + if (tempBanEnabled) { + appendField(fields, "\u26A0\uFE0F Versuche", + attempts + " / " + tempBanMaxAttempts + + (attempts >= tempBanMaxAttempts ? " \u2192 IP gebannt!" : ""), true); + } + + String body = "{\"username\":\"" + esc(webhookUsername) + "\"," + + "\"embeds\":[{" + + "\"title\":\"\uD83D\uDD12 Multi-Account erkannt\"," + + "\"description\":\"Ein Spieler hat versucht mit einem zweiten Account beizutreten.\"," + + "\"color\":15158332," + + "\"fields\":[" + fields + "]," + + "\"footer\":{\"text\":\"StatusAPI \u2022 MultiAccountGuard\"}," + + "\"timestamp\":\"" + Instant.now() + "\"" + + (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty() + ? ",\"thumbnail\":{\"url\":\"" + esc(webhookThumbnailUrl) + "\"}" : "") + + "}]}"; + + HttpURLConnection conn = null; + try { + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + conn = (HttpURLConnection) new URL(webhookUrl).openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(8000); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + conn.setRequestProperty("Content-Length", String.valueOf(bytes.length)); + try (OutputStream os = conn.getOutputStream()) { os.write(bytes); } + int code = conn.getResponseCode(); + if (code < 200 || code >= 300) log.warning("[MultiAccountGuard] Webhook HTTP " + code); + else log.info("[MultiAccountGuard] Webhook gesendet (HTTP " + code + ")"); + } catch (Exception e) { + log.warning("[MultiAccountGuard] Webhook-Fehler: " + e.getMessage()); + } finally { + if (conn != null) conn.disconnect(); + } + } + + private void appendField(StringBuilder sb, String name, String value, boolean inline) { + if (sb.length() > 0) sb.append(","); + sb.append("{\"name\":\"").append(esc(name)) + .append("\",\"value\":\"").append(esc(value)) + .append("\",\"inline\":").append(inline).append("}"); + } + + private String esc(String s) { + if (s == null) return ""; + return s.replace("\\","\\\\").replace("\"","\\\"") + .replace("\n","\\n").replace("\r","\\r"); + } + + // ------------------------------------------------------------------------- + // Bypass – NUR LuckPerms, kein OP-Fallback + // ------------------------------------------------------------------------- + + private boolean hasBypass(ProxiedPlayer player) { + try { + User user = LuckPermsProvider.get().getUserManager().getUser(player.getUniqueId()); + if (user == null) return false; + for (Node node : user.getNodes()) { + if (node instanceof PermissionNode) { + PermissionNode pn = (PermissionNode) node; + if (pn.getPermission().equalsIgnoreCase(BYPASS_PERM) && pn.getValue()) { + return true; + } + } + } + return false; + } catch (Exception e) { + log.warning("[MultiAccountGuard] LuckPerms-Check fehlgeschlagen für " + player.getName() + ": " + e.getMessage()); + return false; + } + } + + // ------------------------------------------------------------------------- + // Config + // ------------------------------------------------------------------------- + + private void loadConfig() { + File file = new File(plugin.getDataFolder(), CONFIG_FILE); + if (!file.exists()) { + log.info("[MultiAccountGuard] Config nicht gefunden – Defaults werden verwendet."); + return; + } + Properties p = new Properties(); + try (FileInputStream fis = new FileInputStream(file); + InputStreamReader r = new InputStreamReader(fis, StandardCharsets.UTF_8)) { + p.load(r); + + enabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.enabled", "true")); + checkIp = Boolean.parseBoolean(p.getProperty("multiaccountguard.check_ip", "true")); + kickExisting = Boolean.parseBoolean(p.getProperty("multiaccountguard.kick_existing", "false")); + kickMessage = p.getProperty("multiaccountguard.kick_message", kickMessage); + + staffNotifyEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.staff_notify.enabled", "true")); + staffNotifyFormat = p.getProperty("multiaccountguard.staff_notify.format", staffNotifyFormat); + + tempBanEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.tempban.enabled", "true")); + tempBanMaxAttempts = parseInt(p.getProperty("multiaccountguard.tempban.max_attempts", "3"), 3); + tempBanDurationSecs = parseInt(p.getProperty("multiaccountguard.tempban.duration_secs", "300"), 300); + + webhookEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.webhook.enabled", "true")); + webhookUrl = p.getProperty("networkinfo.webhook.url", "").trim(); + webhookUsername = p.getProperty("networkinfo.webhook.username", "StatusAPI").trim(); + webhookThumbnailUrl = p.getProperty("networkinfo.webhook.thumbnail_url", "").trim(); + + } catch (Exception e) { + log.warning("[MultiAccountGuard] Config-Fehler: " + e.getMessage()); + } + } + + private int parseInt(String s, int fallback) { + try { return Integer.parseInt(s == null ? "" : s.trim()); } + catch (Exception ignored) { return fallback; } + } + + // ------------------------------------------------------------------------- + + private String extractIp(SocketAddress addr) { + if (addr instanceof InetSocketAddress) + return ((InetSocketAddress) addr).getAddress().getHostAddress(); + return null; + } + + public boolean isEnabled() { return enabled; } + public boolean isCheckIp() { return checkIp; } + public boolean isKickExisting() { return kickExisting; } +} \ No newline at end of file diff --git a/StatusAPI/src/main/resources/network-guard.properties b/StatusAPI/src/main/resources/network-guard.properties index fa98c59..caef562 100644 --- a/StatusAPI/src/main/resources/network-guard.properties +++ b/StatusAPI/src/main/resources/network-guard.properties @@ -7,9 +7,9 @@ networkinfo.command.enabled=true networkinfo.include_player_names=false # Discord Webhook fuer Status-, Warn- und Attack-Meldungen -networkinfo.webhook.enabled=true +networkinfo.webhook.enabled=false networkinfo.webhook.url= -networkinfo.webhook.username=StatusAPI +networkinfo.webhook.username= networkinfo.webhook.thumbnail_url= networkinfo.webhook.notify_start_stop=true # compact = kurze Texte | detailed = strukturierte Embeds mit Feldern @@ -27,9 +27,9 @@ networkinfo.alert.tps_threshold=18.0 # Attack Meldungen (Detected/Stopped) networkinfo.attack.enabled=true # Nutzt automatisch networkinfo.webhook.url -networkinfo.attack.source=Viper-Network +networkinfo.attack.source= # API-Key fuer POST /network/attack -networkinfo.attack.api_key=2jN8xQ4mL9vK3sT7pR1yW6dH5cF0bZ +networkinfo.attack.api_key= # =========================== # ANTIBOT / ATTACK GUARD @@ -99,4 +99,37 @@ backendguard.allowed_proxy_cidrs=10.0.0.0/24 # Optionaler API-Key fuer GET /network/backendguard/config # Leer = kein API-Key erforderlich (nur im internen Netzwerk empfohlen) -backendguard.sync.api_key=bgSync_7Rk9pQ2nLm5xV8cH4tW1yZ6 +backendguard.sync.api_key= + +# =========================== +# MULTI ACCOUNT GUARD +# =========================== +# Verhindert, dass ein Spieler mit zwei Accounts gleichzeitig online ist. +multiaccountguard.enabled=true + +# IP-Check: Gleiche IP mit unterschiedlichem Namen -> blockieren +multiaccountguard.check_ip=true + +# UUID-Check: Gleiche UUID mit unterschiedlichem Namen -> blockieren (Bedrock-Edge-Cases) +multiaccountguard.check_uuid=true + +# true = bestehenden (alten) Account rauswerfen, neuen reinlassen +# false = neuen Account blockieren (Standard) +multiaccountguard.kick_existing=false + +# Kick-Nachricht (& fuer Farbcodes, \n fuer Zeilenumbruch) +multiaccountguard.kick_message=&cDu bist bereits mit einem anderen Account online!\n&7Bitte trenne deinen anderen Account zuerst. + +# Staff-Benachrichtigung bei Konflikt (Permission: statusapi.staff.notify) +multiaccountguard.staff_notify.enabled=true +multiaccountguard.staff_notify.format=&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip} + +# Temporaerer IP-Bann nach X Versuchen (Integration mit AntiBotModule) +# max_attempts: Anzahl Konflikte bevor die IP gebannt wird +# duration_secs: Bann-Dauer in Sekunden +multiaccountguard.tempban.enabled=true +multiaccountguard.tempban.max_attempts=3 +multiaccountguard.tempban.duration_secs=300 + +# Discord-Meldung bei jedem Konflikt (nutzt networkinfo.webhook.url automatisch) +multiaccountguard.webhook.enabled=true \ No newline at end of file diff --git a/StatusAPI/src/main/resources/plugin.yml b/StatusAPI/src/main/resources/plugin.yml index c4d4267..12ca058 100644 --- a/StatusAPI/src/main/resources/plugin.yml +++ b/StatusAPI/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: StatusAPI main: net.viper.status.StatusAPI -version: 4.1.2 +version: 4.1.3 author: M_Viper description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule # Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung @@ -10,6 +10,13 @@ softdepend: - Geyser-BungeeCord commands: + # ── HelpModule ──────────────────────────────────────────── + help: + description: Zeigt alle verfügbaren Befehle (Admin-Befehle nur mit Berechtigung) + usage: / help + # Hinweis: Der Befehlsname ist in verify.properties unter statusapi.help konfigurierbar + # Beispiel: statusapi.help=vn → /vn help + # ── ScoreboardModule ────────────────────────────────────── scoreboard: description: Scoreboard ein-/ausblenden oder zwischen Player/Admin wechseln @@ -219,6 +226,16 @@ permissions: description: Zugriff auf /automessage reload default: op + # ── MultiAccountGuard ───────────────────────────────────── + # KEIN default – Permission muss manuell vergeben werden! + # lp user permission set statusapi.multiaccountguard.bypass true + statusapi.multiaccountguard.bypass: + description: Erlaubt mehrere gleichzeitige Accounts (nur manuell vergeben) + + statusapi.staff.notify: + description: Empfaengt Ingame-Benachrichtigungen vom MultiAccountGuard + default: false + # ── ChatModule – Kanaele ────────────────────────────────── chat.channel.local: description: Zugang zum Local-Kanal @@ -292,4 +309,4 @@ permissions: # ── ServerSwitcherModule ────────────────────────────────── serverswitcher.use: description: Zugriff auf /go (Schneller Serverwechsel) - default: false + default: false \ No newline at end of file diff --git a/StatusAPI/src/main/resources/verify.properties b/StatusAPI/src/main/resources/verify.properties index 019fa9e..d3a9e88 100644 --- a/StatusAPI/src/main/resources/verify.properties +++ b/StatusAPI/src/main/resources/verify.properties @@ -17,7 +17,16 @@ broadcast.format=%prefixColored% %messageColored% # =========================== statusapi.port=9191 +# =========================== +# INGAME HILFE +# =========================== +# Befehlsname für die Ingame-Hilfe (Standard: help) +# Beispiel: statusapi.help=vn → Befehl wird /vn +statusapi.help=sapi +# Permission, die Admin-Befehle in der Hilfe sichtbar macht +# (OP und Spieler mit dieser Permission sehen die Admin-Sektion) +statusapi.help.permission=statusapi.admin # =========================== # WORDPRESS / VERIFY EINSTELLUNGEN @@ -93,3 +102,5 @@ economy.mysql.database=survivalplus economy.mysql.username=root economy.mysql.password= economy.start-balance=500.0 + +