diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index f0d8f0b864..29ea3df5d5 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -557,7 +557,7 @@ private static void InitializeClient() } } - if (Config.Main.General.AccountType == LoginType.microsoft + if ((Config.Main.General.AccountType == LoginType.microsoft || Config.Main.General.AccountType == LoginType.yggdrasil) && (InternalConfig.Account.Password != "-" || Config.Main.General.Method == LoginMethod.browser) && Config.Signature.LoginWithSecureProfile && protocolversion >= 759 /* 1.19 and above */) @@ -582,7 +582,7 @@ private static void InitializeClient() if (playerKeyPair == null || playerKeyPair.NeedRefresh()) { ConsoleIO.WriteLineFormatted(Translations.mcc_fetching_key, acceptnewlines: true); - playerKeyPair = KeyUtils.GetNewProfileKeys(session.ID); + playerKeyPair = KeyUtils.GetNewProfileKeys(session.ID, Config.Main.General.AccountType == LoginType.yggdrasil); if (Config.Main.Advanced.ProfileKeyCache != CacheType.none && playerKeyPair != null) { KeysCache.Store(loginLower, playerKeyPair); diff --git a/MinecraftClient/Protocol/Handlers/Forge/FMLVersion.cs b/MinecraftClient/Protocol/Handlers/Forge/FMLVersion.cs index 2590d473ec..d9980dc3d2 100644 --- a/MinecraftClient/Protocol/Handlers/Forge/FMLVersion.cs +++ b/MinecraftClient/Protocol/Handlers/Forge/FMLVersion.cs @@ -7,6 +7,7 @@ enum FMLVersion { FML, - FML2 + FML2, + FML3 } } diff --git a/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs b/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs index 781cfd5a99..ffbd4233b7 100755 --- a/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs +++ b/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs @@ -46,6 +46,13 @@ internal ForgeInfo(FMLVersion fmlVersion) }; Version = fmlVersion; break; + case FMLVersion.FML3: + Mods = new List + { + new ForgeMod("forge", "ANY") + }; + Version = fmlVersion; + break; default: throw new InvalidOperationException(Translations.error_forgeforce); } @@ -133,10 +140,125 @@ internal ForgeInfo(Json.JSONData data, FMLVersion fmlVersion) } break; + case FMLVersion.FML3: + // Example ModInfo for Minecraft 1.18 and greater (FML3) + + // { + // "enforcesSecureChat": true, + // "forgeData": { + // "channels": [], + // "mods": [], + // "truncated": false, // legacy versions see truncated lists, modern versions ignore this truncated flag (binary data has its own) + // "fmlNetworkVersion": 3, + // "d": "ȳ\u0000\u0000ࠨ㐤獋㙖⹌ᦘ̺⸱恤䒸⡑⛧沮婙㨹牥ఈㄵচ₀沮婙㨹牥ఈㄵচ倠⹡岙㜲獥䋊㷍᭳ႇׇ஌᜘㘴娘▅筳ص䰭宛㘲、\u0000ᠸጋ囗湌夜㘲杩棐䐱ᅱ挃☥ోᤗ㌮ఀ׈䬣 坖ɍ䮌ᤘ\r\n旉䠳ዣ◆䲌㜃瑥廮ⷉࠋ–䁠奚Ҵ㔱摜䂸ᅱ獳ౠᡚ㜷汥戊䂸űဓĠ嵛㖱数嫤Ǎ塰䛶ⶎᮚ㞳晲擞ᖝ″ዣ䘆ఋʂ潦令ඕ爈䖔⺁ᥚ⾹潳棤㦥ᬻ挐؅䅀㠹楬ۨ㣄উ瀀渀嬛㘼扩搢䃀熁挂♥\r\n墋㒺摬牜ࣜ䁠嘗湌孛㜴浩惂䠙熙排٥孁㒰ͮ屢Ӏ䠐⚐䷮ᣛ㊴瑳戚䢸熁匒إ஍᜚ܴ䫜巑፻ᚷؠ䀀ㆃ牵䋨㦥ࠫ㋣䗆䂌㨈慲䫬ᖱᮓᘧ汬尚ㆰ٫屲㣄ᆉ恳ಭ川㤷፫擨妅挫♖乮塘 㖱慰\r\n囆䓩\t" + // }, + // "description": { + // "text": "A Minecraft Server" + // }, + // "players": { + // "max": 100, + // "online": 0 + // }, + // "version": { + // "name": "1.20.1", + // "protocol": 763 + // } + // } + + // All buffer data are encoded and write to forgeData["d"] + // https://github.com/MinecraftForge/MinecraftForge/blob/cb12df41e13da576b781be695f80728b9594c25f/src/main/java/net/minecraftforge/network/ServerStatusPing.java#L264 + + // 1.18 and greater, the buffer is encoded for efficiency + // see https://github.com/MinecraftForge/MinecraftForge/pull/8169 + + string encodedData = data.Properties["d"].StringValue; + Queue dataPackage = decodeOptimized(encodedData); + DataTypes dataTypes = new DataTypes(Protocol18Handler.MC_1_18_1_Version); + + // + // [truncated][boolean] placeholder for whether we are truncating + // [Mod Size][unsigned short] short so that we can replace it later in case of truncation + // + bool truncated = dataTypes.ReadNextBool(dataPackage); + var modsSize = dataTypes.ReadNextUShort(dataPackage); + + Dictionary channels = new(); + Dictionary mods = new(); + + for (var i = 0; i < modsSize; i++) { + var channelSizeAndVersionFlag = dataTypes.ReadNextVarInt(dataPackage); + var channelSize = channelSizeAndVersionFlag >> 1; + + int VERSION_FLAG_IGNORESERVERONLY = 0b1; + var isIgnoreServerOnly = (channelSizeAndVersionFlag & VERSION_FLAG_IGNORESERVERONLY) != 0; + + var modId = dataTypes.ReadNextString(dataPackage); + + string IGNORESERVERONLY = "";// it was "OHNOES\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31"; + var modVersion = isIgnoreServerOnly ? IGNORESERVERONLY : dataTypes.ReadNextString(dataPackage); + + for (var i1 = 0; i1 < channelSize; i1++) { + var channelName = dataTypes.ReadNextString(dataPackage); + var channelVersion = dataTypes.ReadNextString(dataPackage); + var requiredOnClient = dataTypes.ReadNextBool(dataPackage); + channels.Add(modId + ":" + channelName, channelVersion + ":" + requiredOnClient); + } + + mods.Add(modId, modVersion); + Mods.Add(new ForgeMod(modId, modVersion)); + } + + var nonModChannelCount = dataTypes.ReadNextVarInt(dataPackage); + for (var i = 0; i < nonModChannelCount; i++) { + var channelName = dataTypes.ReadNextString(dataPackage); + var channelVersion = dataTypes.ReadNextString(dataPackage); + var requiredOnClient = dataTypes.ReadNextBool(dataPackage); + channels.Add(channelName, channelVersion + ":" + requiredOnClient); + } + break; default: throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!"); } } + + // https://github.com/MinecraftForge/MinecraftForge/blob/cb12df41e13da576b781be695f80728b9594c25f/src/main/java/net/minecraftforge/network/ServerStatusPing.java#L361 + // Decode binary data ForgeData["d"] to Queue + private static Queue decodeOptimized(string encodedData) { + // Console.WriteLine("Got encoded data:" + encodedData + ", decoding..."); + int size0 = encodedData[0]; + int size1 = encodedData[1]; + int size = size0 | (size1 << 15); + + List packageData = new(); + + int stringIndex = 2; + int buffer = 0; + int bitsInBuf = 0; + + while (stringIndex < encodedData.Length) + { + while (bitsInBuf >= 8) + { + packageData.Add((byte)buffer); + buffer >>= 8; + bitsInBuf -= 8; + } + + char c = encodedData[stringIndex]; + buffer |= (c & 0x7FFF) << bitsInBuf; + bitsInBuf += 15; + stringIndex++; + } + + while (packageData.Count < size) + { + packageData.Add((byte)buffer); + buffer >>= 8; + bitsInBuf -= 8; + } + + return new Queue(packageData.ToArray()); + } } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs b/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs index 1f1c5f4d22..99fb4c728f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs @@ -233,7 +233,7 @@ public bool HandlePluginMessage(string channel, Queue packetData, ref int } /// - /// Handle Forge plugin messages during login phase (Forge Protocol version 2: FML2) + /// Handle Forge plugin messages during login phase (Forge Protocol version 2: FML2 or Forge Protocol version 3: FML3) /// /// Plugin message channel /// Plugin message data @@ -241,15 +241,19 @@ public bool HandlePluginMessage(string channel, Queue packetData, ref int /// 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") + if (ForgeEnabled() && (forgeInfo!.Version == FMLVersion.FML2 || forgeInfo!.Version == FMLVersion.FML3) && 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 + // https://github.com/MinecraftForge/MinecraftForge/blob/1.13.x/src/main/java/net/minecraftforge/fml/network/FMLNetworkConstants.java + // https://github.com/MinecraftForge/MinecraftForge/blob/1.13.x/src/main/java/net/minecraftforge/fml/network/FMLHandshakeHandler.java + // https://github.com/MinecraftForge/MinecraftForge/blob/1.13.x/src/main/java/net/minecraftforge/fml/network/NetworkInitialization.java + // https://github.com/MinecraftForge/MinecraftForge/blob/1.13.x/src/main/java/net/minecraftforge/fml/network/FMLLoginWrapper.java + // https://github.com/MinecraftForge/MinecraftForge/blob/1.13.x/src/main/java/net/minecraftforge/fml/network/FMLHandshakeMessages.java // + // FML3 packets: + // https://github.com/MinecraftForge/MinecraftForge/blob/1.18.x/src/main/java/net/minecraftforge/network/NetworkInitialization.java + // https://github.com/MinecraftForge/MinecraftForge/blob/1.18.x/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: // @@ -274,6 +278,8 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref // 2 = Client to Server - Mod List // 3 = Server to Client - Registry // 4 = Server to Client - Config + // 5 = Server to Client - Mod Data List (FML3) + // 6 = Server to Client - MismatchedMod List (FML3) // // 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. @@ -320,6 +326,15 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref for (int i = 0; i < registryCount; i++) registries.Add(dataTypes.ReadNextString(packetData)); + // FML3 specific, + List dataPackRegistries = new(); + if (forgeInfo!.Version == FMLVersion.FML3 && packetData.Count != 0) + { + int dataPackRegistryCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < dataPackRegistryCount; i++) + dataPackRegistries.Add(dataTypes.ReadNextString(packetData)); + } + // Server Mod List Reply: FMLHandshakeMessages.java > C2SModListReply > encode() // // [ Mod Count ][ VarInt ] @@ -375,7 +390,7 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref string registryName = dataTypes.ReadNextString(packetData); ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_registry, registryName)); } - + fmlResponsePacket.AddRange(DataTypes.GetVarInt(99)); fmlResponseReady = true; break; @@ -399,6 +414,53 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref fmlResponseReady = true; break; + case 5: + // FML 3 + // Server Config: FMLHandshakeMessages.java > S2CModData > decode() + // + // We're ignoring this packet in MCC + + /* + // Uncomment this code block if needed + var size = dataTypes.ReadNextVarInt(packetData); + Dictionary modsData = new(); + for (int i = 0; i < size; i++) + { + var modId = dataTypes.ReadNextString(packetData); + var displayName = dataTypes.ReadNextString(packetData); + var version = dataTypes.ReadNextString(packetData); + modsData.Add(modId, displayName + ":" + version); + } + */ + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8" + "Received FML3 Server Mod Data List"); + } + break; + + case 6: + // FML 3 + // Server Config: FMLHandshakeMessages.java > S2CChannelMismatchData > decode() + // + // We're ignoring this packet in MCC + + /* + // Uncomment this code block if needed + Dictionary mismatchedMods = new(); + var size0 = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < size0; i++) + { + var modId = dataTypes.ReadNextString(packetData); + var version = dataTypes.ReadNextString(packetData); + mismatchedMods.Add(modId, version); + } + */ + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8" + "Received FML3 Server Mismatched Mods List"); + } + break; + default: if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_unknown, packetID)); @@ -442,7 +504,8 @@ private void SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, b public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo? forgeInfo) { return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower - || ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2); // MC 1.13 and greater + || ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2) // MC 1.13 to 1.17 + || ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML3); // MC 1.18 and greater } /// @@ -463,14 +526,22 @@ public static bool ServerMayForceForge(int protocolVersion) public static ForgeInfo ServerForceForge(int protocolVersion) { if (ServerMayForceForge(protocolVersion)) - { - return new ForgeInfo(FMLVersion.FML2); + { + // 1.17 is still FML2 + // https://github.com/MinecraftForge/MinecraftForge/blob/50b5414033de82f46be23201db50484f36c37d4f/src/main/java/net/minecraftforge/fmllegacy/network/FMLNetworkConstants.java#L37C29-L37C42 + // 1.18 change the constant FMLNETVERSION to 3 + // https://github.com/MinecraftForge/MinecraftForge/blob/cb12df41e13da576b781be695f80728b9594c25f/src/main/java/net/minecraftforge/network/NetworkConstants.java#L28 + if (protocolVersion > ProtocolHandler.MCVer2ProtocolVersion("1.18")) + { + return new ForgeInfo(FMLVersion.FML3); + } + return new ForgeInfo(FMLVersion.FML2); } else throw new InvalidOperationException(Translations.error_forgeforce); } /// - /// Server Info: Check for For Forge on a Minecraft server Ping result (Handles FML and FML2 + /// Server Info: Check for For Forge on a Minecraft server Ping result (Handles FML and FML2 and FML3 /// /// JSON data returned by the server /// ForgeInfo to populate @@ -494,6 +565,11 @@ private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInf versionField = "fmlNetworkVersion"; versionString = "2"; break; + case FMLVersion.FML3: + forgeDataTag = "forgeData"; + versionField = "fmlNetworkVersion"; + versionString = "3"; + break; default: throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!"); } @@ -523,6 +599,6 @@ private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInf } } return false; - } + } } } diff --git a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs index 4a8d5c66e4..a4bc6f2b22 100644 --- a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs +++ b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs @@ -5,6 +5,7 @@ using MinecraftClient.Protocol.Handlers; using MinecraftClient.Protocol.Message; using static MinecraftClient.Protocol.Message.LastSeenMessageList; +using Newtonsoft.Json.Linq; namespace MinecraftClient.Protocol.ProfileKey { @@ -14,27 +15,34 @@ static class KeyUtils private static readonly string certificates = "https://api.minecraftservices.com/player/certificates"; - public static PlayerKeyPair? GetNewProfileKeys(string accessToken) + public static PlayerKeyPair? GetNewProfileKeys(string accessToken, bool isYggdrasil) { ProxiedWebRequest.Response? response = null; try { - var request = new ProxiedWebRequest(certificates) - { - Accept = "application/json" - }; - request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); + if (!isYggdrasil) { + var request = new ProxiedWebRequest(certificates) + { + Accept = "application/json" + }; + request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); - response = request.Post("application/json", ""); + response = request.Post("application/json", ""); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLine(response.Body.ToString()); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine(response.Body.ToString()); + } } - string jsonString = response.Body; + // see https://github.com/yushijinhun/authlib-injector/blob/da910956eaa30d2f6c2c457222d188aeb53b0d1f/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java#L49 + // POST to "https://api.minecraftservices.com/player/certificates" with authlib-injector will get a dummy response + string jsonString = isYggdrasil ? MakeDummyResponse() : response!.Body; Json.JSONData json = Json.ParseJson(jsonString); + Console.WriteLine("Got public key:" + json.Properties["keyPair"].Properties["publicKey"].StringValue); + Console.WriteLine("Got private key:" + json.Properties["keyPair"].Properties["privateKey"].StringValue); + // Error here PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue, sig: json.Properties["publicKeySignature"].StringValue, sigV2: json.Properties["publicKeySignatureV2"].StringValue); @@ -230,5 +238,30 @@ public static string EscapeString(string src) sb.Append(src, start, src.Length - start); return sb.ToString(); } + + public static string MakeDummyResponse() + { + RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048); + var mimePublicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()); + var mimePrivateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey()); + string publicKeyPEM = $"-----BEGIN RSA PUBLIC KEY-----\n{mimePublicKey}\n-----END RSA PUBLIC KEY-----\n"; + string privateKeyPEM = $"-----BEGIN RSA PRIVATE KEY-----\n{mimePrivateKey}\n-----END RSA PRIVATE KEY-----\n"; + DateTime now = DateTime.UtcNow; + DateTime expiresAt = now.AddHours(48); + DateTime refreshedAfter = now.AddHours(36); + JObject response = new JObject(); + JObject keyPairObj = new JObject + { + { "privateKey", privateKeyPEM }, + { "publicKey", publicKeyPEM } + }; + response.Add("keyPair", keyPairObj); + response.Add("publicKeySignature", "AA=="); + response.Add("publicKeySignatureV2", "AA=="); + string format = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; + response.Add("expiresAt", expiresAt.ToString(format)); + response.Add("refreshedAfter", refreshedAfter.ToString(format)); + return response.ToString(); + } } } diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 92e53ce25d..ee47347210 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -1100,7 +1100,8 @@ private static int DoHTTPSRequest(List headers, string host,int port, re statusCode = int.Parse(raw_result.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture); if (statusCode != 204) { - postResult = raw_result[(raw_result.IndexOf("\r\n\r\n") + 4)..].Split("\r\n")[1]; + var splited = raw_result[(raw_result.IndexOf("\r\n\r\n") + 4)..].Split("\r\n"); + postResult = splited[1] + splited[3]; } else {