using System.Net; using System.Security.Cryptography; using System.Text.Json.Nodes; using Microsoft.AspNetCore.Mvc; using Csob.Client; using Csob.Credentials; namespace Csob.Infrastructure; /// /// Translates domain exceptions into JSON responses. Credentials are /// never logged — only upstream status codes and error codes (the upstream's own payload). Errors /// are always logged (no silent failures). /// public sealed class ExceptionHandlingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (MissingCredentialsException ex) { await WriteProblem(context, HttpStatusCode.Unauthorized, ex.Message, new Dictionary { ["missingHeaders"] = ex.MissingHeaders, }); } catch (CryptographicException ex) { // Invalid Base64 PFX or wrong passphrase in the certificate header. Do not log the value. _logger.LogWarning("Client certificate could not be loaded: {Message}", ex.Message); await WriteProblem(context, HttpStatusCode.BadRequest, "The provided client certificate could not be loaded. Check X-CSOB-Certificate (Base64 PFX) and X-CSOB-Certificate-Password.", null); } catch (CsobApiException ex) { _logger.LogWarning("ČSOB PSD2 API call failed: {Status} {Codes}", (int)ex.StatusCode, string.Join(",", ex.ErrorCodes)); await WriteProblem(context, ex.StatusCode, ex.Message, new Dictionary { ["csobErrorCodes"] = ex.ErrorCodes, ["upstreamBody"] = ParseBody(ex.RawBody), }); } catch (TaskCanceledException) when (!context.RequestAborted.IsCancellationRequested) { _logger.LogWarning("ČSOB PSD2 API call timed out."); await WriteProblem(context, HttpStatusCode.GatewayTimeout, "The ČSOB PSD2 API did not respond in time.", null); } catch (HttpRequestException ex) { // Network failure, or the mutual-TLS handshake was rejected (e.g. wrong/expired eIDAS certificate). _logger.LogWarning(ex, "ČSOB PSD2 API unreachable or TLS handshake failed."); await WriteProblem(context, HttpStatusCode.BadGateway, "The ČSOB PSD2 API is unreachable or the mutual-TLS handshake failed.", null); } catch (Exception ex) { _logger.LogError(ex, "Unhandled error while processing the request."); await WriteProblem(context, HttpStatusCode.InternalServerError, "An unexpected error occurred.", null); } } private static JsonNode? ParseBody(string? rawBody) { if (string.IsNullOrWhiteSpace(rawBody)) { return null; } try { return JsonNode.Parse(rawBody); } catch (System.Text.Json.JsonException) { return JsonValue.Create(rawBody); } } private static async Task WriteProblem(HttpContext context, HttpStatusCode status, string detail, IDictionary? extensions) { if (context.Response.HasStarted) { return; } var problem = new ProblemDetails { Status = (int)status, Title = ReasonPhrase(status), Detail = detail, }; if (extensions is not null) { foreach (var (key, value) in extensions) { problem.Extensions[key] = value; } } context.Response.Clear(); context.Response.StatusCode = (int)status; context.Response.ContentType = "application/problem+json"; await context.Response.WriteAsJsonAsync(problem); } private static string ReasonPhrase(HttpStatusCode status) => status switch { HttpStatusCode.Unauthorized => "Unauthorized", HttpStatusCode.BadRequest => "Bad Request", HttpStatusCode.BadGateway => "Upstream ČSOB PSD2 API error", HttpStatusCode.GatewayTimeout => "Upstream ČSOB PSD2 API timeout", _ => status.ToString(), }; }