From 24cd5e00f39b037d1adeae1079ed4df51b24384c Mon Sep 17 00:00:00 2001 From: Kayori Date: Sat, 17 Jan 2026 19:50:00 +0100 Subject: [PATCH] Clean up and added force encoding option in plugins --- Abstractions/IMedusaPlugin.cs | 3 + Core/Client/package-lock.json | 12 +- Core/Server/Program.cs | 293 +++++++++++++------------ Core/Server/Services/HandlerService.cs | 8 +- Core/Server/Services/PluginService.cs | 16 +- 5 files changed, 185 insertions(+), 147 deletions(-) diff --git a/Abstractions/IMedusaPlugin.cs b/Abstractions/IMedusaPlugin.cs index 95c1fdf..e498a9b 100644 --- a/Abstractions/IMedusaPlugin.cs +++ b/Abstractions/IMedusaPlugin.cs @@ -1,3 +1,4 @@ +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -11,6 +12,8 @@ public interface IMedusaPlugin string GameCode { get; } int? MinVer { get; } int? MaxVer { get; } + Encoding? ForcedEncoding { get; } + Task OnBuilderInitialize(WebApplicationBuilder builder); Task OnAppInitialize(WebApplication app); Task DoesProfileExist(string cardId); diff --git a/Core/Client/package-lock.json b/Core/Client/package-lock.json index 5fc6c33..41e9624 100644 --- a/Core/Client/package-lock.json +++ b/Core/Client/package-lock.json @@ -1319,6 +1319,7 @@ "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -1363,6 +1364,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -1845,6 +1847,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2402,6 +2405,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2458,6 +2462,7 @@ "integrity": "sha512-b/Y05HYmnB/32wqVcjxjHZzNpwxj1onBOvqW89W+V+XNG1dRuaFbNd3vT9CLbr2LXjEoq+3vn8DanWf7XU22Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "globals": "^13.24.0", @@ -4439,7 +4444,8 @@ "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-primeui": { "version": "0.6.1", @@ -4552,6 +4558,7 @@ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4699,6 +4706,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4780,6 +4788,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", @@ -4826,6 +4835,7 @@ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/devtools-api": "^6.6.4" }, diff --git a/Core/Server/Program.cs b/Core/Server/Program.cs index ff8976f..e0bb4b3 100644 --- a/Core/Server/Program.cs +++ b/Core/Server/Program.cs @@ -1,24 +1,26 @@ -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using KbinXml.Net; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Server; -using Server.Extensions; -using Server.Middlewares; -using Server.Services; -using Server.Utils; -using System.Security.Cryptography; -using System.Text; -using System.Xml.Linq; using Abstractions.Entities; using Abstractions.Services; using Abstractions.Utils; +using KbinXml.Net; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Server; using Server.Authentication; +using Server.Extensions; +using Server.Middlewares; using Server.Models.Request; +using Server.Services; +using Server.Utils; +using System.Net; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; var key = Convert.FromHexString("00000000000069D74627D985EE2187161570D08D93B12455035B6DF0D8205DF5"); @@ -98,143 +100,157 @@ app.MapIdentityApi(); var eamuseGroup = app.MapGroup("eamuse"); -eamuseGroup.MapPost("/{model}/{module}/{method}", (string model, string module, string method, - [FromServices] IHttpContextAccessor httpContextAccessor) => -{ - -}); - -eamuseGroup.MapPost("/{m}", async (string m, [FromQuery] string model, [FromQuery] string? module, [FromQuery] string? method, [FromQuery] string? f, - HttpContext httpContext, [FromServices] ILogger logger, [FromServices] IHandlerService handlerService) => -{ - // Enable buffering to allow multiple reads of the request body - httpContext.Request.EnableBuffering(); - - // The body is 932 encoded xml - using var reader = new StreamReader(httpContext.Request.Body, Encoding.GetEncoding(932), false, 1024, true); - var body = await reader.ReadToEndAsync(); - - httpContext.Request.Body.Position = 0; - - var compress = httpContext.Request.Headers["X-Compress"].ToString().Contains("lz77"); - var encrypt = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault() is not null; - - var amusementRequest = new AmusementRequest() { Model = model, Module = module ?? "", Method = method ?? "" }; - - if(!string.IsNullOrEmpty(f)) + eamuseGroup.MapPost("/{model}/{module}/{method}", async (string model, string module, string method, + HttpContext httpContext, [FromServices] ILogger logger, [FromServices] IHandlerService handlerService, [FromServices] IPluginService pluginService) => { - var fParts = f.Split('.'); - amusementRequest.Module = fParts[0]; - amusementRequest.Method = fParts[1]; - } + // Enable buffering to allow multiple reads of the request body + httpContext.Request.EnableBuffering(); + var body = ""; + // The body is 932 encoded xml + if(httpContext.Request.Body.Length != 0) + { + using var reader = new StreamReader(httpContext.Request.Body, Encoding.GetEncoding(932), false, 1024, true); + body = await reader.ReadToEndAsync(); + } - logger.LogInformation("Handling {Module} {Method}", amusementRequest.Module, amusementRequest.Method); + httpContext.Request.Body.Position = 0; + + var compress = httpContext.Request.Headers["X-Compress"].ToString().Contains("lz77"); + var encrypt = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault() is not null; + + var amusementRequest = new AmusementRequest() { Model = model, Module = module ?? "", Method = method ?? "" }; + + var encoding = httpContext.Items["Encoding"]?.ToString() ?? "ShiftJIS"; + + httpContext.Request.Headers.TryGetValue("IsEncoded", out var isEncoded); + + var originalInfo = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault() ?? ""; + + httpContext.Response.Headers.Append("X-Eamuse-Info", originalInfo); + + var result = await HandleEAmuseRequest(amusementRequest, body, originalInfo, compress, encrypt, isEncoded == "true", encoding, logger, handlerService, pluginService); + + return TypedResults.Bytes(result, "application/octet-stream"); + }); + + eamuseGroup.MapPost("/{m}", async (string m, [FromQuery] string model, [FromQuery] string? module, [FromQuery] string? method, [FromQuery] string? f, + HttpContext httpContext, [FromServices] ILogger logger, [FromServices] IHandlerService handlerService, [FromServices] IPluginService pluginService) => + { + // Enable buffering to allow multiple reads of the request body + httpContext.Request.EnableBuffering(); + var body = ""; + // The body is 932 encoded xml + if(httpContext.Request.Body.Length != 0) + { + using var reader = new StreamReader(httpContext.Request.Body, Encoding.GetEncoding(932), false, 1024, true); + body = await reader.ReadToEndAsync(); + } + + httpContext.Request.Body.Position = 0; + + var compress = httpContext.Request.Headers["X-Compress"].ToString().Contains("lz77"); + var encrypt = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault() is not null; + + var amusementRequest = new AmusementRequest() { Model = model, Module = module ?? "", Method = method ?? "" }; + + if(!string.IsNullOrEmpty(f)) + { + var fParts = f.Split('.'); + amusementRequest.Module = fParts[0]; + amusementRequest.Method = fParts[1]; + } + + var encoding = httpContext.Items["Encoding"]?.ToString() ?? "ShiftJIS"; + + httpContext.Request.Headers.TryGetValue("IsEncoded", out var isEncoded); + + var originalInfo = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault() ?? ""; + + httpContext.Response.Headers.Append("X-Eamuse-Info", originalInfo); + + var result = await HandleEAmuseRequest(amusementRequest, body, originalInfo, compress, encrypt, isEncoded == "true", encoding, logger, handlerService, pluginService); + + return TypedResults.Bytes(result, "application/octet-stream"); + }); + + eamuseGroup.MapPost("/", async ([FromQuery] string model, [FromQuery] string? module, [FromQuery] string? method, [FromQuery] string? f, + HttpContext httpContext, [FromServices] ILogger logger, [FromServices] IHandlerService handlerService, [FromServices] IPluginService pluginService) => + { + + // Enable buffering to allow multiple reads of the request body + httpContext.Request.EnableBuffering(); + var body = ""; + // The body is 932 encoded xml + if(httpContext.Request.Body.Length != 0) + { + using var reader = new StreamReader(httpContext.Request.Body, Encoding.GetEncoding(932), false, 1024, true); + body = await reader.ReadToEndAsync(); + } + + httpContext.Request.Body.Position = 0; + + var compress = httpContext.Request.Headers["X-Compress"].ToString().Contains("lz77"); + var encrypt = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault() is not null; + + var amusementRequest = new AmusementRequest() { Model = model, Module = module ?? "", Method = method ?? "" }; + + if(!string.IsNullOrEmpty(f)) + { + var fParts = f.Split('.'); + amusementRequest.Module = fParts[0]; + amusementRequest.Method = fParts[1]; + } + + var encoding = httpContext.Items["Encoding"]?.ToString() ?? "ShiftJIS"; + + httpContext.Request.Headers.TryGetValue("IsEncoded", out var isEncoded); + + var originalInfo = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault() ?? ""; + + httpContext.Response.Headers.Append("X-Eamuse-Info", originalInfo); + + var result = await HandleEAmuseRequest(amusementRequest, body, originalInfo, compress, encrypt, isEncoded == "true", encoding, logger, handlerService, pluginService); + + return TypedResults.Bytes(result, "application/octet-stream"); + }); + +async Task HandleEAmuseRequest(AmusementRequest request, string body, string info, bool compress, bool encrypt, bool isEncoded, string encoding, ILogger logger, IHandlerService handlerService, IPluginService pluginService) +{ + logger.LogInformation("Handling {Module} {Method}", request.Module, request.Method); var document = new XDocument(); - if (!string.IsNullOrEmpty(body)) - document = XDocument.Parse(body); - - var responseXml = await handlerService.Handle(amusementRequest.Model, amusementRequest.Module, amusementRequest.Method, document); - - var encoding = httpContext.Items["Encoding"]?.ToString() ?? "ShiftJIS"; - - var encodingEnum = encoding switch - { - "shift_jis" or "ShiftJIS" or "SHIFT_JIS" => KnownEncodings.ShiftJIS, - "us-ascii" => KnownEncodings.ASCII, - "utf-8" => KnownEncodings.UTF8, - "euc-jp" => KnownEncodings.EUC_JP, - "iso-8859-1" => KnownEncodings.ISO_8859_1, - _ => throw new ArgumentException($"Unknown encoding: {encoding}") - }; - - if (amusementRequest.Module == "package") - { - logger.LogInformation("{xml}", responseXml); - } - - var encodedBody = KbinConverter.Write(responseXml, encodingEnum); - - if (amusementRequest.Module == "package") - { - File.WriteAllBytes("test.bin", encodedBody); - } - - if(compress) - { - encodedBody = LZ77.CompressEmpty(encodedBody); - } - - if (!encrypt) return TypedResults.Bytes(encodedBody, "application/octet-stream"); - var originalInfo = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault()?.Split('-') ?? []; - var part = Convert.FromHexString((originalInfo[1] + originalInfo[2])); - for(var i = 0; i < 6; i++) - key[i] = part[i]; - var rc4Key = MD5.HashData(key); - encodedBody = RC4.Encrypt(rc4Key, encodedBody); - - httpContext.Response.Headers.Append("X-Eamuse-Info", string.Join('-', originalInfo)); - - return TypedResults.Bytes(encodedBody, "application/octet-stream"); -}); - -eamuseGroup.MapPost("/", async ([FromQuery] string model, [FromQuery] string? module, [FromQuery] string? method, [FromQuery] string? f, - HttpContext httpContext, [FromServices] ILogger logger, [FromServices] IHandlerService handlerService) => -{ - // Enable buffering to allow multiple reads of the request body - httpContext.Request.EnableBuffering(); - var body = ""; - // The body is 932 encoded xml - if(httpContext.Request.Body.Length != 0) - { - using var reader = new StreamReader(httpContext.Request.Body, Encoding.GetEncoding(932), false, 1024, true); - body = await reader.ReadToEndAsync(); - } - - httpContext.Request.Body.Position = 0; - - var compress = httpContext.Request.Headers["X-Compress"].ToString().Contains("lz77"); - var encrypt = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault() is not null; - - var amusementRequest = new AmusementRequest() { Model = model, Module = module ?? "", Method = method ?? ""}; - - if (!string.IsNullOrEmpty(f)) - { - var fParts = f.Split('.'); - amusementRequest.Module = fParts[0]; - amusementRequest.Method = fParts[1]; - } - - logger.LogInformation("Handling {Module} {Method}", amusementRequest.Module, amusementRequest.Method); - - var document = new XDocument(); - if(!string.IsNullOrEmpty(body)) document = XDocument.Parse(body); - var responseXml = await handlerService.Handle(amusementRequest.Model, amusementRequest.Module, amusementRequest.Method, document); + var responseXml = await handlerService.Handle(request.Model, request.Module, request.Method, document); - var encoding = httpContext.Items["Encoding"]?.ToString() ?? "ShiftJIS"; + var plugins = pluginService.GetPlugins(); + var forcedEncoding = plugins.FirstOrDefault(p => p.GameCode == request.Model)?.ForcedEncoding; var encodingEnum = encoding switch { - "shift_jis" or "ShiftJIS" or "SHIFT_JIS" => KnownEncodings.ShiftJIS, - "us-ascii" => KnownEncodings.ASCII, - "utf-8" => KnownEncodings.UTF8, - "euc-jp" => KnownEncodings.EUC_JP, + "shift_jis" or "ShiftJIS" or "SHIFT_JIS" => KnownEncodings.ShiftJIS, + "us-ascii" => KnownEncodings.ASCII, + "utf-8" => KnownEncodings.UTF8, + "euc-jp" => KnownEncodings.EUC_JP, "iso-8859-1" => KnownEncodings.ISO_8859_1, _ => throw new ArgumentException($"Unknown encoding: {encoding}") }; - if (amusementRequest.Module == "package") + if(forcedEncoding is not null) + { + encodingEnum = forcedEncoding.ToKnownEncoding(); + } + + if(request.Module == "package") { logger.LogInformation("{xml}", responseXml); } - + byte[] encodedBody; - - if (httpContext.Request.Headers.TryGetValue("IsEncoded", out var value) && value == "false") + + if(!isEncoded) { var encoder = Encoding.GetEncoding(encoding); encodedBody = encoder.GetBytes(responseXml.ToString()); @@ -242,8 +258,8 @@ eamuseGroup.MapPost("/", async ([FromQuery] string model, [FromQuery] string? mo else { encodedBody = KbinConverter.Write(responseXml, encodingEnum); - - if (amusementRequest.Module == "package") + + if(request.Module == "package") { File.WriteAllBytes("test.bin", encodedBody); } @@ -253,18 +269,17 @@ eamuseGroup.MapPost("/", async ([FromQuery] string model, [FromQuery] string? mo encodedBody = LZ77.CompressEmpty(encodedBody); } - if (!encrypt) return TypedResults.Bytes(encodedBody, "application/octet-stream"); - var originalInfo = httpContext.Request.Headers["X-Eamuse-Info"].FirstOrDefault()?.Split('-') ?? []; + var originalInfo = info.Split('-'); + + if(!encrypt) return encodedBody; var part = Convert.FromHexString((originalInfo[1] + originalInfo[2])); for(var i = 0; i < 6; i++) key[i] = part[i]; var rc4Key = MD5.HashData(key); encodedBody = RC4.Encrypt(rc4Key, encodedBody); - httpContext.Response.Headers.Append("X-Eamuse-Info", string.Join('-', originalInfo)); - - return TypedResults.Bytes(encodedBody, "application/octet-stream"); -}); + return encodedBody; +} app.MapFallbackToFile("/index.html"); diff --git a/Core/Server/Services/HandlerService.cs b/Core/Server/Services/HandlerService.cs index 005dd0c..1b0ff3f 100644 --- a/Core/Server/Services/HandlerService.cs +++ b/Core/Server/Services/HandlerService.cs @@ -69,14 +69,12 @@ public class HandlerService(IServiceScopeFactory serviceScopeFactory, ILogger t.GetInterfaces().Contains(typeof(IMedusaPlugin))); + var assembly = Assembly.LoadFrom(dlls[i]); + Type? medusaPlugin = null; + + try + { + medusaPlugin = assembly.GetTypes().FirstOrDefault(t => t.GetInterfaces().Contains(typeof(IMedusaPlugin))); + } + catch(ReflectionTypeLoadException ex) + { + logger.LogWarning("Plugin '{}' appears to be outdated or incompatible. Please update it to the latest version of Medusa. Details: {}", + assembly.GetName().Name, + string.Join("; ", ex.LoaderExceptions.Select(e => e?.Message ?? "Unknown error"))); + continue; + } if (medusaPlugin is null) {