Refactor into maven modules.

This commit is contained in:
md_5
2013-01-10 17:41:37 +11:00
parent addf81f92a
commit a7f7a49fc3
56 changed files with 122 additions and 66 deletions

View File

@@ -0,0 +1,340 @@
package net.md_5.bungee;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import static net.md_5.bungee.Logger.$;
import net.md_5.bungee.command.*;
import net.md_5.bungee.packet.DefinedPacket;
import net.md_5.bungee.packet.PacketFAPluginMessage;
import net.md_5.bungee.plugin.JavaPluginManager;
import net.md_5.bungee.tablist.GlobalPingTabList;
import net.md_5.bungee.tablist.GlobalTabList;
import net.md_5.bungee.tablist.ServerUniqueTabList;
import net.md_5.bungee.tablist.TabListHandler;
/**
* Main BungeeCord proxy class.
*/
public class BungeeCord
{
/**
* Server protocol version.
*/
public static final int PROTOCOL_VERSION = 51;
/**
* Server game version.
*/
public static final String GAME_VERSION = "1.4.6";
/**
* Current software instance.
*/
public static BungeeCord instance;
/**
* 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 ReconnectSaveThread saveThread = new ReconnectSaveThread();
/**
* Server socket listener.
*/
private ListenThread listener;
/**
* Current version.
*/
public static String version = (BungeeCord.class.getPackage().getImplementationVersion() == null) ? "unknown" : BungeeCord.class.getPackage().getImplementationVersion();
/**
* Fully qualified connections.
*/
public Map<String, UserConnection> connections = new ConcurrentHashMap<>();
public Map<String, List<UserConnection>> connectionsByServer = new ConcurrentHashMap<>();
/**
* Registered commands.
*/
public Map<String, Command> commandMap = new HashMap<>();
/**
* Tab list handler
*/
public TabListHandler tabListHandler;
/**
* Registered Global Plugin Channels
*/
public Queue<String> globalPluginChannels = new ConcurrentLinkedQueue<>();
/**
* Plugin manager.
*/
public final JavaPluginManager pluginManager = new JavaPluginManager();
{
commandMap.put("greload", new CommandReload());
commandMap.put("end", new CommandEnd());
commandMap.put("glist", new CommandList());
commandMap.put("server", new CommandServer());
commandMap.put("ip", new CommandIP());
commandMap.put("alert", new CommandAlert());
commandMap.put("motd", new CommandMotd());
commandMap.put("bungee", new CommandBungee());
}
/**
* 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
{
instance = new BungeeCord();
$().info("Enabled BungeeCord version " + instance.version);
instance.start();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (instance.isRunning)
{
String line = br.readLine();
if (line != null)
{
boolean handled = instance.dispatchCommand(line, ConsoleCommandSender.instance);
if (!handled)
{
System.err.println("Command not found");
}
}
}
}
/**
* Dispatch a command by formatting the arguments and then executing it.
*
* @param commandLine the entire command and arguments string
* @param sender which executed the command
* @return whether the command was handled or not.
*/
public boolean dispatchCommand(String commandLine, CommandSender sender)
{
String[] split = commandLine.trim().split(" ");
String commandName = split[0].toLowerCase();
Command command = commandMap.get(commandName);
if (config.disabledCommands != null && config.disabledCommands.contains(commandName))
{
return false;
} else if (command != null)
{
String[] args = Arrays.copyOfRange(split, 1, split.length);
try
{
command.execute(sender, args);
} catch (Exception ex)
{
sender.sendMessage(ChatColor.RED + "An error occurred while executing this command!");
$().severe("----------------------- [Start of command error] -----------------------");
$().log(Level.SEVERE, "", ex);
$().severe("----------------------- [End of command error] -----------------------");
}
}
return command != null;
}
/**
* Start this proxy instance by loading the configuration, plugins and
* starting the connect thread.
*
* @throws IOException
*/
public void start() throws IOException
{
config.load();
isRunning = true;
pluginManager.loadPlugins();
switch (config.tabList)
{
default:
case 1:
tabListHandler = new GlobalPingTabList();
break;
case 2:
tabListHandler = new GlobalTabList();
break;
case 3:
tabListHandler = new ServerUniqueTabList();
break;
}
// Add RubberBand to the global plugin channel list
globalPluginChannels.add("RubberBand");
InetSocketAddress addr = Util.getAddr(config.bindHost);
listener = new ListenThread(addr);
listener.start();
saveThread.start();
$().info("Listening on " + addr);
if (config.metricsEnabled)
{
new Metrics().start();
}
}
/**
* Destroy this proxy instance cleanly by kicking all users, saving the
* configuration and closing all sockets.
*/
public void stop()
{
this.isRunning = false;
$().info("Disabling plugin");
pluginManager.onDisable();
$().info("Closing listen thread");
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");
saveThread.interrupt();
try
{
saveThread.join();
} catch (InterruptedException ex)
{
}
$().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.timeout);
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);
}
}
/**
* Broadcasts a plugin message to all servers with currently connected
* players.
*
* @param channel name
* @param message to send
*/
public void broadcastPluginMessage(String channel, String message)
{
broadcastPluginMessage(channel, message, null);
}
/**
* Broadcasts a plugin message to all servers with currently connected
* players.
*
* @param channel name
* @param message to send
* @param server the message was sent from originally
*/
public void broadcastPluginMessage(String channel, String message, String sourceServer)
{
for (String server : connectionsByServer.keySet())
{
if (sourceServer == null || !sourceServer.equals(server))
{
List<UserConnection> conns = BungeeCord.instance.connectionsByServer.get(server);
if (conns != null && conns.size() > 0)
{
UserConnection user = conns.get(0);
user.sendPluginMessage(channel, message.getBytes());
}
}
}
}
/**
* Send a plugin message to a specific server if it has currently connected
* players.
*
* @param channel name
* @param message to send
* @param server the message is to be sent to
*/
public void sendPluginMessage(String channel, String message, String targetServer)
{
List<UserConnection> conns = BungeeCord.instance.connectionsByServer.get(targetServer);
if (conns != null && conns.size() > 0)
{
UserConnection user = conns.get(0);
user.sendPluginMessage(channel, message.getBytes());
}
}
/**
* Register a plugin channel for all users
*
* @param channel name
*/
public void registerPluginChannel(String channel)
{
globalPluginChannels.add(channel);
broadcast(new PacketFAPluginMessage("REGISTER", channel.getBytes()));
}
}

View File

@@ -0,0 +1,156 @@
package net.md_5.bungee;
import java.util.regex.Pattern;
/**
* Simplistic enumeration of all supported color values for chat.
*/
public enum ChatColor
{
/**
* Represents black.
*/
BLACK('0'),
/**
* Represents dark blue.
*/
DARK_BLUE('1'),
/**
* Represents dark green.
*/
DARK_GREEN('2'),
/**
* Represents dark blue (aqua).
*/
DARK_AQUA('3'),
/**
* Represents dark red.
*/
DARK_RED('4'),
/**
* Represents dark purple.
*/
DARK_PURPLE('5'),
/**
* Represents gold.
*/
GOLD('6'),
/**
* Represents gray.
*/
GRAY('7'),
/**
* Represents dark gray.
*/
DARK_GRAY('8'),
/**
* Represents blue.
*/
BLUE('9'),
/**
* Represents green.
*/
GREEN('a'),
/**
* Represents aqua.
*/
AQUA('b'),
/**
* Represents red.
*/
RED('c'),
/**
* Represents light purple.
*/
LIGHT_PURPLE('d'),
/**
* Represents yellow.
*/
YELLOW('e'),
/**
* Represents white.
*/
WHITE('f'),
/**
* Represents magical characters that change around randomly.
*/
MAGIC('k'),
/**
* Makes the text bold.
*/
BOLD('l'),
/**
* Makes a line appear through the text.
*/
STRIKETHROUGH('m'),
/**
* Makes the text appear underlined.
*/
UNDERLINE('n'),
/**
* Makes the text italic.
*/
ITALIC('o'),
/**
* Resets all previous chat colors or formats.
*/
RESET('r');
/**
* The special character which prefixes all chat colour codes. Use this if
* you need to dynamically convert colour codes from your custom format.
*/
public static final char COLOR_CHAR = '\u00A7';
/**
* Pattern to remove all colour codes.
*/
private static final Pattern STRIP_COLOR_PATTERN = Pattern.compile("(?i)" + String.valueOf(COLOR_CHAR) + "[0-9A-FK-OR]");
/**
* This colour's colour char prefixed by the {@link #COLOR_CHAR}.
*/
private final String toString;
private ChatColor(char code)
{
this.toString = new String(new char[]
{
COLOR_CHAR, code
});
}
@Override
public String toString()
{
return toString;
}
/**
* Strips the given message of all color codes
*
* @param input String to strip of color
* @return A copy of the input string, without any coloring
*/
public static String stripColor(final String input)
{
if (input == null)
{
return null;
}
return STRIP_COLOR_PATTERN.matcher(input).replaceAll("");
}
public static String translateAlternateColorCodes(char altColorChar, String textToTranslate)
{
char[] b = textToTranslate.toCharArray();
for (int i = 0; i < b.length - 1; i++)
{
if (b[i] == altColorChar && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(b[i + 1]) > -1)
{
b[i] = ChatColor.COLOR_CHAR;
b[i + 1] = Character.toLowerCase(b[i + 1]);
}
}
return new String(b);
}
}

View File

@@ -0,0 +1,314 @@
package net.md_5.bungee;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static net.md_5.bungee.Logger.$;
import net.md_5.bungee.command.CommandSender;
import net.md_5.bungee.command.ConsoleCommandSender;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
/**
* Core configuration for the proxy.
*/
public class Configuration
{
/**
* Reconnect locations file.
*/
private transient File reconnect = new File("locations.yml");
/**
* Loaded reconnect locations.
*/
private transient Map<String, String> reconnectLocations;
/**
* Config file.
*/
private transient File file = new File("config.yml");
/**
* Yaml instance.
*/
private transient Yaml yaml;
/**
* Loaded config.
*/
private transient Map<String, Object> config;
/**
* Bind host.
*/
public String bindHost = "0.0.0.0:25577";
/**
* Server ping motd.
*/
public String motd = "BungeeCord Proxy Instance";
/**
* Name of default server.
*/
public String defaultServerName = "default";
/**
* Max players as displayed in list ping. Soft limit.
*/
public int maxPlayers = 1;
/**
* Tab list 1: For a tab list that is global over all server (using their
* Minecraft name) and updating their ping frequently 2: Same as 1 but does
* not update their ping frequently, just once, 3: Makes the individual
* servers handle the tab list (server unique).
*/
public int tabList = 1;
/**
* Socket timeout.
*/
public int timeout = 15000;
/**
* All servers.
*/
public Map<String, String> servers = new HashMap<String, String>()
{
{
put(defaultServerName, "127.0.0.1:1338");
put("pvp", "127.0.0.1:1337");
}
};
/**
* Forced servers.
*/
public Map<String, String> forcedServers = new HashMap<String, String>()
{
{
put("pvp.md-5.net", "pvp");
}
};
/**
* Proxy admins.
*/
public List<String> admins = new ArrayList<String>()
{
{
add("Insert Admins Here");
}
};
/**
* Proxy moderators.
*/
public List<String> moderators = new ArrayList<String>()
{
{
add("Insert Moderators Here");
}
};
/**
* Commands which will be blocked completely.
*/
public List<String> disabledCommands = new ArrayList<String>()
{
{
add("glist");
}
};
/**
* Maximum number of lines to log before old ones are removed.
*/
public int logNumLines = 1 << 14;
/**
* UUID for Metrics.
*/
public String statsUuid = UUID.randomUUID().toString();
public boolean metricsEnabled = true;
public boolean forceDefaultServer = false;
/**
* Load the configuration and save default values.
*/
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 LinkedHashMap<>();
}
$().info("-------------- Loading configuration ----------------");
for (Field field : getClass().getDeclaredFields())
{
if (!Modifier.isTransient(field.getModifiers()))
{
String name = Util.normalize(field.getName());
try
{
Object def = field.get(this);
Object value = get(name, def);
field.set(this, value);
$().info(name + ": " + value);
} catch (IllegalAccessException ex)
{
$().severe("Could not get config node: " + name);
}
}
}
$().info("-----------------------------------------------------");
if (servers.get(defaultServerName) == null)
{
throw new IllegalArgumentException("Server '" + defaultServerName + "' not defined");
}
if (forcedServers != null)
{
for (String server : forcedServers.values())
{
if (!servers.containsKey(server))
{
throw new IllegalArgumentException("Forced server " + server + " is not defined in servers");
}
}
}
motd = ChatColor.translateAlternateColorCodes('&', motd);
reconnect.createNewFile();
try (FileInputStream recon = new FileInputStream(reconnect))
{
reconnectLocations = (Map) yaml.load(recon);
}
if (reconnectLocations == null)
{
reconnectLocations = new LinkedHashMap<>();
}
} catch (IOException ex)
{
$().severe("Could not load config!");
ex.printStackTrace();
}
}
private <T> T get(String path, T def)
{
if (!config.containsKey(path))
{
config.put(path, def);
save(file, config);
}
return (T) config.get(path);
}
private void save(File fileToSave, Map toSave)
{
try
{
try (FileWriter wr = new FileWriter(fileToSave))
{
yaml.dump(toSave, wr);
}
} catch (IOException ex)
{
$().severe("Could not save config file " + fileToSave);
ex.printStackTrace();
}
}
/**
* Get which server a user should be connected to, taking into account their
* name and virtual host.
*
* @param user to get a server for
* @param requestedHost the host which they connected to
* @return the name of the server which they should be connected to.
*/
public String getServer(String user, String requestedHost)
{
String server = (forcedServers == null) ? null : forcedServers.get(requestedHost);
if (server == null)
{
server = reconnectLocations.get(user);
}
if (server == null)
{
server = defaultServerName;
}
return server;
}
/**
* Save the last server which the user was on.
*
* @param user the name of the user
* @param server which they were last on
*/
public void setServer(UserConnection user, String server)
{
reconnectLocations.put(user.username, server);
}
/**
* Gets the connectable address of a server defined in the configuration.
*
* @param name the friendly name of a server
* @return the usable {@link InetSocketAddress} mapped to this server
*/
public InetSocketAddress getServer(String name)
{
String server = servers.get((name == null) ? defaultServerName : name);
return (server != null) ? Util.getAddr(server) : getServer(null);
}
/**
* Save the current mappings of users to servers.
*/
public void saveHosts()
{
save(reconnect, reconnectLocations);
$().info("Saved reconnect locations to " + reconnect);
}
/**
* Get the highest permission a player has.
*
* @param sender to get permissions of
* @return their permission
*/
public Permission getPermission(CommandSender sender)
{
Permission permission = Permission.DEFAULT;
if (admins.contains(sender.getName()) || sender instanceof ConsoleCommandSender)
{
permission = Permission.ADMIN;
} else if (moderators.contains(sender.getName()))
{
permission = Permission.MODERATOR;
}
return permission;
}
}

View File

@@ -0,0 +1,138 @@
package net.md_5.bungee;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import net.md_5.bungee.packet.PacketFCEncryptionResponse;
import net.md_5.bungee.packet.PacketFDEncryptionRequest;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.engines.AESFastEngine;
import org.bouncycastle.crypto.modes.CFBBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
* Class containing all encryption related methods for the proxy.
*/
public class EncryptionUtil
{
private static final Random random = new Random();
private static KeyPair keys;
private static SecretKey secret = new SecretKeySpec(new byte[16], "AES");
static
{
Security.addProvider(new BouncyCastleProvider());
}
public static PacketFDEncryptionRequest encryptRequest() throws NoSuchAlgorithmException
{
if (keys == null)
{
keys = KeyPairGenerator.getInstance("RSA").generateKeyPair();
}
String hash = Long.toString(random.nextLong(), 16);
byte[] pubKey = keys.getPublic().getEncoded();
byte[] verify = new byte[4];
random.nextBytes(verify);
return new PacketFDEncryptionRequest(hash, pubKey, verify);
}
public static SecretKey getSecret(PacketFCEncryptionResponse resp, PacketFDEncryptionRequest request) throws BadPaddingException, IllegalBlockSizeException, IllegalStateException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException
{
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, keys.getPrivate());
byte[] decrypted = cipher.doFinal(resp.verifyToken);
if (!Arrays.equals(request.verifyToken, decrypted))
{
throw new IllegalStateException("Key pairs do not match!");
}
cipher.init(Cipher.DECRYPT_MODE, keys.getPrivate());
byte[] shared = resp.sharedSecret;
byte[] secret = cipher.doFinal(shared);
return new SecretKeySpec(secret, "AES");
}
public static boolean isAuthenticated(String username, String connectionHash, SecretKey shared) throws NoSuchAlgorithmException, IOException
{
String encName = URLEncoder.encode(username, "UTF-8");
MessageDigest sha = MessageDigest.getInstance("SHA-1");
for (byte[] bit : new byte[][]
{
connectionHash.getBytes("ISO_8859_1"), shared.getEncoded(), keys.getPublic().getEncoded()
})
{
sha.update(bit);
}
String encodedHash = URLEncoder.encode(new BigInteger(sha.digest()).toString(16), "UTF-8");
String authURL = "http://session.minecraft.net/game/checkserver.jsp?user=" + encName + "&serverId=" + encodedHash;
String reply;
try (BufferedReader in = new BufferedReader(new InputStreamReader(new URL(authURL).openStream())))
{
reply = in.readLine();
}
return "YES".equals(reply);
}
public static BufferedBlockCipher getCipher(boolean forEncryption, Key shared)
{
BufferedBlockCipher cip = new BufferedBlockCipher(new CFBBlockCipher(new AESFastEngine(), 8));
cip.init(forEncryption, new ParametersWithIV(new KeyParameter(shared.getEncoded()), shared.getEncoded()));
return cip;
}
public static SecretKey getSecret()
{
return secret;
}
public static PublicKey getPubkey(PacketFDEncryptionRequest request) throws InvalidKeySpecException, NoSuchAlgorithmException
{
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(request.publicKey));
}
public static byte[] encrypt(Key key, byte[] b) throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException
{
Cipher hasher = Cipher.getInstance("RSA");
hasher.init(Cipher.ENCRYPT_MODE, key);
return hasher.doFinal(b);
}
public static byte[] getShared(SecretKey key, PublicKey pubkey) throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException
{
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubkey);
return cipher.doFinal(key.getEncoded());
}
}

View File

@@ -0,0 +1,156 @@
package net.md_5.bungee;
/**
* Class to rewrite integers within packets.
*/
public class EntityMap
{
public final static int[][] entityIds = new int[256][];
static
{
entityIds[0x05] = new int[]
{
1
};
entityIds[0x07] = new int[]
{
1, 5
};
entityIds[0x11] = new int[]
{
1
};
entityIds[0x12] = new int[]
{
1
};
entityIds[0x13] = new int[]
{
1
};
entityIds[0x14] = new int[]
{
1
};
entityIds[0x16] = new int[]
{
1, 5
};
entityIds[0x17] = new int[]
{
1, 18
};
entityIds[0x18] = new int[]
{
1
};
entityIds[0x19] = new int[]
{
1
};
entityIds[0x1A] = new int[]
{
1
};
entityIds[0x1C] = new int[]
{
1
};
entityIds[0x1E] = new int[]
{
1
};
entityIds[0x1F] = new int[]
{
1
};
entityIds[0x20] = new int[]
{
1
};
entityIds[0x21] = new int[]
{
1
};
entityIds[0x22] = new int[]
{
1
};
entityIds[0x23] = new int[]
{
1
};
entityIds[0x26] = new int[]
{
1
};
entityIds[0x27] = new int[]
{
1, 5
};
entityIds[0x28] = new int[]
{
1
};
entityIds[0x29] = new int[]
{
1
};
entityIds[0x2A] = new int[]
{
1
};
entityIds[0x37] = new int[]
{
1
};
entityIds[0x47] = new int[]
{
1
};
}
public static void rewrite(byte[] packet, int oldId, int newId)
{
int packetId = Util.getId(packet);
if (packetId == 0x1D)
{ // bulk entity
for (int pos = 2; pos < packet.length; pos += 4)
{
if (oldId == readInt(packet, pos))
{
setInt(packet, pos, newId);
}
}
} else
{
int[] idArray = entityIds[packetId];
if (idArray != null)
{
for (int pos : idArray)
{
if (oldId == readInt(packet, pos))
{
setInt(packet, pos, newId);
}
}
}
}
}
private static void setInt(byte[] buf, int pos, int i)
{
buf[pos] = (byte) (i >> 24);
buf[pos + 1] = (byte) (i >> 16);
buf[pos + 2] = (byte) (i >> 8);
buf[pos + 3] = (byte) i;
}
private static int readInt(byte[] buf, int pos)
{
return (((buf[pos] & 0xFF) << 24) | ((buf[pos + 1] & 0xFF) << 16) | ((buf[pos + 2] & 0xFF) << 8) | buf[pos + 3] & 0xFF);
}
}

View File

@@ -0,0 +1,60 @@
package net.md_5.bungee;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import static net.md_5.bungee.Logger.$;
import net.md_5.bungee.packet.PacketFFKick;
import net.md_5.bungee.packet.PacketInputStream;
/**
* Class to represent a Minecraft connection.
*/
@EqualsAndHashCode
@RequiredArgsConstructor
public class GenericConnection
{
protected final Socket socket;
protected final PacketInputStream in;
protected final OutputStream out;
public String username;
public String tabListName;
/**
* Close the socket with the specified reason.
*
* @param reason to disconnect
*/
public void disconnect(String reason)
{
if (socket.isClosed())
{
return;
}
log("disconnected with " + reason);
try
{
out.write(new PacketFFKick("[Proxy] " + reason).getPacket());
} catch (IOException ex)
{
} finally
{
try
{
out.flush();
out.close();
socket.close();
} catch (IOException ioe)
{
}
}
}
public void log(String message)
{
$().info(socket.getInetAddress() + ((username == null) ? " " : " [" + username + "] ") + message);
}
}

View File

@@ -0,0 +1,138 @@
package net.md_5.bungee;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.SecretKey;
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
{
private final Socket socket;
private PacketInputStream in;
private OutputStream out;
public InitialHandler(Socket socket) throws IOException
{
this.socket = socket;
in = new PacketInputStream(socket.getInputStream());
out = socket.getOutputStream();
}
@Override
public void run()
{
try
{
byte[] packet = in.readPacket();
int id = Util.getId(packet);
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());
}
PacketFDEncryptionRequest request = EncryptionUtil.encryptRequest();
out.write(request.getPacket());
PacketFCEncryptionResponse response = new PacketFCEncryptionResponse(in.readPacket());
SecretKey shared = EncryptionUtil.getSecret(response, request);
if (!EncryptionUtil.isAuthenticated(handshake.username, request.serverId, shared))
{
throw new KickException("Not authenticated with minecraft.net");
}
// fire post auth event
BungeeCord.instance.pluginManager.onLogin(event);
if (event.isCancelled())
{
throw new KickException(event.getCancelReason());
}
out.write(new PacketFCEncryptionResponse().getPacket());
in = new PacketInputStream(new CipherInputStream(socket.getInputStream(), EncryptionUtil.getCipher(false, shared)));
out = new CipherOutputStream(socket.getOutputStream(), EncryptionUtil.getCipher(true, shared));
List<byte[]> customPackets = new ArrayList<>();
byte[] custom;
while (Util.getId((custom = in.readPacket())) != 0xCD)
{
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);
break;
case 0xFE:
socket.setSoTimeout(100);
boolean newPing = false;
try
{
socket.getInputStream().read();
newPing = true;
} catch (IOException ex)
{
}
Configuration conf = BungeeCord.instance.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;
throw new KickException(ping);
default:
if (id == 0xFA)
{
run(); // WTF Spoutcraft
} else
{
// throw new IllegalArgumentException("Wasn't ready for packet id " + Util.hex(id));
}
}
} catch (KickException ex)
{
kick(ex.getMessage());
} catch (Exception ex)
{
kick("[Proxy Error] " + Util.exception(ex));
}
}
private void kick(String message)
{
try
{
out.write(new PacketFFKick(message).getPacket());
} catch (IOException ioe)
{
} finally
{
try
{
out.flush();
socket.close();
} catch (IOException ioe2)
{
}
}
}
}

View File

@@ -0,0 +1,14 @@
package net.md_5.bungee;
/**
* Exception, which when thrown will disconnect the player from the proxy with
* the specified message.
*/
public class KickException extends RuntimeException
{
public KickException(String message)
{
super(message);
}
}

View File

@@ -0,0 +1,45 @@
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.$;
/**
* Thread to listen and dispatch incoming connections to the proxy.
*/
public class ListenThread extends Thread
{
public final ServerSocket socket;
public ListenThread(InetSocketAddress addr) throws IOException
{
super("Listen Thread");
socket = new ServerSocket();
socket.bind(addr);
}
@Override
public void run()
{
while (BungeeCord.instance.isRunning)
{
try
{
Socket client = socket.accept();
BungeeCord.instance.setSocketOptions(client);
$().info(client.getInetAddress() + " has connected");
InitialHandler handler = new InitialHandler(client);
BungeeCord.instance.threadPool.submit(handler);
} catch (SocketException ex)
{
} catch (IOException ex)
{
ex.printStackTrace(); // TODO
}
}
}
}

View File

@@ -0,0 +1,108 @@
package net.md_5.bungee;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
/**
* Logger to handle formatting and storage of the proxy's logger.
*/
public class Logger extends java.util.logging.Logger
{
private static final Formatter formatter = new ConsoleLogFormatter();
private static final Logger instance = new Logger();
public Logger()
{
super("RubberBand", null);
try
{
FileHandler handler = new FileHandler("proxy.log", BungeeCord.instance.config.logNumLines, 1, true);
handler.setFormatter(formatter);
addHandler(handler);
} catch (IOException ex)
{
System.err.println("Could not register logger!");
ex.printStackTrace();
}
}
@Override
public void log(LogRecord record)
{
super.log(record);
String message = formatter.format(record);
if (record.getLevel() == Level.SEVERE || record.getLevel() == Level.WARNING)
{
System.err.print(message);
} else
{
System.out.print(message);
}
}
/**
* Gets the current logger instance.
*
* @return the current logger instance
*/
public static Logger $()
{
return instance;
}
public static class ConsoleLogFormatter extends Formatter
{
private SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
@Override
public String format(LogRecord logrecord)
{
StringBuilder formatted = new StringBuilder();
formatted.append(formatter.format(logrecord.getMillis()));
Level level = logrecord.getLevel();
if (level == Level.FINEST)
{
formatted.append(" [FINEST] ");
} else if (level == Level.FINER)
{
formatted.append(" [FINER] ");
} else if (level == Level.FINE)
{
formatted.append(" [FINE] ");
} else if (level == Level.INFO)
{
formatted.append(" [INFO] ");
} else if (level == Level.WARNING)
{
formatted.append(" [WARNING] ");
} else if (level == Level.SEVERE)
{
formatted.append(" [SEVERE] ");
}
formatted.append(logrecord.getMessage());
formatted.append('\n');
Throwable throwable = logrecord.getThrown();
if (throwable != null)
{
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
formatted.append(writer);
}
return formatted.toString();
}
}
}

View File

@@ -0,0 +1,143 @@
package net.md_5.bungee;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import static net.md_5.bungee.Logger.$;
public class Metrics extends Thread
{
/**
* The current revision number
*/
private final static int REVISION = 5;
/**
* The base url of the metrics domain
*/
private static final String BASE_URL = "http://mcstats.org";
/**
* The url used to report a server's status
*/
private static final String REPORT_URL = "/report/%s";
/**
* Interval of time to ping (in minutes)
*/
private final static int PING_INTERVAL = 10;
public Metrics()
{
super("Metrics Gathering Thread");
setDaemon(true);
}
@Override
public void run()
{
boolean firstPost = true;
while (true)
{
try
{
// We use the inverse of firstPost because if it is the first time we are posting,
// it is not a interval ping, so it evaluates to FALSE
// Each time thereafter it will evaluate to TRUE, i.e PING!
postPlugin(!firstPost);
// After the first post we set firstPost to false
// Each post thereafter will be a ping
firstPost = false;
} catch (IOException ex)
{
$().info("[Metrics] " + ex.getMessage());
}
try
{
sleep(PING_INTERVAL * 1000 * 60);
} catch (InterruptedException ex)
{
break;
}
}
}
/**
* Generic method that posts a plugin to the metrics website
*/
private void postPlugin(boolean isPing) throws IOException
{
// 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);
encodeDataPair(data, "server", "0");
encodeDataPair(data, "players", Integer.toString(BungeeCord.instance.connections.size()));
encodeDataPair(data, "revision", String.valueOf(REVISION));
// If we're pinging, append it
if (isPing)
{
encodeDataPair(data, "ping", "true");
}
// Create the url
URL url = new URL(BASE_URL + String.format(REPORT_URL, encode("BungeeCord")));
// Connect to the website
URLConnection connection;
connection = url.openConnection();
connection.setDoOutput(true);
final BufferedReader reader;
final String response;
try (OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()))
{
writer.write(data.toString());
writer.flush();
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
response = reader.readLine();
}
reader.close();
if (response == null || response.startsWith("ERR"))
{
throw new IOException(response); //Throw the exception
}
}
/**
* <p>Encode a key/value data pair to be used in a HTTP post request. This
* INCLUDES a & so the first key/value pair MUST be included manually,
* e.g:</p>
* <code>
* StringBuffer data = new StringBuffer();
* data.append(encode("guid")).append('=').append(encode(guid));
* encodeDataPair(data, "version", description.getVersion());
* </code>
*
* @param buffer the StringBuilder to append the data pair onto
* @param key the key value
* @param value the value
*/
private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException
{
buffer.append('&').append(encode(key)).append('=').append(encode(value));
}
/**
* Encode text as UTF-8
*
* @param text the text to encode
* @return the encoded text, as UTF-8
*/
private static String encode(final String text) throws UnsupportedEncodingException
{
return URLEncoder.encode(text, "UTF-8");
}
}

View File

@@ -0,0 +1,18 @@
package net.md_5.bungee;
public enum Permission
{
/**
* Can access all commands.
*/
ADMIN,
/**
* Can access commands which do not affect everyone.
*/
MODERATOR,
/**
* Can access other commands.
*/
DEFAULT;
}

View File

@@ -0,0 +1,30 @@
package net.md_5.bungee;
/**
* Class to call the {@link Configuration#saveHosts() } method at 5 minute
* intervals.
*/
public class ReconnectSaveThread extends Thread
{
public ReconnectSaveThread()
{
super("Location Save Thread");
setPriority(Thread.MIN_PRIORITY);
}
@Override
public void run()
{
while (BungeeCord.instance.isRunning)
{
try
{
Thread.sleep(5 * 1000 * 60); // 5 minutes
} catch (InterruptedException ex)
{
}
BungeeCord.instance.config.saveHosts();
}
}
}

View File

@@ -0,0 +1,104 @@
package net.md_5.bungee;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.PublicKey;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.crypto.SecretKey;
import net.md_5.bungee.packet.DefinedPacket;
import net.md_5.bungee.packet.Packet1Login;
import net.md_5.bungee.packet.Packet2Handshake;
import net.md_5.bungee.packet.PacketCDClientStatus;
import net.md_5.bungee.packet.PacketFAPluginMessage;
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 org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.io.CipherOutputStream;
/**
* Class representing a connection from the proxy to the server; ie upstream.
*/
public class ServerConnection extends GenericConnection
{
public final String name;
public final Packet1Login loginPacket;
public Queue<DefinedPacket> packetQueue = new ConcurrentLinkedQueue<>();
public ServerConnection(String name, Socket socket, PacketInputStream in, OutputStream out, Packet1Login loginPacket)
{
super(socket, in, out);
this.name = name;
this.loginPacket = loginPacket;
}
public static ServerConnection connect(UserConnection user, String name, InetSocketAddress address, Packet2Handshake handshake, boolean retry)
{
try
{
Socket socket = new Socket();
socket.connect(address, BungeeCord.instance.config.timeout);
BungeeCord.instance.setSocketOptions(socket);
PacketInputStream in = new PacketInputStream(socket.getInputStream());
OutputStream out = socket.getOutputStream();
out.write(handshake.getPacket());
PacketFDEncryptionRequest encryptRequest = new PacketFDEncryptionRequest(in.readPacket());
SecretKey myKey = EncryptionUtil.getSecret();
PublicKey pub = EncryptionUtil.getPubkey(encryptRequest);
PacketFCEncryptionResponse response = new PacketFCEncryptionResponse(EncryptionUtil.getShared(myKey, pub), EncryptionUtil.encrypt(pub, encryptRequest.verifyToken));
out.write(response.getPacket());
int ciphId = Util.getId(in.readPacket());
if (ciphId != 0xFC)
{
throw new RuntimeException("Server did not send encryption enable");
}
in = new PacketInputStream(new CipherInputStream(socket.getInputStream(), EncryptionUtil.getCipher(false, myKey)));
out = new CipherOutputStream(out, EncryptionUtil.getCipher(true, myKey));
for (byte[] custom : user.loginPackets)
{
out.write(custom);
}
out.write(new PacketCDClientStatus((byte) 0).getPacket());
byte[] loginResponse = in.readPacket();
if (Util.getId(loginResponse) == 0xFF)
{
throw new KickException("[Kicked] " + new PacketFFKick(loginResponse).message);
}
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());
}
return new ServerConnection(name, socket, in, out, login);
} catch (KickException ex)
{
throw ex;
} catch (Exception ex)
{
InetSocketAddress def = BungeeCord.instance.config.getServer(null);
if (retry && !address.equals(def))
{
return connect(user, name, def, handshake, false);
} else
{
throw new RuntimeException("Could not connect to target server " + Util.exception(ex));
}
}
}
}

View File

@@ -0,0 +1,382 @@
package net.md_5.bungee;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import net.md_5.bungee.command.CommandSender;
import net.md_5.bungee.packet.*;
import net.md_5.bungee.plugin.ChatEvent;
import net.md_5.bungee.plugin.PluginMessageEvent;
import net.md_5.bungee.plugin.PluginMessageEvent.Destination;
import net.md_5.bungee.plugin.ServerConnectEvent;
public class UserConnection extends GenericConnection implements CommandSender
{
public final Packet2Handshake handshake;
public Queue<DefinedPacket> packetQueue = new ConcurrentLinkedQueue<>();
public List<byte[]> loginPackets = new ArrayList<>();
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;
private int ping;
public UserConnection instance = this;
public UserConnection(Socket socket, PacketInputStream in, OutputStream out, Packet2Handshake handshake, List<byte[]> loginPackets)
{
super(socket, in, out);
this.handshake = handshake;
username = handshake.username;
tabListName = handshake.username;
this.loginPackets = loginPackets;
BungeeCord.instance.connections.put(username, this);
BungeeCord.instance.tabListHandler.onJoin(this);
}
public void setTabListName(String newName)
{
BungeeCord.instance.tabListHandler.onDisconnect(this);
tabListName = newName;
BungeeCord.instance.tabListHandler.onJoin(this);
}
public void connect(String server)
{
ServerConnectEvent event = new ServerConnectEvent(this.server == null, this, server);
event.setNewServer(server);
BungeeCord.instance.pluginManager.onServerConnect(event);
if (event.getMessage() != null)
{
this.sendMessage(event.getMessage());
}
if (event.getNewServer() == null)
{
if (event.isFirstTime())
{
event.setNewServer(BungeeCord.instance.config.defaultServerName);
} else
{
return;
}
}
InetSocketAddress addr = BungeeCord.instance.config.getServer(event.getNewServer());
connect(server, addr);
}
private void connect(String name, InetSocketAddress serverAddr)
{
BungeeCord.instance.tabListHandler.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, name, serverAddr, handshake, true);
if (server == null)
{
clientEntityId = newServer.loginPacket.entityId;
serverEntityId = newServer.loginPacket.entityId;
out.write(newServer.loginPacket.getPacket());
upBridge = new UpstreamBridge();
upBridge.start();
} else
{
try
{
downBridge.interrupt();
downBridge.join();
} catch (InterruptedException ie)
{
}
server.disconnect("Quitting");
Packet1Login login = newServer.loginPacket;
serverEntityId = login.entityId;
out.write(new Packet9Respawn(login.dimension, login.difficulty, login.gameMode, (short) 256, login.levelType).getPacket());
}
reconnecting = false;
downBridge = new DownstreamBridge();
if (server != null)
{
List<UserConnection> conns = BungeeCord.instance.connectionsByServer.get(server.name);
if (conns != null)
{
conns.remove(this);
}
}
server = newServer;
List<UserConnection> conns = BungeeCord.instance.connectionsByServer.get(server.name);
if (conns == null)
{
conns = new ArrayList<>();
BungeeCord.instance.connectionsByServer.put(server.name, conns);
}
if (!conns.contains(this))
{
conns.add(this);
}
downBridge.start();
} catch (KickException ex)
{
destroySelf(ex.getMessage());
} catch (Exception ex)
{
destroySelf("Could not connect to server - " + ex.getClass().getSimpleName());
ex.printStackTrace(); // TODO: Remove
}
}
public String getServer()
{
return server.name;
}
public SocketAddress getAddress()
{
return socket.getRemoteSocketAddress();
}
public int getPing()
{
return ping;
}
private void setPing(int ping)
{
BungeeCord.instance.tabListHandler.onPingChange(this, ping);
this.ping = ping;
}
private void destroySelf(String reason)
{
if (BungeeCord.instance.isRunning)
{
BungeeCord.instance.connections.remove(username);
if (server != null)
{
List<UserConnection> conns = BungeeCord.instance.connectionsByServer.get(server.name);
if (conns != null)
{
conns.remove(this);
}
}
}
disconnect(reason);
if (server != null)
{
server.disconnect("Quitting");
BungeeCord.instance.config.setServer(this, server.name);
}
}
@Override
public void disconnect(String reason)
{
BungeeCord.instance.tabListHandler.onDisconnect(this);
super.disconnect(reason);
}
@Override
public void sendMessage(String message)
{
packetQueue.add(new Packet3Chat(message));
}
public void sendPluginMessage(String tag, byte[] data)
{
server.packetQueue.add(new PacketFAPluginMessage(tag, data));
}
@Override
public String getName()
{
return username;
}
private class UpstreamBridge extends Thread
{
public UpstreamBridge()
{
super("Upstream Bridge - " + username);
}
@Override
public void run()
{
while (!socket.isClosed())
{
try
{
byte[] packet = in.readPacket();
boolean sendPacket = true;
int id = Util.getId(packet);
if (id == 0xFA)
{
// Call the onPluginMessage event
PacketFAPluginMessage message = new PacketFAPluginMessage(packet);
PluginMessageEvent event = new PluginMessageEvent(Destination.SERVER, instance);
event.setTag(message.tag);
event.setData(new String(message.data));
BungeeCord.instance.pluginManager.onPluginMessage(event);
if (event.isCancelled())
{
continue;
}
} else if (id == 0x03)
{
Packet3Chat chat = new Packet3Chat(packet);
String message = chat.message;
if (message.startsWith("/"))
{
sendPacket = !BungeeCord.instance.dispatchCommand(message.substring(1), UserConnection.this);
} else
{
ChatEvent chatEvent = new ChatEvent(ChatEvent.Destination.SERVER, instance);
chatEvent.setText(message);
BungeeCord.instance.pluginManager.onChat(chatEvent);
sendPacket = !chatEvent.isCancelled();
}
} else if (id == 0x00)
{
if (trackingPingId == new Packet0KeepAlive(packet).id)
{
setPing((int) (System.currentTimeMillis() - pingTime));
}
}
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 - " + username);
}
@Override
public void run()
{
try
{
while (!reconnecting)
{
byte[] packet = server.in.readPacket();
int id = Util.getId(packet);
if (id == 0xFA)
{
// Call the onPluginMessage event
PacketFAPluginMessage message = new PacketFAPluginMessage(packet);
PluginMessageEvent event = new PluginMessageEvent(Destination.CLIENT, instance);
event.setTag(message.tag);
event.setData(new String(message.data));
BungeeCord.instance.pluginManager.onPluginMessage(event);
if (event.isCancelled())
{
continue;
}
message.tag = event.getTag();
message.data = event.getData().getBytes();
// Allow a message for killing the connection outright
if (message.tag.equals("KillCon"))
{
break;
}
if (message.tag.equals("RubberBand"))
{
String server = new String(message.data);
connect(server);
break;
}
} else if (id == 0x00)
{
trackingPingId = new Packet0KeepAlive(packet).id;
pingTime = System.currentTimeMillis();
} else if (id == 0x03)
{
Packet3Chat chat = new Packet3Chat(packet);
String message = chat.message;
ChatEvent chatEvent = new ChatEvent(ChatEvent.Destination.CLIENT, instance);
chatEvent.setText(message);
BungeeCord.instance.pluginManager.onChat(chatEvent);
if (chatEvent.isCancelled())
{
continue;
}
} else if (id == 0xC9)
{
if (!BungeeCord.instance.tabListHandler.onPacketC9(UserConnection.this, new PacketC9PlayerListItem(packet)))
{
continue;
}
}
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));
}
}
}
}

View File

@@ -0,0 +1,85 @@
package net.md_5.bungee;
import java.net.InetSocketAddress;
/**
* Series of utility classes to perform various operations.
*/
public class Util
{
private static final int DEFAULT_PORT = 25565;
/**
* Method to transform human readable addresses into usable address objects.
*
* @param hostline in the format of 'host:port'
* @return the constructed hostname + port.
*/
public static InetSocketAddress getAddr(String hostline)
{
String[] split = hostline.split(":");
int port = DEFAULT_PORT;
if (split.length > 1)
{
port = Integer.parseInt(split[1]);
}
return new InetSocketAddress(split[0], port);
}
/**
* Gets the value of the first unsigned byte of the specified array. Useful
* for getting the id of a packet array .
*
* @param b the array to read from
* @return the unsigned value of the first byte
*/
public static int getId(byte[] b)
{
return b[0] & 0xFF;
}
/**
* Normalizes a config path by prefix upper case letters with '_' and
* turning them to lowercase.
*
* @param s the string to normalize
* @return the normalized path
*/
public static String normalize(String s)
{
StringBuilder result = new StringBuilder();
for (char c : s.toCharArray())
{
if (Character.isUpperCase(c))
{
result.append("_");
}
result.append(Character.toLowerCase(c));
}
return result.toString();
}
/**
* Formats an integer as a hex value.
*
* @param i the integer to format
* @return the hex representation of the integer
*/
public static String hex(int i)
{
return String.format("0x%02X", i);
}
/**
* Constructs a pretty one line version of a {@link Throwable}. Useful for
* debugging.
*
* @param t the {@link Throwable} to format.
* @return a string representing information about the {@link Throwable}
*/
public static String exception(Throwable t)
{
return t.getClass().getSimpleName() + " : " + t.getMessage() + " @ " + t.getStackTrace()[0].getClassName() + ":" + t.getStackTrace()[0].getLineNumber();
}
}

View File

@@ -0,0 +1,26 @@
package net.md_5.bungee.command;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.Permission;
/**
* Class which represents a proxy command. The {@link #execute(net.md_5.bungee.command.CommandSender, java.lang.String[])
* } method will be called to dispatch the command.
*/
public abstract class Command
{
/**
* Execute this command.
*
* @param sender the sender executing this command
* @param args the parameters to this command, does not include the '/' or
* the original command.
*/
public abstract void execute(CommandSender sender, String[] args);
public Permission getPermission(CommandSender sender)
{
return BungeeCord.instance.config.getPermission(sender);
}
}

View File

@@ -0,0 +1,45 @@
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;
public class CommandAlert extends Command
{
@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.");
} else
{
StringBuilder builder = new StringBuilder();
if (!args[0].startsWith("&h"))
{
builder.append(ChatColor.DARK_PURPLE);
builder.append("[Alert] ");
}
for (String s : args)
{
s = s.replace("&h", "");
builder.append(ChatColor.translateAlternateColorCodes('&', s));
builder.append(" ");
}
String message = builder.substring(0, builder.length() - 1);
for (UserConnection con : BungeeCord.instance.connections.values())
{
con.sendMessage(message);
}
}
}
}

View File

@@ -0,0 +1,15 @@
package net.md_5.bungee.command;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.ChatColor;
public class CommandBungee extends Command
{
@Override
public void execute(CommandSender sender, String[] args)
{
sender.sendMessage(ChatColor.BLUE + "This server is running BungeeCord version " + BungeeCord.version + " by md_5");
sender.sendMessage(ChatColor.BLUE + "Your current permission level is " + getPermission(sender).name());
}
}

View File

@@ -0,0 +1,24 @@
package net.md_5.bungee.command;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.ChatColor;
import net.md_5.bungee.Permission;
/**
* Command to terminate the proxy instance. May only be used by the console.
*/
public class CommandEnd extends Command
{
@Override
public void execute(CommandSender sender, String[] args)
{
if (getPermission(sender) != Permission.ADMIN)
{
sender.sendMessage(ChatColor.RED + "You do not have permission to use this command");
} else
{
BungeeCord.instance.stop();
}
}
}

View File

@@ -0,0 +1,33 @@
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;
public class CommandIP extends Command
{
@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]);
if (user == null)
{
sender.sendMessage(ChatColor.RED + "That user is not online");
} else
{
sender.sendMessage(ChatColor.BLUE + "IP of " + args[0] + " is " + user.getAddress());
}
}
}

View File

@@ -0,0 +1,45 @@
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;
/**
* Command to list all players connected to the proxy.
*/
public class CommandList extends Command
{
@Override
public void execute(CommandSender sender, String[] args)
{
StringBuilder users = new StringBuilder();
Collection<UserConnection> connections = BungeeCord.instance.connections.values();
if (connections.isEmpty())
{
sender.sendMessage(ChatColor.BLUE + "Currently no players online.");
return;
}
for (UserConnection con : connections)
{
switch (getPermission(con))
{
case ADMIN:
users.append(ChatColor.RED);
break;
case MODERATOR:
users.append(ChatColor.GREEN);
break;
}
users.append(con.username);
users.append(", ");
users.append(ChatColor.RESET);
}
users.setLength(users.length() - 2);
sender.sendMessage(ChatColor.BLUE + "Currently online across all servers (" + connections.size() + "): " + ChatColor.RESET + users);
}
}

View File

@@ -0,0 +1,31 @@
package net.md_5.bungee.command;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.ChatColor;
import net.md_5.bungee.Permission;
/**
* Command to set a temp copy of the motd in real-time without stopping the
* proxy.
*/
public class CommandMotd extends Command
{
@Override
public void execute(CommandSender sender, String[] args)
{
if (getPermission(sender) != Permission.ADMIN)
{
sender.sendMessage(ChatColor.RED + "You do not have permission to use this command");
} else
{
String newMOTD = "";
for (String s : args)
{
newMOTD = newMOTD + s + " ";
}
newMOTD = newMOTD.substring(0, newMOTD.length() - 1);
BungeeCord.instance.config.motd = ChatColor.translateAlternateColorCodes('&', newMOTD);
}
}
}

View File

@@ -0,0 +1,21 @@
package net.md_5.bungee.command;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.ChatColor;
import net.md_5.bungee.Permission;
public class CommandReload extends Command
{
@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;
}
BungeeCord.instance.config.load();
sender.sendMessage(ChatColor.GREEN + "Reloaded config, please restart if you have any issues");
}
}

View File

@@ -0,0 +1,19 @@
package net.md_5.bungee.command;
public interface CommandSender
{
/**
* Sends a message to the client at the earliest available opportunity.
*
* @param message the message to send
*/
public abstract void sendMessage(String message);
/**
* Get the senders name or CONSOLE for console.
*
* @return the friendly name of the player.
*/
public abstract String getName();
}

View File

@@ -0,0 +1,48 @@
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;
/**
* Command to list and switch a player between available servers.
*/
public class CommandServer extends Command
{
@Override
public void execute(CommandSender sender, String[] args)
{
if (!(sender instanceof UserConnection))
{
return;
}
UserConnection con = (UserConnection) sender;
Collection<String> servers = BungeeCord.instance.config.servers.keySet();
if (args.length <= 0)
{
StringBuilder serverList = new StringBuilder();
for (String server : servers)
{
serverList.append(server);
serverList.append(", ");
}
serverList.setLength(serverList.length() - 2);
con.sendMessage(ChatColor.GOLD + "You may connect to the following servers at this time: " + serverList.toString());
} else
{
String server = args[0];
if (!servers.contains(server))
{
con.sendMessage(ChatColor.RED + "The specified server does not exist");
} else if (args[0].equals(con.getServer()))
{
con.sendMessage(ChatColor.RED + "You are already on this server.");
} else
{
con.connect(server);
}
}
}
}

View File

@@ -0,0 +1,24 @@
package net.md_5.bungee.command;
import net.md_5.bungee.ChatColor;
/**
* Command sender representing the proxy console.
*/
public class ConsoleCommandSender implements CommandSender
{
public static final ConsoleCommandSender instance = new ConsoleCommandSender();
@Override
public void sendMessage(String message)
{
System.out.println(ChatColor.stripColor(message));
}
@Override
public String getName()
{
return "CONSOLE";
}
}

View File

@@ -0,0 +1,109 @@
package net.md_5.bungee.packet;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.io.DataInput;
import java.io.DataOutput;
import lombok.Delegate;
import net.md_5.bungee.Util;
/**
* This class represents a packet which has been given a special definition. All
* subclasses can read and write to the backing byte array which can be
* retrieved via the {@link #getPacket()} method.
*/
public abstract class DefinedPacket implements DataInput, DataOutput
{
private interface Overriden
{
void readUTF();
void writeUTF(String s);
}
@Delegate(excludes = Overriden.class)
private ByteArrayDataInput in;
@Delegate(excludes = Overriden.class)
private ByteArrayDataOutput out;
/**
* Packet id.
*/
public final int id;
/**
* Already constructed packet.
*/
private byte[] packet;
public DefinedPacket(int id, byte[] buf)
{
in = ByteStreams.newDataInput(buf);
if (readUnsignedByte() != id)
{
throw new IllegalArgumentException("Wasn't expecting packet id " + Util.hex(id));
}
this.id = id;
packet = buf;
}
public DefinedPacket(int id)
{
out = ByteStreams.newDataOutput();
this.id = id;
writeByte(id);
}
/**
* Gets the bytes that make up this packet.
*
* @return the bytes which make up this packet, either the original byte
* array or the newly written one.
*/
public byte[] getPacket()
{
return packet == null ? out.toByteArray() : packet;
}
@Override
public void writeUTF(String s)
{
writeShort(s.length());
writeChars(s);
}
@Override
public String readUTF()
{
short len = readShort();
char[] chars = new char[len];
for (int i = 0; i < len; i++)
{
chars[i] = this.readChar();
}
return new String(chars);
}
public void writeArray(byte[] b)
{
writeShort(b.length);
write(b);
}
public byte[] readArray()
{
short len = readShort();
byte[] ret = new byte[len];
readFully(ret);
return ret;
}
@Override
public abstract boolean equals(Object obj);
@Override
public abstract int hashCode();
@Override
public abstract String toString();
}

View File

@@ -0,0 +1,18 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class Packet0KeepAlive extends DefinedPacket
{
public int id;
public Packet0KeepAlive(byte[] buffer)
{
super(0x00, buffer);
id = readInt();
}
}

View File

@@ -0,0 +1,42 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class Packet1Login extends DefinedPacket
{
public int entityId;
public String levelType;
public byte gameMode;
public byte dimension;
public byte difficulty;
public byte unused;
public byte maxPlayers;
public Packet1Login(int entityId, String levelType, byte gameMode, byte dimension, byte difficulty, byte unused, byte maxPlayers)
{
super(0x01);
writeInt(entityId);
writeUTF(levelType);
writeByte(gameMode);
writeByte(dimension);
writeByte(difficulty);
writeByte(unused);
writeByte(maxPlayers);
}
public Packet1Login(byte[] buf)
{
super(0x01, buf);
this.entityId = readInt();
this.levelType = readUTF();
this.gameMode = readByte();
this.dimension = readByte();
this.difficulty = readByte();
this.unused = readByte();
this.maxPlayers = readByte();
}
}

View File

@@ -0,0 +1,33 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class Packet2Handshake extends DefinedPacket
{
public byte procolVersion;
public String username;
public String host;
public int port;
public Packet2Handshake(byte protocolVersion, String username, String host, int port)
{
super(0x02);
writeByte(protocolVersion);
writeUTF(username);
writeUTF(host);
writeInt(port);
}
public Packet2Handshake(byte[] buf)
{
super(0x02, buf);
this.procolVersion = readByte();
this.username = readUTF();
this.host = readUTF();
this.port = readInt();
}
}

View File

@@ -0,0 +1,24 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class Packet3Chat extends DefinedPacket
{
public String message;
public Packet3Chat(String message)
{
super(0x03);
writeUTF(message);
}
public Packet3Chat(byte[] buf)
{
super(0x03, buf);
this.message = readUTF();
}
}

View File

@@ -0,0 +1,36 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class Packet9Respawn extends DefinedPacket
{
public int dimension;
public byte difficulty;
public byte gameMode;
public short worldHeight;
public String levelType;
public Packet9Respawn(int dimension, byte difficulty, byte gameMode, short worldHeight, String levelType)
{
super(0x09);
writeInt(dimension);
writeByte(difficulty);
writeByte(gameMode);
writeShort(worldHeight);
writeUTF(levelType);
}
public Packet9Respawn(byte[] buf)
{
super(0x09, buf);
this.dimension = readInt();
this.difficulty = readByte();
this.gameMode = readByte();
this.worldHeight = readShort();
this.levelType = readUTF();
}
}

View File

@@ -0,0 +1,30 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class PacketC9PlayerListItem extends DefinedPacket
{
public String username;
public boolean online;
public int ping;
public PacketC9PlayerListItem(byte[] packet)
{
super(0xC9, packet);
username = readUTF();
online = readBoolean();
ping = readShort();
}
public PacketC9PlayerListItem(String username, boolean online, int ping)
{
super(0xC9);
writeUTF(username);
writeBoolean(online);
writeShort(ping);
}
}

View File

@@ -0,0 +1,21 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class PacketCDClientStatus extends DefinedPacket
{
/**
* Sent from the client to the server upon respawn,
*
* @param payload 0 if initial spawn, 1 if respawn after death.
*/
public PacketCDClientStatus(byte payload)
{
super(0xCD);
writeByte(payload);
}
}

View File

@@ -0,0 +1,29 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class PacketFAPluginMessage extends DefinedPacket
{
public String tag;
public byte[] data;
public PacketFAPluginMessage(String tag, byte[] data)
{
super(0xFA);
writeUTF(tag);
writeArray(data);
this.tag = tag;
this.data = data;
}
public PacketFAPluginMessage(byte[] buf)
{
super(0xFA, buf);
this.tag = readUTF();
this.data = readArray();
}
}

View File

@@ -0,0 +1,34 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class PacketFCEncryptionResponse extends DefinedPacket
{
public byte[] sharedSecret;
public byte[] verifyToken;
public PacketFCEncryptionResponse()
{
super(0xFC);
writeArray(new byte[0]);
writeArray(new byte[0]);
}
public PacketFCEncryptionResponse(byte[] sharedSecret, byte[] verifyToken)
{
super(0xFC);
writeArray(sharedSecret);
writeArray(verifyToken);
}
public PacketFCEncryptionResponse(byte[] buf)
{
super(0xFC, buf);
this.sharedSecret = readArray();
this.verifyToken = readArray();
}
}

View File

@@ -0,0 +1,33 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class PacketFDEncryptionRequest extends DefinedPacket
{
public String serverId;
public byte[] publicKey;
public byte[] verifyToken;
public PacketFDEncryptionRequest(String serverId, byte[] publicKey, byte[] verifyToken)
{
super(0xFD);
writeUTF(serverId);
writeArray(publicKey);
writeArray(verifyToken);
this.serverId = serverId;
this.publicKey = publicKey;
this.verifyToken = verifyToken;
}
public PacketFDEncryptionRequest(byte[] buf)
{
super(0xFD, buf);
serverId = readUTF();
publicKey = readArray();
verifyToken = readArray();
}
}

View File

@@ -0,0 +1,24 @@
package net.md_5.bungee.packet;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode(callSuper = false)
public class PacketFFKick extends DefinedPacket
{
public String message;
public PacketFFKick(String message)
{
super(0xFF);
writeUTF(message);
}
public PacketFFKick(byte[] buf)
{
super(0xFF, buf);
this.message = readUTF();
}
}

View File

@@ -0,0 +1,64 @@
package net.md_5.bungee.packet;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import net.md_5.bungee.Util;
import net.md_5.mendax.PacketDefinitions;
import net.md_5.mendax.datainput.DataInputPacketReader;
/**
* A specialized input stream to parse packets using the Mojang packet
* definitions and then return them as a byte array.
*/
public class PacketInputStream
{
private final DataInputStream dataInput;
private final TrackingInputStream tracker;
public PacketInputStream(InputStream in)
{
tracker = new TrackingInputStream(in);
dataInput = new DataInputStream(tracker);
}
/**
* Read an entire packet from the stream and return it as a byte array.
*
* @return the read packet
* @throws IOException when the underlying input stream throws an exception
*/
public byte[] readPacket() throws IOException
{
tracker.out.reset();
DataInputPacketReader.readPacket(dataInput);
return tracker.out.toByteArray();
}
/**
* Input stream which will wrap another stream and copy all bytes read to a
* {@link ByteArrayOutputStream}.
*/
private class TrackingInputStream extends InputStream
{
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
private final InputStream wrapped;
public TrackingInputStream(InputStream wrapped)
{
this.wrapped = wrapped;
}
@Override
public int read() throws IOException
{
int ret = wrapped.read();
out.write(ret);
return ret;
}
}
}

View File

@@ -0,0 +1,22 @@
package net.md_5.bungee.plugin;
/**
* An event which may be canceled and this be prevented from happening.
*/
public interface Cancellable
{
/**
* Sets the canceled state of this event.
*
* @param canceled whether this event is canceled or not
*/
public void setCancelled(boolean canceled);
/**
* Gets the canceled state of this event.
*
* @return whether this event is canceled or not
*/
public boolean isCancelled();
}

View File

@@ -0,0 +1,36 @@
package net.md_5.bungee.plugin;
import lombok.Data;
import net.md_5.bungee.UserConnection;
@Data
public class ChatEvent implements Cancellable
{
/**
* Canceled state.
*/
private boolean cancelled;
/**
* Whether this packet is destined for the server or the client.
*/
private final Destination destination;
/**
* User in question.
*/
private final UserConnection connection;
/**
* Text contained in this chat.
*/
private String text;
/**
* An enum that signifies the destination for this packet.
*/
public enum Destination
{
SERVER,
CLIENT
}
}

View File

@@ -0,0 +1,20 @@
package net.md_5.bungee.plugin;
/**
* Exception thrown when a plugin could not be loaded for any reason.
*/
public class InvalidPluginException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public InvalidPluginException(String message, Throwable cause)
{
super(message, cause);
}
public InvalidPluginException(String message)
{
super(message);
}
}

View File

@@ -0,0 +1,75 @@
package net.md_5.bungee.plugin;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.command.Command;
/**
* Base class which all proxy plugins should extend.
*/
public abstract class JavaPlugin
{
/**
* Description file.
*/
PluginDescription description;
/**
* Called on enable.
*/
public void onEnable()
{
}
/**
* Called on disable.
*/
public void onDisable()
{
}
/**
* Called when a user connects with their name and address. To keep things
* simple this name has not been checked with minecraft.net.
*/
public void onHandshake(LoginEvent event)
{
}
/**
* Called after a user has been authed with minecraftt.net and is about to
* log into the proxy.
*/
public void onLogin(LoginEvent event)
{
}
/**
* Called when a user is connecting to a new server.
*/
public void onServerConnect(ServerConnectEvent event)
{
}
/**
* Called when a plugin message is sent to the client or server
*/
public void onPluginMessage(PluginMessageEvent event)
{
}
/**
* Called when a chat message is sent to the client or server
*/
public void onChat(ChatEvent event)
{
}
/**
* Register a command for use with the proxy.
*/
protected final void registerCommand(String label, Command command)
{
BungeeCord.instance.commandMap.put(label, command);
}
}

View File

@@ -0,0 +1,127 @@
package net.md_5.bungee.plugin;
import com.google.common.io.PatternFilenameFilter;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import lombok.Getter;
import static net.md_5.bungee.Logger.$;
/**
* Plugin manager to handle loading and saving other JavaPlugin's. This class is
* itself a plugin for ease of use.
*/
public class JavaPluginManager extends JavaPlugin
{
/**
* Set of loaded plugins.
*/
@Getter
private final Set<JavaPlugin> plugins = new HashSet<>();
/**
* Load all plugins from the plugins folder. This method must only be called
* once per instance.
*/
public void loadPlugins()
{
File dir = new File("plugins");
dir.mkdir();
for (File file : dir.listFiles(new PatternFilenameFilter(".*\\.jar")))
{
try
{
JarFile jar = new JarFile(file);
ZipEntry entry = jar.getEntry("plugin.yml");
if (entry == null)
{
throw new InvalidPluginException("Jar does not contain a plugin.yml");
}
PluginDescription description;
try (InputStream is = jar.getInputStream(entry))
{
description = PluginDescription.load(is);
}
URLClassLoader classloader = new URLClassLoader(new URL[]
{
file.toURI().toURL()
}, getClass().getClassLoader());
Class<?> clazz = Class.forName(description.getMain(), true, classloader);
Class<? extends JavaPlugin> subClazz = clazz.asSubclass(JavaPlugin.class);
JavaPlugin plugin = subClazz.getDeclaredConstructor().newInstance();
plugin.description = description;
plugin.onEnable();
plugins.add(plugin);
$().info("Loaded plugin: " + plugin.description.getName());
} catch (Exception ex)
{
$().severe("Could not load plugin: " + file);
ex.printStackTrace();
}
}
}
@Override
public void onDisable()
{
for (JavaPlugin p : plugins)
{
p.onDisable();
}
}
@Override
public void onHandshake(LoginEvent event)
{
for (JavaPlugin p : plugins)
{
p.onHandshake(event);
}
}
@Override
public void onLogin(LoginEvent event)
{
for (JavaPlugin p : plugins)
{
p.onLogin(event);
}
}
@Override
public void onServerConnect(ServerConnectEvent event)
{
for (JavaPlugin p : plugins)
{
p.onServerConnect(event);
}
}
@Override
public void onPluginMessage(PluginMessageEvent event)
{
for (JavaPlugin p : plugins)
{
p.onPluginMessage(event);
}
}
@Override
public void onChat(ChatEvent event)
{
for (JavaPlugin p : plugins)
{
p.onChat(event);
}
}
}

View File

@@ -0,0 +1,33 @@
package net.md_5.bungee.plugin;
import java.net.InetAddress;
import lombok.Data;
/**
* Event called to represent a player logging in.
*/
@Data
public class LoginEvent implements Cancellable
{
/**
* Canceled state.
*/
private boolean cancelled;
/**
* Message to use when kicking if this event is canceled.
*/
private String cancelReason;
/**
* Username which the player wishes to use.
*/
private final String username;
/**
* IP address of the remote connection.
*/
private final InetAddress address;
/**
* Hostname which the user tried to connect to.
*/
private final String hostname;
}

View File

@@ -0,0 +1,48 @@
package net.md_5.bungee.plugin;
import java.io.InputStream;
import java.lang.reflect.Field;
import lombok.Data;
import org.yaml.snakeyaml.Yaml;
/**
* File which contains information about a plugin, its authors, and how to load
* it.
*/
@Data
public class PluginDescription
{
private String name;
private String main;
private String version;
private String author;
private PluginDescription()
{
}
public static PluginDescription load(InputStream is)
{
PluginDescription ret = new Yaml().loadAs(is, PluginDescription.class);
if (ret == null)
{
throw new InvalidPluginException("Could not load plugin description file.");
}
for (Field f : PluginDescription.class.getDeclaredFields())
{
try
{
if (f.get(ret) == null)
{
throw new InvalidPluginException(f.getName() + " is not set properly in plugin description");
}
} catch (IllegalArgumentException | IllegalAccessException ex)
{
}
}
return ret;
}
}

View File

@@ -0,0 +1,47 @@
package net.md_5.bungee.plugin;
import lombok.Data;
import net.md_5.bungee.UserConnection;
/**
* Event called when a plugin message is sent to the client or server
*/
@Data
public class PluginMessageEvent implements Cancellable
{
/**
* Canceled state.
*/
private boolean cancelled;
/**
* Message to use when kicking if this event is canceled.
*/
private String cancelReason;
/**
* Whether this packet is destined for the server or the client
*/
private final Destination destination;
/**
* User in question
*/
private final UserConnection connection;
/**
* Tag specified for this plugin message.
*/
private String tag;
/**
* Data contained in this plugin message.
*/
private String data;
/**
* An enum that signifies the destination for this packet
*/
public enum Destination
{
SERVER,
CLIENT
}
}

View File

@@ -0,0 +1,33 @@
package net.md_5.bungee.plugin;
import lombok.Data;
import net.md_5.bungee.UserConnection;
/**
* Event called when the decision is made to decide which server to connect to.
*/
@Data
public class ServerConnectEvent
{
/**
* If the player currently has no server, this is true
*/
private final boolean firstTime;
/**
* Message to send just before the change. null for no message
*/
private String message;
/**
* User in question.
*/
private final UserConnection connection;
/**
* Name of the server they are connecting to.
*/
private final String server;
/**
* Name of the server which they will be forwarded to instead.
*/
private String newServer;
}

View File

@@ -0,0 +1,26 @@
package net.md_5.bungee.tablist;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.packet.PacketC9PlayerListItem;
public class GlobalPingTabList extends GlobalTabList
{
public static final int PING_THRESHOLD = 20;
private Map<UserConnection, Integer> lastPings = Collections.synchronizedMap(new WeakHashMap<UserConnection, Integer>());
@Override
public void onPingChange(final UserConnection con, final int ping)
{
Integer lastPing = lastPings.get(con);
if (lastPing == null || (ping - PING_THRESHOLD > lastPing && ping + PING_THRESHOLD < lastPing))
{
BungeeCord.instance.broadcast(new PacketC9PlayerListItem(con.tabListName, true, ping));
lastPings.put(con, ping);
}
}
}

View File

@@ -0,0 +1,51 @@
package net.md_5.bungee.tablist;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.packet.PacketC9PlayerListItem;
public class GlobalTabList implements TabListHandler
{
private Set<UserConnection> sentPings = Collections.synchronizedSet(new HashSet<UserConnection>());
@Override
public void onJoin(UserConnection con)
{
for (UserConnection c : BungeeCord.instance.connections.values())
{
con.packetQueue.add(new PacketC9PlayerListItem(c.tabListName, true, c.getPing()));
}
}
@Override
public void onServerChange(UserConnection con)
{
}
@Override
public void onPingChange(final UserConnection con, final int ping)
{
if (!sentPings.contains(con))
{
BungeeCord.instance.broadcast(new PacketC9PlayerListItem(con.tabListName, true, con.getPing()));
sentPings.add(con);
}
}
@Override
public void onDisconnect(final UserConnection con)
{
BungeeCord.instance.broadcast(new PacketC9PlayerListItem(con.tabListName, false, 9999));
sentPings.remove(con);
}
@Override
public boolean onPacketC9(UserConnection con, PacketC9PlayerListItem packet)
{
return false;
}
}

View File

@@ -0,0 +1,68 @@
package net.md_5.bungee.tablist;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.packet.PacketC9PlayerListItem;
public class ServerUniqueTabList implements TabListHandler
{
private Map<UserConnection, Set<String>> sentUsernames = Collections.synchronizedMap(new WeakHashMap<UserConnection, Set<String>>());
@Override
public void onJoin(UserConnection con)
{
}
@Override
public void onServerChange(UserConnection con)
{
Set<String> usernames = sentUsernames.get(con);
if (usernames != null)
{
synchronized (usernames)
{
for (String username : usernames)
{
con.packetQueue.add(new PacketC9PlayerListItem(username, false, 9999));
}
usernames.clear();
}
}
}
@Override
public void onPingChange(UserConnection con, int ping)
{
}
@Override
public void onDisconnect(UserConnection con)
{
}
@Override
public boolean onPacketC9(final UserConnection con, final PacketC9PlayerListItem packet)
{
Set<String> usernames = sentUsernames.get(con);
if (usernames == null)
{
usernames = new LinkedHashSet<>();
sentUsernames.put(con, usernames);
}
if (packet.online)
{
usernames.add(packet.username);
} else
{
usernames.remove(packet.username);
}
return true;
}
}

View File

@@ -0,0 +1,18 @@
package net.md_5.bungee.tablist;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.packet.PacketC9PlayerListItem;
public interface TabListHandler
{
public void onJoin(UserConnection con);
public void onServerChange(UserConnection con);
public void onPingChange(UserConnection con, int ping);
public void onDisconnect(UserConnection con);
public boolean onPacketC9(UserConnection con, PacketC9PlayerListItem packet);
}