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,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project-shared-configuration>
<!--
This file contains additional configuration written by modules in the NetBeans IDE.
The configuration is intended to be shared among all the users of project and
therefore it is assumed to be part of version control checkout.
Without this configuration present, some functionality in the IDE may be limited or fail altogether.
-->
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
<!--
Properties that influence various parts of the IDE, especially code formatting and the like.
You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
That way multiple projects can share the same settings (useful for formatting rules for example).
Any value defined here will override the pom.xml file value but is only applicable to the current project.
-->
<netbeans.hint.jdkPlatform>JDK_1.7</netbeans.hint.jdkPlatform>
</properties>
</project-shared-configuration>

98
proxy/pom.xml Normal file
View File

@@ -0,0 +1,98 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-proxy</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>BungeeCord</name>
<description>Proxy component of the Elastic Portal Suite</description>
<dependencies>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>13.0.1</version>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>mendax</artifactId>
<version>1.4.6-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk15on</artifactId>
<version>1.47</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>0.11.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Main-Class>net.md_5.bungee.BungeeCord</Main-Class>
<Implementation-Version>${describe}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>**/*.java</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.SF</exclude>
<exclude>**/*.DSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
</plugins>
</build>
</project>

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);
}