Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

View File

@@ -0,0 +1,88 @@
using System.Security.Claims;
using System.Text.Json;
namespace StellaOps.Gateway.WebService.Middleware;
public sealed class ClaimsPropagationMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ClaimsPropagationMiddleware> _logger;
public ClaimsPropagationMiddleware(RequestDelegate next, ILogger<ClaimsPropagationMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (GatewayRoutes.IsSystemPath(context.Request.Path))
{
await _next(context);
return;
}
var principal = context.User;
SetHeaderIfMissing(context, "sub", principal.FindFirstValue("sub"));
SetHeaderIfMissing(context, "tid", principal.FindFirstValue("tid"));
var scopes = principal.FindAll("scope").Select(c => c.Value).ToArray();
if (scopes.Length > 0)
{
SetHeaderIfMissing(context, "scope", string.Join(" ", scopes));
}
var cnfJson = principal.FindFirstValue("cnf");
if (!string.IsNullOrWhiteSpace(cnfJson))
{
context.Items[GatewayContextKeys.CnfJson] = cnfJson;
if (TryParseCnf(cnfJson, out var jkt))
{
context.Items[GatewayContextKeys.DpopThumbprint] = jkt;
SetHeaderIfMissing(context, "cnf.jkt", jkt);
}
}
await _next(context);
}
private void SetHeaderIfMissing(HttpContext context, string name, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
if (!context.Request.Headers.ContainsKey(name))
{
context.Request.Headers[name] = value;
}
else
{
_logger.LogDebug("Request header {Header} already set; skipping claim propagation", name);
}
}
private static bool TryParseCnf(string json, out string? jkt)
{
jkt = null;
try
{
using var document = JsonDocument.Parse(json);
if (document.RootElement.TryGetProperty("jkt", out var jktElement) &&
jktElement.ValueKind == JsonValueKind.String)
{
jkt = jktElement.GetString();
}
return !string.IsNullOrWhiteSpace(jkt);
}
catch (JsonException)
{
return false;
}
}
}

View File

@@ -0,0 +1,30 @@
namespace StellaOps.Gateway.WebService.Middleware;
public sealed class CorrelationIdMiddleware
{
public const string HeaderName = "X-Correlation-Id";
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Headers.TryGetValue(HeaderName, out var headerValue) &&
!string.IsNullOrWhiteSpace(headerValue))
{
context.TraceIdentifier = headerValue.ToString();
}
else if (string.IsNullOrWhiteSpace(context.TraceIdentifier))
{
context.TraceIdentifier = Guid.NewGuid().ToString("N");
}
context.Response.Headers[HeaderName] = context.TraceIdentifier;
await _next(context);
}
}

View File

@@ -0,0 +1,9 @@
namespace StellaOps.Gateway.WebService.Middleware;
public static class GatewayContextKeys
{
public const string TenantId = "Gateway.TenantId";
public const string DpopThumbprint = "Gateway.DpopThumbprint";
public const string MtlsThumbprint = "Gateway.MtlsThumbprint";
public const string CnfJson = "Gateway.CnfJson";
}

View File

@@ -0,0 +1,34 @@
namespace StellaOps.Gateway.WebService.Middleware;
public static class GatewayRoutes
{
private static readonly HashSet<string> SystemPaths = new(StringComparer.OrdinalIgnoreCase)
{
"/health",
"/health/live",
"/health/ready",
"/health/startup",
"/metrics",
"/openapi.json",
"/openapi.yaml",
"/.well-known/openapi"
};
public static bool IsSystemPath(PathString path)
{
var value = path.Value ?? string.Empty;
return SystemPaths.Contains(value);
}
public static bool IsHealthPath(PathString path)
{
var value = path.Value ?? string.Empty;
return value.StartsWith("/health", StringComparison.OrdinalIgnoreCase);
}
public static bool IsMetricsPath(PathString path)
{
var value = path.Value ?? string.Empty;
return string.Equals(value, "/metrics", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,89 @@
using System.Text;
using System.Text.Json;
using StellaOps.Gateway.WebService.Services;
namespace StellaOps.Gateway.WebService.Middleware;
public sealed class HealthCheckMiddleware
{
private readonly RequestDelegate _next;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
public HealthCheckMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, GatewayServiceStatus status, GatewayMetrics metrics)
{
if (GatewayRoutes.IsMetricsPath(context.Request.Path))
{
await WriteMetricsAsync(context, metrics);
return;
}
if (!GatewayRoutes.IsHealthPath(context.Request.Path))
{
await _next(context);
return;
}
var path = context.Request.Path.Value ?? string.Empty;
if (path.Equals("/health/live", StringComparison.OrdinalIgnoreCase))
{
await WriteHealthAsync(context, StatusCodes.Status200OK, "live", status);
return;
}
if (path.Equals("/health/ready", StringComparison.OrdinalIgnoreCase))
{
var readyStatus = status.IsReady ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
await WriteHealthAsync(context, readyStatus, "ready", status);
return;
}
if (path.Equals("/health/startup", StringComparison.OrdinalIgnoreCase))
{
var startupStatus = status.IsStarted ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
await WriteHealthAsync(context, startupStatus, "startup", status);
return;
}
await WriteHealthAsync(context, StatusCodes.Status200OK, "ok", status);
}
private static Task WriteHealthAsync(HttpContext context, int statusCode, string status, GatewayServiceStatus serviceStatus)
{
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json; charset=utf-8";
var payload = new
{
status,
started = serviceStatus.IsStarted,
ready = serviceStatus.IsReady,
traceId = context.TraceIdentifier
};
return context.Response.WriteAsJsonAsync(payload, JsonOptions, context.RequestAborted);
}
private static Task WriteMetricsAsync(HttpContext context, GatewayMetrics metrics)
{
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.ContentType = "text/plain; version=0.0.4";
var builder = new StringBuilder();
builder.AppendLine("# TYPE gateway_active_connections gauge");
builder.Append("gateway_active_connections ").AppendLine(metrics.GetActiveConnections().ToString());
builder.AppendLine("# TYPE gateway_registered_endpoints gauge");
builder.Append("gateway_registered_endpoints ").AppendLine(metrics.GetRegisteredEndpoints().ToString());
return context.Response.WriteAsync(builder.ToString(), context.RequestAborted);
}
}

View File

@@ -0,0 +1,22 @@
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Gateway.Middleware;
namespace StellaOps.Gateway.WebService.Middleware;
public sealed class RequestRoutingMiddleware
{
private readonly TransportDispatchMiddleware _dispatchMiddleware;
public RequestRoutingMiddleware(
RequestDelegate next,
ILogger<RequestRoutingMiddleware> logger,
ILogger<TransportDispatchMiddleware> dispatchLogger)
{
_dispatchMiddleware = new TransportDispatchMiddleware(next, dispatchLogger);
}
public Task InvokeAsync(HttpContext context, ITransportClient transportClient, IGlobalRoutingState routingState)
{
return _dispatchMiddleware.Invoke(context, transportClient, routingState);
}
}

View File

@@ -0,0 +1,214 @@
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Security.Dpop;
using StellaOps.Gateway.WebService.Configuration;
namespace StellaOps.Gateway.WebService.Middleware;
public sealed class SenderConstraintMiddleware
{
private readonly RequestDelegate _next;
private readonly IOptions<GatewayOptions> _options;
private readonly IDpopProofValidator _dpopValidator;
private readonly ILogger<SenderConstraintMiddleware> _logger;
public SenderConstraintMiddleware(
RequestDelegate next,
IOptions<GatewayOptions> options,
IDpopProofValidator dpopValidator,
ILogger<SenderConstraintMiddleware> logger)
{
_next = next;
_options = options;
_dpopValidator = dpopValidator;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (GatewayRoutes.IsSystemPath(context.Request.Path))
{
await _next(context);
return;
}
var authOptions = _options.Value.Auth;
if (context.User.Identity?.IsAuthenticated != true)
{
if (authOptions.AllowAnonymous)
{
await _next(context);
return;
}
await WriteUnauthorizedAsync(context, "unauthenticated", "Authentication required.");
return;
}
var confirmation = ParseConfirmation(context.User.FindFirstValue("cnf"));
if (confirmation.Raw is not null)
{
context.Items[GatewayContextKeys.CnfJson] = confirmation.Raw;
}
var requireDpop = authOptions.DpopEnabled && (!authOptions.MtlsEnabled || !string.IsNullOrWhiteSpace(confirmation.Jkt));
var requireMtls = authOptions.MtlsEnabled && (!authOptions.DpopEnabled || !string.IsNullOrWhiteSpace(confirmation.X5tS256));
if (authOptions.DpopEnabled && authOptions.MtlsEnabled &&
string.IsNullOrWhiteSpace(confirmation.Jkt) && string.IsNullOrWhiteSpace(confirmation.X5tS256))
{
requireDpop = true;
requireMtls = true;
}
if (requireDpop && !await ValidateDpopAsync(context, confirmation))
{
return;
}
if (requireMtls && !await ValidateMtlsAsync(context, confirmation))
{
return;
}
await _next(context);
}
private async Task<bool> ValidateDpopAsync(HttpContext context, ConfirmationClaim confirmation)
{
if (!context.Request.Headers.TryGetValue("DPoP", out var proofHeader) ||
string.IsNullOrWhiteSpace(proofHeader))
{
_logger.LogWarning("Missing DPoP proof for request {TraceId}", context.TraceIdentifier);
await WriteUnauthorizedAsync(context, "dpop_missing", "DPoP proof is required.");
return false;
}
var proof = proofHeader.ToString();
var requestUri = new Uri(context.Request.GetDisplayUrl());
var result = await _dpopValidator.ValidateAsync(
proof,
context.Request.Method,
requestUri,
cancellationToken: context.RequestAborted);
if (!result.IsValid)
{
_logger.LogWarning("DPoP validation failed for {TraceId}: {Error}", context.TraceIdentifier, result.ErrorDescription);
await WriteUnauthorizedAsync(context, result.ErrorCode ?? "dpop_invalid", result.ErrorDescription ?? "DPoP proof invalid.");
return false;
}
if (result.PublicKey is not JsonWebKey jwk)
{
_logger.LogWarning("DPoP validation failed for {TraceId}: JWK missing", context.TraceIdentifier);
await WriteUnauthorizedAsync(context, "dpop_key_invalid", "DPoP proof must include a valid JWK.");
return false;
}
var thumbprint = ComputeJwkThumbprint(jwk);
context.Items[GatewayContextKeys.DpopThumbprint] = thumbprint;
if (!string.IsNullOrWhiteSpace(confirmation.Jkt) &&
!string.Equals(confirmation.Jkt, thumbprint, StringComparison.Ordinal))
{
_logger.LogWarning("DPoP thumbprint mismatch for {TraceId}", context.TraceIdentifier);
await WriteUnauthorizedAsync(context, "dpop_thumbprint_mismatch", "DPoP proof does not match token confirmation.");
return false;
}
return true;
}
private async Task<bool> ValidateMtlsAsync(HttpContext context, ConfirmationClaim confirmation)
{
var certificate = context.Connection.ClientCertificate;
if (certificate is null)
{
_logger.LogWarning("mTLS required but no client certificate provided for {TraceId}", context.TraceIdentifier);
await WriteUnauthorizedAsync(context, "mtls_required", "Client certificate required.");
return false;
}
var hash = certificate.GetCertHash(HashAlgorithmName.SHA256);
var thumbprint = Base64UrlEncoder.Encode(hash);
context.Items[GatewayContextKeys.MtlsThumbprint] = thumbprint;
if (!string.IsNullOrWhiteSpace(confirmation.X5tS256) &&
!string.Equals(confirmation.X5tS256, thumbprint, StringComparison.Ordinal))
{
_logger.LogWarning("mTLS thumbprint mismatch for {TraceId}", context.TraceIdentifier);
await WriteUnauthorizedAsync(context, "mtls_thumbprint_mismatch", "Client certificate does not match token confirmation.");
return false;
}
return true;
}
private static string ComputeJwkThumbprint(JsonWebKey jwk)
{
object rawThumbprint = jwk.ComputeJwkThumbprint();
return rawThumbprint switch
{
string thumbprint => thumbprint,
byte[] bytes => Base64UrlEncoder.Encode(bytes),
_ => throw new InvalidOperationException("Unable to compute JWK thumbprint.")
};
}
private static ConfirmationClaim ParseConfirmation(string? json)
{
if (string.IsNullOrWhiteSpace(json))
{
return ConfirmationClaim.Empty;
}
try
{
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
root.TryGetProperty("jkt", out var jktElement);
root.TryGetProperty("x5t#S256", out var x5tElement);
return new ConfirmationClaim(
json,
jktElement.ValueKind == JsonValueKind.String ? jktElement.GetString() : null,
x5tElement.ValueKind == JsonValueKind.String ? x5tElement.GetString() : null);
}
catch (JsonException)
{
return ConfirmationClaim.Empty;
}
}
private static Task WriteUnauthorizedAsync(HttpContext context, string error, string message)
{
if (context.Response.HasStarted)
{
return Task.CompletedTask;
}
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json; charset=utf-8";
var payload = new
{
error,
message,
traceId = context.TraceIdentifier
};
return context.Response.WriteAsJsonAsync(payload, context.RequestAborted);
}
private sealed record ConfirmationClaim(string? Raw, string? Jkt, string? X5tS256)
{
public static ConfirmationClaim Empty { get; } = new(null, null, null);
}
}

View File

@@ -0,0 +1,40 @@
using System.Security.Claims;
namespace StellaOps.Gateway.WebService.Middleware;
public sealed class TenantMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<TenantMiddleware> _logger;
public TenantMiddleware(RequestDelegate next, ILogger<TenantMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (GatewayRoutes.IsSystemPath(context.Request.Path))
{
await _next(context);
return;
}
var tenantId = context.User.FindFirstValue("tid");
if (!string.IsNullOrWhiteSpace(tenantId))
{
context.Items[GatewayContextKeys.TenantId] = tenantId;
if (!context.Request.Headers.ContainsKey("tid"))
{
context.Request.Headers["tid"] = tenantId;
}
}
else
{
_logger.LogDebug("No tenant claim found on request {TraceId}", context.TraceIdentifier);
}
await _next(context);
}
}