using System.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Http; using Nuuru.Server.Auth; using Nuuru.Server.Services; namespace Nuuru.Server.Middleware { public class IntegrityCheckMiddleware { private readonly RequestDelegate _next; public IntegrityCheckMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, IIntegrityService integrityService) { if (!integrityService.IsEnabled) { await _next(context); return; } // Only check POST requests if (!HttpMethods.IsPost(context.Request.Method)) { await _next(context); return; } var path = context.Request.Path.Value ?? string.Empty; // Only gate API requests if (!path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip integrity endpoints if (path.StartsWith("/api/integrity/", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip SignalR hubs if (path.StartsWith("/api/hubs/", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip Auth endpoints other than register/login if (path.StartsWith("/api/auth/", StringComparison.OrdinalIgnoreCase) && !path.StartsWith("/api/auth/register", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip notification endpoints if (path.StartsWith("/api/notifications/", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip notification/conversation read endpoints if (path.EndsWith("/read", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip voicechat endpoints if (path.Contains("/voice/", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip BBCode endpoints if (path.StartsWith("/api/bbcode", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip PoW endpoints if (path.StartsWith("/api/pow/", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip captcha endpoints if (path.StartsWith("/api/captcha/", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip admin endpoints if (path.StartsWith("/api/admin/", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/api/moderation/", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/api/permission/", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/api/role/", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } string? token = null; context.Request.EnableBuffering(); if (context.Request.HasFormContentType) { var form = await context.Request.ReadFormAsync(); if (form.TryGetValue("integrity-v3", out var formValues)) { token = formValues.FirstOrDefault(); } } else if (context.Request.HasJsonContentType()) { using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true); var bodyText = await reader.ReadToEndAsync(); context.Request.Body.Position = 0; try { if (!string.IsNullOrEmpty(bodyText)) { var jsonDoc = JsonDocument.Parse(bodyText); if (jsonDoc.RootElement.TryGetProperty("integrity-v3", out var tokenElement)) { token = tokenElement.GetString(); } } } catch (JsonException) { // Ignore parsing errors, might not be valid JSON } } var canBypass = context.User.HasClaim("permission", Permissions.User.BypassIntegrity); if (string.IsNullOrEmpty(token) && !canBypass) { context.Response.StatusCode = 403; context.Response.ContentType = "application/json"; await context.Response.WriteAsJsonAsync(new { error = "integrity_token_missing" }); return; } var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); var actionName = "UnknownAction"; var endpoint = context.GetEndpoint(); if (endpoint != null) { var actionDescriptor = endpoint.Metadata.GetMetadata(); if (actionDescriptor != null) { actionName = $"{actionDescriptor.ControllerName}.{actionDescriptor.ActionName}"; } else { actionName = endpoint.DisplayName ?? path; } } var integrityResult = await integrityService.VerifyTokenAsync(token, clientIp, actionName, userId); if (!(integrityResult?.Valid ?? true) && !canBypass) { context.Response.StatusCode = 403; context.Response.ContentType = "application/json"; await context.Response.WriteAsJsonAsync(new { error = "integrity_check_failed" }); return; } if (integrityResult?.AssessmentId.HasValue ?? false) { context.Items[Nuuru.Server.Models.AuditLog.AssessmentIdKey] = integrityResult.AssessmentId.Value; } if (!string.IsNullOrEmpty(integrityResult?.BrowserHash)) { context.Items[Nuuru.Server.Models.AuditLog.BrowserHashKey] = integrityResult.BrowserHash; } await _next(context); } } }