diff --git a/MinecraftClient/McTcpClient.cs b/MinecraftClient/McTcpClient.cs index fed884a1..e1e5c6bd 100644 --- a/MinecraftClient/McTcpClient.cs +++ b/MinecraftClient/McTcpClient.cs @@ -8,6 +8,7 @@ using System.IO; using System.Net; using MinecraftClient.Protocol; using MinecraftClient.Proxy; +using MinecraftClient.Protocol.Handlers.Forge; namespace MinecraftClient { @@ -54,9 +55,9 @@ namespace MinecraftClient /// The server port to use /// Minecraft protocol version to use - public McTcpClient(string username, string uuid, string sessionID, int protocolversion, string server_ip, ushort port) + public McTcpClient(string username, string uuid, string sessionID, int protocolversion, ForgeInfo forgeInfo, string server_ip, ushort port) { - StartClient(username, uuid, sessionID, server_ip, port, protocolversion, false, ""); + StartClient(username, uuid, sessionID, server_ip, port, protocolversion, forgeInfo, false, ""); } /// @@ -70,9 +71,9 @@ namespace MinecraftClient /// Minecraft protocol version to use /// The text or command to send. - public McTcpClient(string username, string uuid, string sessionID, string server_ip, ushort port, int protocolversion, string command) + public McTcpClient(string username, string uuid, string sessionID, string server_ip, ushort port, int protocolversion, ForgeInfo forgeInfo, string command) { - StartClient(username, uuid, sessionID, server_ip, port, protocolversion, true, command); + StartClient(username, uuid, sessionID, server_ip, port, protocolversion, forgeInfo, true, command); } /// @@ -87,7 +88,7 @@ namespace MinecraftClient /// If set to true, the client will send a single command and then disconnect from the server /// The text or command to send. Will only be sent if singlecommand is set to true. - private void StartClient(string user, string uuid, string sessionID, string server_ip, ushort port, int protocolversion, bool singlecommand, string command) + private void StartClient(string user, string uuid, string sessionID, string server_ip, ushort port, int protocolversion, ForgeInfo forgeInfo, bool singlecommand, string command) { bool retry = false; this.sessionid = sessionID; @@ -113,7 +114,7 @@ namespace MinecraftClient { client = ProxyHandler.newTcpClient(host, port); client.ReceiveBufferSize = 1024 * 1024; - handler = Protocol.ProtocolHandler.getProtocolHandler(client, protocolversion, this); + handler = Protocol.ProtocolHandler.getProtocolHandler(client, protocolversion, forgeInfo, this); Console.WriteLine("Version is supported.\nLogging in..."); try diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 9e1fc0c7..9c09a5e1 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -114,6 +114,9 @@ + + + diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index a9e4575d..302e50bb 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -5,6 +5,7 @@ using System.Text; using MinecraftClient.Protocol; using System.Reflection; using System.Threading; +using MinecraftClient.Protocol.Handlers.Forge; namespace MinecraftClient { @@ -145,6 +146,7 @@ namespace MinecraftClient //Get server version int protocolversion = 0; + ForgeInfo forgeInfo = null; if (Settings.ServerVersion != "" && Settings.ServerVersion.ToLower() != "auto") { @@ -166,7 +168,7 @@ namespace MinecraftClient if (protocolversion == 0) { Console.WriteLine("Retrieving Server Info..."); - if (!ProtocolHandler.GetServerInfo(Settings.ServerIP, Settings.ServerPort, ref protocolversion)) + if (!ProtocolHandler.GetServerInfo(Settings.ServerIP, Settings.ServerPort, ref protocolversion, ref forgeInfo)) { HandleFailure("Failed to ping this IP.", true, ChatBots.AutoRelog.DisconnectReason.ConnectionLost); return; @@ -180,9 +182,9 @@ namespace MinecraftClient //Start the main TCP client if (Settings.SingleCommand != "") { - Client = new McTcpClient(Settings.Username, UUID, sessionID, Settings.ServerIP, Settings.ServerPort, protocolversion, Settings.SingleCommand); + Client = new McTcpClient(Settings.Username, UUID, sessionID, Settings.ServerIP, Settings.ServerPort, protocolversion, forgeInfo, Settings.SingleCommand); } - else Client = new McTcpClient(Settings.Username, UUID, sessionID, protocolversion, Settings.ServerIP, Settings.ServerPort); + else Client = new McTcpClient(Settings.Username, UUID, sessionID, protocolversion, forgeInfo, Settings.ServerIP, Settings.ServerPort); //Update console title if (Settings.ConsoleTitle != "") diff --git a/MinecraftClient/Protocol/Handlers/Forge/FMLHandshakeClientState.cs b/MinecraftClient/Protocol/Handlers/Forge/FMLHandshakeClientState.cs new file mode 100755 index 00000000..a72b4ad5 --- /dev/null +++ b/MinecraftClient/Protocol/Handlers/Forge/FMLHandshakeClientState.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient.Protocol.Handlers.Forge +{ + /// + /// Copy of the forge enum for client states. + /// https://github.com/MinecraftForge/MinecraftForge/blob/ebe9b6d4cbc4a5281c386994f1fbda04df5d2e1f/src/main/java/net/minecraftforge/fml/common/network/handshake/FMLHandshakeClientState.java + /// + enum FMLHandshakeClientState : byte + { + START, + HELLO, + WAITINGSERVERDATA, + WAITINGSERVERCOMPLETE, + PENDINGCOMPLETE, + COMPLETE, + DONE, + ERROR + } +} diff --git a/MinecraftClient/Protocol/Handlers/Forge/FMLHandshakeDiscriminator.cs b/MinecraftClient/Protocol/Handlers/Forge/FMLHandshakeDiscriminator.cs new file mode 100755 index 00000000..2402eff0 --- /dev/null +++ b/MinecraftClient/Protocol/Handlers/Forge/FMLHandshakeDiscriminator.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient.Protocol.Handlers.Forge +{ + /// + /// Different "discriminator byte" values for the forge handshake. + /// https://github.com/MinecraftForge/MinecraftForge/blob/ebe9b6d4cbc4a5281c386994f1fbda04df5d2e1f/src/main/java/net/minecraftforge/fml/common/network/handshake/FMLHandshakeCodec.java + /// + enum FMLHandshakeDiscriminator : byte + { + ServerHello = 0, + ClientHello = 1, + ModList = 2, + RegistryData = 3, + HandshakeAck = 255, //-1 + HandshakeReset = 254, //-2 + } +} diff --git a/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs b/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs new file mode 100755 index 00000000..03180432 --- /dev/null +++ b/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient.Protocol.Handlers.Forge +{ + /// + /// Contains information about a modded server install. + /// + public class ForgeInfo + { + /// + /// Represents an individual forge mod. + /// + public class ForgeMod + { + public ForgeMod(String ModID, String Version) + { + this.ModID = ModID; + this.Version = Version; + } + + public readonly String ModID; + public readonly String Version; + + public override string ToString() + { + return ModID + " v" + Version; + } + } + + public List Mods; + + /// + /// Create a new ForgeInfo from the given data. + /// + /// The modinfo JSON tag. + internal ForgeInfo(Json.JSONData data) + { + // Example ModInfo (with spacing): + + // "modinfo": { + // "type": "FML", + // "modList": [{ + // "modid": "mcp", + // "version": "9.05" + // }, { + // "modid": "FML", + // "version": "8.0.99.99" + // }, { + // "modid": "Forge", + // "version": "11.14.3.1512" + // }, { + // "modid": "rpcraft", + // "version": "Beta 1.3 - 1.8.0" + // }] + // } + + this.Mods = new List(); + foreach (Json.JSONData mod in data.Properties["modList"].DataArray) + { + String modid = mod.Properties["modid"].StringValue; + String version = mod.Properties["version"].StringValue; + + this.Mods.Add(new ForgeMod(modid, version)); + } + } + } +} diff --git a/MinecraftClient/Protocol/Handlers/Protocol16.cs b/MinecraftClient/Protocol/Handlers/Protocol16.cs index 1813b9af..89cedbe6 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol16.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol16.cs @@ -615,6 +615,31 @@ namespace MinecraftClient.Protocol.Handlers return false; //Only supported since MC 1.7 } + /// + /// Send a plugin channel packet to the server. + /// + /// Channel to send packet on + /// packet Data + + public bool SendPluginChannelPacket(string channel, byte[] data) + { + try { + byte[] channelLength = BitConverter.GetBytes((short)channel.Length); + Array.Reverse(channelLength); + + byte[] channelData = Encoding.BigEndianUnicode.GetBytes(channel); + + byte[] dataLength = BitConverter.GetBytes((short)data.Length); + Array.Reverse(dataLength); + + Send(concatBytes(new byte[] { 0xFA }, channelLength, channelData, dataLength, data)); + + return true; + } + catch (SocketException) { return false; } + catch (System.IO.IOException) { return false; } + } + public string AutoComplete(string BehindCursor) { if (String.IsNullOrEmpty(BehindCursor)) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index f5fe4071..a9631206 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -7,6 +7,7 @@ using System.Threading; using MinecraftClient.Crypto; using MinecraftClient.Proxy; using System.Security.Cryptography; +using MinecraftClient.Protocol.Handlers.Forge; namespace MinecraftClient.Protocol.Handlers { @@ -17,7 +18,7 @@ namespace MinecraftClient.Protocol.Handlers class Protocol18Handler : IMinecraftCom { private const int MC18Version = 47; - + private int compression_treshold = 0; private bool autocomplete_received = false; private string autocomplete_result = ""; @@ -25,18 +26,23 @@ namespace MinecraftClient.Protocol.Handlers private bool encrypted = false; private int protocolversion; + // Server forge info -- may be null. + private ForgeInfo forgeInfo; + private FMLHandshakeClientState fmlHandshakeState = FMLHandshakeClientState.START; + IMinecraftComHandler handler; Thread netRead; IAesStream s; TcpClient c; - public Protocol18Handler(TcpClient Client, int ProtocolVersion, IMinecraftComHandler Handler) + public Protocol18Handler(TcpClient Client, int ProtocolVersion, IMinecraftComHandler Handler, ForgeInfo ForgeInfo) { ConsoleIO.SetAutoCompleteEngine(this); ChatParser.InitTranslations(); this.c = Client; this.protocolversion = ProtocolVersion; this.handler = Handler; + this.forgeInfo = ForgeInfo; } private Protocol18Handler(TcpClient Client) @@ -133,94 +139,234 @@ namespace MinecraftClient.Protocol.Handlers return false; //Ignored packet } } - else //Regular in-game packets + // Regular in-game packets + + switch (packetID) { - switch (packetID) - { - case 0x00: //Keep-Alive - SendPacket(0x00, packetData); - break; - case 0x01: //Join game - handler.OnGameJoined(); - break; - case 0x02: //Chat message - string message = readNextString(ref packetData); - try + case 0x00: //Keep-Alive + SendPacket(0x00, packetData); + break; + case 0x01: //Join game + handler.OnGameJoined(); + break; + case 0x02: //Chat message + string message = readNextString(ref packetData); + try + { + //Hide system messages or xp bar messages? + byte messageType = readData(1, ref packetData)[0]; + if ((messageType == 1 && !Settings.DisplaySystemMessages) + || (messageType == 2 && !Settings.DisplayXPBarMessages)) + break; + } + catch (IndexOutOfRangeException) { /* No message type */ } + handler.OnTextReceived(ChatParser.ParseText(message)); + break; + case 0x38: //Player List update + if (protocolversion >= MC18Version) + { + int action = readNextVarInt(ref packetData); + int numActions = readNextVarInt(ref packetData); + for (int i = 0; i < numActions; i++) { - //Hide system messages or xp bar messages? - byte messageType = readData(1, ref packetData)[0]; - if ((messageType == 1 && !Settings.DisplaySystemMessages) - || (messageType == 2 && !Settings.DisplayXPBarMessages)) - break; - } - catch (IndexOutOfRangeException) { /* No message type */ } - handler.OnTextReceived(ChatParser.ParseText(message)); - break; - case 0x38: //Player List update - if (protocolversion >= MC18Version) - { - int action = readNextVarInt(ref packetData); - int numActions = readNextVarInt(ref packetData); - for (int i = 0; i < numActions; i++) + Guid uuid = readNextUUID(ref packetData); + switch (action) { - Guid uuid = readNextUUID(ref packetData); - switch (action) - { - case 0x00: //Player Join - string name = readNextString(ref packetData); - handler.OnPlayerJoin(uuid, name); - break; - case 0x04: //Player Leave - handler.OnPlayerLeave(uuid); - break; - default: - //Unknown player list item type - break; - } + case 0x00: //Player Join + string name = readNextString(ref packetData); + handler.OnPlayerJoin(uuid, name); + break; + case 0x04: //Player Leave + handler.OnPlayerLeave(uuid); + break; + default: + //Unknown player list item type + break; } } - else //MC 1.7.X does not provide UUID in tab-list updates + } + else //MC 1.7.X does not provide UUID in tab-list updates + { + string name = readNextString(ref packetData); + bool online = readNextBool(ref packetData); + short ping = readNextShort(ref packetData); + Guid FakeUUID = new Guid(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(name)).Take(16).ToArray()); + if (online) + handler.OnPlayerJoin(FakeUUID, name); + else handler.OnPlayerLeave(FakeUUID); + } + break; + case 0x3A: //Tab-Complete Result + int autocomplete_count = readNextVarInt(ref packetData); + string tab_list = ""; + for (int i = 0; i < autocomplete_count; i++) + { + autocomplete_result = readNextString(ref packetData); + if (autocomplete_result != "") + tab_list = tab_list + autocomplete_result + " "; + } + autocomplete_received = true; + tab_list = tab_list.Trim(); + if (tab_list.Length > 0) + ConsoleIO.WriteLineFormatted("§8" + tab_list, false); + break; + case 0x3F: //Plugin message. + String channel = readNextString(ref packetData); + if (protocolversion < MC18Version) + { + if (forgeInfo == null) { - string name = readNextString(ref packetData); - bool online = readNextBool(ref packetData); - short ping = readNextShort(ref packetData); - Guid FakeUUID = new Guid(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(name)).Take(16).ToArray()); - if (online) - handler.OnPlayerJoin(FakeUUID, name); - else handler.OnPlayerLeave(FakeUUID); + // 1.7 and lower prefix plugin channel packets with the length. + // We can skip it, though. + readNextShort(ref packetData); } - break; - case 0x3A: //Tab-Complete Result - int autocomplete_count = readNextVarInt(ref packetData); - string tab_list = ""; - for (int i = 0; i < autocomplete_count; i++) + else { - autocomplete_result = readNextString(ref packetData); - if (autocomplete_result != "") - tab_list = tab_list + autocomplete_result + " "; + // Forge does something even weirder with the length. + readNextVarShort(ref packetData); } - autocomplete_received = true; - tab_list = tab_list.Trim(); - if (tab_list.Length > 0) - ConsoleIO.WriteLineFormatted("§8" + tab_list, false); - break; - case 0x40: //Kick Packet - handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, ChatParser.ParseText(readNextString(ref packetData))); - return false; - case 0x46: //Network Compression Treshold Info - if (protocolversion >= MC18Version) - compression_treshold = readNextVarInt(ref packetData); - break; - case 0x48: //Resource Pack Send - string url = readNextString(ref packetData); - string hash = readNextString(ref packetData); - //Send back "accepted" and "successfully loaded" responses for plugins making use of resource pack mandatory - SendPacket(0x19, concatBytes(getVarInt(hash.Length), Encoding.UTF8.GetBytes(hash), getVarInt(3))); - SendPacket(0x19, concatBytes(getVarInt(hash.Length), Encoding.UTF8.GetBytes(hash), getVarInt(0))); - break; - default: - return false; //Ignored packet - } + } + if (forgeInfo != null) + { + if (channel == "FML|HS") + { + FMLHandshakeDiscriminator discriminator = (FMLHandshakeDiscriminator)readNextByte(ref packetData); + + if (discriminator == FMLHandshakeDiscriminator.HandshakeReset) + { + fmlHandshakeState = FMLHandshakeClientState.START; + return true; + } + + switch (fmlHandshakeState) + { + case FMLHandshakeClientState.START: + if (discriminator != FMLHandshakeDiscriminator.ServerHello) + return false; + + // Send the plugin channel registration. + // REGISTER is somewhat special in that it doesn't actually include length information, + // and is also \0-separated. + // Also, yes, "FML" is there twice. Don't ask me why, but that's the way forge does it. + string[] channels = { "FML|HS", "FML", "FML|MP", "FML", "FORGE" }; + SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels))); + + byte fmlProtocolVersion = readNextByte(ref packetData); + // There's another value afterwards for the dimension, but we don't need it. + + ConsoleIO.WriteLineFormatted("§8Forge protocol version : " + fmlProtocolVersion); + + // Tell the server we're running the same version. + SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion }); + + // Then tell the server that we're running the same mods. + ConsoleIO.WriteLineFormatted("§8Sending falsified mod list to server..."); + byte[][] mods = new byte[forgeInfo.Mods.Count][]; + for (int i = 0; i < forgeInfo.Mods.Count; i++) + { + ForgeInfo.ForgeMod mod = forgeInfo.Mods[i]; + mods[i] = concatBytes(getString(mod.ModID), getString(mod.Version)); + } + SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList, + concatBytes(getVarInt(forgeInfo.Mods.Count), concatBytes(mods))); + + fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERDATA; + + return true; + case FMLHandshakeClientState.WAITINGSERVERDATA: + if (discriminator != FMLHandshakeDiscriminator.ModList) + return false; + + Thread.Sleep(2000); + + ConsoleIO.WriteLineFormatted("§8Accepting server mod list..."); + // Tell the server that yes, we are OK with the mods it has + // even though we don't actually care what mods it has. + + SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, + new byte[] { (byte)FMLHandshakeClientState.WAITINGSERVERDATA }); + + fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERCOMPLETE; + return false; + case FMLHandshakeClientState.WAITINGSERVERCOMPLETE: + // The server now will tell us a bunch of registry information. + // We need to read it all, though, until it says that there is no more. + if (discriminator != FMLHandshakeDiscriminator.RegistryData) + return false; + + if (protocolversion < MC18Version) + { + // 1.7.10 and below have one registry + // with blocks and items. + int registrySize = readNextVarInt(ref packetData); + + ConsoleIO.WriteLineFormatted("§8Received registry " + + "with " + registrySize + " entries"); + + fmlHandshakeState = FMLHandshakeClientState.PENDINGCOMPLETE; + } + else + { + // 1.8+ has more than one registry. + + bool hasNextRegistry = readNextBool(ref packetData); + string registryName = readNextString(ref packetData); + int registrySize = readNextVarInt(ref packetData); + + ConsoleIO.WriteLineFormatted("§8Received registry " + registryName + + " with " + registrySize + " entries"); + + if (!hasNextRegistry) + { + fmlHandshakeState = FMLHandshakeClientState.PENDINGCOMPLETE; + } + } + + return false; + case FMLHandshakeClientState.PENDINGCOMPLETE: + // The server will ask us to accept the registries. + // Just say yes. + if (discriminator != FMLHandshakeDiscriminator.HandshakeAck) + return false; + + ConsoleIO.WriteLineFormatted("§8Accepting server registries..."); + + SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, + new byte[] { (byte)FMLHandshakeClientState.PENDINGCOMPLETE }); + fmlHandshakeState = FMLHandshakeClientState.COMPLETE; + + return true; + case FMLHandshakeClientState.COMPLETE: + // One final "OK". On the actual forge source, a packet is sent from + // the client to the client saying that the connection was complete, but + // we don't need to do that. + + SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, + new byte[] { (byte)FMLHandshakeClientState.COMPLETE }); + ConsoleIO.WriteLine("Forge server connection complete!"); + + fmlHandshakeState = FMLHandshakeClientState.DONE; + return true; + } + } + } + return false; + case 0x40: //Kick Packet + handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, ChatParser.ParseText(readNextString(ref packetData))); + return false; + case 0x46: //Network Compression Treshold Info + if (protocolversion >= MC18Version) + compression_treshold = readNextVarInt(ref packetData); + break; + case 0x48: //Resource Pack Send + string url = readNextString(ref packetData); + string hash = readNextString(ref packetData); + //Send back "accepted" and "successfully loaded" responses for plugins making use of resource pack mandatory + SendPacket(0x19, concatBytes(getVarInt(hash.Length), Encoding.UTF8.GetBytes(hash), getVarInt(3))); + SendPacket(0x19, concatBytes(getVarInt(hash.Length), Encoding.UTF8.GetBytes(hash), getVarInt(0))); + break; + default: + return false; //Ignored packet } return true; //Packet processed } @@ -258,7 +404,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Amount of bytes to read /// The data read from the network as an array - + private byte[] readDataRAW(int offset) { if (offset > 0) @@ -273,7 +419,7 @@ namespace MinecraftClient.Protocol.Handlers } return new byte[] { }; } - + /// /// Read some data from a cache of bytes and remove it from the cache /// @@ -326,6 +472,18 @@ namespace MinecraftClient.Protocol.Handlers return BitConverter.ToInt16(rawValue, 0); } + /// + /// Read an unsigned short integer from a cache of bytes and remove it from the cache + /// + /// The unsigned short integer value + + private static ushort readNextUShort(ref byte[] cache) + { + byte[] rawValue = readData(2, ref cache); + Array.Reverse(rawValue); //Endianness + return BitConverter.ToUInt16(rawValue, 0); + } + /// /// Read a uuid from a cache of bytes and remove it from the cache /// @@ -372,7 +530,7 @@ namespace MinecraftClient.Protocol.Handlers } return i; } - + /// /// Read an integer from a cache of bytes and remove it from the cache /// @@ -396,6 +554,36 @@ namespace MinecraftClient.Protocol.Handlers return i; } + /// + /// Read an "extended short", which is actually an int of some kind, from the cache of bytes. + /// This is only done with forge. It looks like it's a normal short, except that if the high + /// bit is set, it has an extra byte. + /// + /// Cache of bytes to read from + /// The int + + private static int readNextVarShort(ref byte[] cache) + { + ushort low = readNextUShort(ref cache); + byte high = 0; + if ((low & 0x8000) != 0) + { + low &= 0x7FFF; + high = readNextByte(ref cache); + } + return ((high & 0xFF) << 15) | low; + } + + /// + /// Read a single byte from a cache of bytes and remove it from the cache + /// + /// The byte that was read + + private static byte readNextByte(ref byte[] cache) + { + return readData(1, ref cache)[0]; + } + /// /// Build an integer for sending over the network /// @@ -431,6 +619,19 @@ namespace MinecraftClient.Protocol.Handlers else return concatBytes(getVarInt(array.Length), array); } + /// + /// Get a byte array from the given string for sending over the network, with length information prepended. + /// + /// String to process + /// Array ready to send + + private byte[] getString(string text) + { + byte[] bytes = Encoding.UTF8.GetBytes(text); + + return concatBytes(getVarInt(bytes.Length), bytes); + } + /// /// Easily append several byte arrays /// @@ -473,6 +674,17 @@ namespace MinecraftClient.Protocol.Handlers } } + /// + /// Send a forge plugin channel packet ("FML|HS"). Compression and encryption will be handled automatically + /// + /// Discriminator to use. + /// packet Data + + private void SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data) + { + SendPluginChannelPacket("FML|HS", concatBytes(new byte[] { (byte)discriminator }, data)); + } + /// /// Send a packet to the server, compression and encryption will be handled automatically /// @@ -500,7 +712,7 @@ namespace MinecraftClient.Protocol.Handlers } } - SendRAW(concatBytes(getVarInt(the_packet.Length), the_packet)); + SendRAW(concatBytes(getVarInt(the_packet.Length), the_packet)); } /// @@ -525,7 +737,7 @@ namespace MinecraftClient.Protocol.Handlers public bool Login() { byte[] protocol_version = getVarInt(protocolversion); - byte[] server_adress_val = Encoding.UTF8.GetBytes(handler.GetServerHost()); + byte[] server_adress_val = Encoding.UTF8.GetBytes(handler.GetServerHost() + (forgeInfo != null ? "\0FML\0" : "")); byte[] server_adress_len = getVarInt(server_adress_val.Length); byte[] server_port = BitConverter.GetBytes((ushort)handler.GetServerPort()); Array.Reverse(server_port); byte[] next_state = getVarInt(2); @@ -560,6 +772,15 @@ namespace MinecraftClient.Protocol.Handlers { ConsoleIO.WriteLineFormatted("§8Server is in offline mode."); login_phase = false; + + if (forgeInfo != null) { + // Do the forge handshake. + if (!CompleteForgeHandshake()) + { + return false; + } + } + StartUpdating(); return true; //No need to check session or start encryption } @@ -567,6 +788,33 @@ namespace MinecraftClient.Protocol.Handlers } } + /// + /// Completes the Minecraft Forge handshake. + /// + /// Whether the handshake was successful. + private bool CompleteForgeHandshake() + { + int packetID = -1; + byte[] packetData = new byte[0]; + + while (fmlHandshakeState != FMLHandshakeClientState.DONE) + { + readNextPacket(ref packetID, ref packetData); + + if (packetID == 0x40) // Disconect + { + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(readNextString(ref packetData))); + return false; + } + else + { + handlePacket(packetID, packetData); + } + } + + return true; + } + /// /// Start network encryption. Automatically called by Login() if the server requests encryption. /// @@ -614,6 +862,16 @@ namespace MinecraftClient.Protocol.Handlers else if (packetID == 0x02) //Login successful { login_phase = false; + + if (forgeInfo != null) + { + // Do the forge handshake. + if (!CompleteForgeHandshake()) + { + return false; + } + } + StartUpdating(); return true; } @@ -669,13 +927,34 @@ namespace MinecraftClient.Protocol.Handlers { if (String.IsNullOrEmpty(brandInfo)) return false; + + return SendPluginChannelPacket("MC|Brand", getString(brandInfo)); + } + + /// + /// Send a plugin channel packet (0x17) to the server, compression and encryption will be handled automatically + /// + /// Channel to send packet on + /// packet Data + + public bool SendPluginChannelPacket(string channel, byte[] data) + { try { - byte[] channel = Encoding.UTF8.GetBytes("MC|Brand"); - byte[] channelLen = getVarInt(channel.Length); - byte[] brand = Encoding.UTF8.GetBytes(brandInfo); - byte[] brandLen = getVarInt(brand.Length); - SendPacket(0x17, concatBytes(channelLen, channel, brandLen, brand)); + // In 1.7, length needs to be included. + // In 1.8, it must not be. + if (protocolversion < MC18Version) + { + byte[] length = BitConverter.GetBytes((short)data.Length); + Array.Reverse(length); + + SendPacket(0x17, concatBytes(getString(channel), length, data)); + } + else + { + SendPacket(0x17, concatBytes(getString(channel), data)); + } + return true; } catch (SocketException) { return false; } @@ -730,7 +1009,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// True if ping was successful - public static bool doPing(string host, int port, ref int protocolversion) + public static bool doPing(string host, int port, ref int protocolversion, ref ForgeInfo forgeInfo) { string version = ""; TcpClient tcp = ProxyHandler.newTcpClient(host, port); @@ -760,26 +1039,43 @@ namespace MinecraftClient.Protocol.Handlers if (readNextVarInt(ref packetData) == 0x00) //Read Packet ID { string result = readNextString(ref packetData); //Get the Json data + if (!String.IsNullOrEmpty(result) && result.StartsWith("{") && result.EndsWith("}")) { Json.JSONData jsonData = Json.ParseJson(result); if (jsonData.Type == Json.JSONData.DataType.Object && jsonData.Properties.ContainsKey("version")) { - jsonData = jsonData.Properties["version"]; + Json.JSONData versionData = jsonData.Properties["version"]; //Retrieve display name of the Minecraft version - if (jsonData.Properties.ContainsKey("name")) - version = jsonData.Properties["name"].StringValue; + if (versionData.Properties.ContainsKey("name")) + version = versionData.Properties["name"].StringValue; //Retrieve protocol version number for handling this server - if (jsonData.Properties.ContainsKey("protocol")) - protocolversion = atoi(jsonData.Properties["protocol"].StringValue); + if (versionData.Properties.ContainsKey("protocol")) + protocolversion = atoi(versionData.Properties["protocol"].StringValue); //Automatic fix for BungeeCord 1.8 reporting itself as 1.7... if (protocolversion < 47 && version.Split(' ', '/').Contains("1.8")) protocolversion = ProtocolHandler.MCVer2ProtocolVersion("1.8.0"); ConsoleIO.WriteLineFormatted("§8Server version : " + version + " (protocol v" + protocolversion + ")."); + + // Check for forge on the server. + if (jsonData.Properties.ContainsKey("modinfo") && jsonData.Properties["modinfo"].Type == Json.JSONData.DataType.Object) + { + Json.JSONData modData = jsonData.Properties["modinfo"]; + if (modData.Properties.ContainsKey("type") && modData.Properties["type"].StringValue == "FML") + { + forgeInfo = new ForgeInfo(modData); + + ConsoleIO.WriteLineFormatted("§8Server is running forge. Mod list:"); + foreach (ForgeInfo.ForgeMod mod in forgeInfo.Mods) + { + ConsoleIO.WriteLineFormatted("§8 " + mod.ToString()); + } + } + } return true; } } diff --git a/MinecraftClient/Protocol/IMinecraftCom.cs b/MinecraftClient/Protocol/IMinecraftCom.cs index 9c351ebc..218f49d4 100644 --- a/MinecraftClient/Protocol/IMinecraftCom.cs +++ b/MinecraftClient/Protocol/IMinecraftCom.cs @@ -51,5 +51,16 @@ namespace MinecraftClient.Protocol /// True if brand info was successfully sent bool SendBrandInfo(string brandInfo); + + /// + /// Send a plugin channel packet to the server. + /// + /// http://dinnerbone.com/blog/2012/01/13/minecraft-plugin-channels-messaging/ + /// + /// Channel to send packet on + /// packet Data + /// True if message was successfully sent + + bool SendPluginChannelPacket(string channel, byte[] data); } } diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index dcd507d7..067ab4bf 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -6,6 +6,7 @@ using MinecraftClient.Protocol.Handlers; using MinecraftClient.Proxy; using System.Net.Sockets; using System.Net.Security; +using MinecraftClient.Protocol.Handlers.Forge; namespace MinecraftClient.Protocol { @@ -23,16 +24,17 @@ namespace MinecraftClient.Protocol /// Will contain protocol version, if ping successful /// TRUE if ping was successful - public static bool GetServerInfo(string serverIP, ushort serverPort, ref int protocolversion) + public static bool GetServerInfo(string serverIP, ushort serverPort, ref int protocolversion, ref ForgeInfo forgeInfo) { bool success = false; int protocolversionTmp = 0; + ForgeInfo forgeInfoTmp = null; if (AutoTimeout.Perform(() => { try { if (Protocol16Handler.doPing(serverIP, serverPort, ref protocolversionTmp) - || Protocol18Handler.doPing(serverIP, serverPort, ref protocolversionTmp)) + || Protocol18Handler.doPing(serverIP, serverPort, ref protocolversionTmp, ref forgeInfoTmp)) { success = true; } @@ -40,11 +42,12 @@ namespace MinecraftClient.Protocol } catch (Exception e) { - ConsoleIO.WriteLineFormatted("§8" + e.Message); + ConsoleIO.WriteLineFormatted("§8" + e.ToString()); } }, TimeSpan.FromSeconds(30))) { protocolversion = protocolversionTmp; + forgeInfo = forgeInfoTmp; return success; } else @@ -62,14 +65,14 @@ namespace MinecraftClient.Protocol /// Handler with the appropriate callbacks /// - public static IMinecraftCom getProtocolHandler(TcpClient Client, int ProtocolVersion, IMinecraftComHandler Handler) + public static IMinecraftCom getProtocolHandler(TcpClient Client, int ProtocolVersion, ForgeInfo forgeInfo, IMinecraftComHandler Handler) { int[] supportedVersions_Protocol16 = { 51, 60, 61, 72, 73, 74, 78 }; if (Array.IndexOf(supportedVersions_Protocol16, ProtocolVersion) > -1) return new Protocol16Handler(Client, ProtocolVersion, Handler); int[] supportedVersions_Protocol18 = { 4, 5, 47 }; if (Array.IndexOf(supportedVersions_Protocol18, ProtocolVersion) > -1) - return new Protocol18Handler(Client, ProtocolVersion, Handler); + return new Protocol18Handler(Client, ProtocolVersion, Handler, forgeInfo); throw new NotSupportedException("The protocol version no." + ProtocolVersion + " is not supported."); }