From e3b06b110fd02b09d8195d7384922d92bc7d9a28 Mon Sep 17 00:00:00 2001 From: Menci Date: Tue, 26 Nov 2024 00:03:50 +0800 Subject: [PATCH] [+] LogNetworkRequests (#90) * It doesn't work... * Implement * rename --- AquaMai/AquaMai.Core/Helpers/Shim.cs | 167 ++++++++++------ AquaMai/AquaMai.Mods/AquaMai.Mods.csproj | 1 + .../GameSystem/RemoveEncryption.cs | 3 +- .../AquaMai.Mods/Utils/LogNetworkRequests.cs | 188 ++++++++++++++++++ 4 files changed, 296 insertions(+), 63 deletions(-) create mode 100644 AquaMai/AquaMai.Mods/Utils/LogNetworkRequests.cs diff --git a/AquaMai/AquaMai.Core/Helpers/Shim.cs b/AquaMai/AquaMai.Core/Helpers/Shim.cs index e2d72e83..44b9725f 100644 --- a/AquaMai/AquaMai.Core/Helpers/Shim.cs +++ b/AquaMai/AquaMai.Core/Helpers/Shim.cs @@ -6,85 +6,128 @@ using HarmonyLib; using MAI2.Util; using Manager; using Manager.UserDatas; +using MelonLoader; +using Net; using Net.Packet; using Net.Packet.Mai2; +using Net.VO; namespace AquaMai.Core.Helpers; public static class Shim { + private static T Iife(Func func) => func(); + + public static readonly string apiSuffix = Iife(() => + { + try + { + var baseNetQueryConstructor = typeof(NetQuery) + .GetConstructors() + .First(); + return ((INetQuery)baseNetQueryConstructor.Invoke( + baseNetQueryConstructor + .GetParameters() + .Select((parameter, i) => i == 0 ? "" : parameter.DefaultValue) + .ToArray())).Api; + } + catch (Exception e) + { + MelonLogger.Error($"Failed to resolve the API suffix: {e}"); + return null; + } + }); + + public static string RemoveApiSuffix(string api) + { + return !string.IsNullOrEmpty(apiSuffix) && api.EndsWith(apiSuffix) + ? api.Substring(0, api.Length - apiSuffix.Length) + : api; + } + public delegate string GetAccessTokenMethod(int index); - public static readonly GetAccessTokenMethod GetAccessToken = new Func(() => { - var tOperationManager = Traverse.Create(Singleton.Instance); - var tGetAccessToken = tOperationManager.Method("GetAccessToken", [typeof(int)]); - if (!tGetAccessToken.MethodExists()) - { - return (index) => throw new MissingMethodException("No matching OperationManager.GetAccessToken() method found"); - } - return (index) => tGetAccessToken.GetValue(index); - })(); + public static readonly GetAccessTokenMethod GetAccessToken = Iife(() => + { + var tOperationManager = Traverse.Create(Singleton.Instance); + var tGetAccessToken = tOperationManager.Method("GetAccessToken", [typeof(int)]); + if (!tGetAccessToken.MethodExists()) + { + return (index) => throw new MissingMethodException("No matching OperationManager.GetAccessToken() method found"); + } + return (index) => tGetAccessToken.GetValue(index); + }); public delegate PacketUploadUserPlaylog PacketUploadUserPlaylogCreator(int index, UserData src, int trackNo, Action onDone, Action onError = null); - public static readonly PacketUploadUserPlaylogCreator CreatePacketUploadUserPlaylog = new Func(() => { - var type = typeof(PacketUploadUserPlaylog); - if (type.GetConstructor([typeof(int), typeof(UserData), typeof(int), typeof(Action), typeof(Action)]) is ConstructorInfo ctor1) { - return (index, src, trackNo, onDone, onError) => { - var args = new object[] {index, src, trackNo, onDone, onError}; - return (PacketUploadUserPlaylog)ctor1.Invoke(args); - }; - } - else if (type.GetConstructor([typeof(int), typeof(UserData), typeof(int), typeof(string), typeof(Action), typeof(Action)]) is ConstructorInfo ctor2) { - return (index, src, trackNo, onDone, onError) => { - var accessToken = GetAccessToken(index); - var args = new object[] {index, src, trackNo, accessToken, onDone, onError}; - return (PacketUploadUserPlaylog)ctor2.Invoke(args); - }; - } - else - { - throw new MissingMethodException("No matching PacketUploadUserPlaylog constructor found"); - } - })(); + public static readonly PacketUploadUserPlaylogCreator CreatePacketUploadUserPlaylog = Iife(() => + { + var type = typeof(PacketUploadUserPlaylog); + if (type.GetConstructor([typeof(int), typeof(UserData), typeof(int), typeof(Action), typeof(Action)]) is ConstructorInfo ctor1) + { + return (index, src, trackNo, onDone, onError) => + { + var args = new object[] { index, src, trackNo, onDone, onError }; + return (PacketUploadUserPlaylog)ctor1.Invoke(args); + }; + } + else if (type.GetConstructor([typeof(int), typeof(UserData), typeof(int), typeof(string), typeof(Action), typeof(Action)]) is ConstructorInfo ctor2) + { + return (index, src, trackNo, onDone, onError) => + { + var accessToken = GetAccessToken(index); + var args = new object[] { index, src, trackNo, accessToken, onDone, onError }; + return (PacketUploadUserPlaylog)ctor2.Invoke(args); + }; + } + else + { + throw new MissingMethodException("No matching PacketUploadUserPlaylog constructor found"); + } + }); public delegate PacketUpsertUserAll PacketUpsertUserAllCreator(int index, UserData src, Action onDone, Action onError = null); - public static readonly PacketUpsertUserAllCreator CreatePacketUpsertUserAll = new Func(() => { - var type = typeof(PacketUpsertUserAll); - if (type.GetConstructor([typeof(int), typeof(UserData), typeof(Action), typeof(Action)]) is ConstructorInfo ctor1) { - return (index, src, onDone, onError) => { - var args = new object[] {index, src, onDone, onError}; - return (PacketUpsertUserAll)ctor1.Invoke(args); - }; - } - else if (type.GetConstructor([typeof(int), typeof(UserData), typeof(string), typeof(Action), typeof(Action)]) is ConstructorInfo ctor2) { - return (index, src, onDone, onError) => { - var accessToken = GetAccessToken(index); - var args = new object[] {index, src, accessToken, onDone, onError}; - return (PacketUpsertUserAll)ctor2.Invoke(args); - }; - } - else - { - throw new MissingMethodException("No matching PacketUpsertUserAll constructor found"); - } - })(); + public static readonly PacketUpsertUserAllCreator CreatePacketUpsertUserAll = Iife(() => + { + var type = typeof(PacketUpsertUserAll); + if (type.GetConstructor([typeof(int), typeof(UserData), typeof(Action), typeof(Action)]) is ConstructorInfo ctor1) + { + return (index, src, onDone, onError) => + { + var args = new object[] { index, src, onDone, onError }; + return (PacketUpsertUserAll)ctor1.Invoke(args); + }; + } + else if (type.GetConstructor([typeof(int), typeof(UserData), typeof(string), typeof(Action), typeof(Action)]) is ConstructorInfo ctor2) + { + return (index, src, onDone, onError) => + { + var accessToken = GetAccessToken(index); + var args = new object[] { index, src, accessToken, onDone, onError }; + return (PacketUpsertUserAll)ctor2.Invoke(args); + }; + } + else + { + throw new MissingMethodException("No matching PacketUpsertUserAll constructor found"); + } + }); public static IEnumerable[] GetUserScoreList(UserData userData) { - var tUserData = Traverse.Create(userData); + var tUserData = Traverse.Create(userData); - var tScoreList = tUserData.Property("ScoreList"); - if (tScoreList.PropertyExists()) - { - return tScoreList.GetValue[]>(); - } + var tScoreList = tUserData.Property("ScoreList"); + if (tScoreList.PropertyExists()) + { + return tScoreList.GetValue[]>(); + } - var tScoreDic = tUserData.Property("ScoreDic"); - if (tScoreDic.PropertyExists()) - { - var scoreDic = tScoreDic.GetValue[]>(); - return scoreDic.Select(dic => dic.Values).ToArray(); - } + var tScoreDic = tUserData.Property("ScoreDic"); + if (tScoreDic.PropertyExists()) + { + var scoreDic = tScoreDic.GetValue[]>(); + return scoreDic.Select(dic => dic.Values).ToArray(); + } - throw new MissingFieldException("No matching UserData.ScoreList/ScoreDic found"); + throw new MissingFieldException("No matching UserData.ScoreList/ScoreDic found"); } } diff --git a/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj b/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj index cb2fb8dd..b04c32a1 100644 --- a/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj +++ b/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj @@ -50,6 +50,7 @@ + diff --git a/AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs b/AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs index 7b1ce8db..9265afc2 100644 --- a/AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs +++ b/AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; using HarmonyLib; using Net.Packet; @@ -25,7 +26,7 @@ public class RemoveEncryption [HarmonyPatch(typeof(Packet), "Obfuscator", typeof(string))] public static bool PreObfuscator(string srcStr, ref string __result) { - __result = srcStr.Replace("MaimaiExp", "").Replace("MaimaiChn", ""); + __result = Shim.RemoveApiSuffix(srcStr); return false; } diff --git a/AquaMai/AquaMai.Mods/Utils/LogNetworkRequests.cs b/AquaMai/AquaMai.Mods/Utils/LogNetworkRequests.cs new file mode 100644 index 00000000..5cd60aeb --- /dev/null +++ b/AquaMai/AquaMai.Mods/Utils/LogNetworkRequests.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; +using Net; +using Net.Packet; +using MelonLoader; +using MelonLoader.TinyJSON; +using HarmonyLib; +using AquaMai.Core.Attributes; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; + +namespace AquaMai.Mods.Utils; + +[ConfigSection( + en: "Log network requests to the MelonLoader console.", + zh: "将网络请求输出到 MelonLoader 控制台")] +public class LogNetworkRequests +{ + [ConfigEntry] + private static readonly bool url = true; + + [ConfigEntry] + private static readonly bool request = true; + [ConfigEntry] + private static readonly string requestOmittedApis = "UploadUserPhotoApi,UploadUserPortraitApi"; + + [ConfigEntry] + private static readonly bool response = true; + [ConfigEntry] + private static readonly string responseOmittedApis = "GetGameEventApi"; + [ConfigEntry( + en: "Only print error responses, without the successful ones.", + zh: "仅输出出错的响应,不输出成功的响应")] + private static readonly bool responseErrorOnly = false; + + private static HashSet requestOmittedApiList = []; + private static HashSet responseOmittedApiList = []; + + private static readonly ConditionalWeakTable errorResponse = new(); + + public static void OnBeforePatch() + { + requestOmittedApiList = [.. requestOmittedApis.Split(',')]; + responseOmittedApiList = [.. responseOmittedApis.Split(',')]; + + if (responseErrorOnly && !response) + { + MelonLogger.Warning("[LogNetworkRequests] `responseErrorOnly` is enabled but `response` is disabled. Will not print any response."); + } + } + + private static string GetApiName(INetQuery netQuery) + { + return Shim.RemoveApiSuffix(netQuery.Api); + } + + [EnableIf(nameof(url))] + [HarmonyPostfix] + [HarmonyPatch(typeof(Packet), "Create")] + public static void PostCreate(Packet __instance) + { + MelonLogger.Msg($"[LogNetworkRequests] {GetApiName(__instance.Query)} URL: {MaybeGetNetPacketUrl(__instance)}"); + } + + private static string MaybeGetNetPacketUrl(Packet __instance) + { + if (Traverse.Create(__instance).Field("Client").GetValue() is not NetHttpClient client) + { + return ""; + } + if (Traverse.Create(client).Field("_request").GetValue() is not HttpWebRequest request) + { + return ""; + } + return request.RequestUri.ToString(); + } + + // Record the error responses of NetHttpClient to display. These responses could not be acquired in other ways. + [HarmonyPrefix] + [HarmonyPatch(typeof(NetHttpClient), "SetError")] + public static void PreSetError(NetHttpClient __instance, HttpWebResponse response) + { + if (response != null) + { + errorResponse.Add(__instance, response); + } + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(Packet), "ProcImpl")] + public static void PreProcImpl(Packet __instance) + { + if (request && __instance.State == PacketState.Ready) + { + var netQuery = __instance.Query; + var api = GetApiName(netQuery); + var displayRequest = InspectRequest(api, netQuery.GetRequest()); + MelonLogger.Msg($"[LogNetworkRequests] {api} Request: {displayRequest}"); + } + else if ( + response && + __instance.State == PacketState.Process && + Traverse.Create(__instance).Field("Client").GetValue() is NetHttpClient client) + { + if (client.State == NetHttpClient.StateDone && !responseErrorOnly) + { + var netQuery = __instance.Query; + var api = GetApiName(netQuery); + var displayResponse = InspectResponse(api, client.GetResponse().ToArray()); + MelonLogger.Msg($"[LogNetworkRequests] {api} Response: {displayResponse}"); + } + else if (client.State == NetHttpClient.StateError) + { + var displayError = InspectError(client); + MelonLogger.Warning($"[LogNetworkRequests] {GetApiName(__instance.Query)} Error: {displayError}"); + } + } + } + + private static string InspectRequest(string api, string request) => + requestOmittedApiList.Contains(api) + ? $"<{request.Length} characters omitted>" + : (request == "" ? "" : request); + + private static string InspectResponse(string api, byte[] response) + { + try + { + var decoded = Encoding.UTF8.GetString(response); + if (responseOmittedApiList.Contains(api)) + { + return $"<{decoded.Length} characters omitted>"; + } + else if (decoded == "") + { + return ""; + } + else if (decoded.IndexOf("\n") != -1) + { + return JSON.Dump(decoded); + } + else + { + return decoded; + } + } + catch (Exception e) + { + // Always non-empty when decoding fails. + return $""; + } + } + + private static string InspectError(NetHttpClient client) => + "<" + + $"WebExceptionStatus.{client.WebException}: " + + $"HttpStatus = {client.HttpStatus}, " + + $"Error = {JSON.Dump(client.Error)}, " + + $"Response = " + + (errorResponse.TryGetValue(client, out var response) + ? InspectErrorResponse(response) + : "null") + + ">"; + + private static string InspectErrorResponse(HttpWebResponse response) + { + try + { + var webConnectionStream = response.GetResponseStream(); + var memoryStream = new MemoryStream(); + webConnectionStream.CopyTo(memoryStream); + return InspectResponse(null, memoryStream.ToArray()); + } + catch (Exception e) + { + // The stream has alraedy been consumed? + return $""; + } + } +}