From 09fdd00900c7efad11c35deba4370464a2bcf545 Mon Sep 17 00:00:00 2001 From: LAX1DUDE Date: Wed, 30 Mar 2022 16:46:54 -0700 Subject: [PATCH] Added user/ip/wildcard/regex ban commands --- ...Java-WebSocket-1.5.1-with-dependencies.jar | Bin 173621 -> 240423 bytes .../main/java/net/md_5/bungee/BungeeCord.java | 71 +- .../java/net/md_5/bungee/UserConnection.java | 13 +- .../net/md_5/bungee/api/CommandSender.java | 3 + .../md_5/bungee/api/config/ListenerInfo.java | 36 +- .../md_5/bungee/command/CommandGlobalBan.java | 61 ++ .../bungee/command/CommandGlobalBanIP.java | 148 +++ .../bungee/command/CommandGlobalBanRegex.java | 106 +++ .../command/CommandGlobalBanReload.java | 21 + .../command/CommandGlobalBanWildcard.java | 125 +++ .../bungee/command/CommandGlobalCheckBan.java | 56 ++ .../bungee/command/CommandGlobalListBan.java | 66 ++ .../bungee/command/CommandGlobalUnban.java | 56 ++ .../net/md_5/bungee/command/CommandIP.java | 10 +- .../bungee/command/ConsoleCommandSender.java | 9 +- .../net/md_5/bungee/config/Configuration.java | 7 + .../net/md_5/bungee/config/YamlConfig.java | 17 +- .../bungee/connection/InitialHandler.java | 43 + .../net/md_5/bungee/eaglercraft/BanList.java | 863 ++++++++++++++++++ .../bungee/eaglercraft/EaglercraftBungee.java | 9 + .../bungee/eaglercraft/WebSocketListener.java | 84 +- .../bungee/eaglercraft/WebSocketProxy.java | 25 +- .../sun/net/util/IPAddressUtil.java | 323 +++++++ 23 files changed, 2116 insertions(+), 36 deletions(-) create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBan.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanIP.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanRegex.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanReload.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanWildcard.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalCheckBan.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalListBan.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalUnban.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/BanList.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/EaglercraftBungee.java create mode 100644 eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/sun/net/util/IPAddressUtil.java diff --git a/eaglercraftbungee/Java-WebSocket-1.5.1-with-dependencies.jar b/eaglercraftbungee/Java-WebSocket-1.5.1-with-dependencies.jar index 8fb33ad5dca94eb91369b2cd939bdf4d2aa71500..8f103a76fc9c59a7af711d86acdf4780882469a3 100644 GIT binary patch delta 70943 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

F~io74SBoMXIl{lj&{s>1l_}~ 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 listeners; private Collection wsListeners; private final Map connections; @@ -111,6 +118,7 @@ public class BungeeCord extends ProxyServer { private final TaskScheduler scheduler; private ConsoleReader consoleReader; private final Logger logger; + private Collection banCommands; public static BungeeCord getInstance() { return (BungeeCord) ProxyServer.getInstance(); @@ -122,6 +130,8 @@ public class BungeeCord extends ProxyServer { this.executors = new BungeeThreadPool(new ThreadFactoryBuilder().setNameFormat("Bungee Pool Thread #%1$d").build()); this.eventLoops = (MultithreadEventLoopGroup) new NioEventLoopGroup(Runtime.getRuntime().availableProcessors(), new ThreadFactoryBuilder().setNameFormat("Netty IO Thread #%1$d").build()); this.saveThread = new Timer("Reconnect Saver"); + this.reloadBanThread = new Timer("Ban List Reload"); + this.closeInactiveSockets = new Timer("close Inactive WebSockets"); this.listeners = new HashSet(); this.wsListeners = new HashSet(); this.connections = (Map) new CaseInsensitiveMap(); @@ -131,6 +141,7 @@ public class BungeeCord extends ProxyServer { this.pluginChannels = new HashSet(); this.pluginsFolder = new File("plugins"); this.scheduler = new BungeeScheduler(); + this.banCommands = new ArrayList(); this.getPluginManager().registerCommand(null, new CommandReload()); this.getPluginManager().registerCommand(null, new CommandEnd()); this.getPluginManager().registerCommand(null, new CommandList()); @@ -153,6 +164,42 @@ public class BungeeCord extends ProxyServer { this.logger.info("NOTE: This error is non crucial, and BungeeCord will still function correctly! Do not bug the author about it unless you are still unable to get it working"); } } + + public void reconfigureBanCommands(boolean replaceBukkit) { + if(banCommands.size() > 0) { + for(Command c : banCommands) { + this.getPluginManager().unregisterCommand(c); + } + banCommands.clear(); + } + + Command cBan = new CommandGlobalBan(replaceBukkit); + Command cUnban = new CommandGlobalUnban(replaceBukkit); + Command cBanReload = new CommandGlobalBanReload(replaceBukkit); + Command cBanIP = new CommandGlobalBanIP(replaceBukkit); + Command cBanWildcard = new CommandGlobalBanWildcard(replaceBukkit); + Command cBanRegex = new CommandGlobalBanRegex(replaceBukkit); + Command cBanCheck = new CommandGlobalCheckBan(replaceBukkit); + Command cBanList = new CommandGlobalListBan(replaceBukkit); + + banCommands.add(cBan); + banCommands.add(cUnban); + banCommands.add(cBanReload); + banCommands.add(cBanIP); + banCommands.add(cBanWildcard); + banCommands.add(cBanRegex); + banCommands.add(cBanCheck); + banCommands.add(cBanList); + + this.getPluginManager().registerCommand(null, cBan); + this.getPluginManager().registerCommand(null, cUnban); + this.getPluginManager().registerCommand(null, cBanReload); + this.getPluginManager().registerCommand(null, cBanIP); + this.getPluginManager().registerCommand(null, cBanWildcard); + this.getPluginManager().registerCommand(null, cBanRegex); + this.getPluginManager().registerCommand(null, cBanCheck); + this.getPluginManager().registerCommand(null, cBanList); + } public static void main(final String[] args) throws Exception { final BungeeCord bungee = new BungeeCord(); @@ -186,6 +233,20 @@ public class BungeeCord extends ProxyServer { BungeeCord.this.getReconnectHandler().save(); } }, 0L, TimeUnit.MINUTES.toMillis(5L)); + this.reloadBanThread.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + BanList.maybeReloadBans(null); + } + }, 0L, TimeUnit.SECONDS.toMillis(3L)); + this.closeInactiveSockets.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + for(WebSocketListener lst : BungeeCord.this.wsListeners) { + lst.closeInactiveSockets(); + } + } + }, 0L, TimeUnit.SECONDS.toMillis(10L)); } public void startListeners() { diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/UserConnection.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/UserConnection.java index cb3d9da..25651d9 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/UserConnection.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/UserConnection.java @@ -4,13 +4,10 @@ package net.md_5.bungee; -import net.md_5.bungee.api.connection.Server; -import net.md_5.bungee.api.connection.PendingConnection; import java.beans.ConstructorProperties; import net.md_5.bungee.util.CaseInsensitiveSet; import java.util.HashSet; import net.md_5.bungee.api.config.TexturePackInfo; -import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.event.PermissionCheckEvent; import java.util.Collections; import java.net.InetSocketAddress; @@ -36,10 +33,14 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import net.md_5.bungee.api.ChatColor; import java.util.Objects; +import java.util.WeakHashMap; + import net.md_5.bungee.api.event.ServerConnectEvent; import net.md_5.bungee.protocol.packet.DefinedPacket; import com.google.common.base.Preconditions; import java.util.Iterator; +import java.util.Map; + import net.md_5.bungee.api.connection.Connection; import net.md_5.bungee.api.score.Scoreboard; import net.md_5.bungee.protocol.packet.PacketCCSettings; @@ -71,6 +72,7 @@ public final class UserConnection implements ProxiedPlayer { private final Scoreboard serverSentScoreboard; private String displayName; private final Connection.Unsafe unsafe; + private final Map attachment = new WeakHashMap(); public void init() { this.displayName = this.name; @@ -383,4 +385,9 @@ public final class UserConnection implements ProxiedPlayer { public String getDisplayName() { return this.displayName; } + + @Override + public Map getAttachment() { + return attachment; + } } diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/api/CommandSender.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/api/CommandSender.java index 7f93805..fe7f9d7 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/api/CommandSender.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/api/CommandSender.java @@ -5,6 +5,7 @@ package net.md_5.bungee.api; import java.util.Collection; +import java.util.Map; public interface CommandSender { String getName(); @@ -22,4 +23,6 @@ public interface CommandSender { boolean hasPermission(final String p0); void setPermission(final String p0, final boolean p1); + + Map getAttachment(); } diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java index 801c1be..9efb56c 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java @@ -5,6 +5,9 @@ package net.md_5.bungee.api.config; import java.beans.ConstructorProperties; +import java.io.File; + +import net.md_5.bungee.api.ServerIcon; import net.md_5.bungee.api.tab.TabListHandler; import java.util.Map; import java.net.InetSocketAddress; @@ -18,13 +21,18 @@ public class ListenerInfo { private final String fallbackServer; private final boolean forceDefault; private final boolean websocket; + private final boolean forwardIp; private final Map forcedHosts; private final TexturePackInfo texturePack; private final Class tabList; + private final String serverIcon; + private final int[] serverIconCache; + private boolean serverIconLoaded; + private boolean serverIconSet; - @ConstructorProperties({ "host", "motd", "maxPlayers", "tabListSize", "defaultServer", "fallbackServer", "forceDefault", "websocket", "forcedHosts", "texturePack", "tabList" }) + @ConstructorProperties({ "host", "motd", "maxPlayers", "tabListSize", "defaultServer", "fallbackServer", "forceDefault", "websocket", "forwardIp", "forcedHosts", "texturePack", "tabList", "serverIcon" }) public ListenerInfo(final InetSocketAddress host, final String motd, final int maxPlayers, final int tabListSize, final String defaultServer, final String fallbackServer, final boolean forceDefault, final boolean websocket, - final Map forcedHosts, final TexturePackInfo texturePack, final Class tabList) { + final boolean forwardIp, final Map forcedHosts, final TexturePackInfo texturePack, final Class tabList, final String serverIcon) { this.host = host; this.motd = motd; this.maxPlayers = maxPlayers; @@ -33,9 +41,14 @@ public class ListenerInfo { this.fallbackServer = fallbackServer; this.forceDefault = forceDefault; this.websocket = websocket; + this.forwardIp = forwardIp; this.forcedHosts = forcedHosts; this.texturePack = texturePack; this.tabList = tabList; + this.serverIcon = serverIcon; + this.serverIconCache = new int[4096]; + this.serverIconLoaded = false; + this.serverIconSet = false; } public InetSocketAddress getHost() { @@ -120,6 +133,9 @@ public class ListenerInfo { if (this.getTabListSize() != other.getTabListSize()) { return false; } + if (this.isWebsocket() != other.isWebsocket()) { + return false; + } final Object this$defaultServer = this.getDefaultServer(); final Object other$defaultServer = other.getDefaultServer(); Label_0165: { @@ -180,6 +196,15 @@ public class ListenerInfo { } else if (this$tabList.equals(other$tabList)) { return true; } + final Object this$getServerIcon = this.getServerIcon(); + final Object other$getServerIcon = other.getServerIcon(); + if (this$getServerIcon == null) { + if (other$getServerIcon == null) { + return true; + } + } else if (this$getServerIcon.equals(other$getServerIcon)) { + return true; + } return false; } @@ -208,6 +233,8 @@ public class ListenerInfo { result = result * 31 + (($texturePack == null) ? 0 : $texturePack.hashCode()); final Object $tabList = this.getTabList(); result = result * 31 + (($tabList == null) ? 0 : $tabList.hashCode()); + final Object $serverIconCache = this.getTabList(); + result = result * 31 + (($serverIconCache == null) ? 0 : $serverIconCache.hashCode()); return result; } @@ -220,4 +247,9 @@ public class ListenerInfo { public boolean isWebsocket() { return websocket; } + + public boolean hasForwardedHeaders() { + return forwardIp; + } + } diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBan.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBan.java new file mode 100644 index 0000000..2ee3dab --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBan.java @@ -0,0 +1,61 @@ +package net.md_5.bungee.command; + +import java.util.Collection; + +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.eaglercraft.BanList; + +public class CommandGlobalBan extends Command { + + private final boolean replaceBukkit; + + public CommandGlobalBan(boolean replaceBukkit) { + super(replaceBukkit ? "ban" : "eag-ban", "bungeecord.command.eag.ban", replaceBukkit ? new String[] { "kickban", "eag-ban", "e-ban", "gban" } : new String[] { "e-ban", "gban" }); + this.replaceBukkit = replaceBukkit; + } + + @Override + public void execute(CommandSender p0, String[] p1) { + if(p1.length >= 1) { + String p = p1[0].trim().toLowerCase(); + if(p0.getName().equalsIgnoreCase(p)) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "You cannot ban yourself"); + return; + } + String reason = "The ban hammer has spoken!"; + if(p1.length >= 2) { + reason = ""; + for(int i = 1; i < p1.length; ++i) { + if(reason.length() > 0) { + reason += " "; + } + reason += p1[i]; + } + } + String wasTheKick = null; + Collection playerz = BungeeCord.getInstance().getPlayers(); + for(ProxiedPlayer pp : playerz) { + if(pp.getName().equalsIgnoreCase(p)) { + wasTheKick = pp.getName(); + pp.disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: " + reason); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.WHITE + "Kicked: " + pp.getName()); + } + } + if(BanList.ban(p, reason)) { + if(wasTheKick == null) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.YELLOW + "Warning! '" + ChatColor.WHITE + p + ChatColor.YELLOW + "' is not currently on this server"); + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Username '" + ChatColor.WHITE + (wasTheKick == null ? p : wasTheKick) + ChatColor.GREEN + "' was added to the ban list"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Username '" + ChatColor.WHITE + p + ChatColor.RED + "' is already banned"); + } + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To ban a player, use: " + ChatColor.WHITE + "/" + (replaceBukkit?"":"eag-") + "ban [reason]"); + } + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanIP.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanIP.java new file mode 100644 index 0000000..0fcbc14 --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanIP.java @@ -0,0 +1,148 @@ +package net.md_5.bungee.command; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.eaglercraft.BanList; +import net.md_5.bungee.eaglercraft.BanList.IPBan; + +public class CommandGlobalBanIP extends Command { + + private final boolean replaceBukkit; + + public CommandGlobalBanIP(boolean replaceBukkit) { + super(replaceBukkit ? "ban-ip" : "eag-ban-ip", "bungeecord.command.eag.banip", (replaceBukkit ? new String[] {"eag-ban-ip", "banip", "e-ban-ip", "gban-ip"} : + new String[] {"gban-ip", "e-ban-ip", "gbanip", "e-banip"}) ); + this.replaceBukkit = replaceBukkit; + } + + @Override + public void execute(CommandSender p0, String[] p1) { + String w = (String) p0.getAttachment().get("banIPWaitingToAdd"); + if(w != null) { + List lst = (List)p0.getAttachment().get("banIPWaitingToKick"); + if(p1.length != 1 || (!p1[0].equalsIgnoreCase("confirm") && !p1[0].equalsIgnoreCase("cancel"))) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-ip" : "/eag-ban-ip") + " confirm" + ChatColor.RED + " to add IP " + ChatColor.WHITE + w + + ChatColor.RED + " and ban " + ChatColor.WHITE + lst.size() + ChatColor.RED + " players"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-ip" : "/eag-ban-ip") + " cancel" + ChatColor.RED + " to cancel this operation"); + }else { + if(p1[0].equalsIgnoreCase("confirm")) { + try { + if(BanList.banIP(w)) { + for(ProxiedPlayer pp : lst) { + pp.disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: banned by IP"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Kicked: " + ChatColor.WHITE + pp.getName()); + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Added IP '" + ChatColor.WHITE + w + ChatColor.GREEN + "' to the ban list"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "IP '" + ChatColor.WHITE + w + ChatColor.RED + "' is already on the ban list"); + } + } catch (UnknownHostException e) { + e.printStackTrace(); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "ERROR: address '" + ChatColor.WHITE + w + ChatColor.RED + "' is suddenly invalid for some reason"); + } + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Canceled ban"); + } + p0.getAttachment().remove("banIPWaitingToAdd"); + p0.getAttachment().remove("banIPWaitingToKick"); + } + return; + } + if(p1.length != 1) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "How to use: " + ChatColor.WHITE + (replaceBukkit ? "/ban-ip" : "/eag-ban-ip") + " "); + return; + } + boolean isPlayer = false; + IPBan p = null; + try { + p = BanList.constructIpBan(p1[0]); + }catch(Throwable t) { + for(ProxiedPlayer pp : BungeeCord.getInstance().getPlayers()) { + if(pp.getName().equalsIgnoreCase(p1[0])) { + Object addr = pp.getAttachment().get("remoteAddr"); + if(addr != null) { + String newAddr = ((InetAddress)addr).getHostAddress(); + isPlayer = true; + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Player '" + ChatColor.WHITE + p1[0] + ChatColor.GREEN + "' has IP " + ChatColor.WHITE + newAddr); + p1[0] = newAddr; + try { + p = BanList.constructIpBan(p1[0]); + }catch(UnknownHostException ex) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Address '" + ChatColor.WHITE + p1[0] + "' is suddenly invalid: " + ChatColor.WHITE + p1[0]); + return; + } + } + break; + } + } + if(!isPlayer) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Player '" + ChatColor.WHITE + p1[0] + "' is not on this server"); + return; + } + } + boolean blocked = false; + for(IPBan b : BanList.blockedBans) { + if(b.checkBan(p.getBaseAddress()) || p.checkBan(b.getBaseAddress())) { + blocked = true; + } + } + if(blocked) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Cannot ban '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "', it will ban local addresses that may break your game"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To force, add to the " + ChatColor.WHITE + "[IPs]" + ChatColor.RED + " section of " + ChatColor.WHITE + "bans.txt" + ChatColor.RED + " in your bungee directory"); + return; + } + boolean isSenderGonnaGetKicked = false; + List usersThatAreGonnaBeKicked = new ArrayList(); + for(ProxiedPlayer pp : BungeeCord.getInstance().getPlayers()) { + Object addr = pp.getAttachment().get("remoteAddr"); + if(addr != null) { + InetAddress addrr = (InetAddress)addr; + if(p.checkBan(addrr)) { + usersThatAreGonnaBeKicked.add(pp); + if(pp.getName().equalsIgnoreCase(p0.getName())) { + isSenderGonnaGetKicked = true; + break; + } + } + } + } + if(isSenderGonnaGetKicked) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "banning address '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' will ban you off of your own server"); + return; + } + if(usersThatAreGonnaBeKicked.size() > 1) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "WARNING: banning address '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is gonna ban " + + ChatColor.WHITE + usersThatAreGonnaBeKicked.size() + ChatColor.RED + " players off of your server"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-ip" : "/eag-ban-ip") + " confirm" + ChatColor.RED + " to continue, or type " + + ChatColor.WHITE + (replaceBukkit ? "/ban-ip" : "/eag-ban-ip") + " cancel" + ChatColor.RED + " to cancel"); + p0.getAttachment().put("banIPWaitingToKick", usersThatAreGonnaBeKicked); + p0.getAttachment().put("banIPWaitingToAdd", p1[0]); + }else { + try { + if(BanList.banIP(p1[0])) { + if(usersThatAreGonnaBeKicked.size() > 0) { + usersThatAreGonnaBeKicked.get(0).disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: banned by IP"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Kicked: " + ChatColor.WHITE + usersThatAreGonnaBeKicked.get(0).getName()); + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Added IP '" + ChatColor.WHITE + p1[0] + ChatColor.GREEN + "' to the ban list"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "IP '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is already on the ban list"); + } + } catch (UnknownHostException e) { + e.printStackTrace(); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "ERROR: address '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is suddenly invalid for some reason"); + return; + } + } + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanRegex.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanRegex.java new file mode 100644 index 0000000..076711f --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanRegex.java @@ -0,0 +1,106 @@ +package net.md_5.bungee.command; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.eaglercraft.BanList; + +public class CommandGlobalBanRegex extends Command { + + private final boolean replaceBukkit; + + public CommandGlobalBanRegex(boolean replaceBukkit) { + super(replaceBukkit ? "ban-regex" : "eag-ban-regex", "bungeecord.command.eag.banregex", replaceBukkit ? new String[] { "eag-ban-regex", "e-ban-regex", + "gban-regex", "eag-banregex", "e-banregex", "gbanregex", "banregex" } : new String[] { "e-ban-regex", "gban-regex", + "eag-banregex", "e-banregex", "gbanregex" }); + this.replaceBukkit = replaceBukkit; + } + + @Override + public void execute(CommandSender p0, String[] p1) { + String w = (String) p0.getAttachment().get("banRegexWaitingToAdd"); + if(w != null) { + List lst = (List)p0.getAttachment().get("banRegexWaitingToKick"); + if(p1.length != 1 || (!p1[0].equalsIgnoreCase("confirm") && !p1[0].equalsIgnoreCase("cancel"))) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-regex" : "/eag-ban-regex") + " confirm" + ChatColor.RED + " to add regex " + ChatColor.WHITE + w + + ChatColor.RED + " and ban " + ChatColor.WHITE + lst.size() + ChatColor.RED + " players"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-regex" : "/eag-ban-regex") + " cancel" + ChatColor.RED + " to cancel this operation"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.YELLOW + "Note: all usernames are converted to lowercase before being matched"); + }else { + if(p1[0].equalsIgnoreCase("confirm")) { + if(BanList.banRegex(w)) { + for(ProxiedPlayer pp : lst) { + pp.disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: banned by regex"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Kicked: " + ChatColor.WHITE + pp.getName()); + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Added regex '" + ChatColor.WHITE + w + ChatColor.GREEN + "' to the ban list"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Regex '" + ChatColor.WHITE + w + ChatColor.RED + "' is already banned"); + } + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Canceled ban"); + } + p0.getAttachment().remove("banRegexWaitingToAdd"); + p0.getAttachment().remove("banRegexWaitingToKick"); + } + return; + } + if(p1.length != 1) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "How to use: " + ChatColor.WHITE + (replaceBukkit ? "/ban-regex" : "/eag-ban-regex") + " "); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.YELLOW + "Note: all usernames are converted to lowercase before being matched"); + return; + } + Pattern p; + try { + p = Pattern.compile(p1[0]); + }catch(Throwable t) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Regex syntax error: " + t.getMessage()); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.YELLOW + "Note: all usernames are converted to lowercase before being matched"); + return; + } + boolean isSenderGonnaGetKicked = false; + List usersThatAreGonnaBeKicked = new ArrayList(); + for(ProxiedPlayer pp : BungeeCord.getInstance().getPlayers()) { + String n = pp.getName().toLowerCase(); + if(p.matcher(n).matches()) { + usersThatAreGonnaBeKicked.add(pp); + if(n.equalsIgnoreCase(p0.getName())) { + isSenderGonnaGetKicked = true; + break; + } + } + } + if(isSenderGonnaGetKicked) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "banning regex '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is gonna ban your own username"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.YELLOW + "Note: all usernames are converted to lowercase before being matched"); + return; + } + if(usersThatAreGonnaBeKicked.size() > 1) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "WARNING: banning regex '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is gonna ban " + + ChatColor.WHITE + usersThatAreGonnaBeKicked.size() + ChatColor.RED + " players off of your server"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-regex" : "/eag-ban-regex") + " confirm" + ChatColor.RED + " to continue, or type " + + ChatColor.WHITE + "/eag-ban-regex cancel" + ChatColor.RED + " to cancel"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.YELLOW + "Note: all usernames are converted to lowercase before being matched"); + p0.getAttachment().put("banRegexWaitingToKick", usersThatAreGonnaBeKicked); + p0.getAttachment().put("banRegexWaitingToAdd", p1[0]); + }else { + if(BanList.banRegex(p1[0])) { + if(usersThatAreGonnaBeKicked.size() > 0) { + usersThatAreGonnaBeKicked.get(0).disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: banned by regex"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Kicked: " + ChatColor.WHITE + usersThatAreGonnaBeKicked.get(0).getName()); + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Added regex '" + ChatColor.WHITE + p1[0] + ChatColor.GREEN + "' to the ban list"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.YELLOW + "Note: all usernames are converted to lowercase before being matched"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Regex '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is already banned"); + } + } + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanReload.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanReload.java new file mode 100644 index 0000000..3063b31 --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanReload.java @@ -0,0 +1,21 @@ +package net.md_5.bungee.command; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.eaglercraft.BanList; + +public class CommandGlobalBanReload extends Command { + + public CommandGlobalBanReload(boolean replaceBukkit) { + super(replaceBukkit ? "reloadban" : "eag-reloadban", "bungeecord.command.eag.reloadban", replaceBukkit ? new String[] { "eag-reloadban", "banreload", "eag-banreload", "e-reloadban", + "e-banreload", "gbanreload", "greloadban"} : new String[] { "eag-banreload", "e-reloadban", "e-banreload", "gbanreload", "greloadban"}); + } + + @Override + public void execute(CommandSender p0, String[] p1) { + BanList.maybeReloadBans(p0); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.WHITE + "Ban list reloaded"); + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanWildcard.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanWildcard.java new file mode 100644 index 0000000..1d4783a --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalBanWildcard.java @@ -0,0 +1,125 @@ +package net.md_5.bungee.command; + +import java.util.ArrayList; +import java.util.List; + +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.eaglercraft.BanList; + +public class CommandGlobalBanWildcard extends Command { + + private final boolean replaceBukkit; + + public CommandGlobalBanWildcard(boolean replaceBukkit) { + super(replaceBukkit ? "ban-wildcard" : "eag-ban-wildcard", "bungeecord.command.eag.banwildcard", replaceBukkit ? new String[] { "eag-ban-wildcard", "e-ban-wildcard", "gban-wildcard", + "banwildcard", "eag-banwildcard", "banwildcard"} : new String[] { "e-ban-wildcard", "gban-wildcard", "eag-banwildcard"}); + this.replaceBukkit = replaceBukkit; + } + + @Override + public void execute(CommandSender p0, String[] p1) { + String w = (String) p0.getAttachment().get("banWildcardWaitingToAdd"); + if(w != null) { + List lst = (List)p0.getAttachment().get("banWildcardWaitingToKick"); + if(p1.length != 1 || (!p1[0].equalsIgnoreCase("confirm") && !p1[0].equalsIgnoreCase("cancel"))) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-wildcard" : "/eag-ban-wildcard") + " confirm" + ChatColor.RED + " to add wildcard " + ChatColor.WHITE + w + + ChatColor.RED + " and ban " + ChatColor.WHITE + lst.size() + ChatColor.RED + " players"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-wildcard" : "/eag-ban-wildcard") + " cancel" + ChatColor.RED + " to cancel this operation"); + }else { + if(p1[0].equalsIgnoreCase("confirm")) { + if(BanList.banWildcard(w)) { + for(ProxiedPlayer pp : lst) { + pp.disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: banned by wildcard"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Kicked: " + ChatColor.WHITE + pp.getName()); + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Added wildcard '" + ChatColor.WHITE + w + ChatColor.GREEN + "' to the ban list"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Wildcard '" + ChatColor.WHITE + w + ChatColor.RED + "' is already banned"); + } + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Canceled ban"); + } + p0.getAttachment().remove("banWildcardWaitingToAdd"); + p0.getAttachment().remove("banWildcardWaitingToKick"); + } + return; + } + if(p1.length != 1) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "How to use: " + ChatColor.WHITE + (replaceBukkit ? "/ban-wildcard" : "/eag-ban-wildcard") + " "); + return; + } + p1[0] = p1[0].toLowerCase(); + String s = p1[0]; + boolean startStar = s.startsWith("*"); + if(startStar) { + s = s.substring(1); + } + boolean endStar = s.endsWith("*"); + if(endStar) { + s = s.substring(0, s.length() - 1); + } + if(!startStar && !endStar) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "'" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is not a wildcard, try '" + + ChatColor.WHITE + "*" + p1[0] + ChatColor.RED + "' or '" + ChatColor.WHITE + p1[0] + "*" + ChatColor.RED + "' or '" + ChatColor.WHITE + + "*" + p1[0] + "*" + ChatColor.RED + "' instead"); + return; + } + boolean isSenderGonnaGetKicked = false; + List usersThatAreGonnaBeKicked = new ArrayList(); + for(ProxiedPlayer pp : BungeeCord.getInstance().getPlayers()) { + String n = pp.getName().toLowerCase(); + if(startStar && endStar) { + if(n.contains(s)) { + usersThatAreGonnaBeKicked.add(pp); + if(pp.getName().equalsIgnoreCase(p0.getName())) { + isSenderGonnaGetKicked = true; + break; + } + } + }else if(startStar) { + if(n.endsWith(s)) { + usersThatAreGonnaBeKicked.add(pp); + if(pp.getName().equalsIgnoreCase(p0.getName())) { + isSenderGonnaGetKicked = true; + break; + } + } + }else if(endStar) { + if(n.startsWith(s)) { + usersThatAreGonnaBeKicked.add(pp); + if(pp.getName().equalsIgnoreCase(p0.getName())) { + isSenderGonnaGetKicked = true; + break; + } + } + } + } + if(isSenderGonnaGetKicked) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "banning wildcard '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is gonna ban your own username"); + return; + } + if(usersThatAreGonnaBeKicked.size() > 1) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "WARNING: banning wildcard '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is gonna ban " + + ChatColor.WHITE + usersThatAreGonnaBeKicked.size() + ChatColor.RED + " players off of your server"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Type " + ChatColor.WHITE + (replaceBukkit ? "/ban-wildcard" : "/eag-ban-wildcard") + " confirm" + ChatColor.RED + " to continue, or type " + + ChatColor.WHITE + (replaceBukkit ? "/ban-wildcard" : "/eag-ban-wildcard") + " cancel" + ChatColor.RED + " to cancel"); + p0.getAttachment().put("banWildcardWaitingToKick", usersThatAreGonnaBeKicked); + p0.getAttachment().put("banWildcardWaitingToAdd", p1[0]); + }else { + if(BanList.banWildcard(p1[0])) { + if(usersThatAreGonnaBeKicked.size() > 0) { + usersThatAreGonnaBeKicked.get(0).disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: banned by wildcard"); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Kicked: " + ChatColor.WHITE + usersThatAreGonnaBeKicked.get(0).getName()); + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Added wildcard '" + ChatColor.WHITE + p1[0] + ChatColor.GREEN + "' to the ban list"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Wildcard '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is already banned"); + } + } + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalCheckBan.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalCheckBan.java new file mode 100644 index 0000000..be2bd85 --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalCheckBan.java @@ -0,0 +1,56 @@ +package net.md_5.bungee.command; + +import java.net.InetAddress; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.eaglercraft.BanList; +import net.md_5.bungee.eaglercraft.BanList.BanCheck; +import net.md_5.bungee.eaglercraft.BanList.BanState; + +public class CommandGlobalCheckBan extends Command { + + private final boolean replaceBukkit; + + public CommandGlobalCheckBan(boolean replaceBukkit) { + super(replaceBukkit ? "banned" : "eag-bannned", "bungeecord.command.eag.banned", replaceBukkit ? new String[] { "eag-banned", "isbanned", "e-banned", "gbanned", "eag-isbanned", "e-isbanned", "gisbanned" } : + new String[] { "e-banned", "gbanned", "eag-isbanned", "e-isbanned", "gisbanned" }); + this.replaceBukkit = replaceBukkit; + } + + @Override + public void execute(CommandSender p0, String[] p1) { + if(p1.length != 1) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To check if a player or IP is banned, use: " + ChatColor.WHITE + (replaceBukkit ? "/banned" : "/eag-banned") + " "); + }else { + BanCheck bc = BanList.checkBanned(p1[0]); + if(!bc.isBanned()) { + try { + InetAddress addr = InetAddress.getByName(p1[0]); + bc = BanList.checkIpBanned(addr); + if(bc.isBanned()) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "IP address '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is banned by: " + + "'" + ChatColor.WHITE + bc.match + ChatColor.RED + "' " + ChatColor.YELLOW + "(" + bc.string + ")"); + return; + } + }catch(Throwable t) { + // no + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Player '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' has not been banned"); + }else { + if(bc.reason == BanState.USER_BANNED) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Player '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is banned by username, reason: " + + ChatColor.YELLOW + "\"" + bc.string + "\""); + }else if(bc.reason == BanState.WILDCARD_BANNED) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Player '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is banned by wildcard: " + + ChatColor.WHITE + "\"" + bc.match + "\""); + }else if(bc.reason == BanState.REGEX_BANNED) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Player '" + ChatColor.WHITE + p1[0] + ChatColor.RED + "' is banned by regex: " + + ChatColor.WHITE + "\"" + bc.match + "\""); + } + } + } + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalListBan.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalListBan.java new file mode 100644 index 0000000..52c36c7 --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalListBan.java @@ -0,0 +1,66 @@ +package net.md_5.bungee.command; + +import java.util.List; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.eaglercraft.BanList; + +public class CommandGlobalListBan extends Command { + + private final boolean replaceBukkit; + + public CommandGlobalListBan(boolean replaceBukkit) { + super(replaceBukkit ? "banlist" : "eag-banlist", "bungeecord.command.eag.banlist", replaceBukkit ? new String[] { "eag-banlist", "gbanlist", "e-banlist", + "gbanlist" } : new String[] { "gbanlist", "e-banlist" }); + this.replaceBukkit = replaceBukkit; + } + + @Override + public void execute(CommandSender p0, String[] p1) { + if(p1.length == 0 || (p1.length == 1 && (p1[0].equalsIgnoreCase("user") || p1[0].equalsIgnoreCase("username") + || p1[0].equalsIgnoreCase("users") || p1[0].equalsIgnoreCase("usernames")))) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Players banned by username: " + ChatColor.WHITE + BanList.listAllBans()); + return; + }else if(p1.length == 1) { + if(p1[0].equalsIgnoreCase("regex") || p1[0].equalsIgnoreCase("regexes")) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Regex ban list: " + ChatColor.WHITE + BanList.listAllRegexBans()); + return; + }else if(p1[0].equalsIgnoreCase("wildcard") || p1[0].equalsIgnoreCase("wildcards")) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Wildcard ban list: " + ChatColor.WHITE + BanList.listAllWildcardBans()); + return; + }else if(p1[0].equalsIgnoreCase("ip") || p1[0].equalsIgnoreCase("ips")) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To list IP bans, use: " + ChatColor.WHITE + (replaceBukkit ? "/banlist" : "/eag-banlist") + " ip [v4|v6]"); + return; + } + }else if(p1.length > 1 && p1.length <= 3 && (p1[0].equalsIgnoreCase("ip") || p1[0].equalsIgnoreCase("ips"))) { + int addrOrNetmask = 0; + if(p1[1].equalsIgnoreCase("addr") || p1[1].equalsIgnoreCase("addrs")) { + addrOrNetmask = 1; + }else if(p1[1].equalsIgnoreCase("netmask") || p1[1].equalsIgnoreCase("netmasks")) { + addrOrNetmask = 2; + } + if(addrOrNetmask > 0) { + boolean yes = false; + if(p1.length == 2 || (p1.length == 3 && (p1[2].equalsIgnoreCase("v4") || p1[2].equals("4")))) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "IPv4 " + (addrOrNetmask == 2 ? "netmask" : "address") + " ban list: " + ChatColor.WHITE + BanList.listAllIPBans(false, addrOrNetmask == 2)); + yes = true; + } + if(p1.length == 2 || (p1.length == 3 && (p1[2].equalsIgnoreCase("v6") || p1[2].equals("6")))) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "IPv6 " + (addrOrNetmask == 2 ? "netmask" : "address") + " ban list: " + ChatColor.WHITE + BanList.listAllIPBans(true, addrOrNetmask == 2)); + yes = true; + } + if(yes) { + return; + } + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To list IP bans, use: " + ChatColor.WHITE + (replaceBukkit ? "/banlist" : "/eag-banlist") + " ip [v4|v6]"); + return; + } + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To list all user bans, use: " + ChatColor.WHITE + (replaceBukkit ? "/banlist" : "/eag-banlist")); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To list ips, regexes, and wildcards, use: " + ChatColor.WHITE + (replaceBukkit ? "/banlist" : "/eag-banlist") + " "); + return; + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalUnban.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalUnban.java new file mode 100644 index 0000000..aac2039 --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandGlobalUnban.java @@ -0,0 +1,56 @@ +package net.md_5.bungee.command; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.eaglercraft.BanList; + +public class CommandGlobalUnban extends Command { + + private final boolean replaceBukkit; + + public CommandGlobalUnban(boolean replaceBukkit) { + super(replaceBukkit ? "unban" : "eag-unban", "bungeecord.command.eag.unban", replaceBukkit ? new String[] {"eag-unban", "e-unban", "gunban"} :new String[] {"e-unban", "gunban"}); + this.replaceBukkit = replaceBukkit; + } + + @Override + public void execute(CommandSender p0, String[] p1) { + if(p1.length != 2 || (!p1[0].equalsIgnoreCase("user") && !p1[0].equalsIgnoreCase("username") && !p1[0].equalsIgnoreCase("player") + && !p1[0].equalsIgnoreCase("wildcard") && !p1[0].equalsIgnoreCase("regex") && !p1[0].equalsIgnoreCase("ip"))) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To unban a player, use: " + ChatColor.WHITE + "/" + (replaceBukkit?"":"eag-") + "unban user "); + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "To unban an ip/wildcard/regex, use: " + ChatColor.WHITE + "/" + (replaceBukkit?"":"eag-") + "unban "); + return; + } + if(p1[0].equalsIgnoreCase("user") || p1[0].equalsIgnoreCase("username") || p1[0].equalsIgnoreCase("player")) { + if(BanList.unban(p1[1])) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "User '" + ChatColor.WHITE + p1[1] + ChatColor.GREEN + "' was unbanned"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "User '" + ChatColor.WHITE + p1[1] + ChatColor.RED + "' is not banned"); + } + }else if(p1[0].equalsIgnoreCase("ip")) { + try { + if(BanList.unbanIP(p1[1])) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "IP '" + ChatColor.WHITE + p1[1] + ChatColor.GREEN + "' was unbanned"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "IP '" + ChatColor.WHITE + p1[1] + ChatColor.RED + "' is not banned"); + } + }catch(Throwable t) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "IP address '" + ChatColor.WHITE + p1[1] + ChatColor.RED + "' is invalid: " + t.getMessage()); + } + }else if(p1[0].equalsIgnoreCase("wildcard")) { + if(BanList.unbanWildcard(p1[1])) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Wildcard '" + ChatColor.WHITE + p1[1] + ChatColor.GREEN + "' was unbanned"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Wildcard '" + ChatColor.WHITE + p1[1] + ChatColor.RED + "' is not banned"); + } + }else if(p1[0].equalsIgnoreCase("regex")) { + if(BanList.unbanRegex(p1[1])) { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.GREEN + "Regex '" + ChatColor.WHITE + p1[1] + ChatColor.GREEN + "' was unbanned"); + }else { + p0.sendMessage(BanList.banChatMessagePrefix + ChatColor.RED + "Regex '" + ChatColor.WHITE + p1[1] + ChatColor.RED + "' is not banned"); + } + } + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandIP.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandIP.java index 376fbfb..608d5e6 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandIP.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/CommandIP.java @@ -6,6 +6,9 @@ package net.md_5.bungee.command; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.ProxyServer; + +import java.net.InetAddress; + import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.plugin.Command; @@ -25,7 +28,12 @@ public class CommandIP extends Command { if (user == null) { sender.sendMessage(ChatColor.RED + "That user is not online"); } else { - sender.sendMessage(ChatColor.BLUE + "IP of " + args[0] + " is " + user.getAddress()); + Object o = user.getAttachment().get("remoteAddr"); + if(o != null) { + sender.sendMessage(ChatColor.BLUE + "IP of " + args[0] + " is " + (InetAddress)o); + }else { + sender.sendMessage(ChatColor.BLUE + "IP of " + args[0] + " is " + user.getAddress()); + } } } } diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java index c9f4cd4..d89e7fd 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java @@ -4,14 +4,16 @@ package net.md_5.bungee.command; -import java.util.Collections; import java.util.HashSet; +import java.util.Map; +import java.util.WeakHashMap; import java.util.Collection; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.CommandSender; public class ConsoleCommandSender implements CommandSender { private static final ConsoleCommandSender instance; + private static final Map attachment = new WeakHashMap(); private ConsoleCommandSender() { } @@ -65,4 +67,9 @@ public class ConsoleCommandSender implements CommandSender { static { instance = new ConsoleCommandSender(); } + + @Override + public Map getAttachment() { + return attachment; + } } diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/config/Configuration.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/config/Configuration.java index dc17dcf..09e06af 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/config/Configuration.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/config/Configuration.java @@ -12,6 +12,7 @@ import com.google.common.base.Preconditions; import net.md_5.bungee.api.ProxyServer; import java.util.UUID; import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.eaglercraft.EaglercraftBungee; import gnu.trove.map.TMap; import net.md_5.bungee.api.config.ListenerInfo; import java.util.Collection; @@ -24,6 +25,7 @@ public class Configuration { private AuthServiceInfo authInfo; private boolean onlineMode; private int playerLimit; + private String name; public Configuration() { this.timeout = 30000; @@ -41,6 +43,7 @@ public class Configuration { this.authInfo = adapter.getAuthSettings(); this.onlineMode = false; this.playerLimit = adapter.getInt("player_limit", this.playerLimit); + this.name = adapter.getString("server_name", EaglercraftBungee.brand + " Server"); Preconditions.checkArgument(this.listeners != null && !this.listeners.isEmpty(), (Object) "No listeners defined."); final Map newServers = adapter.getServers(); Preconditions.checkArgument(newServers != null && !newServers.isEmpty(), (Object) "No servers defined"); @@ -88,4 +91,8 @@ public class Configuration { public AuthServiceInfo getAuthInfo() { return authInfo; } + + public String getServerName() { + return name; + } } diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/config/YamlConfig.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/config/YamlConfig.java index 20b90d4..e465ac9 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/config/YamlConfig.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/config/YamlConfig.java @@ -67,18 +67,11 @@ public class YamlConfig implements ConfigurationAdapter { final Map permissions = this.get("permissions", new HashMap()); if (permissions.isEmpty()) { permissions.put("default", Arrays.asList("bungeecord.command.server", "bungeecord.command.list")); - permissions.put("admin", Arrays.asList("bungeecord.command.alert", "bungeecord.command.end", "bungeecord.command.ip", "bungeecord.command.reload")); + permissions.put("admin", Arrays.asList("bungeecord.command.alert", "bungeecord.command.end", "bungeecord.command.ip", "bungeecord.command.reload", + "bungeecord.command.eag.ban", "bungeecord.command.eag.banwildcard", "bungeecord.command.eag.banip", "bungeecord.command.eag.banregex", + "bungeecord.command.eag.reloadban", "bungeecord.command.eag.banned", "bungeecord.command.eag.banlist", "bungeecord.command.eag.unban")); } this.get("groups", new HashMap()); - /* - final Map auth = this.get("authservice", new HashMap()); - if(auth.isEmpty()) { - auth.put("enabled", false); - auth.put("limbo", "lobby"); - auth.put("authfile", "passwords.yml"); - auth.put("timeout", 30); - } - */ } private T get(final String path, final T def) { @@ -159,6 +152,7 @@ public class YamlConfig implements ConfigurationAdapter { final String fallbackServer = this.get("fallback_server", defaultServer, val); final boolean forceDefault = this.get("force_default_server", true, val); final boolean websocket = this.get("websocket", true, val); + final boolean forwardIp = this.get("forward_ip", false, val); final String host = this.get("host", "0.0.0.0:25565", val); final int tabListSize = this.get("tab_size", 60, val); final InetSocketAddress address = Util.getAddr(host); @@ -167,11 +161,12 @@ public class YamlConfig implements ConfigurationAdapter { final int textureSize = this.get("texture_size", 16, val); final TexturePackInfo texture = (textureURL == null) ? null : new TexturePackInfo(textureURL, textureSize); final String tabListName = this.get("tab_list", "GLOBAL_PING", val); + final String serverIcon = this.get("server_icon", "server-icon.png", val); DefaultTabList value = DefaultTabList.valueOf(tabListName.toUpperCase()); if (value == null) { value = DefaultTabList.GLOBAL_PING; } - final ListenerInfo info = new ListenerInfo(address, motd, maxPlayers, tabListSize, defaultServer, fallbackServer, forceDefault, websocket, forced, texture, value.clazz); + final ListenerInfo info = new ListenerInfo(address, motd, maxPlayers, tabListSize, defaultServer, fallbackServer, forceDefault, websocket, forwardIp, forced, texture, value.clazz, serverIcon); ret.add(info); } return ret; diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/connection/InitialHandler.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/connection/InitialHandler.java index 3e7e62b..10e4530 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/connection/InitialHandler.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/connection/InitialHandler.java @@ -6,6 +6,7 @@ package net.md_5.bungee.connection; import java.beans.ConstructorProperties; import java.util.ArrayList; +import java.net.InetAddress; import java.net.InetSocketAddress; import net.md_5.bungee.protocol.packet.PacketFFKick; import net.md_5.bungee.api.config.ServerInfo; @@ -37,6 +38,9 @@ import net.md_5.bungee.protocol.Forge; import net.md_5.bungee.netty.PacketDecoder; import com.google.common.base.Preconditions; import net.md_5.bungee.api.event.ProxyPingEvent; +import net.md_5.bungee.eaglercraft.BanList; +import net.md_5.bungee.eaglercraft.WebSocketProxy; +import net.md_5.bungee.eaglercraft.BanList.BanCheck; import net.md_5.bungee.api.ServerPing; import net.md_5.bungee.protocol.packet.PacketFEPing; import net.md_5.bungee.Util; @@ -112,8 +116,12 @@ public class InitialHandler extends PacketHandler implements PendingConnection { if (handshake.getProcolVersion() == 69) { skipEncryption = true; this.handshake.swapProtocol((byte) 61); + }else if(handshake.getProcolVersion() == 71) { + this.disconnect("this server does not support microsoft accounts"); + return; }else if(handshake.getProcolVersion() != 61) { this.disconnect("minecraft 1.5.2 required for eaglercraft backdoor access"); + return; } String un = handshake.getUsername(); if (un.length() < 3) { @@ -128,6 +136,37 @@ public class InitialHandler extends PacketHandler implements PendingConnection { this.disconnect("Go fuck yourself"); return; } + InetAddress sc = WebSocketProxy.localToRemote.get(this.ch.getHandle().remoteAddress()); + if(sc == null) { + System.out.println("WARNING: player '" + un + "' doesn't have a websocket IP, remote address: " + this.ch.getHandle().remoteAddress().toString()); + }else { + BanCheck bc = BanList.checkIpBanned(sc); + if(bc.isBanned()) { + System.err.println("Player '" + un + "' [" + sc.toString() + "] is banned by IP: " + bc.match + " (" + bc.string + ")"); + this.disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: " + bc.string); + return; + }else { + System.out.println("Player '" + un + "' [" + sc.toString() + "] has remote websocket IP: " + sc.getHostAddress()); + } + } + BanCheck bc = BanList.checkBanned(un); + if(bc.isBanned()) { + switch(bc.reason) { + case USER_BANNED: + System.err.println("Player '" + un + "' is banned by username, because '" + bc.string + "'"); + break; + case WILDCARD_BANNED: + System.err.println("Player '" + un + "' is banned by wildcard: " + bc.match); + break; + case REGEX_BANNED: + System.err.println("Player '" + un + "' is banned by regex: " + bc.match); + break; + default: + System.err.println("Player '" + un + "' is banned: " + bc.string); + } + this.disconnect("" + ChatColor.RED + "You are banned.\n" + ChatColor.DARK_GRAY + "Reason: " + bc.string); + return; + } final int limit = BungeeCord.getInstance().config.getPlayerLimit(); if (limit > 0 && this.bungee.getOnlineCount() > limit) { this.disconnect(this.bungee.getTranslation("proxy_full")); @@ -194,6 +233,10 @@ public class InitialHandler extends PacketHandler implements PendingConnection { public void handle(final PacketCDClientStatus clientStatus) throws Exception { Preconditions.checkState(this.thisState == State.LOGIN, (Object) "Not expecting LOGIN"); final UserConnection userCon = new UserConnection(this.bungee, this.ch, this.getName(), this); + InetAddress ins = WebSocketProxy.localToRemote.get(this.ch.getHandle().remoteAddress()); + if(ins != null) { + userCon.getAttachment().put("remoteAddr", ins); + } userCon.init(); this.bungee.getPluginManager().callEvent(new PostLoginEvent(userCon)); ((HandlerBoss) this.ch.getHandle().pipeline().get((Class) HandlerBoss.class)).setHandler(new UpstreamBridge(this.bungee, userCon)); diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/BanList.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/BanList.java new file mode 100644 index 0000000..49ae738 --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/BanList.java @@ -0,0 +1,863 @@ +package net.md_5.bungee.eaglercraft; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.math.BigInteger; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.eaglercraft.sun.net.util.IPAddressUtil; + +public class BanList { + + private static final Object banListMutex = new Object(); + + public static enum BanState { + NOT_BANNED, USER_BANNED, IP_BANNED, WILDCARD_BANNED, REGEX_BANNED; + } + + public static class BanCheck { + public final BanState reason; + public final String match; + public final String string; + private BanCheck(BanState reason, String match, String string) { + this.reason = reason; + this.match = match; + this.string = string; + } + public boolean isBanned() { + return reason != BanState.NOT_BANNED; + } + } + + private static class RegexBan { + public final String string; + public final Pattern compiled; + private RegexBan(String string, Pattern compiled) { + this.string = string; + this.compiled = compiled; + } + public String toString() { + return string; + } + public int hashCode() { + return string.hashCode(); + } + } + + public static interface IPBan { + + boolean checkBan(InetAddress addr); + InetAddress getBaseAddress(); + boolean hasNetMask(); + + } + + private static class IPBan4 implements IPBan { + + private final int addr; + private final InetAddress addrI; + private final int mask; + private final String string; + + protected IPBan4(Inet4Address addr, String s, int mask) { + if(mask >= 32) { + this.mask = 0xFFFFFFFF; + }else { + this.mask = ~((1 << (32 - mask)) - 1); + } + this.string = s; + byte[] bits = addr.getAddress(); + this.addr = this.mask & ((bits[0] << 24) | (bits[1] << 16) | (bits[2] << 8) | (bits[3] & 0xFF)); + this.addrI = addr; + } + + @Override + public boolean checkBan(InetAddress addr4) { + if(addr4 instanceof Inet4Address) { + Inet4Address a = (Inet4Address)addr4; + byte[] bits = a.getAddress(); + int addrBits = ((bits[0] << 24) | (bits[1] << 16) | (bits[2] << 8) | (bits[3] & 0xFF)); + return (mask & addrBits) == addr; + }else { + return false; + } + } + + @Override + public InetAddress getBaseAddress() { + return addrI; + } + + @Override + public String toString() { + return string; + } + + @Override + public int hashCode() { + return string.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o != null && o instanceof IPBan4 && ((IPBan4)o).addr == addr && ((IPBan4)o).mask == mask; + } + + @Override + public boolean hasNetMask() { + return mask != 0xFFFFFFFF; + } + + } + + private static class IPBan6 implements IPBan { + + private static final BigInteger mask128 = new BigInteger(1, new byte[] { + (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF, + (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF, + (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF, + (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF + }); + + private final BigInteger addr; + private final InetAddress addrI; + private final BigInteger mask; + private final String string; + + protected IPBan6(Inet6Address addr, String s, int mask) { + this.mask = BigInteger.valueOf(1l).shiftLeft(128 - mask).subtract(BigInteger.valueOf(1l)).xor(mask128); + this.string = s.toLowerCase(); + this.addr = new BigInteger(1, addr.getAddress()).and(this.mask); + this.addrI = addr; + } + + @Override + public boolean checkBan(InetAddress addr6) { + if(addr6 instanceof Inet6Address) { + Inet6Address a = (Inet6Address)addr6; + BigInteger addrBits = new BigInteger(1, a.getAddress()).and(this.mask); + return addr.equals(addrBits); + }else { + return false; + } + } + + @Override + public InetAddress getBaseAddress() { + return addrI; + } + + @Override + public String toString() { + return string; + } + + @Override + public int hashCode() { + return string.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o != null && o instanceof IPBan6 && ((IPBan6)o).addr.equals(addr) && ((IPBan6)o).mask.equals(mask); + } + + @Override + public boolean hasNetMask() { + return !mask.equals(mask128); + } + + } + + public static final File bansFile = new File("bans.txt"); + + public static final String banChatMessagePrefix = ChatColor.GOLD + "[BanList] "; + + public static final Map userBans = new HashMap(); + public static final Set ipBans = new HashSet(); + public static final Set wildcardBans = new HashSet(); + public static final Set regexBans = new HashSet(); + private static List currentBanList = null; + + public static final List blockedBans = new ArrayList(); + + static { + try { + blockedBans.add(constructIpBan("127.0.0.0/8")); + }catch(UnknownHostException e) { + System.err.println("Error: could not whitelist '127.0.0.0/8'"); + e.printStackTrace(); + } + try { + blockedBans.add(constructIpBan("10.0.0.0/8")); + }catch(UnknownHostException e) { + System.err.println("Error: could not whitelist '10.0.0.0/8'"); + e.printStackTrace(); + } + try { + blockedBans.add(constructIpBan("172.24.0.0/14")); + }catch(UnknownHostException e) { + System.err.println("Error: could not whitelist '172.24.0.0/14'"); + e.printStackTrace(); + } + try { + blockedBans.add(constructIpBan("192.168.0.0/16")); + }catch(UnknownHostException e) { + System.err.println("Error: could not whitelist '192.168.0.0/16'"); + e.printStackTrace(); + } + try { + blockedBans.add(constructIpBan("::1/128")); + }catch(UnknownHostException e) { + System.err.println("Error: could not whitelist '::1/128'"); + e.printStackTrace(); + } + } + + private static long lastListTest = 0l; + private static long lastListLoad = 0l; + private static boolean fileIsBroken = false; + + public static BanCheck checkIpBanned(InetAddress addr) { + synchronized(banListMutex) { + for(IPBan b : ipBans) { + if(b.checkBan(addr)) { + return new BanCheck(BanState.IP_BANNED, b.toString(), b.hasNetMask() ? "Banned by Netmask" : "Banned by IP"); + } + } + } + return new BanCheck(BanState.NOT_BANNED, "none", "not banned"); + } + + public static BanCheck checkBanned(String player) { + synchronized(banListMutex) { + player = player.trim().toLowerCase(); + String r = userBans.get(player); + if(r != null) { + if(r.length() <= 0) { + r = "The ban hammer has spoken"; + } + return new BanCheck(BanState.USER_BANNED, player, r); + } + for(String ss : wildcardBans) { + String s = ss; + boolean startStar = s.startsWith("*"); + if(startStar) { + s = s.substring(1); + } + boolean endStar = s.endsWith("*"); + if(endStar) { + s = s.substring(0, s.length() - 1); + } + if(startStar && endStar) { + if(player.contains(s)) { + return new BanCheck(BanState.WILDCARD_BANNED, ss, "You've been banned via wildcard"); + } + }else if(endStar) { + if(player.startsWith(s)) { + return new BanCheck(BanState.WILDCARD_BANNED, ss, "You've been banned via wildcard"); + } + }else if(startStar) { + if(player.endsWith(s)) { + return new BanCheck(BanState.WILDCARD_BANNED, ss, "You've been banned via wildcard"); + } + }else { + if(player.equals(s)) { + return new BanCheck(BanState.WILDCARD_BANNED, ss, "You've been banned via wildcard"); + } + } + } + for(RegexBan p : regexBans) { + if(p.compiled.matcher(player).matches()) { + return new BanCheck(BanState.REGEX_BANNED, p.string, "You've been banned via regex"); + } + } + } + return new BanCheck(BanState.NOT_BANNED, "none", "not banned"); + } + + private static void saveCurrentBanListLines() { + try { + PrintWriter pf = new PrintWriter(new FileWriter(bansFile)); + for(String s : currentBanList) { + pf.println(s); + } + pf.close(); + lastListLoad = lastListTest = System.currentTimeMillis(); + }catch(Throwable t) { + System.err.println("ERROR: the ban list could not be saved to file '" + bansFile.getName() + "', please fix this or you will lose all your bans next time this server restarts"); + t.printStackTrace(); + } + } + + private static boolean addEntryToFile(BanState b, String s) { + if(b == null || b == BanState.NOT_BANNED) { + return false; + } + String wantedHeader = b == BanState.USER_BANNED ? "[Usernames]" : (b == BanState.WILDCARD_BANNED ? "[Wildcards]" : (b == BanState.REGEX_BANNED ? "[Regex]" : (b == BanState.IP_BANNED ? "[IPs]" : "shit"))); + int lastFullPart = -1; + boolean isFilePart = false; + boolean isPartStart = false; + for(int i = 0, l = currentBanList.size(); i < l; ++i) { + String ss = currentBanList.get(i).trim(); + if(ss.length() <= 0) { + continue; + } + if(ss.startsWith("#")) { + continue; + } + if(ss.equalsIgnoreCase(wantedHeader)) { + isFilePart = true; + isPartStart = true; + lastFullPart = i; + }else if(ss.indexOf('[') != -1) { + if(isFilePart) { + break; + } + }else { + if(isFilePart) { + lastFullPart = i; + isPartStart = false; + } + } + } + if(lastFullPart != -1) { + if(isPartStart) { + lastFullPart += 1; + currentBanList.add(lastFullPart, ""); + } + lastFullPart += 1; + currentBanList.add(lastFullPart, s); + lastFullPart += 1; + if(currentBanList.size() > lastFullPart && currentBanList.get(lastFullPart).trim().length() > 0) { + currentBanList.add(lastFullPart, ""); + } + }else { + if(currentBanList.size() > 0 && currentBanList.get(currentBanList.size() - 1).trim().length() > 0) { + currentBanList.add(""); + } + currentBanList.add(wantedHeader); + currentBanList.add(""); + currentBanList.add(s); + currentBanList.add(""); + } + saveCurrentBanListLines(); + return true; + } + + private static boolean removeEntryFromFile(BanState b, String s, boolean ignoreCase) { + if(b == null || b == BanState.NOT_BANNED) { + return false; + } + String wantedHeader = b == BanState.USER_BANNED ? "[Usernames]" : (b == BanState.WILDCARD_BANNED ? "[Wildcards]" : (b == BanState.REGEX_BANNED ? "[Regex]" : (b == BanState.IP_BANNED ? "[IPs]" : "shit"))); + Iterator lns = currentBanList.iterator(); + boolean isFilePart = false; + boolean wasRemoved = false; + while(lns.hasNext()) { + String ss = lns.next().trim(); + if(ss.length() <= 0) { + continue; + } + if(ss.startsWith("#")) { + continue; + } + if(ss.equalsIgnoreCase(wantedHeader)) { + isFilePart = true; + }else if(ss.indexOf('[') != -1) { + isFilePart = false; + }else { + if(b == BanState.USER_BANNED && ss.contains(":")) { + ss = ss.substring(0, ss.indexOf(':')).trim(); + } + if(isFilePart && (ignoreCase ? ss.equalsIgnoreCase(s) : ss.equals(s))) { + lns.remove(); + wasRemoved = true; + } + } + } + if(wasRemoved) { + saveCurrentBanListLines(); + } + return wasRemoved; + } + + public static boolean unban(String player) { + synchronized(banListMutex) { + String s = player.trim().toLowerCase(); + if(userBans.remove(s) != null) { + removeEntryFromFile(BanState.USER_BANNED, player, true); + return true; + }else { + return false; + } + } + } + + public static boolean ban(String player, String reason) { + synchronized(banListMutex) { + player = player.trim().toLowerCase(); + if(userBans.put(player, reason) == null) { + addEntryToFile(BanState.USER_BANNED, player + (reason == null || reason.length() <= 0 ? "" : ": " + reason)); + return true; + }else { + return false; + } + } + } + + public static boolean banWildcard(String wc) throws PatternSyntaxException { + synchronized(banListMutex) { + wc = wc.trim().toLowerCase(); + boolean b = wc.contains("*"); + if(!b || (b && !wc.startsWith("*") && !wc.endsWith("*"))) { + throw new PatternSyntaxException("Wildcard can only begin and/or end with *", wc, 0); + } + if(wildcardBans.add(wc)) { + addEntryToFile(BanState.WILDCARD_BANNED, wc); + return true; + }else { + return false; + } + } + } + + public static boolean unbanWildcard(String wc) { + synchronized(banListMutex) { + wc = wc.trim().toLowerCase(); + if(wildcardBans.remove(wc)) { + removeEntryFromFile(BanState.WILDCARD_BANNED, wc, true); + return true; + }else { + return false; + } + } + } + + public static boolean banRegex(String regex) throws PatternSyntaxException { + synchronized(banListMutex) { + regex = regex.trim(); + Pattern p = Pattern.compile(regex); + if(regexBans.add(new RegexBan(regex, p))) { + addEntryToFile(BanState.REGEX_BANNED, regex); + return true; + }else { + return false; + } + } + } + + public static boolean unbanRegex(String regex) { + synchronized(banListMutex) { + regex = regex.trim(); + Iterator banz = regexBans.iterator(); + while(banz.hasNext()) { + if(banz.next().string.equals(regex)) { + banz.remove(); + removeEntryFromFile(BanState.REGEX_BANNED, regex, false); + return true; + } + } + return false; + } + } + + public static IPBan constructIpBan(String ip) throws UnknownHostException { + synchronized(banListMutex) { + ip = ip.trim(); + String s = ip; + int subnet = -1; + int i = s.indexOf('/'); + if(i != -1) { + String s2 = s.substring(i + 1); + s = s.substring(0, i); + try { + subnet = Integer.parseInt(s2); + }catch(Throwable t) { + throw new UnknownHostException("Invalid netmask: '" + s + "'"); + } + } + + if(!IPAddressUtil.isIPv4LiteralAddress(s) && !IPAddressUtil.isIPv6LiteralAddress(s)) { + throw new UnknownHostException("Invalid address: '" + s + "'"); + } + + InetAddress aa = InetAddress.getByName(s); + if(aa instanceof Inet4Address) { + if(subnet > 32 || subnet < -1) { + throw new UnknownHostException("IPv4 netmask '" + subnet + "' is invalid"); + } + if(subnet == -1) { + subnet = 32; + } + return new IPBan4((Inet4Address)aa, ip, subnet); + }else if(aa instanceof Inet6Address) { + if(subnet > 128 || subnet < -1) { + throw new UnknownHostException("IPv6 netmask '" + subnet + "' is invalid"); + } + if(subnet == -1) { + subnet = 128; + } + return new IPBan6((Inet6Address)aa, ip, subnet); + }else { + throw new UnknownHostException("Only ipv4 and ipv6 addresses allowed in Eaglercraft"); + } + } + } + + public static boolean banIP(String ip) throws UnknownHostException { + synchronized(banListMutex) { + ip = ip.trim(); + IPBan b = constructIpBan(ip); + if(b != null) { + if(ipBans.add(b)) { + addEntryToFile(BanState.IP_BANNED, ip); + return true; + } + } + return false; + } + } + + public static boolean unbanIP(String ip) throws UnknownHostException { + synchronized(banListMutex) { + ip = ip.trim(); + IPBan b = constructIpBan(ip); + if(b != null) { + Iterator banz = ipBans.iterator(); + while(banz.hasNext()) { + IPBan bb = banz.next(); + if(bb.equals(b)) { + banz.remove(); + removeEntryFromFile(BanState.IP_BANNED, bb.toString(), true); + return true; + } + } + } + return false; + } + } + + private static final int MAX_CHAT_LENGTH = 118; + + public static String listAllBans() { + synchronized(banListMutex) { + String ret = ""; + for(String s : userBans.keySet()) { + if(ret.length() > 0) { + ret += ", "; + } + ret += s; + } + return ret.length() > 0 ? ret : "(none)"; + } + } + + public static String listAllWildcardBans() { + synchronized(banListMutex) { + String ret = ""; + for(String s : wildcardBans) { + if(ret.length() > 0) { + ret += ", "; + } + ret += s; + } + return ret.length() > 0 ? ret : "(none)"; + } + } + + public static String listAllRegexBans() { + synchronized(banListMutex) { + String ret = ""; + for(RegexBan s : regexBans) { + if(ret.length() > 0) { + ret += " | "; + } + ret += s.string; + } + return ret.length() > 0 ? ret : "(none)"; + } + } + + public static String listAllIPBans(boolean v6, boolean netmask) { + synchronized(banListMutex) { + String ret = ""; + for(IPBan b : ipBans) { + if(v6) { + if(b instanceof IPBan6) { + IPBan6 b2 = (IPBan6)b; + if(netmask == b2.hasNetMask()) { + if(ret.length() > 0) { + ret += ", "; + } + ret += b2.string; + } + } + }else { + if(b instanceof IPBan4) { + IPBan4 b2 = (IPBan4)b; + if(netmask == b2.hasNetMask()) { + if(ret.length() > 0) { + ret += ", "; + } + ret += b2.string; + } + } + } + } + if(ret.length() <= 0) { + ret = "(none)"; + } + return ret; + } + } + + public static void maybeReloadBans(CommandSender cs) { + synchronized(banListMutex) { + long st = System.currentTimeMillis(); + if(cs == null && st - lastListTest < 1000l) { + return; + } + lastListTest = st; + boolean ex = bansFile.exists(); + if(!fileIsBroken && !ex) { + try { + PrintWriter p = new PrintWriter(new FileWriter(bansFile)); + p.println(); + p.println("#"); + p.println("# This file allows you to configure bans for eaglercraftbungee"); + p.println("# When it is saved, eaglercraft should reload it automatically"); + p.println("# (check the console though to be safe)"); + p.println("#"); + p.println("# For a [Usernames] ban, just add the player's name. Use a colon ':' to put in a ban reason"); + p.println("# For a [IPs] ban, just add the player's IP, or a subnet like 69.69.0.0/16 to ban all IPs beginning with 69.69.*"); + p.println("# For a [Wildcards] ban, type a string and prefix and/or suffix it with * to define the wildcard"); + p.println("# For a [Regex] ban, type a valid regular expression in the java.util.regex format"); + p.println("#"); + p.println("# All bans are case-insensitive, USERNAMES ARE CONVERTED TO LOWERCASE BEFORE BEING MATCHED VIA REGEX"); + p.println("# Java regex syntax: https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html"); + p.println("#"); + p.println(); + p.println("# set this to false to use \"/eag-ban\" to ban on bungee instead of just \"/ban\""); + p.println("# (most likely needs a restart to take effect)"); + p.println("replace-bukkit=true"); + p.println(); + p.println(); + p.println("[Usernames]"); + p.println(); + p.println("# ban_test1: The ban hammer has spoken!"); + p.println("# ban_test2: custom ban message here"); + p.println("# ban_test3"); + p.println(); + p.println("# (remove the '#' before each line to enable)"); + p.println(); + p.println("[IPs]"); + p.println(); + p.println("# WARNING: if you're using nginx, banning any player's IP is gonna ban ALL PLAYERS on your server"); + p.println("# For this reason, the ban IP command doesn't ban 127.0.0.1 or any other 'private' range IPs"); + p.println(); + p.println("# 101.202.69.11"); + p.println("# 123.21.43.0/24"); + p.println("# 2601:1062:69:418:BEEF::10"); + p.println("# 2601:6090:420::/48"); + p.println(); + p.println(); + p.println("[Wildcards]"); + p.println(); + p.println("# *fuck*"); + p.println("# nigg*"); + p.println(); + p.println(); + p.println("[Regex]"); + p.println(); + p.println("# you.+are.(a|the).+bitch"); + p.println(); + p.println(); + p.println("# end of file"); + p.println(); + p.close(); + System.out.println("Wrote a new bans.txt to: " + bansFile.getAbsolutePath()); + lastListLoad = 0l; + }catch(Throwable t) { + fileIsBroken = true; + if(cs != null) { + cs.sendMessage(banChatMessagePrefix + ChatColor.RED + "Could not create blank 'bans.txt' list file"); + cs.sendMessage(banChatMessagePrefix + ChatColor.RED + "(Reason: " + t.toString() + ")"); + } + System.err.println("Could not create blank 'bans.txt' list file"); + System.err.println("(Reason: " + t.toString() + ")"); + t.printStackTrace(); + } + return; + } + if(fileIsBroken && ex) { + fileIsBroken = false; + lastListLoad = 0l; + } + if(fileIsBroken) { + return; + } + long lastEdit = bansFile.lastModified(); + if(cs != null || lastEdit - lastListLoad > 400l) { + try { + BufferedReader r = new BufferedReader(new FileReader(bansFile)); + currentBanList = new LinkedList(); + String s; + while((s = r.readLine()) != null) { + currentBanList.add(s); + } + r.close(); + lastListLoad = lastEdit; + System.out.println("Server bans.txt changed, it will be reloaded automatically"); + if(cs == null) { + for(ProxiedPlayer pp : BungeeCord.getInstance().getPlayers()) { + if(pp.hasPermission("bungeecord.command.eag.reloadban")) { + pp.sendMessage(BanList.banChatMessagePrefix + ChatColor.WHITE + "Your Eaglercraftbungee bans.txt just got modified, it will be reloaded asap"); + pp.sendMessage(BanList.banChatMessagePrefix + ChatColor.YELLOW + "Stop your server and check your config immediately if you don't know how this happened!!"); + } + } + } + parseListFrom(); + System.out.println("Reload complete"); + }catch(Throwable t) { + if(cs != null) { + cs.sendMessage(banChatMessagePrefix + ChatColor.RED + "Could not reload 'bans.txt' list file"); + cs.sendMessage(banChatMessagePrefix + ChatColor.RED + "(Reason: " + t.toString() + ")"); + } + System.err.println("Could not reload 'bans.txt' list file"); + System.err.println("(Reason: " + t.toString() + ")"); + } + } + } + } + + private static void parseListFrom() { + userBans.clear(); + ipBans.clear(); + wildcardBans.clear(); + regexBans.clear(); + + int filePart = 0; + boolean replaceBukkit = false; + + for(String s : currentBanList) { + s = s.trim(); + if(s.length() <= 0) { + continue; + } + if(s.startsWith("#")) { + continue; + } + if(s.equals("[Usernames]")) { + filePart = 1; + }else if(s.equals("[Wildcards]")) { + filePart = 2; + }else if(s.equals("[Regex]")) { + filePart = 3; + }else if(s.equals("[IPs]")) { + filePart = 4; + }else if(s.equals("replace-bukkit=true")) { + replaceBukkit = true; + }else if(s.equals("replace-bukkit=false")) { + continue; + }else { + if(filePart == 1) { + int i = s.indexOf(':'); + if(i == -1) { + userBans.put(s.toLowerCase(), ""); + }else { + userBans.put(s.substring(0, i).trim().toLowerCase(), s.substring(i + 1).trim()); + } + }else if(filePart == 2) { + boolean ws = s.startsWith("*"); + boolean we = s.endsWith("*"); + if(!ws && !we) { + if(s.contains("*")) { + System.err.println("Error: wildcard '" + s + "' contains a '*' not at the start/end of the string"); + }else { + System.err.println("Error: wildcard '" + s + "' does not contain a '*' wildcard character"); + } + }else { + int total = (ws ? 1 : 0) + (we ? 1 : 0); + int t2 = 0; + for(char c : s.toCharArray()) { + if(c == '*') ++t2; + } + if(total != t2) { + System.err.println("Error: wildcard '" + s + "' contains a '*' not at the start/end of the string"); + } + } + wildcardBans.add(s.toLowerCase()); + }else if(filePart == 3) { + Pattern p = null; + try { + p = Pattern.compile(s); + }catch(Throwable t) { + System.err.println("Error: the regex " + s.toLowerCase() + " is invalid"); + System.err.println("Reason: " + t.getClass().getSimpleName() + ": " + t.getLocalizedMessage()); + } + if(p != null) { + regexBans.add(new RegexBan(s, p)); + } + }else if(filePart == 4) { + String ss = s; + int subnet = -1; + int i = s.indexOf('/'); + if(i != -1) { + String s2 = s.substring(i + 1); + s = s.substring(0, i); + try { + subnet = Integer.parseInt(s2); + }catch(Throwable t) { + System.err.println("Error: the subnet '"+ s2 +"' for IP ban address " + s + " was invalid"); + subnet = -2; + } + } + if(subnet >= -1) { + try { + InetAddress aa = InetAddress.getByName(s); + if(aa instanceof Inet4Address) { + if(subnet == -1) { + subnet = 32; + } + ipBans.add(new IPBan4((Inet4Address)aa, ss, subnet)); + }else if(aa instanceof Inet6Address) { + if(subnet == -1) { + subnet = 128; + } + ipBans.add(new IPBan6((Inet6Address)aa, ss, subnet)); + }else { + throw new UnknownHostException("Only ipv4 and ipv6 addresses allowed in Eaglercraft"); + } + }catch(Throwable t) { + System.err.println("Error: the IP ban address " + s + " could not be parsed"); + t.printStackTrace(); + } + } + } + } + } + + BungeeCord.getInstance().reconfigureBanCommands(replaceBukkit); + } + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/EaglercraftBungee.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/EaglercraftBungee.java new file mode 100644 index 0000000..8668b11 --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/EaglercraftBungee.java @@ -0,0 +1,9 @@ +package net.md_5.bungee.eaglercraft; + +public class EaglercraftBungee { + + public static final String brand = "Eagtek"; + public static final String version = "0.1.0"; + public static final boolean cracked = true; + +} diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/WebSocketListener.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/WebSocketListener.java index ff87ca0..c23ce41 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/WebSocketListener.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/WebSocketListener.java @@ -1,33 +1,50 @@ package net.md_5.bungee.eaglercraft; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.nio.ByteBuffer; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; +import net.md_5.bungee.BungeeCord; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.config.ListenerInfo; public class WebSocketListener extends WebSocketServer { + + public static class PendingSocket { + public long openTime; + public InetAddress realAddress; + protected PendingSocket(long openTime, InetAddress realAddress) { + this.openTime = openTime; + this.realAddress = realAddress; + } + } private InetSocketAddress bungeeProxy; private ProxyServer bungeeCord; + private ListenerInfo info; public WebSocketListener(ListenerInfo info, InetSocketAddress sock, ProxyServer bungeeCord) { super(info.getHost()); this.setTcpNoDelay(true); this.setConnectionLostTimeout(5); this.start(); + this.info = info; this.bungeeProxy = sock; this.bungeeCord = bungeeCord; } @Override public void onClose(WebSocket arg0, int arg1, String arg2, boolean arg3) { - if(arg0.getAttachment() != null) { - ((WebSocketProxy)arg0.getAttachment()).killConnection(); + Object o = arg0.getAttachment(); + if(o != null) { + if(o instanceof WebSocketProxy) { + ((WebSocketProxy)arg0.getAttachment()).killConnection(); + } } System.out.println("websocket closed - " + arg0.getRemoteSocketAddress()); } @@ -43,24 +60,73 @@ public class WebSocketListener extends WebSocketServer { @Override public void onMessage(WebSocket arg0, ByteBuffer arg1) { - if(arg0.getAttachment() != null) { - ((WebSocketProxy)arg0.getAttachment()).sendPacket(arg1); + Object o = arg0.getAttachment(); + if(o == null || (o instanceof PendingSocket)) { + InetAddress realAddr; + if(o == null) { + realAddr = arg0.getRemoteSocketAddress().getAddress(); + }else { + realAddr = ((PendingSocket)o).realAddress; + } + System.out.println("connection is binary - " + realAddr); + WebSocketProxy proxyObj = new WebSocketProxy(arg0, realAddr, bungeeProxy); + arg0.setAttachment(proxyObj); + if(!proxyObj.connect()) { + System.err.println("loopback to '" + bungeeProxy.toString() + "' failed - " + realAddr); + arg0.close(); + return; + } + o = proxyObj; + } + if(o != null) { + if(o instanceof WebSocketProxy) { + ((WebSocketProxy)o).sendPacket(arg1); + }else { + System.out.println("error: recieved binary data on text websocket - " + arg0.getRemoteSocketAddress()); + arg0.close(); + } } } @Override public void onOpen(WebSocket arg0, ClientHandshake arg1) { System.out.println("websocket opened - " + arg0.getRemoteSocketAddress()); - WebSocketProxy proxyObj = new WebSocketProxy(arg0, bungeeProxy); - arg0.setAttachment(proxyObj); - if(!proxyObj.connect()) { - arg0.close(); + if(info.hasForwardedHeaders()) { + String s = arg1.getFieldValue("X-Real-IP"); + if(s != null) { + try { + InetAddress addr = InetAddress.getByName(s); + arg0.setAttachment(new PendingSocket(System.currentTimeMillis(), addr)); + System.out.println("real IP of '" + arg0.getRemoteSocketAddress().toString() + "' is '" + addr.getHostAddress() + "'"); + }catch(UnknownHostException e) { + System.out.println("invalid 'X-Real-IP' header - " + e.toString()); + arg0.close(); + } + }else { + arg0.setAttachment(new PendingSocket(System.currentTimeMillis(), arg0.getRemoteSocketAddress().getAddress())); + } + }else { + arg0.setAttachment(new PendingSocket(System.currentTimeMillis(), arg0.getRemoteSocketAddress().getAddress())); } } @Override public void onStart() { - + } + + public void closeInactiveSockets() { + for(WebSocket w : this.getConnections()) { + Object o = w.getAttachment(); + if(o == null) { + System.out.println("close inactive websocket - " + w.getRemoteSocketAddress()); + w.close(); + }else if(o instanceof PendingSocket) { + if(System.currentTimeMillis() - ((PendingSocket)o).openTime > 5000l) { + System.out.println("close inactive websocket - " + ((PendingSocket)o).realAddress); + w.close(); + } + } + } } } diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/WebSocketProxy.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/WebSocketProxy.java index 69738c0..e0d0d6d 100644 --- a/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/WebSocketProxy.java +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/WebSocketProxy.java @@ -1,7 +1,9 @@ package net.md_5.bungee.eaglercraft; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; +import java.util.HashMap; import org.java_websocket.WebSocket; @@ -16,6 +18,8 @@ import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; /** * Not the ideal solution but what are we supposed to do @@ -25,20 +29,22 @@ public class WebSocketProxy extends SimpleChannelInboundHandler { private WebSocket client; private InetSocketAddress tcpListener; + private InetSocketAddress localAddress; + private InetAddress realRemoteAddr; private NioSocketChannel tcpChannel; private static final EventLoopGroup group = new NioEventLoopGroup(4); + public static final HashMap localToRemote = new HashMap(); - public WebSocketProxy(WebSocket w, InetSocketAddress addr) { + public WebSocketProxy(WebSocket w, InetAddress remoteAddr, InetSocketAddress addr) { client = w; + realRemoteAddr = remoteAddr; tcpListener = addr; tcpChannel = null; } public void killConnection() { - if(client.isOpen()) { - client.close(); - } + localToRemote.remove(localAddress); if(tcpChannel != null && tcpChannel.isOpen()) { try { tcpChannel.disconnect().sync(); @@ -59,9 +65,16 @@ public class WebSocketProxy extends SimpleChannelInboundHandler { clientBootstrap.handler(new ChannelInitializer() { protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(WebSocketProxy.this); + socketChannel.closeFuture().addListener(new GenericFutureListener>() { + @Override + public void operationComplete(Future paramF) throws Exception { + localToRemote.remove(localAddress); + } + }); } }); tcpChannel = (NioSocketChannel) clientBootstrap.connect().sync().channel(); + localToRemote.put(localAddress = tcpChannel.localAddress(), realRemoteAddr); return true; } }catch(Throwable t) { @@ -89,4 +102,8 @@ public class WebSocketProxy extends SimpleChannelInboundHandler { } } + public void finalize() { + localToRemote.remove(localAddress); + } + } diff --git a/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/sun/net/util/IPAddressUtil.java b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/sun/net/util/IPAddressUtil.java new file mode 100644 index 0000000..4cef80d --- /dev/null +++ b/eaglercraftbungee/src/main/java/net/md_5/bungee/eaglercraft/sun/net/util/IPAddressUtil.java @@ -0,0 +1,323 @@ +package net.md_5.bungee.eaglercraft.sun.net.util; + +import java.net.URL; +import java.util.Arrays; + +public class IPAddressUtil { + private static final int INADDR4SZ = 4; + + private static final int INADDR16SZ = 16; + + private static final int INT16SZ = 2; + + private static final long L_IPV6_DELIMS = 0L; + + private static final long H_IPV6_DELIMS = 671088640L; + + private static final long L_GEN_DELIMS = -8935000888854970368L; + + private static final long H_GEN_DELIMS = 671088641L; + + private static final long L_AUTH_DELIMS = 288230376151711744L; + + private static final long H_AUTH_DELIMS = 671088641L; + + private static final long L_COLON = 288230376151711744L; + + private static final long H_COLON = 0L; + + private static final long L_SLASH = 140737488355328L; + + private static final long H_SLASH = 0L; + + private static final long L_BACKSLASH = 0L; + + private static final long H_BACKSLASH = 268435456L; + + private static final long L_NON_PRINTABLE = 4294967295L; + + private static final long H_NON_PRINTABLE = -9223372036854775808L; + + private static final long L_EXCLUDE = -8935000884560003073L; + + private static final long H_EXCLUDE = -9223372035915251711L; + + public static byte[] textToNumericFormatV4(String paramString) { + byte[] arrayOfByte = new byte[4]; + long l = 0L; + byte b1 = 0; + boolean bool = true; + int i = paramString.length(); + if (i == 0 || i > 15) + return null; + for (byte b2 = 0; b2 < i; b2++) { + char c = paramString.charAt(b2); + if (c == '.') { + if (bool || l < 0L || l > 255L || b1 == 3) + return null; + arrayOfByte[b1++] = (byte) (int) (l & 0xFFL); + l = 0L; + bool = true; + } else { + int j = Character.digit(c, 10); + if (j < 0) + return null; + l *= 10L; + l += j; + bool = false; + } + } + if (bool || l < 0L || l >= 1L << (4 - b1) * 8) + return null; + switch (b1) { + case 0 : + arrayOfByte[0] = (byte) (int) (l >> 24L & 0xFFL); + case 1 : + arrayOfByte[1] = (byte) (int) (l >> 16L & 0xFFL); + case 2 : + arrayOfByte[2] = (byte) (int) (l >> 8L & 0xFFL); + case 3 : + arrayOfByte[3] = (byte) (int) (l >> 0L & 0xFFL); + break; + } + return arrayOfByte; + } + + public static byte[] textToNumericFormatV6(String paramString) { + if (paramString.length() < 2) + return null; + char[] arrayOfChar = paramString.toCharArray(); + byte[] arrayOfByte1 = new byte[16]; + int j = arrayOfChar.length; + int k = paramString.indexOf("%"); + if (k == j - 1) + return null; + if (k != -1) + j = k; + byte b = -1; + byte b1 = 0, b2 = 0; + if (arrayOfChar[b1] == ':' && arrayOfChar[++b1] != ':') + return null; + byte b3 = b1; + boolean bool = false; + int i = 0; + while (b1 < j) { + char c = arrayOfChar[b1++]; + int m = Character.digit(c, 16); + if (m != -1) { + i <<= 4; + i |= m; + if (i > 65535) + return null; + bool = true; + continue; + } + if (c == ':') { + b3 = b1; + if (!bool) { + if (b != -1) + return null; + b = b2; + continue; + } + if (b1 == j) + return null; + if (b2 + 2 > 16) + return null; + arrayOfByte1[b2++] = (byte) (i >> 8 & 0xFF); + arrayOfByte1[b2++] = (byte) (i & 0xFF); + bool = false; + i = 0; + continue; + } + if (c == '.' && b2 + 4 <= 16) { + String str = paramString.substring(b3, j); + byte b4 = 0; + int n = 0; + while ((n = str.indexOf('.', n)) != -1) { + b4++; + n++; + } + if (b4 != 3) + return null; + byte[] arrayOfByte = textToNumericFormatV4(str); + if (arrayOfByte == null) + return null; + for (byte b5 = 0; b5 < 4; b5++) + arrayOfByte1[b2++] = arrayOfByte[b5]; + bool = false; + break; + } + return null; + } + if (bool) { + if (b2 + 2 > 16) + return null; + arrayOfByte1[b2++] = (byte) (i >> 8 & 0xFF); + arrayOfByte1[b2++] = (byte) (i & 0xFF); + } + if (b != -1) { + int m = b2 - b; + if (b2 == 16) + return null; + for (b1 = 1; b1 <= m; b1++) { + arrayOfByte1[16 - b1] = arrayOfByte1[b + m - b1]; + arrayOfByte1[b + m - b1] = 0; + } + b2 = 16; + } + if (b2 != 16) + return null; + byte[] arrayOfByte2 = convertFromIPv4MappedAddress(arrayOfByte1); + if (arrayOfByte2 != null) + return arrayOfByte2; + return arrayOfByte1; + } + + public static boolean isIPv4LiteralAddress(String paramString) { + return (textToNumericFormatV4(paramString) != null); + } + + public static boolean isIPv6LiteralAddress(String paramString) { + return (textToNumericFormatV6(paramString) != null); + } + + public static byte[] convertFromIPv4MappedAddress(byte[] paramArrayOfbyte) { + if (isIPv4MappedAddress(paramArrayOfbyte)) { + byte[] arrayOfByte = new byte[4]; + System.arraycopy(paramArrayOfbyte, 12, arrayOfByte, 0, 4); + return arrayOfByte; + } + return null; + } + + private static boolean isIPv4MappedAddress(byte[] paramArrayOfbyte) { + if (paramArrayOfbyte.length < 16) + return false; + if (paramArrayOfbyte[0] == 0 && paramArrayOfbyte[1] == 0 && paramArrayOfbyte[2] == 0 && paramArrayOfbyte[3] == 0 + && paramArrayOfbyte[4] == 0 && paramArrayOfbyte[5] == 0 && paramArrayOfbyte[6] == 0 + && paramArrayOfbyte[7] == 0 && paramArrayOfbyte[8] == 0 && paramArrayOfbyte[9] == 0 + && paramArrayOfbyte[10] == -1 && paramArrayOfbyte[11] == -1) + return true; + return false; + } + + public static boolean match(char paramChar, long paramLong1, long paramLong2) { + if (paramChar < '@') + return ((1L << paramChar & paramLong1) != 0L); + if (paramChar < '€') + return ((1L << paramChar - 64 & paramLong2) != 0L); + return false; + } + + public static int scan(String paramString, long paramLong1, long paramLong2) { + byte b = -1; + int i; + if (paramString == null || (i = paramString.length()) == 0) + return -1; + boolean bool = false; + while (++b < i && !(bool = match(paramString.charAt(b), paramLong1, paramLong2))); + if (bool) + return b; + return -1; + } + + public static int scan(String paramString, long paramLong1, long paramLong2, char[] paramArrayOfchar) { + byte b = -1; + int i; + if (paramString == null || (i = paramString.length()) == 0) + return -1; + boolean bool = false; + char c2 = paramArrayOfchar[0]; + char c1; + while (++b < i && !(bool = match(c1 = paramString.charAt(b), paramLong1, paramLong2))) { + if (c1 >= c2 && Arrays.binarySearch(paramArrayOfchar, c1) > -1) { + bool = true; + break; + } + } + if (bool) + return b; + return -1; + } + + private static String describeChar(char paramChar) { + if (paramChar < ' ' || paramChar == '') { + if (paramChar == '\n') + return "LF"; + if (paramChar == '\r') + return "CR"; + return "control char (code=" + paramChar + ")"; + } + if (paramChar == '\\') + return "'\\'"; + return "'" + paramChar + "'"; + } + + private static String checkUserInfo(String paramString) { + int i = scan(paramString, -9223231260711714817L, -9223372035915251711L); + if (i >= 0) + return "Illegal character found in user-info: " + describeChar(paramString.charAt(i)); + return null; + } + + private static String checkHost(String paramString) { + if (paramString.startsWith("[") && paramString.endsWith("]")) { + paramString = paramString.substring(1, paramString.length() - 1); + if (isIPv6LiteralAddress(paramString)) { + int j = paramString.indexOf('%'); + if (j >= 0) { + j = scan(paramString = paramString.substring(j), 4294967295L, -9223372036183687168L); + if (j >= 0) + return "Illegal character found in IPv6 scoped address: " + describeChar(paramString.charAt(j)); + } + return null; + } + return "Unrecognized IPv6 address format"; + } + int i = scan(paramString, -8935000884560003073L, -9223372035915251711L); + if (i >= 0) + return "Illegal character found in host: " + describeChar(paramString.charAt(i)); + return null; + } + + private static String checkAuth(String paramString) { + int i = scan(paramString, -9223231260711714817L, -9223372036586340352L); + if (i >= 0) + return "Illegal character found in authority: " + describeChar(paramString.charAt(i)); + return null; + } + + public static String checkAuthority(URL paramURL) { + if (paramURL == null) + return null; + String str1; + String str2; + if ((str1 = checkUserInfo(str2 = paramURL.getUserInfo())) != null) + return str1; + String str3; + if ((str1 = checkHost(str3 = paramURL.getHost())) != null) + return str1; + if (str3 == null && str2 == null) + return checkAuth(paramURL.getAuthority()); + return null; + } + + public static String checkExternalForm(URL paramURL) { + if (paramURL == null) + return null; + String str; + int i = scan(str = paramURL.getUserInfo(), 140741783322623L, Long.MIN_VALUE); + if (i >= 0) + return "Illegal character found in authority: " + describeChar(str.charAt(i)); + if ((str = checkHostString(paramURL.getHost())) != null) + return str; + return null; + } + + public static String checkHostString(String paramString) { + if (paramString == null) + return null; + return null; + } +} \ No newline at end of file