From a28409043ca66f811103e2cd57a6b8130baf7768 Mon Sep 17 00:00:00 2001 From: ORelio Date: Tue, 11 Aug 2020 12:52:38 +0200 Subject: [PATCH] Implement Forge FML2 protocol (MC 1.13+) (#1184) Forge uses a different handshake scheme in FML2 protocol. This handshake scheme uses LoginPluginRequest/Response packets. --- MinecraftClient/MinecraftClient.csproj | 1 + .../Protocol/Handlers/Forge/FMLVersion.cs | 17 ++ .../Protocol/Handlers/Forge/ForgeInfo.cs | 150 +++++------ .../Protocol/Handlers/Protocol18.cs | 30 ++- .../Protocol/Handlers/Protocol18Forge.cs | 239 ++++++++++++++++-- 5 files changed, 346 insertions(+), 91 deletions(-) create mode 100644 MinecraftClient/Protocol/Handlers/Forge/FMLVersion.cs diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 0b6964ed..b3f84dac 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -143,6 +143,7 @@ + diff --git a/MinecraftClient/Protocol/Handlers/Forge/FMLVersion.cs b/MinecraftClient/Protocol/Handlers/Forge/FMLVersion.cs new file mode 100644 index 00000000..8cf4de09 --- /dev/null +++ b/MinecraftClient/Protocol/Handlers/Forge/FMLVersion.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient.Protocol.Handlers.Forge +{ + /// + /// Version of the FML protocol + /// + /// + enum FMLVersion + { + FML, + FML2 + } +} diff --git a/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs b/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs index 088049b8..cd9d3732 100755 --- a/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs +++ b/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs @@ -31,92 +31,94 @@ namespace MinecraftClient.Protocol.Handlers.Forge } public List Mods; + internal FMLVersion Version; /// /// Create a new ForgeInfo from the given data. /// /// The modinfo JSON tag. - /// Thrown on missing mod list in JSON data - internal ForgeInfo(Json.JSONData data) + /// Forge protocol version + internal ForgeInfo(Json.JSONData data, FMLVersion fmlVersion) { this.Mods = new List(); - bool listFound = false; + this.Version = fmlVersion; - // Example ModInfo for Minecraft 1.12 and lower (FML) - - // "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" - // }] - // } - - if (data.Properties.ContainsKey("modList") && data.Properties["modList"].Type == Json.JSONData.DataType.Array) + switch (fmlVersion) { - listFound = true; + case FMLVersion.FML: - foreach (Json.JSONData mod in data.Properties["modList"].DataArray) - { - String modid = mod.Properties["modid"].StringValue; - String version = mod.Properties["version"].StringValue; + // Example ModInfo for Minecraft 1.12 and lower (FML) - this.Mods.Add(new ForgeMod(modid, version)); - } + // "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" + // }] + // } + + foreach (Json.JSONData mod in data.Properties["modList"].DataArray) + { + String modid = mod.Properties["modid"].StringValue; + String modversion = mod.Properties["version"].StringValue; + + this.Mods.Add(new ForgeMod(modid, modversion)); + } + + break; + + case FMLVersion.FML2: + + // Example ModInfo for Minecraft 1.13 and greater (FML2) + + // "forgeData": { + // "channels": [{ + // "res": "minecraft:unregister", + // "version": "FML2", + // "required": true + // }, { + // "res": "minecraft:register", + // "version": "FML2", + // "required": true + // }], + // "mods": [{ + // "modId": "minecraft", + // "modmarker": "1.15.2" + // }, { + // "modId": "forge", + // "modmarker": "ANY" + // }, { + // "modId": "rats", + // "modmarker": "5.3.2" + // }, { + // "modId": "citadel", + // "modmarker": "1.1.11" + // }], + // "fmlNetworkVersion": 2 + // } + + foreach (Json.JSONData mod in data.Properties["mods"].DataArray) + { + String modid = mod.Properties["modId"].StringValue; + String modmarker = mod.Properties["modmarker"].StringValue; + + this.Mods.Add(new ForgeMod(modid, modmarker)); + } + + break; + + default: + throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!"); } - - // Example ModInfo for Minecraft 1.13 and greater (FML2) - - // "forgeData": { - // "channels": [{ - // "res": "minecraft:unregister", - // "version": "FML2", - // "required": true - // }, { - // "res": "minecraft:register", - // "version": "FML2", - // "required": true - // }], - // "mods": [{ - // "modId": "minecraft", - // "modmarker": "1.15.2" - // }, { - // "modId": "forge", - // "modmarker": "ANY" - // }, { - // "modId": "rats", - // "modmarker": "5.3.2" - // }, { - // "modId": "citadel", - // "modmarker": "1.1.11" - // }], - // "fmlNetworkVersion": 2 - // } - - if (data.Properties.ContainsKey("mods") && data.Properties["mods"].Type == Json.JSONData.DataType.Array) - { - listFound = true; - - foreach (Json.JSONData mod in data.Properties["mods"].DataArray) - { - String modid = mod.Properties["modId"].StringValue; - String version = mod.Properties["modmarker"].StringValue; - - this.Mods.Add(new ForgeMod(modid, version)); - } - } - - if (!listFound) - throw new ArgumentException("Missing mod list", "data"); } } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index de30a254..aae34d65 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -169,7 +169,7 @@ namespace MinecraftClient.Protocol.Handlers { packetData.Clear(); int size = dataTypes.ReadNextVarIntRAW(socketWrapper); //Packet size - byte[] rawpacket = socketWrapper.ReadDataRAW(size);//Packet contents + byte[] rawpacket = socketWrapper.ReadDataRAW(size); //Packet contents for (int i = 0; i < rawpacket.Length; i++) packetData.Enqueue(rawpacket[i]); @@ -209,6 +209,13 @@ namespace MinecraftClient.Protocol.Handlers if (protocolversion >= MC18Version) compression_treshold = dataTypes.ReadNextVarInt(packetData); break; + case 0x04: + int messageId = dataTypes.ReadNextVarInt(packetData); + string channel = dataTypes.ReadNextString(packetData); + List responseData = new List(); + bool understood = pForge.HandleLoginPluginRequest(channel, packetData, ref responseData); + SendLoginPluginResponse(messageId, understood, responseData.ToArray()); + return understood; default: return false; //Ignored packet } @@ -1012,7 +1019,7 @@ namespace MinecraftClient.Protocol.Handlers { byte[] protocol_version = dataTypes.GetVarInt(protocolversion); string server_address = pForge.GetServerAddress(handler.GetServerHost()); - byte[] server_port = BitConverter.GetBytes((ushort)handler.GetServerPort()); Array.Reverse(server_port); + byte[] server_port = dataTypes.GetUShort((ushort)handler.GetServerPort()); byte[] next_state = dataTypes.GetVarInt(2); byte[] handshake_packet = dataTypes.ConcatBytes(protocol_version, dataTypes.GetString(server_address), server_port, next_state); @@ -1445,6 +1452,25 @@ namespace MinecraftClient.Protocol.Handlers catch (ObjectDisposedException) { return false; } } + /// + /// Send a Login Plugin Response packet (0x02) + /// + /// Login Plugin Request message Id + /// TRUE if the request was understood + /// Response to the request + /// TRUE if successfully sent + public bool SendLoginPluginResponse(int messageId, bool understood, byte[] data) + { + try + { + SendPacket(0x02, dataTypes.ConcatBytes(dataTypes.GetVarInt(messageId), dataTypes.GetBool(understood), data)); + return true; + } + catch (SocketException) { return false; } + catch (System.IO.IOException) { return false; } + catch (ObjectDisposedException) { return false; } + } + /// /// Send an Interact Entity Packet to server /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs b/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs index 5df42cba..cd429bca 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs @@ -44,17 +44,17 @@ namespace MinecraftClient.Protocol.Handlers public string GetServerAddress(string serverAddress) { if (ForgeEnabled()) - return serverAddress + "\0FML\0"; + return serverAddress + "\0" + forgeInfo.Version + "\0"; return serverAddress; } /// - /// Completes the Minecraft Forge handshake. + /// Completes the Minecraft Forge handshake (Forge Protocol version 1: FML) /// /// Whether the handshake was successful. public bool CompleteForgeHandshake() { - if (ForgeEnabled()) + if (ForgeEnabled() && forgeInfo.Version == FMLVersion.FML) { int packetID = -1; Queue packetData = new Queue(); @@ -98,7 +98,7 @@ namespace MinecraftClient.Protocol.Handlers } /// - /// Handle Forge plugin messages + /// Handle Forge plugin messages (Forge Protocol version 1: FML) /// /// Plugin message channel /// Plugin message data @@ -106,7 +106,7 @@ namespace MinecraftClient.Protocol.Handlers /// TRUE if the plugin message was recognized and handled public bool HandlePluginMessage(string channel, Queue packetData, ref int currentDimension) { - if (ForgeEnabled() && fmlHandshakeState != FMLHandshakeClientState.DONE) + if (ForgeEnabled() && forgeInfo.Version == FMLVersion.FML && fmlHandshakeState != FMLHandshakeClientState.DONE) { if (channel == "FML|HS") { @@ -234,7 +234,198 @@ namespace MinecraftClient.Protocol.Handlers } /// - /// Send a forge plugin channel packet ("FML|HS"). Compression and encryption will be handled automatically. + /// Handle Forge plugin messages during login phase (Forge Protocol version 2: FML2) + /// + /// Plugin message channel + /// Plugin message data + /// Response data to return to server + /// TRUE/FALSE depending on whether the packet was understood or not + public bool HandleLoginPluginRequest(string channel, Queue packetData, ref List responseData) + { + if (ForgeEnabled() && forgeInfo.Version == FMLVersion.FML2 && channel == "fml:loginwrapper") + { + // Forge Handshake handler source code used to implement the FML2 packets: + // https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLNetworkConstants.java + // https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLHandshakeHandler.java + // https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/NetworkInitialization.java + // https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLLoginWrapper.java + // https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLHandshakeMessages.java + // + // During Login, Forge will send a set of LoginPluginRequest packets and we need to respond accordingly. + // Each login plugin message contains in its payload field an inner packet created by FMLLoginWrapper.java: + // + // [ ResourceLocation ][ String ] // FML Channel name + // [ Inner Packet ][ Packet ] // Minecraft-Like packet + // + // The channel name allows identifying which handler in Forge will process the packet + // For instance, the handshake channel is fml:handshake as per FMLNetworkConstants.java + // + // The inner packet has the same layout as a Minecraft packet. + // Forge uses Minecraft's PacketBuffer class to decode the packet. + // We assume no network compression is active at this point. + // + // [ Length ][ VarInt ] + // [ PacketID ][ VarInt ] + // [ Data ][ .... ] + // + // Once decoded, the packet ID for fml:handshake is mapped in NetworkInitialization.java: + // + // 99 = Client to Server - Client ACK + // 1 = Server to Client - Mod List + // 2 = Client to Server - Mod List + // 3 = Server to Client - Registry + // 4 = Server to Client - Config + // + // The content of each message is mapped into a class inside FMLHandshakeMessages.java + // FMLHandshakeHandler will then process the packet, e.g. handleServerModListOnClient() for Server Mod List. + + string fmlChannel = dataTypes.ReadNextString(packetData); + dataTypes.ReadNextVarInt(packetData); // Packet length + int packetID = dataTypes.ReadNextVarInt(packetData); + + if (fmlChannel == "fml:handshake") + { + bool fmlResponseReady = false; + List fmlResponsePacket = new List(); + + switch (packetID) + { + case 1: + // Server Mod List: FMLHandshakeMessages.java > S2CModList > decode() + // + // [ Mod Count ][ VarInt ] + // [ Mod Name ][ String ] // Amount of entries according to Mod Count + // [ Channel Count ][ VarInt ] + // [ Chan Name ][ String ] // Amount of entries according to Channel Count + // [ Version ][ String ] // Each entry is a pair of Channel Name + Version (1) + // [ Registry Count ][ VarInt ] + // [ Registry ][ String ] // Amount of entries according to Registry Count + // + // [1]: Version is usually set to "FML2" for FML stuff and "1" for mods + + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Received FML2 Server Mod List"); + + List mods = new List(); + int modCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < modCount; i++) + mods.Add(dataTypes.ReadNextString(packetData)); + + Dictionary channels = new Dictionary(); + int channelCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < channelCount; i++) + channels.Add(dataTypes.ReadNextString(packetData), dataTypes.ReadNextString(packetData)); + + List registries = new List(); + int registryCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < registryCount; i++) + registries.Add(dataTypes.ReadNextString(packetData)); + + // Server Mod List Reply: FMLHandshakeMessages.java > C2SModListReply > encode() + // + // [ Mod Count ][ VarInt ] + // [ Mod Name ][ String ] // Amount of entries according to Mod Count + // [ Channel Count ][ VarInt ] + // [ Chan Name ][ String ] // Amount of entries according to Channel Count + // [ Version ][ String ] // Each entry is a pair of Channel Name + Version + // [ Registry Count ][ VarInt ] + // [ Registry ][ String ] // Amount of entries according to Registry Count + // [ Version ][ String ] // Each entry is a pair of Registry Name + Version + // + // We are supposed to validate server info against our set of installed mods, then reply with our list + // In MCC, we just want to send a valid response so we'll reply back with data collected from the server. + + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Sending back FML2 Client Mod List"); + + // Packet ID 2: Client to Server Mod List + fmlResponsePacket.AddRange(dataTypes.GetVarInt(2)); + fmlResponsePacket.AddRange(dataTypes.GetVarInt(mods.Count)); + foreach (string mod in mods) + fmlResponsePacket.AddRange(dataTypes.GetString(mod)); + + fmlResponsePacket.AddRange(dataTypes.GetVarInt(channels.Count)); + foreach (KeyValuePair item in channels) + { + fmlResponsePacket.AddRange(dataTypes.GetString(item.Key)); + fmlResponsePacket.AddRange(dataTypes.GetString(item.Value)); + } + + fmlResponsePacket.AddRange(dataTypes.GetVarInt(registries.Count)); + foreach (string registry in registries) + { + fmlResponsePacket.AddRange(dataTypes.GetString(registry)); + // We don't have Registry mapping from server, leave it empty + fmlResponsePacket.AddRange(dataTypes.GetString("")); + } + fmlResponseReady = true; + break; + + case 3: + // Server Registry: FMLHandshakeMessages.java > S2CRegistry > decode() + // + // [ Registry Name ][ String ] + // [ Snapshot Present ][ Bool ] + // [ Snapshot data ][ .... ] // Only if "Snapshot Present" is True + // + // Registry Snapshot: ForgeRegistry.java > Snapshot > read(PacketBuffer) + // Not documented yet. We're ignoring this packet in MCC + + if (Settings.DebugMessages) + { + string registryName = dataTypes.ReadNextString(packetData); + ConsoleIO.WriteLineFormatted("§8Acknowledging FML2 Server Registry: " + registryName); + } + + fmlResponsePacket.AddRange(dataTypes.GetVarInt(99)); + fmlResponseReady = true; + break; + + case 4: + // Server Config: FMLHandshakeMessages.java > S2CConfigData > decode() + // + // [ Config Name ][ String ] + // [ Config Data ][ .... ] // Remaining packet data (1) + // + // [1] Config data may containt a standard Minecraft string readable with dataTypes.readNextString() + // We're ignoring this packet in MCC + + if (Settings.DebugMessages) + { + string configName = dataTypes.ReadNextString(packetData); + ConsoleIO.WriteLineFormatted("§8Acknowledging FML2 Server Config: " + configName); + } + + fmlResponsePacket.AddRange(dataTypes.GetVarInt(99)); + fmlResponseReady = true; + break; + + default: + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Got Unknown FML2 Handshake message no. " + packetID); + break; + } + + if (fmlResponseReady) + { + // Wrap our FML packet into a LoginPluginResponse payload + responseData.Clear(); + responseData.AddRange(dataTypes.GetString(fmlChannel)); + responseData.AddRange(dataTypes.GetVarInt(fmlResponsePacket.Count)); + responseData.AddRange(fmlResponsePacket); + return true; + } + } + else if (Settings.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8Ignoring Unknown FML2 LoginMessage channel: " + fmlChannel); + } + } + return false; + } + + /// + /// Send a forge plugin channel packet ("FML|HS"). Compression and encryption will be handled automatically. (Forge Protocol version 1: FML) /// /// Discriminator to use. /// packet Data @@ -244,34 +435,52 @@ namespace MinecraftClient.Protocol.Handlers } /// - /// Server Info: Check for For Forge on a Minecraft server Ping result + /// Server Info: Check for For Forge versions 1 and 2 on a Minecraft server Ping result /// /// JSON data returned by the server /// ForgeInfo to populate /// True if the server is running Forge public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo forgeInfo) { - return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, "modinfo", "type", "FML") // MC 1.12 and lower - || ServerInfoCheckForgeSub(jsonData, ref forgeInfo, "forgeData", "fmlNetworkVersion", "2"); // MC 1.13 and greater + return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower + || ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2); // MC 1.13 and greater } /// - /// Server Info: Check for For Forge on a Minecraft server Ping result + /// Server Info: Check for For Forge on a Minecraft server Ping result (Handles FML and FML2 /// /// JSON data returned by the server /// ForgeInfo to populate - /// ForgeData JSON field, e.g. "modinfo" - /// ForgeData version field, e.g. "type" - /// ForgeData version value, e.g. "FML" + /// Forge protocol version /// True if the server is running Forge - private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo forgeInfo, string forgeDataTag, string versionField, string versionString) + private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo forgeInfo, FMLVersion fmlVersion) { + string forgeDataTag; + string versionField; + string versionString; + + switch (fmlVersion) + { + case FMLVersion.FML: + forgeDataTag = "modinfo"; + versionField = "type"; + versionString = "FML"; + break; + case FMLVersion.FML2: + forgeDataTag = "forgeData"; + versionField = "fmlNetworkVersion"; + versionString = "2"; + break; + default: + throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!"); + } + if (jsonData.Properties.ContainsKey(forgeDataTag) && jsonData.Properties[forgeDataTag].Type == Json.JSONData.DataType.Object) { Json.JSONData modData = jsonData.Properties[forgeDataTag]; if (modData.Properties.ContainsKey(versionField) && modData.Properties[versionField].StringValue == versionString) { - forgeInfo = new ForgeInfo(modData); + forgeInfo = new ForgeInfo(modData, fmlVersion); if (forgeInfo.Mods.Any()) { ConsoleIO.WriteLineFormatted(String.Format("§8Server is running Forge with {0} mods.", forgeInfo.Mods.Count));