From b876fb2e1bd395c37f47b020c2f0e778812c0c61 Mon Sep 17 00:00:00 2001 From: md_5 Date: Fri, 5 Oct 2012 11:01:31 +1000 Subject: [PATCH] Initial commit. --- .gitignore | 38 +++ LICENSE | 26 ++ README.md | 13 + pom.xml | 159 ++++++++++++ src/main/java/net/md_5/bungee/BungeeCord.java | 108 ++++++++ src/main/java/net/md_5/bungee/ChatColor.java | 236 ++++++++++++++++++ .../java/net/md_5/bungee/Configuration.java | 201 +++++++++++++++ .../java/net/md_5/bungee/InitialHandler.java | 61 +++++ .../java/net/md_5/bungee/ListenThread.java | 35 +++ .../net/md_5/bungee/ReconnectSaveThread.java | 20 ++ src/main/java/net/md_5/bungee/Util.java | 70 ++++++ .../net/md_5/bungee/packet/DefinedPacket.java | 88 +++++++ .../md_5/bungee/packet/Packet10HeldItem.java | 21 ++ .../md_5/bungee/packet/Packet2Handshake.java | 30 +++ .../net/md_5/bungee/packet/Packet3Chat.java | 21 ++ .../md_5/bungee/packet/Packet46GameState.java | 21 ++ .../md_5/bungee/packet/Packet9Respawn.java | 33 +++ .../bungee/packet/PacketFAPluginMessage.java | 26 ++ .../packet/PacketFCEncryptionResponse.java | 24 ++ .../packet/PacketFDEncryptionRequest.java | 23 ++ .../net/md_5/bungee/packet/PacketFFKick.java | 21 ++ .../md_5/bungee/packet/PacketInputStream.java | 48 ++++ .../md_5/bungee/packet/VanillaPackets.java | 172 +++++++++++++ 23 files changed, 1495 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/net/md_5/bungee/BungeeCord.java create mode 100644 src/main/java/net/md_5/bungee/ChatColor.java create mode 100644 src/main/java/net/md_5/bungee/Configuration.java create mode 100644 src/main/java/net/md_5/bungee/InitialHandler.java create mode 100644 src/main/java/net/md_5/bungee/ListenThread.java create mode 100644 src/main/java/net/md_5/bungee/ReconnectSaveThread.java create mode 100644 src/main/java/net/md_5/bungee/Util.java create mode 100644 src/main/java/net/md_5/bungee/packet/DefinedPacket.java create mode 100644 src/main/java/net/md_5/bungee/packet/Packet10HeldItem.java create mode 100644 src/main/java/net/md_5/bungee/packet/Packet2Handshake.java create mode 100644 src/main/java/net/md_5/bungee/packet/Packet3Chat.java create mode 100644 src/main/java/net/md_5/bungee/packet/Packet46GameState.java create mode 100644 src/main/java/net/md_5/bungee/packet/Packet9Respawn.java create mode 100644 src/main/java/net/md_5/bungee/packet/PacketFAPluginMessage.java create mode 100644 src/main/java/net/md_5/bungee/packet/PacketFCEncryptionResponse.java create mode 100644 src/main/java/net/md_5/bungee/packet/PacketFDEncryptionRequest.java create mode 100644 src/main/java/net/md_5/bungee/packet/PacketFFKick.java create mode 100644 src/main/java/net/md_5/bungee/packet/PacketInputStream.java create mode 100644 src/main/java/net/md_5/bungee/packet/VanillaPackets.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b091776d --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Eclipse stuff +/.classpath +/.project +/.settings + +# netbeans +/nbproject +/nbactions.xml +/nb-configuration.xml + +# we use maven! +/build.xml + +# maven +/target +/dependency-reduced-pom.xml + +# vim +.*.sw[a-p] + +# various other potential build files +/build +/bin +/dist +/manifest.mf + +# Mac filesystem dust +/.DS_Store + +# intellij +*.iml +*.ipr +*.iws +.idea/ + +# other files +*log* +*.yml diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..612487b8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2012, md_5. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +The name of the author may not be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..9a2744b3 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +BungeeCord +====== + +The most reliable Minecraft server portal suite. + +Coding guidelines +----------- + +Its not fun to always have to stick to a set of rules, but here are are few to make sure everyone gets the best experience. +* Have verbose commit messages, this makes it much easier to revert commits if the need arises. +* Rebase your commits onto the tracked branch. No merges please. +* Use for spaces, NO tabs. +* Use lombok wherever possible. diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..d846d9be --- /dev/null +++ b/pom.xml @@ -0,0 +1,159 @@ + + + 4.0.0 + + net.md-5 + bungeecord + 1.0-SNAPSHOT + jar + + BungeeCord + https://github.com/Shadowraze/BungeeCord + 2012 + + + The BSD 3-Clause License + http://opensource.org/licenses/BSD-3-Clause + repo + + + + + scm:git:git@github.com:Shadowraze/BungeeCord.git + scm:git:git@github.com:Shadowraze/BungeeCord.git + git@github.com:Shadowraze/BungeeCord.git + + + GitHub + https://github.com/Shadowraze/BungeeCord/issues + + + jenkins + http://ci.md-5.net/job/BungeeCord + + + + UTF-8 + unknown + net.md_5.bungee.BungeeCord + + + + + com.google.guava + guava + 13.0.1 + + + org.bouncycastle + bcprov-jdk15on + 1.47 + + + org.bukkit + minecraft-server + 1.3.2 + + + org.yaml + snakeyaml + 1.11 + + + org.projectlombok + lombok + 0.11.4 + provided + + + + + + repobo-snap + http://repo.bukkit.org/content/groups/public + + + + + bukkit-plugins + http://repo.bukkit.org/content/groups/public + + + + + ${project.name} + + + com.lukegb.mojo + gitdescribe-maven-plugin + 1.3 + + git-${project.name}-${project.version}- + -${build.number} + + + + compile + + gitdescribe + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${main.class} + ${describe} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.0 + + + package + + shade + + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + + + + org.bukkit:minecraft-server + + org/bouncycastle/** + + + + true + + + + + diff --git a/src/main/java/net/md_5/bungee/BungeeCord.java b/src/main/java/net/md_5/bungee/BungeeCord.java new file mode 100644 index 00000000..af860b9e --- /dev/null +++ b/src/main/java/net/md_5/bungee/BungeeCord.java @@ -0,0 +1,108 @@ +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.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import static net.md_5.bungee.Logger.$; + +public class BungeeCord { + + /** + * 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. + */ + private String version = (getClass().getPackage().getImplementationVersion() == null) ? "unknown" : getClass().getPackage().getImplementationVersion(); + + public static void main(String[] args) throws IOException { + System.out.println(Util.hex(15)); + 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) { + if (line.equals("end")) { + instance.stop(); + } + } + } + } + + public void start() throws IOException { + config.load(); + isRunning = true; + + InetSocketAddress addr = Util.getAddr(config.bindHost); + listener = new ListenThread(addr); + listener.start(); + + saveThread.start(); + $().info("Listening on " + addr); + } + + public void stop() { + this.isRunning = false; + + $().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 " + "x" + " connections"); + // TODO: Kick everyone + + $().info("Saving reconnect locations"); + saveThread.interrupt(); + try { + saveThread.join(); + } catch (InterruptedException ex) { + } + + $().info("Thank you and goodbye"); + } + + public int getOnlinePlayers() { + return 123; + } + + public void setSocketOptions(Socket socket) throws IOException { + socket.setSoTimeout(config.timeout); + socket.setTrafficClass(0x18); + socket.setTcpNoDelay(true); + } +} diff --git a/src/main/java/net/md_5/bungee/ChatColor.java b/src/main/java/net/md_5/bungee/ChatColor.java new file mode 100644 index 00000000..771aa17b --- /dev/null +++ b/src/main/java/net/md_5/bungee/ChatColor.java @@ -0,0 +1,236 @@ +package net.md_5.bungee; + +import com.google.common.collect.Maps; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * All supported color values for chat + */ +public enum ChatColor { + + /** + * Represents black + */ + BLACK('0', 0x00), + /** + * Represents dark blue + */ + DARK_BLUE('1', 0x1), + /** + * Represents dark green + */ + DARK_GREEN('2', 0x2), + /** + * Represents dark blue (aqua) + */ + DARK_AQUA('3', 0x3), + /** + * Represents dark red + */ + DARK_RED('4', 0x4), + /** + * Represents dark purple + */ + DARK_PURPLE('5', 0x5), + /** + * Represents gold + */ + GOLD('6', 0x6), + /** + * Represents gray + */ + GRAY('7', 0x7), + /** + * Represents dark gray + */ + DARK_GRAY('8', 0x8), + /** + * Represents blue + */ + BLUE('9', 0x9), + /** + * Represents green + */ + GREEN('a', 0xA), + /** + * Represents aqua + */ + AQUA('b', 0xB), + /** + * Represents red + */ + RED('c', 0xC), + /** + * Represents light purple + */ + LIGHT_PURPLE('d', 0xD), + /** + * Represents yellow + */ + YELLOW('e', 0xE), + /** + * Represents white + */ + WHITE('f', 0xF), + /** + * Represents magical characters that change around randomly + */ + MAGIC('k', 0x10, true), + /** + * Makes the text bold. + */ + BOLD('l', 0x11, true), + /** + * Makes a line appear through the text. + */ + STRIKETHROUGH('m', 0x12, true), + /** + * Makes the text appear underlined. + */ + UNDERLINE('n', 0x13, true), + /** + * Makes the text italic. + */ + ITALIC('o', 0x14, true), + /** + * Resets all previous chat colors or formats. + */ + RESET('r', 0x15); + /** + * 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'; + private static final Pattern STRIP_COLOR_PATTERN = Pattern.compile("(?i)" + String.valueOf(COLOR_CHAR) + "[0-9A-FK-OR]"); + private final int intCode; + private final char code; + private final boolean isFormat; + private final String toString; + private final static Map BY_ID = Maps.newHashMap(); + private final static Map BY_CHAR = Maps.newHashMap(); + + private ChatColor(char code, int intCode) { + this(code, intCode, false); + } + + private ChatColor(char code, int intCode, boolean isFormat) { + this.code = code; + this.intCode = intCode; + this.isFormat = isFormat; + this.toString = new String(new char[]{COLOR_CHAR, code}); + } + + /** + * Gets the char value associated with this color + * + * @return A char value of this color code + */ + public char getChar() { + return code; + } + + @Override + public String toString() { + return toString; + } + + /** + * Checks if this code is a format code as opposed to a color code. + */ + public boolean isFormat() { + return isFormat; + } + + /** + * Checks if this code is a color code as opposed to a format code. + */ + public boolean isColor() { + return !isFormat && this != RESET; + } + + /** + * Gets the color represented by the specified color code + * + * @param code Code to check + * @return Associative {@link org.bukkit.ChatColor} with the given code, or + * null if it doesn't exist + */ + public static ChatColor getByChar(char code) { + return BY_CHAR.get(code); + } + + /** + * 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(""); + } + + /** + * Translates a string using an alternate color code character into a string + * that uses the internal ChatColor.COLOR_CODE color code character. The + * alternate color code character will only be replaced if it is immediately + * followed by 0-9, A-F, or a-f. + * + * @param altColorChar The alternate color code character to replace. Ex: & + * @param textToTranslate Text containing the alternate color code + * character. + * @return Text containing the ChatColor.COLOR_CODE color code character. + */ + 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); + } + + /** + * Gets the ChatColors used at the end of the given input string. + * + * @param input Input string to retrieve the colors from. + * @return Any remaining ChatColors to pass onto the next line. + */ + public static String getLastColors(String input) { + String result = ""; + int length = input.length(); + + // Search backwards from the end as it is faster + for (int index = length - 1; index > -1; index--) { + char section = input.charAt(index); + if (section == COLOR_CHAR && index < length - 1) { + char c = input.charAt(index + 1); + ChatColor color = getByChar(c); + + if (color != null) { + result = color.toString() + result; + + // Once we find a color or reset we can stop searching + if (color.isColor() || color.equals(RESET)) { + break; + } + } + } + } + + return result; + } + + static { + for (ChatColor color : values()) { + BY_ID.put(color.intCode, color); + BY_CHAR.put(color.code, color); + } + } +} diff --git a/src/main/java/net/md_5/bungee/Configuration.java b/src/main/java/net/md_5/bungee/Configuration.java new file mode 100644 index 00000000..6f956eea --- /dev/null +++ b/src/main/java/net/md_5/bungee/Configuration.java @@ -0,0 +1,201 @@ +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 static net.md_5.bungee.Logger.$; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +public class Configuration { + + /** + * Reconnect locations file. + */ + private transient File reconnect = new File("locations.yml"); + /** + * Loaded reconnect locations. + */ + private transient Map reconnectLocations; + /** + * Config file. + */ + private transient File file = new File("config.yml"); + /** + * Yaml instance. + */ + private transient Yaml yaml; + /** + * Loaded config. + */ + private transient Map 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; + /** + * Socket timeout. + */ + public int timeout = 15000; + /** + * All servers. + */ + public Map servers = new HashMap() { + { + put(defaultServerName, "127.0.0.1"); + put("pvp", "127.0.0.1:1337"); + } + }; + /** + * Forced servers. + */ + public Map forcedServers = new HashMap() { + { + put("pvp.md-5.net", "pvp"); + } + }; + /** + * Proxy admins. + */ + public List admins = new ArrayList() { + { + add("md_5"); + } + }; + /** + * Proxy moderators. + */ + public List moderators = new ArrayList() { + { + add("mbaxter"); + } + }; + + 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"); + } + for (String server : forcedServers.values()) { + if (!servers.containsKey(server)) { + throw new IllegalArgumentException("Forced server " + server + " is not defined in servers"); + } + } + + 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 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(); + } + } + + public InetSocketAddress getHostFor(String user, InetSocketAddress requestedHost) { + String entry = user + ";" + Util.getAddr(requestedHost); + String host = requestedHost.getHostString(); + + String hostLine; + if (forcedServers.containsKey(host)) { + hostLine = servers.get(forcedServers.get(host)); + } else { + hostLine = reconnectLocations.get(entry); + } + if (hostLine == null) { + hostLine = servers.get(defaultServerName); + } + return Util.getAddr(hostLine); + } + + public InetSocketAddress getServer(String name) { + String hostline = servers.get(name); + if (hostline != null) { + return Util.getAddr(hostline); + } else { + return null; + } + } + + public void saveHosts() { + save(reconnect, reconnectLocations); + $().info("Saved reconnect locations to " + reconnect); + } +} diff --git a/src/main/java/net/md_5/bungee/InitialHandler.java b/src/main/java/net/md_5/bungee/InitialHandler.java new file mode 100644 index 00000000..1690644b --- /dev/null +++ b/src/main/java/net/md_5/bungee/InitialHandler.java @@ -0,0 +1,61 @@ +package net.md_5.bungee; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import net.md_5.bungee.packet.Packet2Handshake; +import net.md_5.bungee.packet.PacketFFKick; +import net.md_5.bungee.packet.PacketInputStream; + +public class InitialHandler implements Runnable { + + private final Socket socket; + private final PacketInputStream in; + private final 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); + break; + case 0xFE: + throw new KickException(BungeeCord.instance.config.motd + ChatColor.COLOR_CHAR + BungeeCord.instance.getOnlinePlayers() + ChatColor.COLOR_CHAR + BungeeCord.instance.config.maxPlayers); + default: + 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 { + socket.close(); + } catch (IOException ioe2) { + } + } + } + + private class KickException extends RuntimeException { + + public KickException(String message) { + super(message); + } + } +} diff --git a/src/main/java/net/md_5/bungee/ListenThread.java b/src/main/java/net/md_5/bungee/ListenThread.java new file mode 100644 index 00000000..e2f5b272 --- /dev/null +++ b/src/main/java/net/md_5/bungee/ListenThread.java @@ -0,0 +1,35 @@ +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.$; + +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 + } + } + } +} diff --git a/src/main/java/net/md_5/bungee/ReconnectSaveThread.java b/src/main/java/net/md_5/bungee/ReconnectSaveThread.java new file mode 100644 index 00000000..dc5f06f0 --- /dev/null +++ b/src/main/java/net/md_5/bungee/ReconnectSaveThread.java @@ -0,0 +1,20 @@ +package net.md_5.bungee; + +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(); + } + } +} diff --git a/src/main/java/net/md_5/bungee/Util.java b/src/main/java/net/md_5/bungee/Util.java new file mode 100644 index 00000000..d069e9c1 --- /dev/null +++ b/src/main/java/net/md_5/bungee/Util.java @@ -0,0 +1,70 @@ +package net.md_5.bungee; + +import java.net.InetSocketAddress; + +public class Util { + + private static final int DEFAULT_PORT = 25565; + + /** + * Basic 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(":"); + if (split.length < 2) { + throw new IllegalArgumentException("Invalid split format"); + } + int port = DEFAULT_PORT; + if (split.length > 1) { + port = Integer.parseInt(split[1]); + } + return new InetSocketAddress(split[0], port); + } + + /** + * Turns a InetSocketAddress into a string that can be reconstructed later. + * + * @param address the address to serialize + * @return + */ + public static String getAddr(InetSocketAddress address) { + return address.getAddress().getHostAddress() + ((address.getPort() != DEFAULT_PORT) ? ":" + address.getPort() : ""); + } + + /** + * Get the packet id of specified byte array. + */ + 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(); + } + + public static String hex(int i) { + return String.format("0x%02X", i); + } + + public static String exception(Throwable t) { + return t.getClass().getSimpleName() + " : " + t.getMessage() + " @ " + t.getStackTrace()[0].getFileName() + ":" + t.getStackTrace()[0].getLineNumber(); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/DefinedPacket.java b/src/main/java/net/md_5/bungee/packet/DefinedPacket.java new file mode 100644 index 00000000..8e2f56a8 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/DefinedPacket.java @@ -0,0 +1,88 @@ +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; + +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); + packet = null; + } + + 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(); +} diff --git a/src/main/java/net/md_5/bungee/packet/Packet10HeldItem.java b/src/main/java/net/md_5/bungee/packet/Packet10HeldItem.java new file mode 100644 index 00000000..d43652b3 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/Packet10HeldItem.java @@ -0,0 +1,21 @@ +package net.md_5.bungee.packet; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ToString +@EqualsAndHashCode(callSuper = false) +public class Packet10HeldItem extends DefinedPacket { + + public short slot; + + public Packet10HeldItem(short slot) { + super(0x10); + writeShort(slot); + } + + public Packet10HeldItem(byte[] buf) { + super(0x10, buf); + this.slot = readShort(); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/Packet2Handshake.java b/src/main/java/net/md_5/bungee/packet/Packet2Handshake.java new file mode 100644 index 00000000..aabca016 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/Packet2Handshake.java @@ -0,0 +1,30 @@ +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(); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/Packet3Chat.java b/src/main/java/net/md_5/bungee/packet/Packet3Chat.java new file mode 100644 index 00000000..3f7e8b9f --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/Packet3Chat.java @@ -0,0 +1,21 @@ +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(); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/Packet46GameState.java b/src/main/java/net/md_5/bungee/packet/Packet46GameState.java new file mode 100644 index 00000000..893aeda9 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/Packet46GameState.java @@ -0,0 +1,21 @@ +package net.md_5.bungee.packet; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ToString +@EqualsAndHashCode(callSuper = false) +public class Packet46GameState extends DefinedPacket { + + /** + * Update game state. When sent with a state of 2, rain will cease. + * + * @param state the new game state + * @param mode the new game mode. Used when state == 3. + */ + public Packet46GameState(byte state, byte mode) { + super(0x46); + writeByte(state); + writeByte(mode); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/Packet9Respawn.java b/src/main/java/net/md_5/bungee/packet/Packet9Respawn.java new file mode 100644 index 00000000..842df433 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/Packet9Respawn.java @@ -0,0 +1,33 @@ +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(); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/PacketFAPluginMessage.java b/src/main/java/net/md_5/bungee/packet/PacketFAPluginMessage.java new file mode 100644 index 00000000..df3dbe10 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/PacketFAPluginMessage.java @@ -0,0 +1,26 @@ +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(); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/PacketFCEncryptionResponse.java b/src/main/java/net/md_5/bungee/packet/PacketFCEncryptionResponse.java new file mode 100644 index 00000000..0e0112a3 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/PacketFCEncryptionResponse.java @@ -0,0 +1,24 @@ +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[] buf) { + super(0xFC, buf); + this.sharedSecret = readArray(); + this.verifyToken = readArray(); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/PacketFDEncryptionRequest.java b/src/main/java/net/md_5/bungee/packet/PacketFDEncryptionRequest.java new file mode 100644 index 00000000..d35752dd --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/PacketFDEncryptionRequest.java @@ -0,0 +1,23 @@ +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; + } +} diff --git a/src/main/java/net/md_5/bungee/packet/PacketFFKick.java b/src/main/java/net/md_5/bungee/packet/PacketFFKick.java new file mode 100644 index 00000000..a3d5dee7 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/PacketFFKick.java @@ -0,0 +1,21 @@ +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(); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/PacketInputStream.java b/src/main/java/net/md_5/bungee/packet/PacketInputStream.java new file mode 100644 index 00000000..4e5d1c13 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/PacketInputStream.java @@ -0,0 +1,48 @@ +package net.md_5.bungee.packet; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import net.md_5.bungee.Util; +import net.minecraft.server.Packet; + +public class PacketInputStream { + + private final DataInputStream dataInput; + private final TrackingInputStream tracker; + + public PacketInputStream(InputStream in) { + tracker = new TrackingInputStream(in); + dataInput = new DataInputStream(tracker); + } + + public byte[] readPacket() throws IOException { + tracker.out.reset(); + int id = tracker.read(); + Packet codec = VanillaPackets.packets[id]; + if (codec == null) { + throw new RuntimeException("No Packet id: " + Util.hex(id)); + } + codec.a(dataInput); + return tracker.out.toByteArray(); + + } + + 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; + } + } +} diff --git a/src/main/java/net/md_5/bungee/packet/VanillaPackets.java b/src/main/java/net/md_5/bungee/packet/VanillaPackets.java new file mode 100644 index 00000000..78d7c92f --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/VanillaPackets.java @@ -0,0 +1,172 @@ +package net.md_5.bungee.packet; + +import java.lang.reflect.InvocationTargetException; +import static net.md_5.bungee.Logger.$; +import net.md_5.bungee.Util; +import net.minecraft.server.Packet; +import net.minecraft.server.Packet0KeepAlive; +import net.minecraft.server.Packet100OpenWindow; +import net.minecraft.server.Packet101CloseWindow; +import net.minecraft.server.Packet102WindowClick; +import net.minecraft.server.Packet103SetSlot; +import net.minecraft.server.Packet104WindowItems; +import net.minecraft.server.Packet105CraftProgressBar; +import net.minecraft.server.Packet106Transaction; +import net.minecraft.server.Packet107SetCreativeSlot; +import net.minecraft.server.Packet108ButtonClick; +import net.minecraft.server.Packet10Flying; +import net.minecraft.server.Packet11PlayerPosition; +import net.minecraft.server.Packet12PlayerLook; +import net.minecraft.server.Packet130UpdateSign; +import net.minecraft.server.Packet131ItemData; +import net.minecraft.server.Packet132TileEntityData; +import net.minecraft.server.Packet13PlayerLookMove; +import net.minecraft.server.Packet14BlockDig; +import net.minecraft.server.Packet15Place; +import net.minecraft.server.Packet16BlockItemSwitch; +import net.minecraft.server.Packet17EntityLocationAction; +import net.minecraft.server.Packet18ArmAnimation; +import net.minecraft.server.Packet19EntityAction; +import net.minecraft.server.Packet1Login; +import net.minecraft.server.Packet200Statistic; +import net.minecraft.server.Packet201PlayerInfo; +import net.minecraft.server.Packet202Abilities; +import net.minecraft.server.Packet203TabComplete; +import net.minecraft.server.Packet204LocaleAndViewDistance; +import net.minecraft.server.Packet205ClientCommand; +import net.minecraft.server.Packet20NamedEntitySpawn; +import net.minecraft.server.Packet21PickupSpawn; +import net.minecraft.server.Packet22Collect; +import net.minecraft.server.Packet23VehicleSpawn; +import net.minecraft.server.Packet24MobSpawn; +import net.minecraft.server.Packet250CustomPayload; +import net.minecraft.server.Packet252KeyResponse; +import net.minecraft.server.Packet253KeyRequest; +import net.minecraft.server.Packet254GetInfo; +import net.minecraft.server.Packet255KickDisconnect; +import net.minecraft.server.Packet25EntityPainting; +import net.minecraft.server.Packet26AddExpOrb; +import net.minecraft.server.Packet28EntityVelocity; +import net.minecraft.server.Packet29DestroyEntity; +import net.minecraft.server.Packet2Handshake; +import net.minecraft.server.Packet30Entity; +import net.minecraft.server.Packet31RelEntityMove; +import net.minecraft.server.Packet32EntityLook; +import net.minecraft.server.Packet33RelEntityMoveLook; +import net.minecraft.server.Packet34EntityTeleport; +import net.minecraft.server.Packet35EntityHeadRotation; +import net.minecraft.server.Packet38EntityStatus; +import net.minecraft.server.Packet39AttachEntity; +import net.minecraft.server.Packet3Chat; +import net.minecraft.server.Packet40EntityMetadata; +import net.minecraft.server.Packet41MobEffect; +import net.minecraft.server.Packet42RemoveMobEffect; +import net.minecraft.server.Packet43SetExperience; +import net.minecraft.server.Packet4UpdateTime; +import net.minecraft.server.Packet51MapChunk; +import net.minecraft.server.Packet52MultiBlockChange; +import net.minecraft.server.Packet53BlockChange; +import net.minecraft.server.Packet54PlayNoteBlock; +import net.minecraft.server.Packet55BlockBreakAnimation; +import net.minecraft.server.Packet56MapChunkBulk; +import net.minecraft.server.Packet5EntityEquipment; +import net.minecraft.server.Packet60Explosion; +import net.minecraft.server.Packet61WorldEvent; +import net.minecraft.server.Packet62NamedSoundEffect; +import net.minecraft.server.Packet6SpawnPosition; +import net.minecraft.server.Packet70Bed; +import net.minecraft.server.Packet71Weather; +import net.minecraft.server.Packet7UseEntity; +import net.minecraft.server.Packet8UpdateHealth; +import net.minecraft.server.Packet9Respawn; + +public class VanillaPackets { + + public static Packet[] packets = new Packet[256]; + + private static void map(int id, Class clazz) { + try { + packets[id] = (Packet) clazz.getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException ex) { + $().severe("Could not register packet id " + Util.hex(id)); + } + } + + static { + map(0, Packet0KeepAlive.class); + map(1, Packet1Login.class); + map(2, Packet2Handshake.class); + map(3, Packet3Chat.class); + map(4, Packet4UpdateTime.class); + map(5, Packet5EntityEquipment.class); + map(6, Packet6SpawnPosition.class); + map(7, Packet7UseEntity.class); + map(8, Packet8UpdateHealth.class); + map(9, Packet9Respawn.class); + map(10, Packet10Flying.class); + map(11, Packet11PlayerPosition.class); + map(12, Packet12PlayerLook.class); + map(13, Packet13PlayerLookMove.class); + map(14, Packet14BlockDig.class); + map(15, Packet15Place.class); + map(16, Packet16BlockItemSwitch.class); + map(17, Packet17EntityLocationAction.class); + map(18, Packet18ArmAnimation.class); + map(19, Packet19EntityAction.class); + map(20, Packet20NamedEntitySpawn.class); + map(21, Packet21PickupSpawn.class); + map(22, Packet22Collect.class); + map(23, Packet23VehicleSpawn.class); + map(24, Packet24MobSpawn.class); + map(25, Packet25EntityPainting.class); + map(26, Packet26AddExpOrb.class); + map(28, Packet28EntityVelocity.class); + map(29, Packet29DestroyEntity.class); + map(30, Packet30Entity.class); + map(31, Packet31RelEntityMove.class); + map(32, Packet32EntityLook.class); + map(33, Packet33RelEntityMoveLook.class); + map(34, Packet34EntityTeleport.class); + map(35, Packet35EntityHeadRotation.class); + map(38, Packet38EntityStatus.class); + map(39, Packet39AttachEntity.class); + map(40, Packet40EntityMetadata.class); + map(41, Packet41MobEffect.class); + map(42, Packet42RemoveMobEffect.class); + map(43, Packet43SetExperience.class); + map(51, Packet51MapChunk.class); + map(52, Packet52MultiBlockChange.class); + map(53, Packet53BlockChange.class); + map(54, Packet54PlayNoteBlock.class); + map(55, Packet55BlockBreakAnimation.class); + map(56, Packet56MapChunkBulk.class); + map(60, Packet60Explosion.class); + map(61, Packet61WorldEvent.class); + map(62, Packet62NamedSoundEffect.class); + map(70, Packet70Bed.class); + map(71, Packet71Weather.class); + map(100, Packet100OpenWindow.class); + map(101, Packet101CloseWindow.class); + map(102, Packet102WindowClick.class); + map(103, Packet103SetSlot.class); + map(104, Packet104WindowItems.class); + map(105, Packet105CraftProgressBar.class); + map(106, Packet106Transaction.class); + map(107, Packet107SetCreativeSlot.class); + map(108, Packet108ButtonClick.class); + map(130, Packet130UpdateSign.class); + map(131, Packet131ItemData.class); + map(132, Packet132TileEntityData.class); + map(200, Packet200Statistic.class); + map(201, Packet201PlayerInfo.class); + map(202, Packet202Abilities.class); + map(203, Packet203TabComplete.class); + map(204, Packet204LocaleAndViewDistance.class); + map(205, Packet205ClientCommand.class); + map(250, Packet250CustomPayload.class); + map(252, Packet252KeyResponse.class); + map(253, Packet253KeyRequest.class); + map(254, Packet254GetInfo.class); + map(255, Packet255KickDisconnect.class); + } +}