Delete src/main/java/net/viper/status/modules/network/MultiAccountGuard.java via Git Manager GUI

This commit is contained in:
2026-05-22 17:25:23 +00:00
parent 6805b33c70
commit 6e23f6c471

View File

@@ -1,445 +0,0 @@
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<String, Integer> 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 <Name> 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<ProxiedPlayer> 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; }
}