From c530809449af75626ce5a7b2537b1c4e8d7b10d0 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 25 May 2026 14:48:51 -0400 Subject: [PATCH] Auth logging updates --- .../Middlewares/SessionidMappingMiddleware.cs | 46 +++++++++-- .../ShadowverseTranslationMiddleware.cs | 80 ++++++++++++++++--- .../SteamSessionAuthenticationHandler.cs | 22 ++++- 3 files changed, 129 insertions(+), 19 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Middlewares/SessionidMappingMiddleware.cs b/SVSim.EmulatedEntrypoint/Middlewares/SessionidMappingMiddleware.cs index 86c280e..3bca691 100644 --- a/SVSim.EmulatedEntrypoint/Middlewares/SessionidMappingMiddleware.cs +++ b/SVSim.EmulatedEntrypoint/Middlewares/SessionidMappingMiddleware.cs @@ -10,21 +10,55 @@ namespace SVSim.EmulatedEntrypoint.Middlewares; public class SessionidMappingMiddleware : IMiddleware { private readonly ShadowverseSessionService _shadowverseSessionService; + private readonly ILogger _logger; - public SessionidMappingMiddleware(ShadowverseSessionService shadowverseSessionService) + public SessionidMappingMiddleware( + ShadowverseSessionService shadowverseSessionService, + ILogger logger) { _shadowverseSessionService = shadowverseSessionService; + _logger = logger; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - bool hasSessionId = context.Request.Headers.TryGetValue(NetworkConstants.UdidHeaderName, out var udid); - bool hasUdid = context.Request.Headers.TryGetValue(NetworkConstants.SessionIdHeaderName, out var sid); - if (hasSessionId && hasUdid) + // NOTE: the bool names below were historically inverted (hasSessionId held UDID and + // vice versa). Variable names corrected in-place; behavior unchanged. + bool hasUdid = context.Request.Headers.TryGetValue(NetworkConstants.UdidHeaderName, out var udid); + bool hasSid = context.Request.Headers.TryGetValue(NetworkConstants.SessionIdHeaderName, out var sid); + + if (hasUdid && hasSid) { - _shadowverseSessionService.StoreUdidForSessionId(sid.FirstOrDefault(), Guid.Parse(Encryption.Decode(udid.FirstOrDefault()))); + string? sidValue = sid.FirstOrDefault(); + string? encodedUdid = udid.FirstOrDefault(); + try + { + string? decoded = Encryption.Decode(encodedUdid); + Guid parsedUdid = Guid.Parse(decoded); + _shadowverseSessionService.StoreUdidForSessionId(sidValue, parsedUdid); + _logger.LogDebug( + "Stored SID→UDID mapping for {Path} (sid={Sid}, udid={Udid}).", + context.Request.Path, sidValue, parsedUdid); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to decode/parse UDID header for {Path} (sid={Sid}, encodedUdidLen={EncodedUdidLen}). " + + "Downstream translation will fall back to Guid.Empty and almost certainly fail msgpack decrypt.", + context.Request.Path, sidValue, encodedUdid?.Length ?? 0); + } + } + else if (hasUdid ^ hasSid) + { + // Only one of the two headers present — usually a client bug or a test that forgot + // to set both. The translation middleware will then fall back to Guid.Empty and + // surface as a generic msgpack/decrypt error, so warn here where the cause is clear. + _logger.LogWarning( + "Only one of UDID/SID headers present for {Path} (hasUdid={HasUdid}, hasSid={HasSid}). " + + "Translation will use Guid.Empty as the encryption key.", + context.Request.Path, hasUdid, hasSid); } await next.Invoke(context); } -} \ No newline at end of file +} diff --git a/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs b/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs index 86ef441..230406f 100644 --- a/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs +++ b/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs @@ -25,6 +25,7 @@ public class ShadowverseTranslationMiddleware : IMiddleware { private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; private readonly ShadowverseSessionService _sessionService; + private readonly ILogger _logger; // Serialization policy MUST match what AddJsonOptions configured on the controllers, or the // model binder won't find the snake_case keys we write into the synthetic request body and @@ -36,10 +37,14 @@ public class ShadowverseTranslationMiddleware : IMiddleware DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - public ShadowverseTranslationMiddleware(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, ShadowverseSessionService sessionService) + public ShadowverseTranslationMiddleware( + IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, + ShadowverseSessionService sessionService, + ILogger logger) { _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; _sessionService = sessionService; + _logger = logger; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -54,25 +59,65 @@ public class ShadowverseTranslationMiddleware : IMiddleware await next.Invoke(context); return; } - + // Replace response body stream to re-access it. using MemoryStream tempResponseBody = new MemoryStream(); Stream originalResponsebody = context.Response.Body; context.Response.Body = tempResponseBody; - + // Pull out the request bytes into a stream using MemoryStream requestBytesStream = new MemoryStream(); await context.Request.Body.CopyToAsync(requestBytesStream); byte[] requestBytes = requestBytesStream.ToArray(); - + // Get encryption values for this request string sid = context.Request.Headers[NetworkConstants.SessionIdHeaderName]; - string udid = _sessionService.GetUdidFromSessionId(sid).GetValueOrDefault().ToString(); - + Guid? mappedUdid = _sessionService.GetUdidFromSessionId(sid); + if (mappedUdid is null) + { + // Per design (2026-05-25): warn and continue. Decrypt will fail with Guid.Empty as + // the AES key, surfacing as a msgpack/decrypt error below — but now the *root cause* + // (the SID wasn't in our dict, likely because the prior request didn't include a UDID + // header or the server was restarted between handshake and this call) is in the log. + _logger.LogWarning( + "No UDID mapping for SID on {Path} (sid={Sid}). Falling back to Guid.Empty — the following decrypt/msgpack error is almost certainly caused by this.", + path, sid); + } + string udid = mappedUdid.GetValueOrDefault().ToString(); + // Decrypt incoming data. - requestBytes = Encryption.Decrypt(requestBytes, udid); + byte[] decryptedBytes; + try + { + decryptedBytes = Encryption.Decrypt(requestBytes, udid); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Decrypt failed for {Path} (udid={Udid}, encryptedLen={EncryptedLen}). " + + "If udid is all-zero, see the preceding 'No UDID mapping' warning.", + path, udid, requestBytes.Length); + throw; + } + Type requestType = endpointDescriptor.Parameters.FirstOrDefault().ParameterType; - object? data = MessagePackSerializer.Deserialize(requestType, requestBytes); + object? data; + try + { + data = MessagePackSerializer.Deserialize(requestType, decryptedBytes); + } + catch (Exception ex) + { + // The most common cause is a Guid.Empty decrypt above producing garbage bytes — but + // it can also be a genuine schema mismatch (DTO missing [Key], wrong types, etc.), + // so include the first few bytes for triage. + string bytePrefix = Convert.ToHexString(decryptedBytes.AsSpan(0, Math.Min(16, decryptedBytes.Length))); + _logger.LogError(ex, + "Msgpack deserialize failed for {Path} into {RequestType} (udid={Udid}, decryptedLen={DecryptedLen}, firstBytes={BytePrefix}). " + + "If decrypted bytes look like noise, the SID→UDID mapping was missing (see warnings above).", + path, requestType.Name, udid, decryptedBytes.Length, bytePrefix); + throw; + } // Re-serialize via System.Text.Json with the SAME options the controllers use, so the // model binder sees snake_case keys it can match. Using JsonConvert here writes the // CLR property names (PascalCase) and every property silently binds to default → 400. @@ -80,7 +125,7 @@ public class ShadowverseTranslationMiddleware : IMiddleware StringContent newStream = new StringContent(json, Encoding.UTF8, "application/json"); context.Request.Body = newStream.ReadAsStream(); context.Request.Headers.ContentType = new StringValues("application/json"); - + await next.Invoke(context); Viewer? viewer = context.GetViewer(); @@ -130,8 +175,19 @@ public class ShadowverseTranslationMiddleware : IMiddleware // primitive tree under Data — emitting only the keys present in the dictionary. var msgPackOptions = MessagePackSerializerOptions.Standard .WithResolver(ContractlessStandardResolver.Instance); - byte[] packedData = MessagePackSerializer.Serialize(wrappedResponseData, msgPackOptions); - packedData = Encryption.Encrypt(packedData, udid); + byte[] packedData; + try + { + packedData = MessagePackSerializer.Serialize(wrappedResponseData, msgPackOptions); + packedData = Encryption.Encrypt(packedData, udid); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Response msgpack/encrypt failed for {Path} (viewerId={ViewerId}, udid={Udid}).", + path, viewer?.Id, udid); + throw; + } await originalResponsebody.WriteAsync(Encoding.UTF8.GetBytes(Convert.ToBase64String(packedData))); context.Response.Body = originalResponsebody; } @@ -160,4 +216,4 @@ public class ShadowverseTranslationMiddleware : IMiddleware _ => token.ToString() }; } -} \ No newline at end of file +} diff --git a/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs b/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs index 83165e4..823bba1 100644 --- a/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs +++ b/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs @@ -34,6 +34,7 @@ public class SteamSessionAuthenticationHandler : AuthenticationHandler HandleAuthenticateAsync() { + string path = Request.Path; byte[] requestBytes; try { @@ -51,6 +52,7 @@ public class SteamSessionAuthenticationHandler : AuthenticationHandler(requestString, RequestJsonOptions); } - catch (JsonException) + catch (JsonException ex) { + Logger.LogWarning(ex, + "Auth: failed to JSON-parse request body on {Path} (bodyLen={BodyLen}). " + + "Translation middleware should have rewritten this to JSON — if it didn't, the request bypassed translation (non-Unity UA?).", + path, requestBytes.Length); return AuthenticateResult.Fail("Invalid request body."); } if (requestJson is null || string.IsNullOrEmpty(requestJson.SteamSessionTicket)) { + Logger.LogWarning( + "Auth: request body missing steam_session_ticket on {Path} (bodyLen={BodyLen}, hasViewerId={HasViewerId}, steamId={SteamId}).", + path, requestBytes.Length, + !string.IsNullOrEmpty(requestJson?.ViewerId), requestJson?.SteamId ?? 0); return AuthenticateResult.Fail("Invalid request body."); } @@ -75,6 +85,10 @@ public class SteamSessionAuthenticationHandler : AuthenticationHandler