| | | 1 | | using System.Diagnostics; |
| | | 2 | | using AsiBackbone.AspNetCore.DependencyInjection; |
| | | 3 | | using AsiBackbone.Core; |
| | | 4 | | using Microsoft.AspNetCore.Http; |
| | | 5 | | using Microsoft.AspNetCore.Routing; |
| | | 6 | | using Microsoft.Extensions.Options; |
| | | 7 | | using Microsoft.Extensions.Primitives; |
| | | 8 | | |
| | | 9 | | namespace AsiBackbone.AspNetCore.Correlation; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Resolves safe request correlation data from the current ASP.NET Core HTTP context. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <remarks> |
| | | 15 | | /// Client-supplied correlation identifiers are ignored by default. A host may explicitly trust configured headers when |
| | | 16 | | /// a trusted ingress removes caller-supplied values and writes its own. Trusted values are accepted only when their |
| | | 17 | | /// trimmed value is printable and does not exceed <see cref="GovernanceIdentifierLimits.MaximumLength" /> characters. |
| | | 18 | | /// The server-owned <see cref="HttpContext.TraceIdentifier" /> is always retained in safe request metadata. |
| | | 19 | | /// </remarks> |
| | | 20 | | public sealed class HttpContextGovernanceRequestCorrelationResolver : IHttpGovernanceRequestCorrelationResolver |
| | | 21 | | { |
| | | 22 | | private readonly IHttpContextAccessor httpContextAccessor; |
| | | 23 | | private readonly AspNetCoreGovernanceOptions options; |
| | | 24 | | |
| | | 25 | | /// <summary> |
| | | 26 | | /// Initializes a new instance of the <see cref="HttpContextGovernanceRequestCorrelationResolver" /> class. |
| | | 27 | | /// </summary> |
| | | 28 | | /// <param name="httpContextAccessor">The ASP.NET Core HTTP context accessor.</param> |
| | | 29 | | /// <param name="options">The request correlation options.</param> |
| | 47 | 30 | | public HttpContextGovernanceRequestCorrelationResolver( |
| | 47 | 31 | | IHttpContextAccessor httpContextAccessor, |
| | 47 | 32 | | IOptions<AspNetCoreGovernanceOptions> options) |
| | | 33 | | { |
| | 47 | 34 | | this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); |
| | 47 | 35 | | this.options = options?.Value ?? throw new ArgumentNullException(nameof(options)); |
| | 47 | 36 | | this.options.Validate(); |
| | 47 | 37 | | } |
| | | 38 | | |
| | | 39 | | /// <inheritdoc /> |
| | | 40 | | public GovernanceHttpRequestCorrelation ResolveRequestCorrelation() |
| | | 41 | | { |
| | 42 | 42 | | HttpContext? httpContext = httpContextAccessor.HttpContext; |
| | | 43 | | |
| | 42 | 44 | | return httpContext is null |
| | 42 | 45 | | ? new GovernanceHttpRequestCorrelation(traceId: Activity.Current?.Id) |
| | 42 | 46 | | : new GovernanceHttpRequestCorrelation( |
| | 42 | 47 | | ResolveCorrelationId(httpContext), |
| | 42 | 48 | | ResolveTraceId(httpContext), |
| | 42 | 49 | | ResolveMetadata(httpContext)); |
| | | 50 | | } |
| | | 51 | | |
| | | 52 | | private string? ResolveCorrelationId(HttpContext httpContext) |
| | | 53 | | { |
| | 41 | 54 | | if (options.TrustInboundCorrelationIdHeaders) |
| | | 55 | | { |
| | 80 | 56 | | foreach (string headerName in options.CorrelationIdHeaderNames) |
| | | 57 | | { |
| | 30 | 58 | | if (string.IsNullOrWhiteSpace(headerName)) |
| | | 59 | | { |
| | | 60 | | continue; |
| | | 61 | | } |
| | | 62 | | |
| | 29 | 63 | | if (!httpContext.Request.Headers.TryGetValue(headerName, out StringValues values)) |
| | | 64 | | { |
| | | 65 | | continue; |
| | | 66 | | } |
| | | 67 | | |
| | 50 | 68 | | foreach (string? candidate in values) |
| | | 69 | | { |
| | 14 | 70 | | string? normalizedCandidate = NormalizeClientCorrelationId(candidate); |
| | 14 | 71 | | if (normalizedCandidate is not null) |
| | | 72 | | { |
| | 4 | 73 | | return normalizedCandidate; |
| | | 74 | | } |
| | | 75 | | } |
| | | 76 | | } |
| | | 77 | | } |
| | | 78 | | |
| | 37 | 79 | | return options.UseHttpContextTraceIdentifierAsCorrelationId |
| | 37 | 80 | | ? httpContext.TraceIdentifier |
| | 37 | 81 | | : null; |
| | 4 | 82 | | } |
| | | 83 | | |
| | | 84 | | private static string? NormalizeClientCorrelationId(string? value) |
| | | 85 | | { |
| | 14 | 86 | | if (string.IsNullOrWhiteSpace(value)) |
| | | 87 | | { |
| | 2 | 88 | | return null; |
| | | 89 | | } |
| | | 90 | | |
| | 12 | 91 | | string normalizedValue = value.Trim(); |
| | 12 | 92 | | if (normalizedValue.Length > GovernanceIdentifierLimits.MaximumLength) |
| | | 93 | | { |
| | 1 | 94 | | return null; |
| | | 95 | | } |
| | | 96 | | |
| | 535 | 97 | | foreach (char character in normalizedValue) |
| | | 98 | | { |
| | 260 | 99 | | if (char.IsControl(character)) |
| | | 100 | | { |
| | 7 | 101 | | return null; |
| | | 102 | | } |
| | | 103 | | } |
| | | 104 | | |
| | 4 | 105 | | return normalizedValue; |
| | | 106 | | } |
| | | 107 | | |
| | | 108 | | private static string? ResolveTraceId(HttpContext httpContext) |
| | | 109 | | { |
| | 41 | 110 | | return Activity.Current?.Id ?? httpContext.TraceIdentifier; |
| | | 111 | | } |
| | | 112 | | |
| | | 113 | | private Dictionary<string, string> ResolveMetadata(HttpContext httpContext) |
| | | 114 | | { |
| | 41 | 115 | | Dictionary<string, string> metadata = new(StringComparer.Ordinal) |
| | 41 | 116 | | { |
| | 41 | 117 | | [GovernanceHttpRequestMetadataKeys.TraceIdentifier] = httpContext.TraceIdentifier |
| | 41 | 118 | | }; |
| | | 119 | | |
| | 41 | 120 | | if (options.IncludeRequestMethod && !string.IsNullOrWhiteSpace(httpContext.Request.Method)) |
| | | 121 | | { |
| | 2 | 122 | | metadata[GovernanceHttpRequestMetadataKeys.Method] = httpContext.Request.Method.Trim(); |
| | | 123 | | } |
| | | 124 | | |
| | 41 | 125 | | if (options.IncludeRequestPath && httpContext.Request.Path.HasValue) |
| | | 126 | | { |
| | 1 | 127 | | metadata[GovernanceHttpRequestMetadataKeys.Path] = httpContext.Request.Path.Value; |
| | | 128 | | } |
| | | 129 | | |
| | 41 | 130 | | Endpoint? endpoint = httpContext.GetEndpoint(); |
| | 41 | 131 | | var routeEndpoint = endpoint as RouteEndpoint; |
| | | 132 | | |
| | 41 | 133 | | if (options.IncludeEndpointDisplayName && !string.IsNullOrWhiteSpace(endpoint?.DisplayName)) |
| | | 134 | | { |
| | 4 | 135 | | metadata[GovernanceHttpRequestMetadataKeys.EndpointDisplayName] = endpoint.DisplayName.Trim(); |
| | | 136 | | } |
| | | 137 | | |
| | 41 | 138 | | if (options.IncludeRoutePattern && !string.IsNullOrWhiteSpace(routeEndpoint?.RoutePattern.RawText)) |
| | | 139 | | { |
| | 2 | 140 | | metadata[GovernanceHttpRequestMetadataKeys.RoutePattern] = routeEndpoint.RoutePattern.RawText.Trim(); |
| | | 141 | | } |
| | | 142 | | |
| | 41 | 143 | | if (options.IncludeRouteValues) |
| | | 144 | | { |
| | 88 | 145 | | foreach (KeyValuePair<string, object?> routeValue in httpContext.Request.RouteValues) |
| | | 146 | | { |
| | 4 | 147 | | if (routeValue.Value is null || string.IsNullOrWhiteSpace(routeValue.Key)) |
| | | 148 | | { |
| | | 149 | | continue; |
| | | 150 | | } |
| | | 151 | | |
| | 2 | 152 | | metadata[$"{GovernanceHttpRequestMetadataKeys.RouteValuePrefix}{routeValue.Key.Trim()}"] = |
| | 2 | 153 | | routeValue.Value.ToString()?.Trim() ?? string.Empty; |
| | | 154 | | } |
| | | 155 | | } |
| | | 156 | | |
| | 41 | 157 | | return metadata; |
| | | 158 | | } |
| | | 159 | | } |