Auth logging updates

This commit is contained in:
gamer147
2026-05-25 14:48:51 -04:00
parent a5e39d71c1
commit c530809449
3 changed files with 129 additions and 19 deletions

View File

@@ -25,6 +25,7 @@ public class ShadowverseTranslationMiddleware : IMiddleware
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
private readonly ShadowverseSessionService _sessionService;
private readonly ILogger<ShadowverseTranslationMiddleware> _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<ShadowverseTranslationMiddleware> 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()
};
}
}
}