diff --git a/src/main/java/net/md_5/bungee/BungeeCord.java b/src/main/java/net/md_5/bungee/BungeeCord.java index af860b9e..d665a559 100644 --- a/src/main/java/net/md_5/bungee/BungeeCord.java +++ b/src/main/java/net/md_5/bungee/BungeeCord.java @@ -5,9 +5,17 @@ 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.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static net.md_5.bungee.Logger.$; +import net.md_5.bungee.command.Command; +import net.md_5.bungee.command.CommandEnd; +import net.md_5.bungee.command.CommandSender; +import net.md_5.bungee.command.ConsoleCommandSender; public class BungeeCord { @@ -39,6 +47,18 @@ public class BungeeCord { * Current version. */ private String version = (getClass().getPackage().getImplementationVersion() == null) ? "unknown" : getClass().getPackage().getImplementationVersion(); + /** + * Fully qualified connections. + */ + public Map connections = new ConcurrentHashMap<>(); + /** + * Registered commands. + */ + private Map commandMap = new HashMap<>(); + + { + commandMap.put("end", new CommandEnd()); + } public static void main(String[] args) throws IOException { System.out.println(Util.hex(15)); @@ -50,13 +70,34 @@ public class BungeeCord { while (instance.isRunning) { String line = br.readLine(); if (line != null) { - if (line.equals("end")) { - instance.stop(); + boolean handled = instance.dispatchCommand(line, ConsoleCommandSender.instance); + if (!handled) { + System.err.println("Command not found"); } } } } + public boolean dispatchCommand(String commandLine, CommandSender sender) { + String[] split = commandLine.trim().split(" "); + String commandName = split[0].toLowerCase(); + if (commandMap.containsKey(commandName)) { + String[] args = Arrays.copyOfRange(split, 1, split.length); + Command c = commandMap.get(commandName); + try { + c.execute(sender, args); + } catch (Exception ex) { + sender.sendMessage(ChatColor.RED + "An error occurred while executing this command!"); + System.err.println("----------------------- [Start of command error] -----------------------"); + ex.printStackTrace(); + System.err.println("----------------------- [End of command error] -----------------------"); + } + return true; + } else { + return false; + } + } + public void start() throws IOException { config.load(); isRunning = true; @@ -83,8 +124,10 @@ public class BungeeCord { $().info("Closing pending connections"); threadPool.shutdown(); - $().info("Disconnecting " + "x" + " connections"); - // TODO: Kick everyone + $().info("Disconnecting " + connections.size() + " connections"); + for (UserConnection user : connections.values()) { + user.disconnect("Proxy restarting, brb."); + } $().info("Saving reconnect locations"); saveThread.interrupt(); @@ -96,10 +139,6 @@ public class BungeeCord { $().info("Thank you and goodbye"); } - public int getOnlinePlayers() { - return 123; - } - public void setSocketOptions(Socket socket) throws IOException { socket.setSoTimeout(config.timeout); socket.setTrafficClass(0x18); diff --git a/src/main/java/net/md_5/bungee/Configuration.java b/src/main/java/net/md_5/bungee/Configuration.java index 6f956eea..3f949b49 100644 --- a/src/main/java/net/md_5/bungee/Configuration.java +++ b/src/main/java/net/md_5/bungee/Configuration.java @@ -92,6 +92,10 @@ public class Configuration { add("mbaxter"); } }; + /** + * Maximum number of lines to log before old ones are removed. + */ + public int logNumLines = 1 << 14; public void load() { try { @@ -124,7 +128,6 @@ public class Configuration { } } } - $().info("-----------------------------------------------------"); if (servers.get(defaultServerName) == null) { @@ -190,7 +193,7 @@ public class Configuration { if (hostline != null) { return Util.getAddr(hostline); } else { - return null; + throw new IllegalArgumentException("No server by name " + name); } } diff --git a/src/main/java/net/md_5/bungee/EncryptionUtil.java b/src/main/java/net/md_5/bungee/EncryptionUtil.java index 26fe790f..1a031a8f 100644 --- a/src/main/java/net/md_5/bungee/EncryptionUtil.java +++ b/src/main/java/net/md_5/bungee/EncryptionUtil.java @@ -6,15 +6,24 @@ 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.SecureRandom; 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; @@ -28,6 +37,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; public class EncryptionUtil { + private static final Random secure = new SecureRandom(); private static final Random random = new Random(); private static KeyPair keys; @@ -86,4 +96,26 @@ public class EncryptionUtil { cip.init(forEncryption, new ParametersWithIV(new KeyParameter(shared.getEncoded()), shared.getEncoded(), 0, 16)); return cip; } + + public static SecretKey getSecret() { + byte[] rand = new byte[32]; + secure.nextBytes(rand); + return new SecretKeySpec(rand, "AES"); + } + + 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()); + } } diff --git a/src/main/java/net/md_5/bungee/GenericConnection.java b/src/main/java/net/md_5/bungee/GenericConnection.java new file mode 100644 index 00000000..d9be636d --- /dev/null +++ b/src/main/java/net/md_5/bungee/GenericConnection.java @@ -0,0 +1,31 @@ +package net.md_5.bungee; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import net.md_5.bungee.packet.PacketFFKick; +import net.md_5.bungee.packet.PacketInputStream; + +@EqualsAndHashCode +@RequiredArgsConstructor +public class GenericConnection { + + private final Socket socket; + private final PacketInputStream in; + private final OutputStream out; + + public void disconnect(String reason) { + try { + out.write(new PacketFFKick(reason).getPacket()); + } catch (IOException ex) { + } finally { + try { + out.flush(); + socket.close(); + } catch (IOException ioe) { + } + } + } +} diff --git a/src/main/java/net/md_5/bungee/InitialHandler.java b/src/main/java/net/md_5/bungee/InitialHandler.java index 7a39b4b7..4e073c00 100644 --- a/src/main/java/net/md_5/bungee/InitialHandler.java +++ b/src/main/java/net/md_5/bungee/InitialHandler.java @@ -53,7 +53,7 @@ public class InitialHandler implements Runnable { break; case 0xFE: - throw new KickException(BungeeCord.instance.config.motd + ChatColor.COLOR_CHAR + BungeeCord.instance.getOnlinePlayers() + ChatColor.COLOR_CHAR + BungeeCord.instance.config.maxPlayers); + throw new KickException(BungeeCord.instance.config.motd + ChatColor.COLOR_CHAR + BungeeCord.instance.connections.size() + ChatColor.COLOR_CHAR + BungeeCord.instance.config.maxPlayers); default: throw new IllegalArgumentException("Wasn't ready for packet id " + Util.hex(id)); } diff --git a/src/main/java/net/md_5/bungee/ServerConnection.java b/src/main/java/net/md_5/bungee/ServerConnection.java new file mode 100644 index 00000000..814def7c --- /dev/null +++ b/src/main/java/net/md_5/bungee/ServerConnection.java @@ -0,0 +1,55 @@ +package net.md_5.bungee; + +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.PublicKey; +import javax.crypto.SecretKey; +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.PacketFCEncryptionResponse; +import net.md_5.bungee.packet.PacketFDEncryptionRequest; +import net.md_5.bungee.packet.PacketInputStream; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.io.CipherOutputStream; + +public class ServerConnection extends GenericConnection { + + private final Packet1Login loginPacket; + + public ServerConnection(Socket socket, PacketInputStream in, OutputStream out, Packet1Login loginPacket) { + super(socket, in, out); + this.loginPacket = loginPacket; + } + + public static ServerConnection connect(InetSocketAddress address, Packet2Handshake handshake) throws Exception { + 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)); + + 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)); + + out.write(new PacketCDClientStatus((byte) 0).getPacket()); + Packet1Login login = new Packet1Login(in.readPacket()); + + return new ServerConnection(socket, in, out, login); + } +} diff --git a/src/main/java/net/md_5/bungee/UserConnection.java b/src/main/java/net/md_5/bungee/UserConnection.java new file mode 100644 index 00000000..617498e2 --- /dev/null +++ b/src/main/java/net/md_5/bungee/UserConnection.java @@ -0,0 +1,16 @@ +package net.md_5.bungee; + +import java.io.OutputStream; +import java.net.Socket; +import net.md_5.bungee.packet.Packet2Handshake; +import net.md_5.bungee.packet.PacketInputStream; + +public class UserConnection extends GenericConnection { + + private final Packet2Handshake handshake; + + public UserConnection(Socket socket, PacketInputStream in, OutputStream out, Packet2Handshake handshake) { + super(socket, in, out); + this.handshake = handshake; + } +} diff --git a/src/main/java/net/md_5/bungee/command/Command.java b/src/main/java/net/md_5/bungee/command/Command.java new file mode 100644 index 00000000..3b90d0c8 --- /dev/null +++ b/src/main/java/net/md_5/bungee/command/Command.java @@ -0,0 +1,31 @@ +package net.md_5.bungee.command; + +import net.md_5.bungee.ChatColor; + +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); + + /** + * Check if the arg count is at least the specified amount + * + * @param sender sender to send message to if unsuccessful + * @param args to check + * @param count to compare + * @return if the arguments are valid + */ + public final boolean testArgs(CommandSender sender, String[] args, int count) { + boolean valid = args.length >= count; + if (!valid) { + sender.sendMessage(ChatColor.RED + "Please review your argument count"); + } + return valid; + } +} diff --git a/src/main/java/net/md_5/bungee/command/CommandEnd.java b/src/main/java/net/md_5/bungee/command/CommandEnd.java new file mode 100644 index 00000000..d0479e85 --- /dev/null +++ b/src/main/java/net/md_5/bungee/command/CommandEnd.java @@ -0,0 +1,11 @@ +package net.md_5.bungee.command; + +import net.md_5.bungee.BungeeCord; + +public class CommandEnd extends Command { + + @Override + public void execute(CommandSender sender, String[] args) { + BungeeCord.instance.stop(); + } +} diff --git a/src/main/java/net/md_5/bungee/command/CommandSender.java b/src/main/java/net/md_5/bungee/command/CommandSender.java new file mode 100644 index 00000000..f89c3148 --- /dev/null +++ b/src/main/java/net/md_5/bungee/command/CommandSender.java @@ -0,0 +1,11 @@ +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); +} diff --git a/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java b/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java new file mode 100644 index 00000000..debfcb36 --- /dev/null +++ b/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java @@ -0,0 +1,13 @@ +package net.md_5.bungee.command; + +import net.md_5.bungee.ChatColor; + +public class ConsoleCommandSender implements CommandSender { + + public static final ConsoleCommandSender instance = new ConsoleCommandSender(); + + @Override + public void sendMessage(String message) { + System.out.println(ChatColor.stripColor(message)); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/PacketCDClientStatus.java b/src/main/java/net/md_5/bungee/packet/PacketCDClientStatus.java new file mode 100644 index 00000000..0d64b998 --- /dev/null +++ b/src/main/java/net/md_5/bungee/packet/PacketCDClientStatus.java @@ -0,0 +1,19 @@ +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); + super.writeByte(payload); + } +} diff --git a/src/main/java/net/md_5/bungee/packet/PacketFCEncryptionResponse.java b/src/main/java/net/md_5/bungee/packet/PacketFCEncryptionResponse.java index 0e0112a3..8a6c3d21 100644 --- a/src/main/java/net/md_5/bungee/packet/PacketFCEncryptionResponse.java +++ b/src/main/java/net/md_5/bungee/packet/PacketFCEncryptionResponse.java @@ -16,6 +16,12 @@ public class PacketFCEncryptionResponse extends DefinedPacket { 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(); diff --git a/src/main/java/net/md_5/bungee/packet/PacketFDEncryptionRequest.java b/src/main/java/net/md_5/bungee/packet/PacketFDEncryptionRequest.java index d35752dd..c2682aad 100644 --- a/src/main/java/net/md_5/bungee/packet/PacketFDEncryptionRequest.java +++ b/src/main/java/net/md_5/bungee/packet/PacketFDEncryptionRequest.java @@ -20,4 +20,11 @@ public class PacketFDEncryptionRequest extends DefinedPacket { this.publicKey = publicKey; this.verifyToken = verifyToken; } + + public PacketFDEncryptionRequest(byte[] buf) { + super(0xFD, buf); + serverId = readUTF(); + publicKey = readArray(); + verifyToken = readArray(); + } }