From 68a7d6cc61cad91b7277216892b8365e327aa1db Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Wed, 13 May 2026 13:35:56 +0900 Subject: [PATCH] We have tightened account management. Accounts created using methods other than email address login will no longer be accessible. (This is a necessary step when implementing multiplayer in the future.) --- .../Packet/Recv/Login/HandlerReqLogin.cs | 42 ++++- SdkServer/Handlers/RouteController.cs | 162 ++++++++++++++---- SdkServer/Utils/LoggingMiddleware.cs | 84 ++++++++- 3 files changed, 239 insertions(+), 49 deletions(-) diff --git a/GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs b/GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs index aec6f53..b4d98be 100644 --- a/GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs +++ b/GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs @@ -11,6 +11,8 @@ using MikuSB.GameServer.Server.Packet.Send.Misc; using MikuSB.Proto; using MikuSB.TcpSharp; using MikuSB.Util; +using System.Text; +using System.Text.Json; using System.Text.Json.Nodes; namespace MikuSB.GameServer.Server.Packet.Recv.Login; @@ -18,21 +20,45 @@ namespace MikuSB.GameServer.Server.Packet.Recv.Login; [Opcode(CmdIds.ReqLogin)] public class HandlerReqLogin : Handler { + private static readonly Logger Logger = new("ReqLogin"); + + private static string? ExtractSdkAuthToken(string? token) + { + if (string.IsNullOrWhiteSpace(token)) + return null; + + try + { + var normalized = Uri.UnescapeDataString(token).Trim(); + var padding = normalized.Length % 4; + if (padding > 0) + normalized = normalized.PadRight(normalized.Length + (4 - padding), '='); + + var json = Encoding.UTF8.GetString(Convert.FromBase64String(normalized)); + using var document = JsonDocument.Parse(json); + return document.RootElement.TryGetProperty("authToken", out var authToken) + ? authToken.GetString() + : null; + } + catch + { + return null; + } + } + public override async Task OnHandle(Connection connection, byte[] data, ushort seqNo) { var req = ReqLogin.Parser.ParseFrom(data); + var sdkAuthToken = ExtractSdkAuthToken(req.Token); var account = AccountData.GetAccountByComboToken(req.Token) ?? AccountData.GetAccountByDispatchToken(req.Token) - ?? AccountData.GetAccountByUid(10001) - ?? AccountData.GetAccountByUid(1); + ?? AccountData.GetAccountByComboToken(sdkAuthToken ?? "") + ?? AccountData.GetAccountByDispatchToken(sdkAuthToken ?? ""); if (account == null) { - account = AccountData.CreateAccount("default@mikusb.local", 10001, ""); - if (account == null) - { - await connection.SendPacket(CmdIds.NtfLogout); - return; - } + Logger.Warn($"Rejected login: provider={req.Provider}, token={req.Token}, authToken={sdkAuthToken}"); + await connection.SendPacket(CmdIds.NtfLogout); + return; } if (!ResourceManager.IsLoaded) // resource manager not loaded, return diff --git a/SdkServer/Handlers/RouteController.cs b/SdkServer/Handlers/RouteController.cs index 91367b6..49be5ea 100644 --- a/SdkServer/Handlers/RouteController.cs +++ b/SdkServer/Handlers/RouteController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using MikuSB.Configuration; using MikuSB.Database.Account; @@ -13,8 +14,6 @@ public class RouteController : ControllerBase { public static ConfigContainer Config = ConfigManager.Config; - private const int DefaultAccountUid = 10001; - public static object BuildServerList(string version = "") { return new @@ -129,29 +128,15 @@ public class RouteController : ControllerBase return Ok(rsp); } - private static AccountData EnsureDefaultAccount() - { - var account = AccountData.GetAccountByUid(DefaultAccountUid) - ?? AccountData.GetAccountByEmail("default@mikusb.local"); - if (account != null) - return account; - - return AccountData.CreateAccount("default@mikusb.local", DefaultAccountUid, ""); - } - - private static AccountData ResolveAccountByUid(string? uid) + private static AccountData? ResolveAccountByUid(string? uid) { if (int.TryParse(uid, out var parsedUid)) - { - var accountByUid = AccountData.GetAccountByUid(parsedUid); - if (accountByUid != null) - return accountByUid; - } + return AccountData.GetAccountByUid(parsedUid); - return EnsureDefaultAccount(); + return null; } - private static AccountData ResolveAccountForSdkLogin(string? email, string? uid, string? token) + private static AccountData? ResolveAccountForSdkLogin(string? email, string? uid, string? token) { if (!string.IsNullOrWhiteSpace(token)) { @@ -174,18 +159,78 @@ public class RouteController : ControllerBase return ResolveAccountByUid(uid); } + private async Task GetJsonBodyValue(string propertyName) + { + if (!Request.HasJsonContentType()) + return null; + + Request.EnableBuffering(); + Request.Body.Position = 0; + + using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + Request.Body.Position = 0; + + if (string.IsNullOrWhiteSpace(body)) + return null; + + try + { + using var document = JsonDocument.Parse(body); + if (document.RootElement.ValueKind != JsonValueKind.Object) + return null; + + return document.RootElement.TryGetProperty(propertyName, out var value) + ? value.GetString() + : null; + } + catch + { + return null; + } + } + + private IActionResult BuildLoginFailedResponse(string message) + { + object rsp = new + { + code = 1001, + data = (object?)null, + msg = message + }; + + return Ok(rsp); + } + + private IActionResult BuildNotFoundResponse(string message) + { + object rsp = new + { + code = 1001, + data = (object?)null, + msg = message + }; + + return Ok(rsp); + } + [HttpGet("/seasun/loginByToken")] [HttpPost("/seasun/loginByToken")] - public IActionResult LoginByToken( + public async Task LoginByToken( [FromQuery] string? uid, [FromQuery] string? token, [FromForm] string? form_uid, [FromForm] string? form_token ) { - var account = ResolveAccountForSdkLogin(null, uid ?? form_uid, token ?? form_token); - var finalUid = account.Uid.ToString(); - var finalToken = account.GenerateComboToken(); + var finalUid = uid ?? form_uid ?? await GetJsonBodyValue("uid"); + var finalToken = token ?? form_token ?? await GetJsonBodyValue("token"); + var account = ResolveAccountForSdkLogin(null, finalUid, finalToken); + if (account == null) + return BuildLoginFailedResponse("Account not found."); + + var responseUid = account.Uid.ToString(); + var responseToken = account.GenerateComboToken(); object rsp = new { @@ -195,13 +240,13 @@ public class RouteController : ControllerBase associatedAccounts = Array.Empty(), isFirstLogin = false, isNeedKoreaSciAuth = false, - ksOpenId = $"ks_{finalUid}", + ksOpenId = $"ks_{responseUid}", nickname = account.Username, - passportId = finalUid, + passportId = responseUid, playerFillAgeUrl = "", status = 0, thirdPartyUid = "", - token = finalToken, + token = responseToken, type = "guest", uid = account.Uid }, @@ -213,7 +258,7 @@ public class RouteController : ControllerBase [HttpGet("/seasun/login")] [HttpPost("/seasun/login")] - public IActionResult Login( + public async Task Login( [FromQuery] string? uid, [FromQuery] string? token, [FromQuery] string? email, @@ -222,10 +267,48 @@ public class RouteController : ControllerBase [FromForm] string? form_email ) { - var finalEmail = email ?? form_email; - var account = ResolveAccountForSdkLogin(finalEmail, uid ?? form_uid, token ?? form_token); - var finalUid = account.Uid.ToString(); - var finalToken = account.GenerateComboToken(); + var finalEmail = email ?? form_email ?? await GetJsonBodyValue("email"); + if (!string.IsNullOrWhiteSpace(finalEmail)) + { + var accountByEmail = AccountData.GetAccountByEmail(finalEmail); + if (accountByEmail == null) + return BuildLoginFailedResponse("Account not found."); + + var finalUidValue = accountByEmail.Uid.ToString(); + var finalTokenValue = accountByEmail.GenerateComboToken(); + + object emailLoginRsp = new + { + code = 0, + data = new + { + associatedAccounts = Array.Empty(), + isFirstLogin = false, + isNeedKoreaSciAuth = false, + ksOpenId = $"ks_{finalUidValue}", + nickname = accountByEmail.Username, + passportId = finalUidValue, + playerFillAgeUrl = "", + status = 0, + thirdPartyUid = "", + token = finalTokenValue, + type = "guest", + uid = accountByEmail.Uid + }, + msg = "操作成功" + }; + + return Ok(emailLoginRsp); + } + + var finalUid = uid ?? form_uid ?? await GetJsonBodyValue("uid"); + var finalToken = token ?? form_token ?? await GetJsonBodyValue("token"); + var account = ResolveAccountForSdkLogin(finalEmail, finalUid, finalToken); + if (account == null) + return BuildLoginFailedResponse("Account not found."); + + var responseUid = account.Uid.ToString(); + var responseToken = account.GenerateComboToken(); object rsp = new { @@ -235,13 +318,13 @@ public class RouteController : ControllerBase associatedAccounts = Array.Empty(), isFirstLogin = false, isNeedKoreaSciAuth = false, - ksOpenId = $"ks_{finalUid}", + ksOpenId = $"ks_{responseUid}", nickname = account.Username, - passportId = finalUid, + passportId = responseUid, playerFillAgeUrl = "", status = 0, thirdPartyUid = "", - token = finalToken, + token = responseToken, type = "guest", uid = account.Uid }, @@ -259,6 +342,9 @@ public class RouteController : ControllerBase ) { var account = ResolveAccountByUid(uid ?? form_uid); + if (account == null) + return BuildNotFoundResponse("Account not found."); + var uidString = account.Uid.ToString(); object rsp = new @@ -338,7 +424,11 @@ public class RouteController : ControllerBase [HttpGet("/account/query-uid/{appId}")] public IActionResult QueryUid(string appId, [FromQuery] string authInfo) { - var uid = ResolveAccountByUid(ExtractUid(authInfo)).Uid.ToString(); + var account = ResolveAccountByUid(ExtractUid(authInfo)); + if (account == null) + return BuildNotFoundResponse("Account not found."); + + var uid = account.Uid.ToString(); object rsp = new { diff --git a/SdkServer/Utils/LoggingMiddleware.cs b/SdkServer/Utils/LoggingMiddleware.cs index 6c253bd..aead4c6 100644 --- a/SdkServer/Utils/LoggingMiddleware.cs +++ b/SdkServer/Utils/LoggingMiddleware.cs @@ -1,34 +1,108 @@ using Microsoft.AspNetCore.Http; using MikuSB.Util; +using System.Text; namespace MikuSB.SdkServer.Utils; public class RequestLoggingMiddleware(RequestDelegate next) { + private const long MaxLoggedBodyBytes = 1024 * 1024; + + private static bool ShouldSkip(string path) + => path.StartsWith("/report") || path.Contains("/log/") || path == "/alive"; + + private static bool ShouldLogBody(HttpRequest request) + { + if (request.ContentLength is null or 0) + return false; + + if (request.ContentLength > MaxLoggedBodyBytes) + return false; + + if (string.IsNullOrWhiteSpace(request.ContentType)) + return false; + + return request.ContentType.Contains("json", StringComparison.OrdinalIgnoreCase) + || request.ContentType.Contains("text", StringComparison.OrdinalIgnoreCase) + || request.ContentType.Contains("xml", StringComparison.OrdinalIgnoreCase) + || request.ContentType.Contains("x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase); + } + + private static string SanitizeForLog(string value) + { + var builder = new StringBuilder(value.Length); + foreach (var ch in value) + { + if (ch == '\r') + { + builder.Append(@"\r"); + continue; + } + + if (ch == '\n') + { + builder.Append(@"\n"); + continue; + } + + if (char.IsControl(ch)) + { + builder.Append(@"\u"); + builder.Append(((int)ch).ToString("x4")); + continue; + } + + builder.Append(ch); + } + + return builder.ToString(); + } + + private static async Task ReadBodyAsString(HttpRequest request) + { + request.EnableBuffering(); + request.Body.Position = 0; + + using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + + request.Body.Position = 0; + return body; + } + public async Task InvokeAsync(HttpContext context, Logger logger) { var request = context.Request; var method = request.Method; - var path = request.Path + request.QueryString; + var path = request.Path.ToString(); + var pathWithQuery = path + request.QueryString; + + if (ConfigManager.Config.HttpServer.EnableLog && !ShouldSkip(path)) + { + var body = ShouldLogBody(request) + ? SanitizeForLog(await ReadBodyAsString(request)) + : ""; + logger.Info($"REQ {method} {pathWithQuery} body={body}"); + } await next(context); var statusCode = context.Response.StatusCode; - if (path.StartsWith("/report") || path.Contains("/log/") || path == "/alive") + if (ShouldSkip(path)) return; if (!ConfigManager.Config.HttpServer.EnableLog) return; if (statusCode == 200) { - logger.Info($"{method} {path} => {statusCode}"); + logger.Info($"{method} {pathWithQuery} => {statusCode}"); } else if (statusCode == 404) { - logger.Warn($"{method} {path} => {statusCode}"); + logger.Warn($"{method} {pathWithQuery} => {statusCode}"); } else { - logger.Error($"{method} {path} => {statusCode}"); + logger.Error($"{method} {pathWithQuery} => {statusCode}"); } } }