diff --git a/.gitignore b/.gitignore
index 92725511..fa02ff80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,31 +1,31 @@
# Eclipse stuff
-/.classpath
-/.project
-/.settings
+.classpath/
+.project/
+.settings/
# netbeans
-/nbproject
-/nbactions.xml
-/nb-configuration.xml
+nbproject/
+nbactions.xml
+nb-configuration.xml
# we use maven!
-/build.xml
+build.xml
# maven
-/target
-/dependency-reduced-pom.xml
+target/
+dependency-reduced-pom.xml
# vim
.*.sw[a-p]
# various other potential build files
-/build
-/bin
-/dist
-/manifest.mf
+build/
+bin/
+dist/
+manifest.mf
# Mac filesystem dust
-/.DS_Store
+.DS_Store/
# intellij
*.iml
@@ -34,5 +34,5 @@
.idea/
# other files
-/*log*
+*.log*
*.yml
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..7ef0f11b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,5 @@
+language: java
+jdk:
+ - openjdk7
+notifications:
+ email: false
diff --git a/api/pom.xml b/api/pom.xml
new file mode 100644
index 00000000..dce9e57c
--- /dev/null
+++ b/api/pom.xml
@@ -0,0 +1,35 @@
+
+
+ 4.0.0
+
+
+ net.md-5
+ bungeecord-parent
+ 1.4.7-SNAPSHOT
+ ../pom.xml
+
+
+ net.md-5
+ bungeecord-api
+ 1.4.7-SNAPSHOT
+ jar
+
+ BungeeCord-API
+ API implemented by the Elastic Portal Suite
+
+
+
+ com.google.guava
+ guava
+ 13.0.1
+ compile
+
+
+ org.yaml
+ snakeyaml
+ 1.11
+ compile
+
+
+
diff --git a/api/src/main/java/net/md_5/bungee/api/Callback.java b/api/src/main/java/net/md_5/bungee/api/Callback.java
new file mode 100644
index 00000000..0cccc175
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/Callback.java
@@ -0,0 +1,19 @@
+package net.md_5.bungee.api;
+
+/**
+ * Represents a method which may be called once a result has been computed
+ * asynchronously.
+ *
+ * @param the type of result
+ */
+public interface Callback
+{
+
+ /**
+ * Called when the result is done.
+ *
+ * @param result the result of the computation
+ * @param error the error(s) that occurred, if any
+ */
+ public void done(V result, Throwable error);
+}
diff --git a/src/main/java/net/md_5/bungee/ChatColor.java b/api/src/main/java/net/md_5/bungee/api/ChatColor.java
similarity index 99%
rename from src/main/java/net/md_5/bungee/ChatColor.java
rename to api/src/main/java/net/md_5/bungee/api/ChatColor.java
index ca157672..a72c01d1 100644
--- a/src/main/java/net/md_5/bungee/ChatColor.java
+++ b/api/src/main/java/net/md_5/bungee/api/ChatColor.java
@@ -1,4 +1,4 @@
-package net.md_5.bungee;
+package net.md_5.bungee.api;
import java.util.regex.Pattern;
diff --git a/api/src/main/java/net/md_5/bungee/api/CommandSender.java b/api/src/main/java/net/md_5/bungee/api/CommandSender.java
new file mode 100644
index 00000000..3596e52f
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/CommandSender.java
@@ -0,0 +1,59 @@
+package net.md_5.bungee.api;
+
+import java.util.Collection;
+
+public interface CommandSender
+{
+
+ /**
+ * Get the unique name of this command sender.
+ *
+ * @return the senders username
+ */
+ public String getName();
+
+ /**
+ * Send a message to this sender.
+ *
+ * @param message the message to send
+ */
+ public void sendMessage(String message);
+
+ /**
+ * Get all groups this user is part of. This returns an unmodifiable
+ * collection.
+ *
+ * @return the users groups
+ */
+ public Collection getGroups();
+
+ /**
+ * Adds groups to a this user for the current session only.
+ *
+ * @param groups the groups to add
+ */
+ public void addGroups(String... groups);
+
+ /**
+ * Remove groups from this user for the current session only.
+ *
+ * @param groups the groups to remove
+ */
+ public void removeGroups(String... groups);
+
+ /**
+ * Checks if this user has the specified permission node.
+ *
+ * @param permission the node to check
+ * @return whether they have this node
+ */
+ public boolean hasPermission(String permission);
+
+ /**
+ * Set a permission node for this user.
+ *
+ * @param permission the node to set
+ * @param value the value of the node
+ */
+ public void setPermission(String permission, boolean value);
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/ProxyServer.java b/api/src/main/java/net/md_5/bungee/api/ProxyServer.java
new file mode 100644
index 00000000..1d4522c6
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/ProxyServer.java
@@ -0,0 +1,177 @@
+package net.md_5.bungee.api;
+
+import net.md_5.bungee.api.plugin.PluginManager;
+import com.google.common.base.Preconditions;
+import java.util.Collection;
+import java.util.Map;
+import java.util.logging.Logger;
+import lombok.Getter;
+import net.md_5.bungee.api.config.ConfigurationAdapter;
+import net.md_5.bungee.api.config.ServerInfo;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.connection.Server;
+import net.md_5.bungee.api.plugin.Plugin;
+
+public abstract class ProxyServer
+{
+
+ @Getter
+ private static ProxyServer instance;
+
+ /**
+ * Sets the proxy instance. This method may only be called once per an
+ * application.
+ *
+ * @param instance the new instance to set
+ */
+ public static void setInstance(ProxyServer instance)
+ {
+ Preconditions.checkNotNull(instance, "instance");
+ Preconditions.checkArgument(ProxyServer.instance == null, "Instance already set");
+ ProxyServer.instance = instance;
+ }
+
+ /**
+ * Gets the name of the currently running proxy software.
+ *
+ * @return the name of this instance
+ */
+ public abstract String getName();
+
+ /**
+ * Gets the version of the currently running proxy software.
+ *
+ * @return the version of this instance
+ */
+ public abstract String getVersion();
+
+ /**
+ * Gets the main logger which can be used as a suitable replacement for
+ * {@link System#out} and {@link System#err}.
+ *
+ * @return the {@link Logger} instance
+ */
+ public abstract Logger getLogger();
+
+ /**
+ * Return all players currently connected.
+ *
+ * @return all connected players
+ */
+ public abstract Collection getPlayers();
+
+ /**
+ * Gets a connected player via their unique username.
+ *
+ * @param name of the player
+ * @return their player instance
+ */
+ public abstract ProxiedPlayer getPlayer(String name);
+
+ /**
+ * Get a server by its name. The instance returned will be taken from a
+ * player currently on that server to facilitate abstract proxy -> server
+ * actions.
+ *
+ * @param name the name to lookup
+ * @return the associated server
+ */
+ public abstract Server getServer(String name);
+
+ /**
+ * Return all servers registered to this proxy, keyed by name. Unlike the
+ * methods in {@link ConfigurationAdapter#getServers()}, this will not
+ * return a fresh map each time.
+ *
+ * @return all registered remote server destinations
+ */
+ public abstract Map getServers();
+
+ /**
+ * Get the {@link PluginManager} associated with loading plugins and
+ * dispatching events. It is recommended that implementations use the
+ * provided PluginManager class.
+ *
+ * @return the plugin manager
+ */
+ public abstract PluginManager getPluginManager();
+
+ /**
+ * Returns the currently in use configuration adapter.
+ *
+ * @return the used configuration adapter
+ */
+ public abstract ConfigurationAdapter getConfigurationAdapter();
+
+ /**
+ * Set the configuration adapter to be used. Must be called from
+ * {@link Plugin#onLoad()}.
+ *
+ * @param adapter the adapter to use
+ */
+ public abstract void setConfigurationAdapter(ConfigurationAdapter adapter);
+
+ /**
+ * Get the currently in use tab list handle.
+ *
+ * @return the tab list handler
+ */
+ public abstract TabListHandler getTabListHandler();
+
+ /**
+ * Set the used tab list handler, should not be changed once players have
+ * connected
+ *
+ * @param handler the tab list handler to set
+ */
+ public abstract void setTabListHandler(TabListHandler handler);
+
+ /**
+ * Get the currently in use reconnect handler.
+ *
+ * @return the in use reconnect handler
+ */
+ public abstract ReconnectHandler getReconnectHandler();
+
+ /**
+ * Sets the reconnect handler to be used for subsequent connections.
+ *
+ * @param handler the new handler
+ */
+ public abstract void setReconnectHandler(ReconnectHandler handler);
+
+ /**
+ * Gracefully mark this instance for shutdown.
+ */
+ public abstract void stop();
+
+ /**
+ * Start this instance so that it may accept connections.
+ *
+ * @throws Exception any exception thrown during startup causing the
+ * instance to fail to boot
+ */
+ public abstract void start() throws Exception;
+
+ /**
+ * Register a channel for use with plugin messages. This is required by some
+ * server / client implementations.
+ *
+ * @param channel the channel to register
+ */
+ public abstract void registerChannel(String channel);
+
+ /**
+ * Unregister a previously registered channel.
+ *
+ * @param channel the channel to unregister
+ */
+ public abstract void unregisterChannel(String channel);
+
+ /**
+ * Get an immutable set of all registered plugin channels.
+ *
+ * @return registered plugin channels
+ */
+ public abstract Collection getChannels();
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/ReconnectHandler.java b/api/src/main/java/net/md_5/bungee/api/ReconnectHandler.java
new file mode 100644
index 00000000..dd417587
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/ReconnectHandler.java
@@ -0,0 +1,30 @@
+package net.md_5.bungee.api;
+
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+
+public interface ReconnectHandler
+{
+
+ /**
+ * Gets the initial server name for a connecting player.
+ *
+ * @param player the connecting player
+ * @return the server name
+ */
+ public String getServer(ProxiedPlayer player);
+
+ /**
+ * Save the server of this player before they disconnect so it can be
+ * retrieved later.
+ *
+ * @param player the player to save
+ */
+ public void setServer(ProxiedPlayer player);
+
+ /**
+ * Save all pending reconnect locations. Whilst not used for database
+ * connections, this method will be called at a predefined interval to allow
+ * the saving of reconnect files.
+ */
+ public void save();
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/ServerPing.java b/api/src/main/java/net/md_5/bungee/api/ServerPing.java
new file mode 100644
index 00000000..d1dda171
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/ServerPing.java
@@ -0,0 +1,33 @@
+package net.md_5.bungee.api;
+
+import lombok.Data;
+
+/**
+ * Represents the standard list data returned by opening a server in the
+ * Minecraft client server list, or hitting it with a packet 0xFE.
+ */
+@Data
+public class ServerPing
+{
+
+ /**
+ * Numeric protocol version supported by the server.
+ */
+ private final byte protocolVersion;
+ /**
+ * Human readable game version.
+ */
+ private final String gameVersion;
+ /**
+ * Server MOTD.
+ */
+ private final String motd;
+ /**
+ * Current amount of players on the server.
+ */
+ private final String currentPlayers;
+ /**
+ * Max amount of players the server will allow.
+ */
+ private final String maxPlayers;
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/TabListHandler.java b/api/src/main/java/net/md_5/bungee/api/TabListHandler.java
new file mode 100644
index 00000000..e4db4e6f
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/TabListHandler.java
@@ -0,0 +1,48 @@
+package net.md_5.bungee.api;
+
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+
+public interface TabListHandler
+{
+
+ /**
+ * Called when a player first connects to the proxy.
+ *
+ * @param player the connecting player
+ */
+ public void onConnect(ProxiedPlayer player);
+
+ /**
+ * Called when a player changes their connected server.
+ *
+ * @param player the player who changed servers
+ */
+ public void onServerChange(ProxiedPlayer player);
+
+ /**
+ * Called when a players ping changes. The new ping will have not updated in
+ * the player instance until this method returns.
+ *
+ * @param player the player who's ping changed
+ * @param ping the player's new ping.
+ */
+ public void onPingChange(ProxiedPlayer player, int ping);
+
+ /**
+ * Called when a player disconnects.
+ *
+ * @param player the disconnected player
+ */
+ public void onDisconnect(ProxiedPlayer player);
+
+ /**
+ * Called when a list update packet is sent from server to client.
+ *
+ * @param player receiving this packet
+ * @param name the player which this packet is relevant to
+ * @param online whether the subject player is online
+ * @param ping ping of the subject player
+ * @return whether to send the packet to the client
+ */
+ public boolean onListUpdate(ProxiedPlayer player, String name, boolean online, int ping);
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/config/ConfigurationAdapter.java b/api/src/main/java/net/md_5/bungee/api/config/ConfigurationAdapter.java
new file mode 100644
index 00000000..54f35220
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/config/ConfigurationAdapter.java
@@ -0,0 +1,61 @@
+package net.md_5.bungee.api.config;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * This class allows plugins to set their own configuration adapter to load
+ * settings from a different place.
+ */
+public interface ConfigurationAdapter
+{
+
+ /**
+ * Gets an integer from the specified path.
+ *
+ * @param path the path to retrieve the integer from
+ * @param def the default value
+ * @return the retrieved integer
+ */
+ public int getInt(String path, int def);
+
+ /**
+ * Gets a string from the specified path.
+ *
+ * @param path the path to retrieve the string from.
+ * @param def the default value
+ * @return the retrieved string
+ */
+ public String getString(String path, String def);
+
+ /**
+ * Get the configuration all servers which may be accessible via the proxy.
+ *
+ * @return all accessible servers, keyed by name
+ */
+ public Map getServers();
+
+ /**
+ * Get information about all hosts to bind the proxy to.
+ *
+ * @return a list of all hosts to bind to
+ */
+ public Collection getListeners();
+
+ /**
+ * Get all groups this player is in.
+ *
+ * @param player the player to check
+ * @return all the player's groups.
+ */
+ public Collection getGroups(String player);
+
+ /**
+ * Get all permission corresponding to the specified group. The result of
+ * this method may or may not be cached, depending on the implementation.
+ *
+ * @param group the group to check
+ * @return all true permissions for this group
+ */
+ public Collection getPermissions(String group);
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java b/api/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java
new file mode 100644
index 00000000..7b0f0997
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java
@@ -0,0 +1,41 @@
+package net.md_5.bungee.api.config;
+
+import java.net.InetSocketAddress;
+import java.util.Map;
+import lombok.Data;
+
+/**
+ * Class representing the configuration of a server listener. Used for allowing
+ * multiple listeners on different ports.
+ */
+@Data
+public class ListenerInfo
+{
+
+ /**
+ * Host to bind to.
+ */
+ private final InetSocketAddress host;
+ /**
+ * Displayed MOTD.
+ */
+ private final String motd;
+ /**
+ * Max amount of slots displayed on the ping page.
+ */
+ private final int maxPlayers;
+ /**
+ * Name of the server which users will be taken to by default.
+ */
+ private final String defaultServer;
+ /**
+ * Whether reconnect locations will be used, or else the user is simply
+ * transferred to the default server on connect.
+ */
+ private final boolean forceDefault;
+ /**
+ * A list of host to server name mappings which will force a user to be
+ * transferred depending on the host they connect to.
+ */
+ private final Map forcedHosts;
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/config/ServerInfo.java b/api/src/main/java/net/md_5/bungee/api/config/ServerInfo.java
new file mode 100644
index 00000000..4ed5ddfa
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/config/ServerInfo.java
@@ -0,0 +1,65 @@
+package net.md_5.bungee.api.config;
+
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Synchronized;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+
+/**
+ * Class used to represent a server to connect to.
+ */
+@Data
+@AllArgsConstructor
+public class ServerInfo
+{
+
+ /**
+ * Name this server displays as.
+ */
+ private final String name;
+ /**
+ * Connectable address of this server.
+ */
+ private final InetSocketAddress address;
+ /**
+ * Players connected to a server defined by these properties.
+ */
+ private final Collection players = new ArrayList<>();
+
+ /**
+ * Add a player to the internal set of this server.
+ *
+ * @param player the player to add
+ */
+ @Synchronized("players")
+ public void addPlayer(ProxiedPlayer player)
+ {
+ players.add(player);
+ }
+
+ /**
+ * Remove a player form the internal set of this server.
+ *
+ * @param player the player to remove
+ */
+ @Synchronized("players")
+ public void removePlayer(ProxiedPlayer player)
+ {
+ players.remove(player);
+ }
+
+ /**
+ * Get the set of all players on this server.
+ *
+ * @return an unmodifiable collection of all players on this server
+ */
+ @Synchronized("players")
+ public Collection getPlayers()
+ {
+ return Collections.unmodifiableCollection(players);
+ }
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/connection/ConnectedPlayer.java b/api/src/main/java/net/md_5/bungee/api/connection/ConnectedPlayer.java
new file mode 100644
index 00000000..cf09a65a
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/connection/ConnectedPlayer.java
@@ -0,0 +1,8 @@
+package net.md_5.bungee.api.connection;
+
+/**
+ * Represents a player physically connected to the world hosted on this server.
+ */
+public interface ConnectedPlayer extends ProxiedPlayer
+{
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/connection/Connection.java b/api/src/main/java/net/md_5/bungee/api/connection/Connection.java
new file mode 100644
index 00000000..3962b9a9
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/connection/Connection.java
@@ -0,0 +1,19 @@
+package net.md_5.bungee.api.connection;
+
+import java.net.InetSocketAddress;
+
+/**
+ * A proxy connection is defined as a connection directly connected to a socket.
+ * It should expose information about the remote peer, however not be specific
+ * to a type of connection, whether server or player.
+ */
+public interface Connection
+{
+
+ /**
+ * Gets the remote address of this connection.
+ *
+ * @return the remote address
+ */
+ public InetSocketAddress getAddress();
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/connection/PendingConnection.java b/api/src/main/java/net/md_5/bungee/api/connection/PendingConnection.java
new file mode 100644
index 00000000..4c952f37
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/connection/PendingConnection.java
@@ -0,0 +1,47 @@
+package net.md_5.bungee.api.connection;
+
+import java.net.InetSocketAddress;
+import net.md_5.bungee.api.config.ListenerInfo;
+
+/**
+ * Represents a user attempting to log into the proxy.
+ */
+public interface PendingConnection extends Connection
+{
+
+ /**
+ * Get the requested username.
+ *
+ * @return the requested username, or null if not set
+ */
+ public String getName();
+
+ /**
+ * Get the numerical client version of the player attempting to log in.
+ *
+ * @return the protocol version of the remote client
+ */
+ public byte getVersion();
+
+ /**
+ * Get the requested virtual host that the client tried to connect to.
+ *
+ * @return request virtual host or null if invalid / not specified.
+ */
+ public InetSocketAddress getVirtualHost();
+
+ /**
+ * Completely kick this user from the proxy and all of its child
+ * connections.
+ *
+ * @param reason the disconnect reason displayed to the player
+ */
+ public void disconnect(String reason);
+
+ /**
+ * Get the listener that accepted this connection.
+ *
+ * @return the accepting listener
+ */
+ public ListenerInfo getListener();
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/connection/ProxiedPlayer.java b/api/src/main/java/net/md_5/bungee/api/connection/ProxiedPlayer.java
new file mode 100644
index 00000000..0cc70ab0
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/connection/ProxiedPlayer.java
@@ -0,0 +1,72 @@
+package net.md_5.bungee.api.connection;
+
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.config.ServerInfo;
+
+/**
+ * Represents a player who's connection is being connected to somewhere else,
+ * whether it be a remote or embedded server.
+ */
+public interface ProxiedPlayer extends Connection, CommandSender
+{
+
+ /**
+ * Gets this player's display name.
+ *
+ * @return the players current display name
+ */
+ public String getDisplayName();
+
+ /**
+ * Sets this players display name to be used as their nametag and tab list
+ * name.
+ *
+ * @param name the name to set
+ */
+ public void setDisplayName(String name);
+
+ /**
+ * Connects / transfers this user to the specified connection, gracefully
+ * closing the current one. Depending on the implementation, this method
+ * might return before the user has been connected.
+ *
+ * @param target the new server to connect to
+ */
+ public void connect(ServerInfo target);
+
+ /**
+ * Gets the server this player is connected to.
+ *
+ * @return the server this player is connected to
+ */
+ public Server getServer();
+
+ /**
+ * Gets the ping time between the proxy and this connection.
+ *
+ * @return the current ping time
+ */
+ public int getPing();
+
+ /**
+ * Disconnect (remove) this player from the proxy with the specified reason.
+ *
+ * @param reason the reason displayed to the player
+ */
+ public void disconnect(String reason);
+
+ /**
+ * Send a plugin message to this player.
+ *
+ * @param channel the channel to send this data via
+ * @param data the data to send
+ */
+ public void sendData(String channel, byte[] data);
+
+ /**
+ * Get the pending connection that belongs to this player.
+ *
+ * @return the pending connection that this player used
+ */
+ public PendingConnection getPendingConnection();
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/connection/Server.java b/api/src/main/java/net/md_5/bungee/api/connection/Server.java
new file mode 100644
index 00000000..caa8dd45
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/connection/Server.java
@@ -0,0 +1,34 @@
+package net.md_5.bungee.api.connection;
+
+import net.md_5.bungee.api.Callback;
+import net.md_5.bungee.api.config.ServerInfo;
+import net.md_5.bungee.api.ServerPing;
+
+/**
+ * Represents a destination which this proxy might connect to.
+ */
+public interface Server extends Connection
+{
+
+ /**
+ * Returns the basic information about this server.
+ *
+ * @return the {@link ServerInfo} for this server
+ */
+ public ServerInfo getInfo();
+
+ /**
+ * Send data by any available means to this server.
+ *
+ * @param channel the channel to send this data via
+ * @param data the data to send
+ */
+ public abstract void sendData(String channel, byte[] data);
+
+ /**
+ * Asynchronously gets the current player count on this server.
+ *
+ * @param callback the callback to call when the count has been retrieved.
+ */
+ public abstract void ping(Callback callback);
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/event/ChatEvent.java b/api/src/main/java/net/md_5/bungee/api/event/ChatEvent.java
new file mode 100644
index 00000000..d0285329
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/event/ChatEvent.java
@@ -0,0 +1,33 @@
+package net.md_5.bungee.api.event;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import net.md_5.bungee.api.connection.Connection;
+import net.md_5.bungee.api.plugin.Cancellable;
+
+/**
+ * Event called when a player sends a message to a server, or a server sends a
+ * message to a player.
+ */
+@Data
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class ChatEvent extends TargetedEvent implements Cancellable
+{
+
+ /**
+ * Cancelled state.
+ */
+ private boolean cancelled;
+ /**
+ * Text contained in this chat.
+ */
+ private String message;
+
+ public ChatEvent(Connection sender, Connection receiver, String message)
+ {
+ super(sender, receiver);
+ this.message = message;
+ }
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/event/LoginEvent.java b/api/src/main/java/net/md_5/bungee/api/event/LoginEvent.java
new file mode 100644
index 00000000..2da25457
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/event/LoginEvent.java
@@ -0,0 +1,31 @@
+package net.md_5.bungee.api.event;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import net.md_5.bungee.api.connection.PendingConnection;
+import net.md_5.bungee.api.plugin.Cancellable;
+import net.md_5.bungee.api.plugin.Event;
+
+/**
+ * Event called to represent a player logging in.
+ */
+@Data
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class LoginEvent extends Event implements Cancellable
+{
+
+ /**
+ * Cancelled state.
+ */
+ private boolean cancelled;
+ /**
+ * Message to use when kicking if this event is canceled.
+ */
+ private String cancelReason;
+ /**
+ * Connection attempting to login.
+ */
+ private final PendingConnection connection;
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/event/PluginMessageEvent.java b/api/src/main/java/net/md_5/bungee/api/event/PluginMessageEvent.java
new file mode 100644
index 00000000..7ac4cf86
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/event/PluginMessageEvent.java
@@ -0,0 +1,37 @@
+package net.md_5.bungee.api.event;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import net.md_5.bungee.api.connection.Connection;
+import net.md_5.bungee.api.plugin.Cancellable;
+
+/**
+ * Event called when a plugin message is sent to the client or server.
+ */
+@Data
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class PluginMessageEvent extends TargetedEvent implements Cancellable
+{
+
+ /**
+ * Cancelled state.
+ */
+ private boolean cancelled;
+ /**
+ * Tag specified for this plugin message.
+ */
+ private final String tag;
+ /**
+ * Data contained in this plugin message.
+ */
+ private final byte[] data;
+
+ public PluginMessageEvent(Connection sender, Connection receiver, String tag, byte[] data)
+ {
+ super(sender, receiver);
+ this.tag = tag;
+ this.data = data;
+ }
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/event/ProxyPingEvent.java b/api/src/main/java/net/md_5/bungee/api/event/ProxyPingEvent.java
new file mode 100644
index 00000000..e48e59cf
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/event/ProxyPingEvent.java
@@ -0,0 +1,32 @@
+package net.md_5.bungee.api.event;
+
+import java.net.InetSocketAddress;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import net.md_5.bungee.api.ServerPing;
+import net.md_5.bungee.api.config.ListenerInfo;
+import net.md_5.bungee.api.plugin.Event;
+
+/**
+ * Called when the proxy is pinged with packet 0xFE from the server list.
+ */
+@Data
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class ProxyPingEvent extends Event
+{
+
+ /**
+ * The address of the user pinging.
+ */
+ private final InetSocketAddress remoteAddress;
+ /**
+ * The data corresponding to the server which received this ping.
+ */
+ private final ListenerInfo server;
+ /**
+ * The data to respond with.
+ */
+ private ServerPing response;
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/event/ServerConnectEvent.java b/api/src/main/java/net/md_5/bungee/api/event/ServerConnectEvent.java
new file mode 100644
index 00000000..2637bc32
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/event/ServerConnectEvent.java
@@ -0,0 +1,26 @@
+package net.md_5.bungee.api.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import net.md_5.bungee.api.config.ServerInfo;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Event;
+
+@Data
+@AllArgsConstructor
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class ServerConnectEvent extends Event
+{
+
+ /**
+ * Player connecting to a new server.
+ */
+ private final ProxiedPlayer player;
+ /**
+ * Server the player will be connected to.
+ */
+ private ServerInfo target;
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/event/ServerConnectedEvent.java b/api/src/main/java/net/md_5/bungee/api/event/ServerConnectedEvent.java
new file mode 100644
index 00000000..e8370b88
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/event/ServerConnectedEvent.java
@@ -0,0 +1,30 @@
+package net.md_5.bungee.api.event;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.connection.Server;
+import net.md_5.bungee.api.plugin.Event;
+
+/**
+ * Not to be confused with {@link ServerConnectEvent}, this event is called once
+ * a connection to a server is fully operational, and is about to hand over
+ * control of the session to the player. It is useful if you wish to send
+ * information to the server before the player logs in.
+ */
+@Data
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class ServerConnectedEvent extends Event
+{
+
+ /**
+ * Player whom the server is for.
+ */
+ private final ProxiedPlayer player;
+ /**
+ * The server itself.
+ */
+ private final Server server;
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/event/TargetedEvent.java b/api/src/main/java/net/md_5/bungee/api/event/TargetedEvent.java
new file mode 100644
index 00000000..c64105ab
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/event/TargetedEvent.java
@@ -0,0 +1,29 @@
+package net.md_5.bungee.api.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import net.md_5.bungee.api.connection.Connection;
+import net.md_5.bungee.api.plugin.Event;
+
+/**
+ * An event which occurs in the communication between two nodes. It is not
+ * recommended to call {@link #setSender(net.md_5.bungee.api.Connection)} or
+ * {@link #setReceiver(net.md_5.bungee.api.Connection)} and the results of doing
+ * so are undefined.
+ */
+@Data
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+public abstract class TargetedEvent extends Event
+{
+
+ /**
+ * Creator of the action.
+ */
+ private Connection sender;
+ /**
+ * Receiver of the action.
+ */
+ private Connection receiver;
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/Cancellable.java b/api/src/main/java/net/md_5/bungee/api/plugin/Cancellable.java
new file mode 100644
index 00000000..2b04a2b5
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/Cancellable.java
@@ -0,0 +1,23 @@
+package net.md_5.bungee.api.plugin;
+
+/**
+ * Events that implement this indicate that they may be cancelled and thus
+ * prevented from happening.
+ */
+public interface Cancellable
+{
+
+ /**
+ * Get whether or not this event is cancelled.
+ *
+ * @return the cancelled state of this event
+ */
+ public boolean isCancelled();
+
+ /**
+ * Sets the cancelled state of this event.
+ *
+ * @param cancel the state to set
+ */
+ public void setCancelled(boolean cancel);
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/Command.java b/api/src/main/java/net/md_5/bungee/api/plugin/Command.java
new file mode 100644
index 00000000..9f5d088f
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/Command.java
@@ -0,0 +1,52 @@
+package net.md_5.bungee.api.plugin;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import net.md_5.bungee.api.CommandSender;
+
+/**
+ * A command that can be executed by a {@link CommandSender}.
+ */
+@Data
+@RequiredArgsConstructor(access = AccessLevel.NONE)
+public abstract class Command
+{
+
+ private final String name;
+ private final String permission;
+ private final String[] aliases;
+
+ /**
+ * Construct a new command with no permissions or aliases.
+ *
+ * @param name the name of this command
+ */
+ public Command(String name)
+ {
+ this(name, null);
+ }
+
+ /**
+ * Construct a new command.
+ *
+ * @param name primary name of this command
+ * @param permission the permission node required to execute this command,
+ * null or empty string allows it to be executed by everyone
+ * @param aliases aliases which map back to this command
+ */
+ public Command(String name, String permission, String... aliases)
+ {
+ this.name = name;
+ this.permission = permission;
+ this.aliases = aliases;
+ }
+
+ /**
+ * Execute this command with the specified sender and arguments.
+ *
+ * @param sender the executor of this command
+ * @param args arguments used to invoke this command
+ */
+ public abstract void execute(CommandSender sender, String[] args);
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/Event.java b/api/src/main/java/net/md_5/bungee/api/plugin/Event.java
new file mode 100644
index 00000000..c164343d
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/Event.java
@@ -0,0 +1,8 @@
+package net.md_5.bungee.api.plugin;
+
+/**
+ * Dummy class which all callable events must extend.
+ */
+public abstract class Event
+{
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/Listener.java b/api/src/main/java/net/md_5/bungee/api/plugin/Listener.java
new file mode 100644
index 00000000..31ed4eea
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/Listener.java
@@ -0,0 +1,8 @@
+package net.md_5.bungee.api.plugin;
+
+/**
+ * Dummy interface which all event subscribers and listeners must implement.
+ */
+public interface Listener
+{
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/Plugin.java b/api/src/main/java/net/md_5/bungee/api/plugin/Plugin.java
new file mode 100644
index 00000000..ce315855
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/Plugin.java
@@ -0,0 +1,48 @@
+package net.md_5.bungee.api.plugin;
+
+import lombok.Getter;
+import net.md_5.bungee.api.config.ConfigurationAdapter;
+
+/**
+ * Represents any Plugin that may be loaded at runtime to enhance existing
+ * functionality.
+ */
+public class Plugin
+{
+
+ @Getter
+ private PluginDescription description;
+
+ /**
+ * Called when the plugin has just been loaded. Most of the proxy will not
+ * be initialized, so only use it for registering
+ * {@link ConfigurationAdapter}'s and other predefined behavior.
+ */
+ public void onLoad()
+ {
+ }
+
+ /**
+ * Called when this plugin is enabled.
+ */
+ public void onEnable()
+ {
+ }
+
+ /**
+ * Called when this plugin is disabled.
+ */
+ public void onDisable()
+ {
+ }
+
+ /**
+ * Called by the loader to initialize the fields in this plugin.
+ *
+ * @param description the description that describes this plugin
+ */
+ final void init(PluginDescription description)
+ {
+ this.description = description;
+ }
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java
new file mode 100644
index 00000000..0739cec8
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java
@@ -0,0 +1,28 @@
+package net.md_5.bungee.api.plugin;
+
+import lombok.Data;
+
+/**
+ * POJO representing the plugin.yml file.
+ */
+@Data
+public class PluginDescription
+{
+
+ /**
+ * Friendly name of the plugin.
+ */
+ private final String name;
+ /**
+ * Plugin main class. Needs to extend {@link Plugin}.
+ */
+ private final String main;
+ /**
+ * Plugin version.
+ */
+ private final String version;
+ /**
+ * Plugin author.
+ */
+ private final String author;
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java
new file mode 100644
index 00000000..684f4141
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java
@@ -0,0 +1,218 @@
+package net.md_5.bungee.api.plugin;
+
+import com.google.common.base.Preconditions;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.logging.Level;
+import java.util.regex.Pattern;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.ProxyServer;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Class to manage bridging between plugin duties and implementation duties, for
+ * example event handling and plugin management.
+ */
+public class PluginManager
+{
+
+ private static final Pattern argsSplit = Pattern.compile(" ");
+ /*========================================================================*/
+ private final Yaml yaml = new Yaml();
+ private final EventBus eventBus = new EventBus();
+ private final Map plugins = new HashMap<>();
+ private final Map commandMap = new HashMap<>();
+
+ /**
+ * Register a command so that it may be executed.
+ *
+ * @param command the command to register
+ */
+ public void registerCommand(Command command)
+ {
+ commandMap.put(command.getName(), command);
+ for (String alias : command.getAliases())
+ {
+ commandMap.put(alias, command);
+ }
+ }
+
+ /**
+ * Unregister a command so it will no longer be executed.
+ *
+ * @param command the command to unregister
+ */
+ public void unregisterCommand(Command command)
+ {
+ commandMap.values().remove(command);
+ }
+
+ /**
+ * Execute a command if it is registered, else return false.
+ *
+ * @param sender the sender executing the command
+ * @param commandLine the complete command line including command name and
+ * arguments
+ * @return whether the command was handled
+ */
+ public boolean dispatchCommand(CommandSender sender, String commandLine)
+ {
+ String[] split = argsSplit.split(commandLine);
+ Command command = commandMap.get(split[0]);
+ if (command == null)
+ {
+ return false;
+ }
+
+ String permission = command.getPermission();
+ if (permission != null && !permission.isEmpty() && !sender.hasPermission(permission))
+ {
+ sender.sendMessage(ChatColor.RED + "You do not have permission to execute this command!");
+ return true;
+ }
+
+ String[] args = Arrays.copyOfRange(split, 1, split.length);
+ try
+ {
+ command.execute(sender, args);
+ } catch (Exception ex)
+ {
+ sender.sendMessage(ChatColor.RED + "An internal error occurred whilst executing this command, please check the console log for details.");
+ ProxyServer.getInstance().getLogger().log(Level.WARNING, "Error in dispatching command", ex);
+ }
+ return true;
+ }
+
+ /**
+ * Returns the {@link Plugin} objects corresponding to all loaded plugins.
+ *
+ * @return the set of loaded plugins
+ */
+ public Collection getPlugins()
+ {
+ return plugins.values();
+ }
+
+ /**
+ * Returns a loaded plugin identified by the specified name.
+ *
+ * @param name of the plugin to retrieve
+ * @return the retrieved plugin or null if not loaded
+ */
+ public Plugin getPlugin(String name)
+ {
+ return plugins.get(name);
+ }
+
+ /**
+ * Enable all plugins by calling the {@link Plugin#onEnable()} method.
+ */
+ public void enablePlugins()
+ {
+ for (Map.Entry plugin : plugins.entrySet())
+ {
+ try
+ {
+ plugin.getValue().onEnable();
+ } catch (Exception ex)
+ {
+ ProxyServer.getInstance().getLogger().log(Level.WARNING, "Exception encountered when loading plugin: " + plugin.getKey(), ex);
+ }
+ }
+ }
+
+ /**
+ * Load a plugin from the specified file. This file must be in jar format.
+ * This will not enable plugins, {@link #enablePlugins()} must be called.
+ *
+ * @param file the file to load from
+ * @throws Exception Any exceptions encountered when loading a plugin from
+ * this file.
+ */
+ public void loadPlugin(File file) throws Exception
+ {
+ Preconditions.checkNotNull(file, "file");
+ Preconditions.checkArgument(file.isFile(), "Must load from file");
+
+ try (JarFile jar = new JarFile(file))
+ {
+ JarEntry pdf = jar.getJarEntry("plugin.yml");
+ try (InputStream in = jar.getInputStream(pdf))
+ {
+ PluginDescription desc = yaml.loadAs(in, PluginDescription.class);
+ URLClassLoader loader = new URLClassLoader(new URL[]
+ {
+ file.toURI().toURL()
+ });
+ Class> main = loader.loadClass(desc.getMain());
+ Plugin plugin = (Plugin) main.getDeclaredConstructor().newInstance();
+
+ plugin.init(desc);
+ plugins.put(pdf.getName(), plugin);
+ plugin.onLoad();
+ }
+ }
+ }
+
+ /**
+ * Load all plugins from the specified folder.
+ *
+ * @param folder the folder to search for plugins in
+ */
+ public void loadPlugins(File folder)
+ {
+ Preconditions.checkNotNull(folder, "folder");
+ Preconditions.checkArgument(folder.isDirectory(), "Must load from a directory");
+
+ for (File file : folder.listFiles())
+ {
+ if (file.isFile() && file.getName().endsWith(".jar"))
+ {
+ try
+ {
+ loadPlugin(file);
+ } catch (Exception ex)
+ {
+ ProxyServer.getInstance().getLogger().log(Level.WARNING, "Could not load plugin from file " + file, ex);
+ }
+ }
+ }
+ }
+
+ /**
+ * Dispatch an event to all subscribed listeners and return the event once
+ * it has been handled by these listeners.
+ *
+ * @param the type bounds, must be a class which extends event
+ * @param event the event to call
+ * @return the called event
+ */
+ public T callEvent(T event)
+ {
+ eventBus.post(event);
+ return event;
+ }
+
+ /**
+ * Register a {@link Listener} for receiving called events. Methods in this
+ * Object which wish to receive events must be annotated with the
+ * {@link Subscribe} annotation.
+ *
+ * @param listener the listener to register events for
+ */
+ public void registerListener(Listener listener)
+ {
+ eventBus.register(listener);
+ }
+}
diff --git a/pom.xml b/pom.xml
index bcd47e5a..ca0dd096 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,12 +10,12 @@
net.md-5
- bungeecord
- 1.0-SNAPSHOT
- jar
+ bungeecord-parent
+ 1.4.7-SNAPSHOT
+ pom
BungeeCord
- Proxy component of the Elastic Portal Suite
+ Parent project for all BungeeCord modules.
https://github.com/ElasticPortalSuite/BungeeCord
2012
@@ -36,6 +36,11 @@
+
+ api
+ proxy
+
+
scm:git:git@github.com:ElasticPortalSuite/BungeeCord.git
scm:git:git@github.com:ElasticPortalSuite/BungeeCord.git
@@ -53,35 +58,9 @@
UTF-8
unknown
- net.md_5.bungee.BungeeCord
-
- com.google.code.findbugs
- jsr305
- 2.0.1
-
-
- com.google.guava
- guava
- 13.0.1
-
-
- net.md-5
- mendax
- 1.4.6-SNAPSHOT
-
-
- org.bouncycastle
- bcprov-ext-jdk15on
- 1.47
-
-
- org.yaml
- snakeyaml
- 1.11
-
org.projectlombok
lombok
@@ -91,7 +70,6 @@
- ${project.name}
com.lukegb.mojo
@@ -119,78 +97,6 @@
1.7
-
- org.apache.maven.plugins
- maven-jar-plugin
- 2.4
-
-
-
- ${main.class}
- ${describe}
-
-
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 2.0
-
-
- package
-
- shade
-
-
-
-
-
-
- *:*
-
- **/*.java
- **/*.properties
- **/*.SF
- **/*.DSA
-
-
-
-
-
-
- com.github.wvengen
- proguard-maven-plugin
- 2.0.6
-
-
- package
-
- proguard
-
-
-
-
- true
- false
-
- ${java.home}/lib/rt.jar
- ${java.home}/lib/jce.jar
-
- false
-
-
-
-
-
-
-
- net.sf.proguard
- proguard-base
- 4.8
-
-
-
diff --git a/proxy/pom.xml b/proxy/pom.xml
new file mode 100644
index 00000000..3a3d0f5a
--- /dev/null
+++ b/proxy/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+
+ net.md-5
+ bungeecord-parent
+ 1.4.7-SNAPSHOT
+ ../pom.xml
+
+
+ net.md-5
+ bungeecord-proxy
+ 1.4.7-SNAPSHOT
+ jar
+
+ BungeeCord-Proxy
+ Proxy component of the Elastic Portal Suite
+
+
+
+ net.md-5
+ mendax
+ 1.4.6-SNAPSHOT
+
+
+ net.md-5
+ bungeecord-api
+ ${project.version}
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ 1.47
+
+
+
+
+ BungeeCord
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 2.4
+
+
+
+ net.md_5.bungee.BungeeCord
+ ${describe}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.0
+
+
+ package
+
+ shade
+
+
+
+
+
+
+ *:*
+
+ **/*.java
+ **/*.properties
+ **/*.SF
+ **/*.DSA
+
+
+
+ true
+
+
+
+
+
diff --git a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java
new file mode 100644
index 00000000..bc0f83ec
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java
@@ -0,0 +1,325 @@
+package net.md_5.bungee;
+
+import net.md_5.bungee.config.Configuration;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.Synchronized;
+import static net.md_5.bungee.Logger.$;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.ReconnectHandler;
+import net.md_5.bungee.api.TabListHandler;
+import net.md_5.bungee.api.config.ConfigurationAdapter;
+import net.md_5.bungee.api.config.ListenerInfo;
+import net.md_5.bungee.api.config.ServerInfo;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.connection.Server;
+import net.md_5.bungee.api.plugin.Plugin;
+import net.md_5.bungee.api.plugin.PluginManager;
+import net.md_5.bungee.command.*;
+import net.md_5.bungee.config.YamlConfig;
+import net.md_5.bungee.packet.DefinedPacket;
+import net.md_5.bungee.packet.PacketFAPluginMessage;
+
+/**
+ * Main BungeeCord proxy class.
+ */
+public class BungeeCord extends ProxyServer
+{
+
+ /**
+ * Server protocol version.
+ */
+ public static final int PROTOCOL_VERSION = 51;
+ /**
+ * Server game version.
+ */
+ public static final String GAME_VERSION = "1.4.6";
+ /**
+ * Current operation state.
+ */
+ public volatile boolean isRunning;
+ /**
+ * Configuration.
+ */
+ public final Configuration config = new Configuration();
+ /**
+ * Thread pool.
+ */
+ public final ExecutorService threadPool = Executors.newCachedThreadPool();
+ /**
+ * locations.yml save thread.
+ */
+ private final Timer saveThread = new Timer("Reconnect Saver");
+ /**
+ * Server socket listener.
+ */
+ private Collection listeners = new HashSet<>();
+ /**
+ * Fully qualified connections.
+ */
+ public Map connections = new ConcurrentHashMap<>();
+ /**
+ * Tab list handler
+ */
+ @Getter
+ @Setter
+ public TabListHandler tabListHandler;
+ /**
+ * Plugin manager.
+ */
+ @Getter
+ public final PluginManager pluginManager = new PluginManager();
+ @Getter
+ @Setter
+ private ReconnectHandler reconnectHandler;
+ @Getter
+ @Setter
+ private ConfigurationAdapter configurationAdapter = new YamlConfig();
+ private final Collection pluginChannels = new HashSet<>();
+
+
+ {
+ getPluginManager().registerCommand(new CommandReload());
+ getPluginManager().registerCommand(new CommandEnd());
+ getPluginManager().registerCommand(new CommandList());
+ getPluginManager().registerCommand(new CommandServer());
+ getPluginManager().registerCommand(new CommandIP());
+ getPluginManager().registerCommand(new CommandAlert());
+ getPluginManager().registerCommand(new CommandBungee());
+
+ registerChannel("BungeeCord::Disconnect");
+ registerChannel("BungeeCord::Connect");
+ registerChannel("BungeeCord::Forward");
+ }
+
+ public static BungeeCord getInstance()
+ {
+ return (BungeeCord) ProxyServer.getInstance();
+ }
+
+ /**
+ * Starts a new instance of BungeeCord.
+ *
+ * @param args command line arguments, currently none are used
+ * @throws IOException when the server cannot be started
+ */
+ public static void main(String[] args) throws IOException
+ {
+ BungeeCord bungee = new BungeeCord();
+ ProxyServer.setInstance(bungee);
+ $().info("Enabled BungeeCord version " + bungee.getVersion());
+ bungee.start();
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+ while (bungee.isRunning)
+ {
+ String line = br.readLine();
+ if (line != null)
+ {
+ boolean handled = getInstance().getPluginManager().dispatchCommand(ConsoleCommandSender.getInstance(), line);
+ if (!handled)
+ {
+ System.err.println("Command not found");
+ }
+ }
+ }
+ }
+
+ /**
+ * Start this proxy instance by loading the configuration, plugins and
+ * starting the connect thread.
+ *
+ * @throws IOException
+ */
+ public void start() throws IOException
+ {
+ config.load();
+ reconnectHandler = new YamlReconnectHandler();
+ isRunning = true;
+
+ File plugins = new File("plugins");
+ plugins.mkdir();
+ pluginManager.loadPlugins(plugins);
+
+ for (ListenerInfo info : config.getListeners())
+ {
+ $().info("Listening on " + info.getHost());
+ ListenThread listener = new ListenThread(info);
+ listener.start();
+ listeners.add(listener);
+ }
+
+ saveThread.scheduleAtFixedRate(new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ getReconnectHandler().save();
+ }
+ }, 0, TimeUnit.MINUTES.toMillis(5));
+
+ new Metrics().start();
+ }
+
+ @Override
+ public void stop()
+ {
+ this.isRunning = false;
+ $().info("Disabling plugins");
+ for (Plugin plugin : pluginManager.getPlugins())
+ {
+ plugin.onDisable();
+ }
+
+ for (ListenThread listener : listeners)
+ {
+ $().log(Level.INFO, "Closing listen thread {0}", listener.socket);
+ try
+ {
+ listener.socket.close();
+ listener.join();
+ } catch (InterruptedException | IOException ex)
+ {
+ $().severe("Could not close listen thread");
+ }
+ }
+
+ $().info("Closing pending connections");
+ threadPool.shutdown();
+
+ $().info("Disconnecting " + connections.size() + " connections");
+ for (UserConnection user : connections.values())
+ {
+ user.disconnect("Proxy restarting, brb.");
+ }
+
+ $().info("Saving reconnect locations");
+ reconnectHandler.save();
+ saveThread.cancel();
+
+ $().info("Thank you and goodbye");
+ System.exit(0);
+ }
+
+ /**
+ * Miscellaneous method to set options on a socket based on those in the
+ * configuration.
+ *
+ * @param socket to set the options on
+ * @throws IOException when the underlying set methods thrown an exception
+ */
+ public void setSocketOptions(Socket socket) throws IOException
+ {
+ socket.setSoTimeout(config.getTimeout());
+ socket.setTrafficClass(0x18);
+ socket.setTcpNoDelay(true);
+ }
+
+ /**
+ * Broadcasts a packet to all clients that is connected to this instance.
+ *
+ * @param packet the packet to send
+ */
+ public void broadcast(DefinedPacket packet)
+ {
+ for (UserConnection con : connections.values())
+ {
+ con.packetQueue.add(packet);
+ }
+ }
+
+ @Override
+ public String getName()
+ {
+ return "BungeeCord";
+ }
+
+ @Override
+ public String getVersion()
+ {
+ return (BungeeCord.class.getPackage().getImplementationVersion() == null) ? "unknown" : BungeeCord.class.getPackage().getImplementationVersion();
+ }
+
+ @Override
+ public Logger getLogger()
+ {
+ return $();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked") // TODO: Abstract more
+ public Collection getPlayers()
+ {
+ return (Collection) connections.values();
+ }
+
+ @Override
+ public ProxiedPlayer getPlayer(String name)
+ {
+ return connections.get(name);
+ }
+
+ @Override
+ public Server getServer(String name)
+ {
+ Collection users = getServers().get(name).getPlayers();
+ return (users != null && !users.isEmpty()) ? users.iterator().next().getServer() : null;
+ }
+
+ @Override
+ public Map getServers()
+ {
+ return config.getServers();
+ }
+
+ @Override
+ @Synchronized("pluginChannels")
+ public void registerChannel(String channel)
+ {
+ pluginChannels.add(channel);
+ }
+
+ @Override
+ @Synchronized("pluginChannels")
+ public void unregisterChannel(String channel)
+ {
+ pluginChannels.remove(channel);
+ }
+
+ @Override
+ @Synchronized("pluginChannels")
+ public Collection getChannels()
+ {
+ return Collections.unmodifiableCollection(pluginChannels);
+ }
+
+ public PacketFAPluginMessage registerChannels()
+ {
+ StringBuilder sb = new StringBuilder();
+ for (String s : getChannels())
+ {
+ sb.append(s);
+ sb.append('\00');
+ }
+ byte[] payload = sb.substring(0, sb.length() - 1).getBytes();
+ return new PacketFAPluginMessage("REGISTER", payload);
+ }
+}
diff --git a/src/main/java/net/md_5/bungee/EncryptionUtil.java b/proxy/src/main/java/net/md_5/bungee/EncryptionUtil.java
similarity index 100%
rename from src/main/java/net/md_5/bungee/EncryptionUtil.java
rename to proxy/src/main/java/net/md_5/bungee/EncryptionUtil.java
diff --git a/src/main/java/net/md_5/bungee/EntityMap.java b/proxy/src/main/java/net/md_5/bungee/EntityMap.java
similarity index 100%
rename from src/main/java/net/md_5/bungee/EntityMap.java
rename to proxy/src/main/java/net/md_5/bungee/EntityMap.java
diff --git a/src/main/java/net/md_5/bungee/GenericConnection.java b/proxy/src/main/java/net/md_5/bungee/GenericConnection.java
similarity index 86%
rename from src/main/java/net/md_5/bungee/GenericConnection.java
rename to proxy/src/main/java/net/md_5/bungee/GenericConnection.java
index b06fa2d2..a4e89301 100644
--- a/src/main/java/net/md_5/bungee/GenericConnection.java
+++ b/proxy/src/main/java/net/md_5/bungee/GenericConnection.java
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import lombok.EqualsAndHashCode;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
import static net.md_5.bungee.Logger.$;
import net.md_5.bungee.packet.PacketFFKick;
@@ -20,8 +21,10 @@ public class GenericConnection
protected final Socket socket;
protected final PacketInputStream in;
protected final OutputStream out;
- public String username;
- public String tabListName;
+ @Getter
+ public String name;
+ @Getter
+ public String displayName;
/**
* Close the socket with the specified reason.
@@ -55,6 +58,6 @@ public class GenericConnection
public void log(String message)
{
- $().info(socket.getInetAddress() + ((username == null) ? " " : " [" + username + "] ") + message);
+ $().info(socket.getInetAddress() + ((name == null) ? " " : " [" + name + "] ") + message);
}
}
diff --git a/src/main/java/net/md_5/bungee/InitialHandler.java b/proxy/src/main/java/net/md_5/bungee/InitialHandler.java
similarity index 59%
rename from src/main/java/net/md_5/bungee/InitialHandler.java
rename to proxy/src/main/java/net/md_5/bungee/InitialHandler.java
index d27d4c7d..542e73a1 100644
--- a/src/main/java/net/md_5/bungee/InitialHandler.java
+++ b/proxy/src/main/java/net/md_5/bungee/InitialHandler.java
@@ -1,31 +1,43 @@
package net.md_5.bungee;
-import java.io.BufferedOutputStream;
+import net.md_5.bungee.config.Configuration;
import java.io.IOException;
import java.io.OutputStream;
+import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.SecretKey;
+import lombok.Getter;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.config.ListenerInfo;
+import net.md_5.bungee.api.config.ServerInfo;
+import net.md_5.bungee.api.connection.PendingConnection;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.packet.Packet2Handshake;
import net.md_5.bungee.packet.PacketFCEncryptionResponse;
import net.md_5.bungee.packet.PacketFDEncryptionRequest;
import net.md_5.bungee.packet.PacketFFKick;
import net.md_5.bungee.packet.PacketInputStream;
-import net.md_5.bungee.plugin.LoginEvent;
import org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.io.CipherOutputStream;
-public class InitialHandler implements Runnable
+public class InitialHandler implements Runnable, PendingConnection
{
private final Socket socket;
+ @Getter
+ private final ListenerInfo listener;
private PacketInputStream in;
private OutputStream out;
+ private Packet2Handshake handshake;
- public InitialHandler(Socket socket) throws IOException
+ public InitialHandler(Socket socket, ListenerInfo info) throws IOException
{
this.socket = socket;
+ this.listener = info;
in = new PacketInputStream(socket.getInputStream());
out = socket.getOutputStream();
}
@@ -40,15 +52,7 @@ public class InitialHandler implements Runnable
switch (id)
{
case 0x02:
- Packet2Handshake handshake = new Packet2Handshake(packet);
- // fire connect event
- LoginEvent event = new LoginEvent(handshake.username, socket.getInetAddress(), handshake.host);
- BungeeCord.instance.pluginManager.onHandshake(event);
- if (event.isCancelled())
- {
- throw new KickException(event.getCancelReason());
- }
-
+ handshake = new Packet2Handshake(packet);
PacketFDEncryptionRequest request = EncryptionUtil.encryptRequest();
out.write(request.getPacket());
PacketFCEncryptionResponse response = new PacketFCEncryptionResponse(in.readPacket());
@@ -60,13 +64,14 @@ public class InitialHandler implements Runnable
}
// Check for multiple connections
- if (BungeeCord.instance.connections.containsKey(handshake.username))
+ ProxiedPlayer old = ProxyServer.getInstance().getPlayer(handshake.username);
+ if (old != null)
{
- throw new KickException("You are already connected to the server");
+ old.disconnect("You are already connected to the server");
}
- // fire post auth event
- BungeeCord.instance.pluginManager.onLogin(event);
+ // fire login event
+ LoginEvent event = new LoginEvent(this);
if (event.isCancelled())
{
throw new KickException(event.getCancelReason());
@@ -82,9 +87,10 @@ public class InitialHandler implements Runnable
customPackets.add(custom);
}
- UserConnection userCon = new UserConnection(socket, in, out, handshake, customPackets);
- String server = (BungeeCord.instance.config.forceDefaultServer) ? BungeeCord.instance.config.defaultServerName : BungeeCord.instance.config.getServer(handshake.username, handshake.host);
- userCon.connect(server);
+ UserConnection userCon = new UserConnection(socket, this, in, out, handshake, customPackets);
+ String server = ProxyServer.getInstance().getReconnectHandler().getServer(userCon);
+ ServerInfo s = BungeeCord.getInstance().config.getServers().get(server);
+ userCon.connect(s);
break;
case 0xFE:
socket.setSoTimeout(100);
@@ -96,14 +102,14 @@ public class InitialHandler implements Runnable
} catch (IOException ex)
{
}
- Configuration conf = BungeeCord.instance.config;
+ Configuration conf = BungeeCord.getInstance().config;
String ping = (newPing) ? ChatColor.COLOR_CHAR + "1"
+ "\00" + BungeeCord.PROTOCOL_VERSION
+ "\00" + BungeeCord.GAME_VERSION
- + "\00" + conf.motd
- + "\00" + BungeeCord.instance.connections.size()
- + "\00" + conf.maxPlayers
- : conf.motd + ChatColor.COLOR_CHAR + BungeeCord.instance.connections.size() + ChatColor.COLOR_CHAR + conf.maxPlayers;
+ + "\00" + listener.getMotd()
+ + "\00" + ProxyServer.getInstance().getPlayers().size()
+ + "\00" + listener.getMaxPlayers()
+ : listener.getMotd() + ChatColor.COLOR_CHAR + ProxyServer.getInstance().getPlayers().size() + ChatColor.COLOR_CHAR + listener.getMaxPlayers();
throw new KickException(ping);
default:
if (id == 0xFA)
@@ -116,18 +122,20 @@ public class InitialHandler implements Runnable
}
} catch (KickException ex)
{
- kick(ex.getMessage());
+ disconnect(ex.getMessage());
} catch (Exception ex)
{
- kick("[Proxy Error] " + Util.exception(ex));
+ disconnect("[Proxy Error] " + Util.exception(ex));
+ ex.printStackTrace();
}
}
- private void kick(String message)
+ @Override
+ public void disconnect(String reason)
{
try
{
- out.write(new PacketFFKick(message).getPacket());
+ out.write(new PacketFFKick(reason).getPacket());
} catch (IOException ioe)
{
} finally
@@ -141,4 +149,28 @@ public class InitialHandler implements Runnable
}
}
}
+
+ @Override
+ public String getName()
+ {
+ return (handshake == null) ? null : handshake.username;
+ }
+
+ @Override
+ public byte getVersion()
+ {
+ return (handshake == null) ? -1 : handshake.procolVersion;
+ }
+
+ @Override
+ public InetSocketAddress getVirtualHost()
+ {
+ return (handshake == null) ? null : new InetSocketAddress(handshake.host, handshake.port);
+ }
+
+ @Override
+ public InetSocketAddress getAddress()
+ {
+ return (InetSocketAddress) socket.getRemoteSocketAddress();
+ }
}
diff --git a/src/main/java/net/md_5/bungee/KickException.java b/proxy/src/main/java/net/md_5/bungee/KickException.java
similarity index 100%
rename from src/main/java/net/md_5/bungee/KickException.java
rename to proxy/src/main/java/net/md_5/bungee/KickException.java
diff --git a/src/main/java/net/md_5/bungee/ListenThread.java b/proxy/src/main/java/net/md_5/bungee/ListenThread.java
similarity index 65%
rename from src/main/java/net/md_5/bungee/ListenThread.java
rename to proxy/src/main/java/net/md_5/bungee/ListenThread.java
index e42e04ad..ab54b1ff 100644
--- a/src/main/java/net/md_5/bungee/ListenThread.java
+++ b/proxy/src/main/java/net/md_5/bungee/ListenThread.java
@@ -1,11 +1,11 @@
package net.md_5.bungee;
import java.io.IOException;
-import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import static net.md_5.bungee.Logger.$;
+import net.md_5.bungee.api.config.ListenerInfo;
/**
* Thread to listen and dispatch incoming connections to the proxy.
@@ -14,26 +14,28 @@ public class ListenThread extends Thread
{
public final ServerSocket socket;
+ private final ListenerInfo info;
- public ListenThread(InetSocketAddress addr) throws IOException
+ public ListenThread(ListenerInfo info) throws IOException
{
- super("Listen Thread");
+ super("Listen Thread - "+ info);
+ this.info = info;
socket = new ServerSocket();
- socket.bind(addr);
+ socket.bind(info.getHost());
}
@Override
public void run()
{
- while (BungeeCord.instance.isRunning)
+ while (BungeeCord.getInstance().isRunning)
{
try
{
Socket client = socket.accept();
- BungeeCord.instance.setSocketOptions(client);
+ BungeeCord.getInstance().setSocketOptions(client);
$().info(client.getInetAddress() + " has connected");
- InitialHandler handler = new InitialHandler(client);
- BungeeCord.instance.threadPool.submit(handler);
+ InitialHandler handler = new InitialHandler(client,info);
+ BungeeCord.getInstance().threadPool.submit(handler);
} catch (SocketException ex)
{
} catch (IOException ex)
diff --git a/src/main/java/net/md_5/bungee/Logger.java b/proxy/src/main/java/net/md_5/bungee/Logger.java
similarity index 96%
rename from src/main/java/net/md_5/bungee/Logger.java
rename to proxy/src/main/java/net/md_5/bungee/Logger.java
index 52f4ffd9..5f570159 100644
--- a/src/main/java/net/md_5/bungee/Logger.java
+++ b/proxy/src/main/java/net/md_5/bungee/Logger.java
@@ -23,7 +23,7 @@ public class Logger extends java.util.logging.Logger
super("RubberBand", null);
try
{
- FileHandler handler = new FileHandler("proxy.log", BungeeCord.instance.config.logNumLines, 1, true);
+ FileHandler handler = new FileHandler("proxy.log", 1 << 14, 1, true);
handler.setFormatter(formatter);
addHandler(handler);
} catch (IOException ex)
diff --git a/src/main/java/net/md_5/bungee/Metrics.java b/proxy/src/main/java/net/md_5/bungee/Metrics.java
similarity index 94%
rename from src/main/java/net/md_5/bungee/Metrics.java
rename to proxy/src/main/java/net/md_5/bungee/Metrics.java
index fc5e81e5..a770a093 100644
--- a/src/main/java/net/md_5/bungee/Metrics.java
+++ b/proxy/src/main/java/net/md_5/bungee/Metrics.java
@@ -9,6 +9,7 @@ import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import static net.md_5.bungee.Logger.$;
+import net.md_5.bungee.api.ProxyServer;
public class Metrics extends Thread
{
@@ -73,10 +74,10 @@ public class Metrics extends Thread
{
// Construct the post data
final StringBuilder data = new StringBuilder();
- data.append(encode("guid")).append('=').append(encode(BungeeCord.instance.config.statsUuid));
- encodeDataPair(data, "version", BungeeCord.instance.version);
+ data.append(encode("guid")).append('=').append(encode(BungeeCord.getInstance().config.getUuid()));
+ encodeDataPair(data, "version", ProxyServer.getInstance().getVersion());
encodeDataPair(data, "server", "0");
- encodeDataPair(data, "players", Integer.toString(BungeeCord.instance.connections.size()));
+ encodeDataPair(data, "players", Integer.toString(ProxyServer.getInstance().getPlayers().size()));
encodeDataPair(data, "revision", String.valueOf(REVISION));
// If we're pinging, append it
diff --git a/src/main/java/net/md_5/bungee/ServerConnection.java b/proxy/src/main/java/net/md_5/bungee/ServerConnection.java
similarity index 60%
rename from src/main/java/net/md_5/bungee/ServerConnection.java
rename to proxy/src/main/java/net/md_5/bungee/ServerConnection.java
index 500971b0..54db34e3 100644
--- a/src/main/java/net/md_5/bungee/ServerConnection.java
+++ b/proxy/src/main/java/net/md_5/bungee/ServerConnection.java
@@ -7,6 +7,14 @@ import java.security.PublicKey;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.crypto.SecretKey;
+import lombok.Getter;
+import net.md_5.bungee.api.Callback;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.ServerPing;
+import net.md_5.bungee.api.config.ServerInfo;
+import net.md_5.bungee.api.connection.Server;
+import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.packet.DefinedPacket;
import net.md_5.bungee.packet.Packet1Login;
import net.md_5.bungee.packet.Packet2Handshake;
@@ -22,27 +30,28 @@ import org.bouncycastle.crypto.io.CipherOutputStream;
/**
* Class representing a connection from the proxy to the server; ie upstream.
*/
-public class ServerConnection extends GenericConnection
+public class ServerConnection extends GenericConnection implements Server
{
- public final String name;
+ @Getter
+ private final ServerInfo info;
public final Packet1Login loginPacket;
public Queue packetQueue = new ConcurrentLinkedQueue<>();
- public ServerConnection(String name, Socket socket, PacketInputStream in, OutputStream out, Packet1Login loginPacket)
+ public ServerConnection(Socket socket, ServerInfo info, PacketInputStream in, OutputStream out, Packet1Login loginPacket)
{
super(socket, in, out);
- this.name = name;
+ this.info = info;
this.loginPacket = loginPacket;
}
- public static ServerConnection connect(UserConnection user, String name, InetSocketAddress address, Packet2Handshake handshake, boolean retry)
+ public static ServerConnection connect(UserConnection user, ServerInfo info, Packet2Handshake handshake, boolean retry)
{
try
{
Socket socket = new Socket();
- socket.connect(address, BungeeCord.instance.config.timeout);
- BungeeCord.instance.setSocketOptions(socket);
+ socket.connect(info.getAddress(), BungeeCord.getInstance().config.getTimeout());
+ BungeeCord.getInstance().setSocketOptions(socket);
PacketInputStream in = new PacketInputStream(socket.getInputStream());
OutputStream out = socket.getOutputStream();
@@ -78,27 +87,45 @@ public class ServerConnection extends GenericConnection
}
Packet1Login login = new Packet1Login(loginResponse);
- // Register all global plugin message channels
- // TODO: Allow player-specific plugin message channels for full mod support
- for (String channel : BungeeCord.instance.globalPluginChannels)
- {
- out.write(new PacketFAPluginMessage("REGISTER", channel.getBytes()).getPacket());
- }
+ ServerConnection server = new ServerConnection(socket, info, in, out, login);
+ ServerConnectedEvent event = new ServerConnectedEvent(user, server);
+ ProxyServer.getInstance().getPluginManager().callEvent(event);
- return new ServerConnection(name, socket, in, out, login);
+ out.write(BungeeCord.getInstance().registerChannels().getPacket());
+
+ return server;
} catch (KickException ex)
{
throw ex;
} catch (Exception ex)
{
- InetSocketAddress def = BungeeCord.instance.config.getServer(null);
- if (retry && !address.equals(def))
+ ServerInfo def = ProxyServer.getInstance().getServers().get(user.getPendingConnection().getListener().getDefaultServer());
+ if (retry && !info.equals(def))
{
- return connect(user, BungeeCord.instance.config.defaultServerName, def, handshake, false);
+ user.sendMessage(ChatColor.RED + "Could not connect to target server, you have been moved to the default server");
+ return connect(user, def, handshake, false);
} else
{
throw new RuntimeException("Could not connect to target server " + Util.exception(ex));
}
}
}
+
+ @Override
+ public void sendData(String channel, byte[] data)
+ {
+ packetQueue.add(new PacketFAPluginMessage(channel, data));
+ }
+
+ @Override
+ public void ping(Callback callback)
+ {
+ throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+ }
+
+ @Override
+ public InetSocketAddress getAddress()
+ {
+ return getInfo().getAddress();
+ }
}
diff --git a/proxy/src/main/java/net/md_5/bungee/UserConnection.java b/proxy/src/main/java/net/md_5/bungee/UserConnection.java
new file mode 100644
index 00000000..32a2c154
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/UserConnection.java
@@ -0,0 +1,428 @@
+package net.md_5.bungee;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import lombok.Getter;
+import lombok.Synchronized;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.config.ServerInfo;
+import net.md_5.bungee.api.connection.PendingConnection;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.connection.Server;
+import net.md_5.bungee.api.event.ChatEvent;
+import net.md_5.bungee.api.event.PluginMessageEvent;
+import net.md_5.bungee.api.event.ServerConnectEvent;
+import net.md_5.bungee.packet.*;
+
+public class UserConnection extends GenericConnection implements ProxiedPlayer
+{
+
+ public final Packet2Handshake handshake;
+ public Queue packetQueue = new ConcurrentLinkedQueue<>();
+ public List loginPackets = new ArrayList<>();
+ @Getter
+ private final PendingConnection pendingConnection;
+ @Getter
+ private ServerConnection server;
+ private UpstreamBridge upBridge;
+ private DownstreamBridge downBridge;
+ // reconnect stuff
+ private int clientEntityId;
+ private int serverEntityId;
+ private volatile boolean reconnecting;
+ // ping stuff
+ private int trackingPingId;
+ private long pingTime;
+ @Getter
+ private int ping;
+ // Permissions
+ private final Collection groups = new HashSet<>();
+ private final Map permissions = new HashMap<>();
+ private final Object permMutex = new Object();
+
+ public UserConnection(Socket socket, PendingConnection pendingConnection, PacketInputStream in, OutputStream out, Packet2Handshake handshake, List loginPackets)
+ {
+ super(socket, in, out);
+ this.handshake = handshake;
+ this.pendingConnection = pendingConnection;
+ name = handshake.username;
+ displayName = handshake.username;
+ this.loginPackets = loginPackets;
+ Collection g = ProxyServer.getInstance().getConfigurationAdapter().getGroups(name);
+ for (String s : g)
+ {
+ addGroups(s);
+ }
+ }
+
+ @Override
+ public void setDisplayName(String name)
+ {
+ ProxyServer.getInstance().getTabListHandler().onDisconnect(this);
+ displayName = name;
+ ProxyServer.getInstance().getTabListHandler().onConnect(this);
+ }
+
+ @Override
+ public void connect(ServerInfo target)
+ {
+ if (server == null)
+ {
+ // First join
+ BungeeCord.getInstance().connections.put(name, this);
+ ProxyServer.getInstance().getTabListHandler().onConnect(this);
+ }
+
+ ServerConnectEvent event = new ServerConnectEvent(this, target);
+ BungeeCord.getInstance().getPluginManager().callEvent(event);
+ target = event.getTarget(); // Update in case the event changed target
+
+ ProxyServer.getInstance().getTabListHandler().onServerChange(this);
+ try
+ {
+ reconnecting = true;
+
+ if (server != null)
+ {
+ out.write(new Packet9Respawn((byte) 1, (byte) 0, (byte) 0, (short) 256, "DEFAULT").getPacket());
+ out.write(new Packet9Respawn((byte) -1, (byte) 0, (byte) 0, (short) 256, "DEFAULT").getPacket());
+ }
+
+ ServerConnection newServer = ServerConnection.connect(this, target, handshake, true);
+ if (server == null)
+ {
+ // Once again, first connection
+ clientEntityId = newServer.loginPacket.entityId;
+ serverEntityId = newServer.loginPacket.entityId;
+ out.write(newServer.loginPacket.getPacket());
+ out.write(BungeeCord.getInstance().registerChannels().getPacket());
+
+ upBridge = new UpstreamBridge();
+ upBridge.start();
+ } else
+ {
+ try
+ {
+ downBridge.interrupt();
+ downBridge.join();
+ } catch (InterruptedException ie)
+ {
+ }
+
+ server.disconnect("Quitting");
+ server.getInfo().removePlayer(this);
+
+ Packet1Login login = newServer.loginPacket;
+ serverEntityId = login.entityId;
+ out.write(new Packet9Respawn(login.dimension, login.difficulty, login.gameMode, (short) 256, login.levelType).getPacket());
+ }
+
+ // Reconnect process has finished, lets get the player moving again
+ reconnecting = false;
+
+ // Add to new
+ target.addPlayer(this);
+
+ // Start the bridges and move on
+ server = newServer;
+ downBridge = new DownstreamBridge();
+ downBridge.start();
+ } catch (KickException ex)
+ {
+ destroySelf(ex.getMessage());
+ } catch (Exception ex)
+ {
+ ex.printStackTrace(); // TODO: Remove
+ destroySelf("Could not connect to server - " + ex.getClass().getSimpleName());
+ }
+ }
+
+ private void destroySelf(String reason)
+ {
+ server.getInfo().removePlayer(this);
+ ProxyServer.getInstance().getPlayers().remove(this);
+
+ disconnect(reason);
+ if (server != null)
+ {
+ server.disconnect("Quitting");
+ ProxyServer.getInstance().getReconnectHandler().setServer(this);
+ }
+ }
+
+ @Override
+ public void disconnect(String reason)
+ {
+ ProxyServer.getInstance().getTabListHandler().onDisconnect(this);
+ super.disconnect(reason);
+ }
+
+ @Override
+ public void sendMessage(String message)
+ {
+ packetQueue.add(new Packet3Chat(message));
+ }
+
+ @Override
+ public void sendData(String channel, byte[] data)
+ {
+ server.packetQueue.add(new PacketFAPluginMessage(channel, data));
+ }
+
+ @Override
+ public InetSocketAddress getAddress()
+ {
+ return (InetSocketAddress) socket.getRemoteSocketAddress();
+ }
+
+ @Override
+ @Synchronized("permMutex")
+ public Collection getGroups()
+ {
+ return Collections.unmodifiableCollection(groups);
+ }
+
+ @Override
+ @Synchronized("permMutex")
+ public void addGroups(String... groups)
+ {
+ for (String group : groups)
+ {
+ this.groups.add(group);
+ for (String permission : ProxyServer.getInstance().getConfigurationAdapter().getPermissions(group))
+ {
+ setPermission(permission, true);
+ }
+ }
+ }
+
+ @Override
+ @Synchronized( "permMutex")
+ public void removeGroups(String... groups)
+ {
+ for (String group : groups)
+ {
+ this.groups.remove(group);
+ for (String permission : ProxyServer.getInstance().getConfigurationAdapter().getPermissions(group))
+ {
+ setPermission(permission, false);
+ }
+ }
+ }
+
+ @Override
+ @Synchronized("permMutex")
+ public boolean hasPermission(String permission)
+ {
+ Boolean val = permissions.get(permission);
+ return (val == null) ? false : val;
+ }
+
+ @Override
+ @Synchronized( "permMutex")
+ public void setPermission(String permission, boolean value)
+ {
+ permissions.put(permission, value);
+ }
+
+ private class UpstreamBridge extends Thread
+ {
+
+ public UpstreamBridge()
+ {
+ super("Upstream Bridge - " + name);
+ }
+
+ @Override
+ public void run()
+ {
+ while (!socket.isClosed())
+ {
+ try
+ {
+ byte[] packet = in.readPacket();
+ boolean sendPacket = true;
+ int id = Util.getId(packet);
+
+ switch (id)
+ {
+ case 0x00:
+ if (trackingPingId == new Packet0KeepAlive(packet).id)
+ {
+ int newPing = (int) (System.currentTimeMillis() - pingTime);
+ ProxyServer.getInstance().getTabListHandler().onPingChange(UserConnection.this, newPing);
+ ping = newPing;
+ }
+ break;
+ case 0x03:
+ Packet3Chat chat = new Packet3Chat(packet);
+ if (chat.message.startsWith("/"))
+ {
+ sendPacket = !ProxyServer.getInstance().getPluginManager().dispatchCommand(UserConnection.this, chat.message.substring(1));
+ } else
+ {
+ ChatEvent chatEvent = new ChatEvent(UserConnection.this, server, chat.message);
+ ProxyServer.getInstance().getPluginManager().callEvent(chatEvent);
+ sendPacket = !chatEvent.isCancelled();
+ }
+ break;
+ case 0xFA:
+ // Call the onPluginMessage event
+ PacketFAPluginMessage message = new PacketFAPluginMessage(packet);
+ PluginMessageEvent event = new PluginMessageEvent(UserConnection.this, server, message.tag, message.data);
+ ProxyServer.getInstance().getPluginManager().callEvent(event);
+
+ if (event.isCancelled())
+ {
+ continue;
+ }
+ break;
+ }
+
+ while (!server.packetQueue.isEmpty())
+ {
+ DefinedPacket p = server.packetQueue.poll();
+ if (p != null)
+ {
+ server.out.write(p.getPacket());
+ }
+ }
+
+ EntityMap.rewrite(packet, clientEntityId, serverEntityId);
+ if (sendPacket && !server.socket.isClosed())
+ {
+ server.out.write(packet);
+ }
+ } catch (IOException ex)
+ {
+ destroySelf("Reached end of stream");
+ } catch (Exception ex)
+ {
+ destroySelf(Util.exception(ex));
+ }
+ }
+ }
+ }
+
+ private class DownstreamBridge extends Thread
+ {
+
+ public DownstreamBridge()
+ {
+ super("Downstream Bridge - " + name);
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ outer:
+ while (!reconnecting)
+ {
+ byte[] packet = server.in.readPacket();
+ int id = Util.getId(packet);
+
+ switch (id)
+ {
+ case 0x00:
+ trackingPingId = new Packet0KeepAlive(packet).id;
+ pingTime = System.currentTimeMillis();
+ break;
+ case 0x03:
+ Packet3Chat chat = new Packet3Chat(packet);
+ ChatEvent chatEvent = new ChatEvent(server, UserConnection.this, chat.message);
+ ProxyServer.getInstance().getPluginManager().callEvent(chatEvent);
+
+ if (chatEvent.isCancelled())
+ {
+ continue;
+ }
+ break;
+ case 0xC9:
+ PacketC9PlayerListItem playerList = new PacketC9PlayerListItem(packet);
+ if (!ProxyServer.getInstance().getTabListHandler().onListUpdate(UserConnection.this, playerList.username, playerList.online, playerList.ping))
+ {
+ continue;
+ }
+ break;
+ case 0xFA:
+ // Call the onPluginMessage event
+ PacketFAPluginMessage message = new PacketFAPluginMessage(packet);
+ DataInputStream in = new DataInputStream(new ByteArrayInputStream(message.data));
+ PluginMessageEvent event = new PluginMessageEvent(server, UserConnection.this, message.tag, message.data);
+ ProxyServer.getInstance().getPluginManager().callEvent(event);
+
+ if (event.isCancelled())
+ {
+ continue;
+ }
+
+ switch (message.tag)
+ {
+ case "BungeeCord::Disconnect":
+ break outer;
+ case "BungeeCord::Forward":
+ String target = in.readUTF();
+ String channel = in.readUTF();
+ short len = in.readShort();
+ byte[] data = new byte[len];
+ in.readFully(data);
+
+ if (target.equals("ALL"))
+ {
+ for (String s : BungeeCord.getInstance().getServers().keySet())
+ {
+ Server server = BungeeCord.getInstance().getServer(s);
+ server.sendData(channel, data);
+ }
+ } else
+ {
+ Server server = BungeeCord.getInstance().getServer(target);
+ server.sendData(channel, data);
+ }
+
+ break;
+ case "BungeeCord::Connect":
+ ServerInfo server = BungeeCord.getInstance().config.getServers().get(in.readUTF());
+ if (server != null)
+ {
+ connect(server);
+ break outer;
+ }
+ break;
+ }
+ }
+
+ while (!packetQueue.isEmpty())
+ {
+ DefinedPacket p = packetQueue.poll();
+ if (p != null)
+ {
+ out.write(p.getPacket());
+ }
+ }
+
+ EntityMap.rewrite(packet, serverEntityId, clientEntityId);
+ out.write(packet);
+ }
+ } catch (Exception ex)
+ {
+ destroySelf(Util.exception(ex));
+ }
+ }
+ }
+}
diff --git a/src/main/java/net/md_5/bungee/Util.java b/proxy/src/main/java/net/md_5/bungee/Util.java
similarity index 100%
rename from src/main/java/net/md_5/bungee/Util.java
rename to proxy/src/main/java/net/md_5/bungee/Util.java
diff --git a/proxy/src/main/java/net/md_5/bungee/YamlReconnectHandler.java b/proxy/src/main/java/net/md_5/bungee/YamlReconnectHandler.java
new file mode 100644
index 00000000..cd9e227a
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/YamlReconnectHandler.java
@@ -0,0 +1,85 @@
+package net.md_5.bungee;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.ReconnectHandler;
+import net.md_5.bungee.api.config.ListenerInfo;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import org.yaml.snakeyaml.Yaml;
+
+public class YamlReconnectHandler implements ReconnectHandler
+{
+
+ private final Yaml yaml = new Yaml();
+ private final File file = new File("locations.yml");
+ /*========================================================================*/
+ private Map data;
+
+ @SuppressWarnings("unchecked")
+ public YamlReconnectHandler()
+ {
+ try
+ {
+ file.createNewFile();
+ try (FileReader rd = new FileReader(file))
+ {
+ data = yaml.loadAs(rd, Map.class);
+ }
+ } catch (IOException ex)
+ {
+ ProxyServer.getInstance().getLogger().log(Level.WARNING, "Could not load reconnect locations", ex);
+ }
+
+ if (data == null)
+ {
+ data = new ConcurrentHashMap<>();
+ } else
+ {
+ data = new ConcurrentHashMap<>(data);
+ }
+ }
+
+ @Override
+ public String getServer(ProxiedPlayer player)
+ {
+ ListenerInfo listener = player.getPendingConnection().getListener();
+ if (listener.isForceDefault())
+ {
+ return listener.getDefaultServer();
+ }
+ String forced = listener.getForcedHosts().get(player.getPendingConnection().getVirtualHost().getHostName());
+ String server = (forced == null) ? data.get(key(player)) : forced;
+ return (server != null) ? server : listener.getDefaultServer();
+ }
+
+ @Override
+ public void setServer(ProxiedPlayer player)
+ {
+ data.put(key(player), player.getServer().getInfo().getName());
+ }
+
+ private String key(ProxiedPlayer player)
+ {
+ InetSocketAddress host = player.getPendingConnection().getVirtualHost();
+ return player.getName() + ";" + host.getHostString() + ":" + host.getPort();
+ }
+
+ @Override
+ public void save()
+ {
+ try (FileWriter wr = new FileWriter(file))
+ {
+ yaml.dump(data, wr);
+ } catch (IOException ex)
+ {
+ ProxyServer.getInstance().getLogger().log(Level.WARNING, "Could not save reconnect locations", ex);
+ }
+ }
+}
diff --git a/src/main/java/net/md_5/bungee/command/CommandAlert.java b/proxy/src/main/java/net/md_5/bungee/command/CommandAlert.java
similarity index 62%
rename from src/main/java/net/md_5/bungee/command/CommandAlert.java
rename to proxy/src/main/java/net/md_5/bungee/command/CommandAlert.java
index 270e89c1..a584e300 100644
--- a/src/main/java/net/md_5/bungee/command/CommandAlert.java
+++ b/proxy/src/main/java/net/md_5/bungee/command/CommandAlert.java
@@ -1,21 +1,22 @@
package net.md_5.bungee.command;
-import net.md_5.bungee.BungeeCord;
-import net.md_5.bungee.ChatColor;
-import net.md_5.bungee.Permission;
-import net.md_5.bungee.UserConnection;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Command;
public class CommandAlert extends Command
{
+ public CommandAlert()
+ {
+ super("alert", "bungee.command.alert");
+ }
+
@Override
public void execute(CommandSender sender, String[] args)
{
- if (getPermission(sender) != Permission.ADMIN)
- {
- sender.sendMessage(ChatColor.RED + "You do not have permission to execute this command!");
- return;
- }
if (args.length == 0)
{
sender.sendMessage(ChatColor.RED + "You must supply a message.");
@@ -30,15 +31,15 @@ public class CommandAlert extends Command
for (String s : args)
{
- s = s.replace("&h", "");
+ s = s.substring(1, s.length());
builder.append(ChatColor.translateAlternateColorCodes('&', s));
builder.append(" ");
}
String message = builder.substring(0, builder.length() - 1);
- for (UserConnection con : BungeeCord.instance.connections.values())
+ for (ProxiedPlayer player : ProxyServer.getInstance().getPlayers())
{
- con.sendMessage(message);
+ player.sendMessage(message);
}
}
}
diff --git a/proxy/src/main/java/net/md_5/bungee/command/CommandBungee.java b/proxy/src/main/java/net/md_5/bungee/command/CommandBungee.java
new file mode 100644
index 00000000..439a9a20
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/command/CommandBungee.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.ProxyServer;
+import net.md_5.bungee.api.plugin.Command;
+
+public class CommandBungee extends Command
+{
+
+ public CommandBungee()
+ {
+ super("bungee");
+ }
+
+ @Override
+ public void execute(CommandSender sender, String[] args)
+ {
+ sender.sendMessage(ChatColor.BLUE + "This server is running BungeeCord version " + ProxyServer.getInstance().getVersion() + " by md_5");
+ }
+}
diff --git a/proxy/src/main/java/net/md_5/bungee/command/CommandEnd.java b/proxy/src/main/java/net/md_5/bungee/command/CommandEnd.java
new file mode 100644
index 00000000..349ef805
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/command/CommandEnd.java
@@ -0,0 +1,23 @@
+package net.md_5.bungee.command;
+
+import net.md_5.bungee.BungeeCord;
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.plugin.Command;
+
+/**
+ * Command to terminate the proxy instance. May only be used by the console.
+ */
+public class CommandEnd extends Command
+{
+
+ public CommandEnd()
+ {
+ super("end", "bungeecord.command.end");
+ }
+
+ @Override
+ public void execute(CommandSender sender, String[] args)
+ {
+ BungeeCord.getInstance().stop();
+ }
+}
diff --git a/src/main/java/net/md_5/bungee/command/CommandIP.java b/proxy/src/main/java/net/md_5/bungee/command/CommandIP.java
similarity index 55%
rename from src/main/java/net/md_5/bungee/command/CommandIP.java
rename to proxy/src/main/java/net/md_5/bungee/command/CommandIP.java
index d1b75f4b..b11c7fea 100644
--- a/src/main/java/net/md_5/bungee/command/CommandIP.java
+++ b/proxy/src/main/java/net/md_5/bungee/command/CommandIP.java
@@ -1,27 +1,28 @@
package net.md_5.bungee.command;
-import net.md_5.bungee.BungeeCord;
-import net.md_5.bungee.ChatColor;
-import net.md_5.bungee.Permission;
-import net.md_5.bungee.UserConnection;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Command;
public class CommandIP extends Command
{
+ public CommandIP()
+ {
+ super("ip", "bungeecord.command.ip");
+ }
+
@Override
public void execute(CommandSender sender, String[] args)
{
- if (getPermission(sender) != Permission.MODERATOR && getPermission(sender) != Permission.ADMIN)
- {
- sender.sendMessage(ChatColor.RED + "You do not have permission to use this command");
- return;
- }
if (args.length < 1)
{
sender.sendMessage(ChatColor.RED + "Please follow this command by a user name");
return;
}
- UserConnection user = BungeeCord.instance.connections.get(args[0]);
+ ProxiedPlayer user = ProxyServer.getInstance().getPlayer(args[0]);
if (user == null)
{
sender.sendMessage(ChatColor.RED + "That user is not online");
diff --git a/src/main/java/net/md_5/bungee/command/CommandList.java b/proxy/src/main/java/net/md_5/bungee/command/CommandList.java
similarity index 56%
rename from src/main/java/net/md_5/bungee/command/CommandList.java
rename to proxy/src/main/java/net/md_5/bungee/command/CommandList.java
index 42584a4d..f836b26b 100644
--- a/src/main/java/net/md_5/bungee/command/CommandList.java
+++ b/proxy/src/main/java/net/md_5/bungee/command/CommandList.java
@@ -1,9 +1,11 @@
package net.md_5.bungee.command;
import java.util.Collection;
-import net.md_5.bungee.BungeeCord;
-import net.md_5.bungee.ChatColor;
-import net.md_5.bungee.UserConnection;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Command;
/**
* Command to list all players connected to the proxy.
@@ -11,11 +13,16 @@ import net.md_5.bungee.UserConnection;
public class CommandList extends Command
{
+ public CommandList()
+ {
+ super("list", "bungeecord.command.list");
+ }
+
@Override
public void execute(CommandSender sender, String[] args)
{
StringBuilder users = new StringBuilder();
- Collection connections = BungeeCord.instance.connections.values();
+ Collection connections = ProxyServer.getInstance().getPlayers();
if (connections.isEmpty())
{
@@ -23,18 +30,9 @@ public class CommandList extends Command
return;
}
- for (UserConnection con : connections)
+ for (ProxiedPlayer player : connections)
{
- switch (getPermission(con))
- {
- case ADMIN:
- users.append(ChatColor.RED);
- break;
- case MODERATOR:
- users.append(ChatColor.GREEN);
- break;
- }
- users.append(con.username);
+ users.append(player.getDisplayName());
users.append(", ");
users.append(ChatColor.RESET);
}
diff --git a/proxy/src/main/java/net/md_5/bungee/command/CommandReload.java b/proxy/src/main/java/net/md_5/bungee/command/CommandReload.java
new file mode 100644
index 00000000..99a47ecc
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/command/CommandReload.java
@@ -0,0 +1,22 @@
+package net.md_5.bungee.command;
+
+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.plugin.Command;
+
+public class CommandReload extends Command
+{
+
+ public CommandReload()
+ {
+ super("greload", "bungeecord.command.reload");
+ }
+
+ @Override
+ public void execute(CommandSender sender, String[] args)
+ {
+ BungeeCord.getInstance().config.load();
+ sender.sendMessage(ChatColor.GREEN + "Reloaded config, please restart if you have any issues");
+ }
+}
diff --git a/proxy/src/main/java/net/md_5/bungee/command/CommandServer.java b/proxy/src/main/java/net/md_5/bungee/command/CommandServer.java
new file mode 100644
index 00000000..1c518efc
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/command/CommandServer.java
@@ -0,0 +1,56 @@
+package net.md_5.bungee.command;
+
+import java.util.Map;
+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.config.ServerInfo;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Command;
+
+/**
+ * Command to list and switch a player between available servers.
+ */
+public class CommandServer extends Command
+{
+
+ public CommandServer()
+ {
+ super("server", "bungeecord.command.server");
+ }
+
+ @Override
+ public void execute(CommandSender sender, String[] args)
+ {
+ if (!(sender instanceof ProxiedPlayer))
+ {
+ return;
+ }
+ ProxiedPlayer player = (ProxiedPlayer) sender;
+ Map servers = BungeeCord.getInstance().config.getServers();
+ if (args.length == 0)
+ {
+ StringBuilder serverList = new StringBuilder();
+ for (String server : servers.keySet())
+ {
+ serverList.append(server);
+ serverList.append(", ");
+ }
+ serverList.setLength(serverList.length() - 2);
+ player.sendMessage(ChatColor.GOLD + "You may connect to the following servers at this time: " + serverList.toString());
+ } else
+ {
+ ServerInfo server = servers.get(args[0]);
+ if (server == null)
+ {
+ player.sendMessage(ChatColor.RED + "The specified server does not exist");
+ } else if (server.equals(player.getServer().getInfo()))
+ {
+ player.sendMessage(ChatColor.RED + "You are already on this server.");
+ } else
+ {
+ player.connect(server);
+ }
+ }
+ }
+}
diff --git a/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java b/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java
new file mode 100644
index 00000000..3c637882
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java
@@ -0,0 +1,63 @@
+package net.md_5.bungee.command;
+
+import java.util.Collection;
+import java.util.Collections;
+import lombok.Getter;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.CommandSender;
+
+/**
+ * Command sender representing the proxy console.
+ */
+public class ConsoleCommandSender implements CommandSender
+{
+
+ @Getter
+ private static final ConsoleCommandSender instance = new ConsoleCommandSender();
+
+ private ConsoleCommandSender()
+ {
+ }
+
+ @Override
+ public void sendMessage(String message)
+ {
+ System.out.println(ChatColor.stripColor(message));
+ }
+
+ @Override
+ public String getName()
+ {
+ return "CONSOLE";
+ }
+
+ @Override
+ public Collection getGroups()
+ {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public void addGroups(String... groups)
+ {
+ throw new UnsupportedOperationException("Console may not have groups");
+ }
+
+ @Override
+ public void removeGroups(String... groups)
+ {
+ throw new UnsupportedOperationException("Console may not have groups");
+ }
+
+ @Override
+ public boolean hasPermission(String permission)
+ {
+ return true;
+ }
+
+ @Override
+ public void setPermission(String permission, boolean value)
+ {
+ throw new UnsupportedOperationException("Console has all permissions");
+ }
+}
diff --git a/proxy/src/main/java/net/md_5/bungee/config/Configuration.java b/proxy/src/main/java/net/md_5/bungee/config/Configuration.java
new file mode 100644
index 00000000..0e6c73e1
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/config/Configuration.java
@@ -0,0 +1,84 @@
+package net.md_5.bungee.config;
+
+import com.google.common.base.Preconditions;
+import java.util.Collection;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Getter;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.config.ConfigurationAdapter;
+import net.md_5.bungee.api.config.ListenerInfo;
+import net.md_5.bungee.api.config.ServerInfo;
+import net.md_5.bungee.tablist.GlobalPingTabList;
+import net.md_5.bungee.tablist.GlobalTabList;
+import net.md_5.bungee.tablist.ServerUniqueTabList;
+
+/**
+ * Core configuration for the proxy.
+ */
+@Getter
+public class Configuration
+{
+
+ /**
+ * The default tab list options available for picking.
+ */
+ private enum DefaultTabList
+ {
+
+ GLOBAL, GLOBAL_PING, SERVER;
+ }
+ /**
+ * Time before users are disconnected due to no network activity.
+ */
+ private int timeout = 30000;
+ /**
+ * UUID used for metrics.
+ */
+ private String uuid = UUID.randomUUID().toString();
+ /**
+ * Set of all listeners.
+ */
+ private Collection listeners;
+ /**
+ * Set of all servers.
+ */
+ private Map servers;
+
+ public void load()
+ {
+ ConfigurationAdapter adapter = ProxyServer.getInstance().getConfigurationAdapter();
+
+ timeout = adapter.getInt("timeout", timeout);
+ uuid = adapter.getString("stats", uuid);
+
+ DefaultTabList tab = DefaultTabList.valueOf(adapter.getString("tab_list", "GLOBAL_PING"));
+ if (tab == null)
+ {
+ tab = DefaultTabList.GLOBAL_PING;
+ }
+ switch (tab)
+ {
+ case GLOBAL:
+ ProxyServer.getInstance().setTabListHandler(new GlobalTabList());
+ break;
+ case GLOBAL_PING:
+ ProxyServer.getInstance().setTabListHandler(new GlobalPingTabList());
+ break;
+ case SERVER:
+ ProxyServer.getInstance().setTabListHandler(new ServerUniqueTabList());
+ break;
+ }
+
+ listeners = adapter.getListeners();
+ Preconditions.checkArgument(listeners != null && !listeners.isEmpty(), "No listeners defined.");
+
+ servers = adapter.getServers();
+ Preconditions.checkArgument(servers != null && !servers.isEmpty(), "No servers defined");
+
+ for (ListenerInfo listener : listeners)
+ {
+ Preconditions.checkArgument(servers.containsKey(listener.getDefaultServer()));
+ }
+ }
+}
diff --git a/proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java b/proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java
new file mode 100644
index 00000000..a3da837e
--- /dev/null
+++ b/proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java
@@ -0,0 +1,205 @@
+package net.md_5.bungee.config;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.logging.Level;
+import net.md_5.bungee.Util;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.config.ConfigurationAdapter;
+import net.md_5.bungee.api.config.ListenerInfo;
+import net.md_5.bungee.api.config.ServerInfo;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+
+public class YamlConfig implements ConfigurationAdapter
+{
+
+ private boolean loaded;
+ private Yaml yaml;
+ private Map config;
+ private final File file = new File("config.yml");
+
+ public void load()
+ {
+ try
+ {
+ file.createNewFile();
+ DumperOptions options = new DumperOptions();
+ options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+ yaml = new Yaml(options);
+
+ try (InputStream is = new FileInputStream(file))
+ {
+ config = (Map) yaml.load(is);
+ }
+
+ if (config == null)
+ {
+ config = new HashMap();
+ }
+
+ loaded = true;
+ } catch (IOException ex)
+ {
+ throw new RuntimeException("Could not load configuration!", ex);
+ }
+
+ Map permissions = get("permissions", new HashMap());
+ if (permissions.isEmpty())
+ {
+ permissions.put("default", Arrays.asList(new String[]
+ {
+ "bungeecord.command.server", "bungeecord.command.list"
+ }));
+ permissions.put("admin", Arrays.asList(new String[]
+ {
+ "bungeecord.command.alert", "bungeecord.command.end", "bungeecord.command.ip", "bungeecord.command.reload"
+ }));
+ }
+
+ Map groups = get("groups", new HashMap());
+ if (groups.isEmpty())
+ {
+ groups.put("md_5", Collections.singletonList("admin"));
+ }
+ }
+
+ private T get(String path, T def)
+ {
+ if (!loaded)
+ {
+ load();
+ }
+ return get(path, def, config);
+ }
+
+ @SuppressWarnings("unchecked")
+ private T get(String path, T def, Map submap)
+ {
+ if (!loaded)
+ {
+ load();
+ }
+
+ int index = path.indexOf('.');
+ if (index == -1)
+ {
+ Object val = submap.get(path);
+ if (val == null)
+ {
+ val = def;
+ submap.put(path, def);
+ save();
+ }
+ return (T) val;
+ } else
+ {
+ String first = path.substring(0, index);
+ String second = path.substring(index + 1, path.length());
+ Map sub = (Map) submap.get(first);
+ return (sub != null) ? get(second, def, sub) : def;
+ }
+ }
+
+ private void save()
+ {
+ try
+ {
+ try (FileWriter wr = new FileWriter(file))
+ {
+ yaml.dump(config, wr);
+ }
+ } catch (IOException ex)
+ {
+ ProxyServer.getInstance().getLogger().log(Level.WARNING, "Could not save config", ex);
+ }
+ }
+
+ @Override
+ public int getInt(String path, int def)
+ {
+ return get(path, def);
+ }
+
+ @Override
+ public String getString(String path, String def)
+ {
+ return get(path, def);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Map getServers()
+ {
+ Map> base = get("servers", (Map) Collections.singletonMap("lobby", new HashMap<>()));
+ Map ret = new HashMap<>();
+
+ for (Map.Entry> entry : base.entrySet())
+ {
+ Map val = entry.getValue();
+ String name = entry.getKey();
+ String addr = get("address", "localhost:25565", val);
+ InetSocketAddress address = Util.getAddr(addr);
+ ServerInfo info = new ServerInfo(name, address);
+ ret.put(name, info);
+ }
+
+ return ret;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Collection getListeners()
+ {
+ Collection