From 36090885a7d8692180690732ebf7063b929ae689 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Mon, 16 Sep 2024 10:45:02 -0500 Subject: [PATCH] change how/where httpexchange is changed to httptrace, more name updates, filter headers during return instead of capture --- .../CloudFoundrySecurityMiddleware.cs | 9 +- .../ConfigureHttpExchangesEndpointOptions.cs | 15 +- .../EndpointServiceCollectionExtensions.cs | 1 + .../Actuators/HttpExchanges/HttpExchange.cs | 10 +- ...ePrincipal.cs => HttpExchangePrincipal.cs} | 4 +- ...TraceRequest.cs => HttpExchangeRequest.cs} | 6 +- ...aceResponse.cs => HttpExchangeResponse.cs} | 6 +- ...TraceSession.cs => HttpExchangeSession.cs} | 4 +- .../HttpExchangesDiagnosticObserver.cs | 61 ++++--- .../HttpExchangesEndpointHandler.cs | 2 - .../HttpExchangesEndpointOptions.cs | 20 +-- .../HttpExchanges/HttpExchangesResult.cs | 2 - .../Actuators/HttpExchanges/TraceResult.cs | 24 --- .../Actuators/Hypermedia/HypermediaService.cs | 13 +- .../src/Endpoint/ConfigurationSchema.json | 54 +++--- .../src/Endpoint/EndpointOptionsExtensions.cs | 9 +- .../src/Endpoint/PublicAPI.Unshipped.txt | 78 ++++----- .../HttpExchanges/EndpointMiddlewareTest.cs | 9 +- .../EndpointServiceCollectionTest.cs | 46 ----- .../HttpExchangesDiagnosticObserverTest.cs | 160 ++++++++++++++++-- .../HttpExchangesEndpointOptionsTest.cs | 48 +++--- .../Actuators/Trace/EndpointMiddlewareTest.cs | 79 --------- 22 files changed, 317 insertions(+), 343 deletions(-) rename src/Management/src/Endpoint/Actuators/HttpExchanges/{TracePrincipal.cs => HttpExchangePrincipal.cs} (81%) rename src/Management/src/Endpoint/Actuators/HttpExchanges/{TraceRequest.cs => HttpExchangeRequest.cs} (74%) rename src/Management/src/Endpoint/Actuators/HttpExchanges/{TraceResponse.cs => HttpExchangeResponse.cs} (69%) rename src/Management/src/Endpoint/Actuators/HttpExchanges/{TraceSession.cs => HttpExchangeSession.cs} (82%) delete mode 100644 src/Management/src/Endpoint/Actuators/HttpExchanges/TraceResult.cs delete mode 100644 src/Management/test/Endpoint.Test/Actuators/Trace/EndpointMiddlewareTest.cs diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs index a8e432c8a..3bd2910ab 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs @@ -144,14 +144,7 @@ internal Task GetPermissionsAsync(HttpContext context) } else { - string? endpointPath = endpointOptions.Path; - - if (endpointOptions.Id == "httpexchanges") - { - endpointPath = "httptrace"; - } - - if (requestPath.StartsWithSegments(basePath + endpointPath)) + if (requestPath.StartsWithSegments(basePath + endpointOptions.Path)) { return endpointOptions; } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/ConfigureHttpExchangesEndpointOptions.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/ConfigureHttpExchangesEndpointOptions.cs index 66f2a0e1e..618535139 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/ConfigureHttpExchangesEndpointOptions.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/ConfigureHttpExchangesEndpointOptions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Net.Http.Headers; +using Steeltoe.Common; using Steeltoe.Management.Endpoint.Configuration; namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; @@ -81,28 +82,26 @@ public override void Configure(HttpExchangesEndpointOptions options) // It's not possible to distinguish between null and an empty list in configuration. // See https://github.com/dotnet/extensions/issues/1341. // As a workaround, we interpret a single empty string element to clear the defaults. - if (options.RequestHeaders.Count == 0) + if (options.RequestHeaders.Count == 0 || !string.IsNullOrEmpty(options.RequestHeaders[0])) { foreach (string defaultKey in DefaultAllowedRequestHeaders) { options.RequestHeaders.Add(defaultKey); } } - else if (options.RequestHeaders is [""]) - { - options.RequestHeaders.Clear(); - } - if (options.ResponseHeaders.Count == 0) + if (options.ResponseHeaders.Count == 0 || !string.IsNullOrEmpty(options.ResponseHeaders[0])) { foreach (string defaultKey in DefaultAllowedResponseHeaders) { options.ResponseHeaders.Add(defaultKey); } } - else if (options.ResponseHeaders is [""]) + + // this should only be needed until Apps Manager supports /httpexchanges + if (Platform.IsCloudFoundry) { - options.ResponseHeaders.Clear(); + options.Id = "httptrace"; } } } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/EndpointServiceCollectionExtensions.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/EndpointServiceCollectionExtensions.cs index fbd5d6510..5b29db6da 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/EndpointServiceCollectionExtensions.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/EndpointServiceCollectionExtensions.cs @@ -41,6 +41,7 @@ public static IServiceCollection AddHttpExchangesActuator(this IServiceCollectio public static IServiceCollection AddHttpExchangesActuator(this IServiceCollection services, Action configureOptions) { ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); services.TryAddSingleton(); services.AddHostedService(); diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchange.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchange.cs index 5623c1157..3d85a207d 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchange.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchange.cs @@ -7,13 +7,13 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; public sealed class HttpExchange { public long Timestamp { get; } - public TracePrincipal? Principal { get; } - public TraceSession? Session { get; } - public TraceRequest Request { get; } - public TraceResponse Response { get; } + public HttpExchangePrincipal? Principal { get; } + public HttpExchangeSession? Session { get; } + public HttpExchangeRequest Request { get; } + public HttpExchangeResponse Response { get; } public long TimeTaken { get; } - public HttpExchange(TraceRequest request, TraceResponse response, long timestamp, TracePrincipal? principal, TraceSession? session, double timeTaken) + public HttpExchange(HttpExchangeRequest request, HttpExchangeResponse response, long timestamp, HttpExchangePrincipal? principal, HttpExchangeSession? session, double timeTaken) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(response); diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/TracePrincipal.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangePrincipal.cs similarity index 81% rename from src/Management/src/Endpoint/Actuators/HttpExchanges/TracePrincipal.cs rename to src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangePrincipal.cs index 42f1bff9e..2e7f976c1 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/TracePrincipal.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangePrincipal.cs @@ -4,11 +4,11 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; -public sealed class TracePrincipal +public sealed class HttpExchangePrincipal { public string Name { get; } - public TracePrincipal(string name) + public HttpExchangePrincipal(string name) { ArgumentException.ThrowIfNullOrEmpty(name); diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceRequest.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeRequest.cs similarity index 74% rename from src/Management/src/Endpoint/Actuators/HttpExchanges/TraceRequest.cs rename to src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeRequest.cs index 8f45b14cc..16cffeaad 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceRequest.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeRequest.cs @@ -4,14 +4,14 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; -public sealed class TraceRequest +public sealed class HttpExchangeRequest { public string Method { get; } public string Uri { get; } - public IDictionary> Headers { get; } + public IDictionary> Headers { get; } public string? RemoteAddress { get; } - public TraceRequest(string method, string uri, IDictionary> headers, string? remoteAddress) + public HttpExchangeRequest(string method, string uri, IDictionary> headers, string? remoteAddress) { ArgumentException.ThrowIfNullOrWhiteSpace(method); ArgumentException.ThrowIfNullOrWhiteSpace(uri); diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceResponse.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeResponse.cs similarity index 69% rename from src/Management/src/Endpoint/Actuators/HttpExchanges/TraceResponse.cs rename to src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeResponse.cs index e3aaeea56..019213f54 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceResponse.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeResponse.cs @@ -4,12 +4,12 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; -public sealed class TraceResponse +public sealed class HttpExchangeResponse { public int Status { get; } - public IDictionary> Headers { get; } + public IDictionary> Headers { get; } - public TraceResponse(int status, IDictionary> headers) + public HttpExchangeResponse(int status, IDictionary> headers) { ArgumentNullException.ThrowIfNull(headers); diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceSession.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeSession.cs similarity index 82% rename from src/Management/src/Endpoint/Actuators/HttpExchanges/TraceSession.cs rename to src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeSession.cs index edeb857ff..33ed0af39 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceSession.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeSession.cs @@ -4,11 +4,11 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; -public sealed class TraceSession +public sealed class HttpExchangeSession { public string Id { get; } - public TraceSession(string id) + public HttpExchangeSession(string id) { ArgumentException.ThrowIfNullOrWhiteSpace(id); diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesDiagnosticObserver.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesDiagnosticObserver.cs index fd1af762e..ffbcd1a05 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesDiagnosticObserver.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesDiagnosticObserver.cs @@ -37,7 +37,14 @@ public HttpExchangesDiagnosticObserver(IOptionsMonitor> requestHeaders = []; + Dictionary> requestHeaders = []; - if (options.AddRequestHeaders) + if (options.IncludeRequestHeaders) { - requestHeaders = GetHeaders(context.Request.Headers, options.RequestHeaders); + requestHeaders = GetHeaders(context.Request.Headers); } - var traceRequest = new TraceRequest(context.Request.Method, requestUri, requestHeaders, remoteAddress); + var traceRequest = new HttpExchangeRequest(context.Request.Method, requestUri, requestHeaders, remoteAddress); - Dictionary> responseHeaders = []; + Dictionary> responseHeaders = []; - if (options.AddResponseHeaders) + if (options.IncludeResponseHeaders) { - responseHeaders = GetHeaders(context.Response.Headers, options.ResponseHeaders); + responseHeaders = GetHeaders(context.Response.Headers); } - var traceResponse = new TraceResponse(context.Response.StatusCode, responseHeaders); + var traceResponse = new HttpExchangeResponse(context.Response.StatusCode, responseHeaders); string? userName = null; - if (options.AddUserPrincipal) + if (options.IncludeUserPrincipal) { userName = GetUserPrincipal(context); } - TracePrincipal? principal = userName == null ? null : new TracePrincipal(userName); + HttpExchangePrincipal? principal = userName == null ? null : new HttpExchangePrincipal(userName); string? sessionId = null; - if (options.AddSessionId) + if (options.IncludeSessionId) { sessionId = GetSessionId(context); } - TraceSession? session = sessionId == null ? null : new TraceSession(sessionId); + HttpExchangeSession? session = sessionId == null ? null : new HttpExchangeSession(sessionId); long timestamp = GetJavaTime(DateTime.UtcNow.Ticks); @@ -130,26 +137,34 @@ private HttpExchange GetHttpExchange(HttpContext context, TimeSpan duration) return GetPropertyOrDefault(instance, "HttpContext"); } - internal static Dictionary> GetHeaders(IHeaderDictionary headers, IList allowedHeaders) + internal static Dictionary> GetHeaders(IHeaderDictionary headers) { - var result = new Dictionary>(); + var result = new Dictionary>(); - foreach (KeyValuePair pair in headers) + foreach ((string key, StringValues value) in headers) { #pragma warning disable S4040 // Strings should be normalized to uppercase - result.Add(pair.Key.ToLowerInvariant(), HeaderShouldBeRedacted(pair.Key, allowedHeaders) ? new StringValues(Redacted) : pair.Value); + result.Add(key.ToLowerInvariant(), value); #pragma warning restore S4040 // Strings should be normalized to uppercase } return result; } + private static void FilterHeaders(IDictionary> headers, IList allowedHeaders) + { + foreach (KeyValuePair> header in headers.Where(header => HeaderShouldBeRedacted(header.Key, allowedHeaders))) + { + headers[header.Key] = [Redacted]; + } + } + private static bool HeaderShouldBeRedacted(string currentHeader, IList allowedHeaders) { - return !allowedHeaders.Contains(currentHeader) || (allowedHeaders.Count == 1 && string.IsNullOrEmpty(allowedHeaders[0])); + return allowedHeaders is not [""] && !allowedHeaders.Any(header => header.Equals(currentHeader, StringComparison.OrdinalIgnoreCase)); } - private void RecordHttpTrace(Activity current, HttpContext context) + internal void RecordHttpExchange(Activity current, HttpContext context) { ArgumentNullException.ThrowIfNull(current); ArgumentNullException.ThrowIfNull(context); diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointHandler.cs index 1acb1554a..9f89fd67c 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointHandler.cs @@ -14,8 +14,6 @@ internal sealed class HttpExchangesEndpointHandler : IHttpExchangesEndpointHandl private readonly IHttpExchangesRepository _httpExchangeRepository; private readonly ILogger _logger; - public MediaTypeVersion Version { get; set; } = MediaTypeVersion.V2; - public EndpointOptions Options => _optionsMonitor.CurrentValue; public HttpExchangesEndpointHandler(IOptionsMonitor optionsMonitor, IHttpExchangesRepository httpExchangeRepository, diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointOptions.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointOptions.cs index f61cbce87..252818259 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointOptions.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointOptions.cs @@ -21,7 +21,7 @@ public sealed class HttpExchangesEndpointOptions : EndpointOptions /// not be logged unless logs are secure and access controlled and the privacy impact assessed. /// /// - public bool AddRequestHeaders { get; set; } = true; + public bool IncludeRequestHeaders { get; set; } = true; /// /// Gets or sets a value indicating whether HTTP headers from the response should be included in traces. @@ -29,42 +29,42 @@ public sealed class HttpExchangesEndpointOptions : EndpointOptions /// If a response header is not present in the , the header name will be logged with a redacted value. /// /// - public bool AddResponseHeaders { get; set; } = true; + public bool IncludeResponseHeaders { get; set; } = true; /// /// Gets or sets a value indicating whether the request path should be included in traces. /// - public bool AddPathInfo { get; set; } = true; + public bool IncludePathInfo { get; set; } = true; /// /// Gets or sets a value indicating whether the name of the user principal making the request should be included in traces. /// - public bool AddUserPrincipal { get; set; } + public bool IncludeUserPrincipal { get; set; } /// /// Gets or sets a value indicating whether the request querystring should be included in traces. /// - public bool AddQueryString { get; set; } + public bool IncludeQueryString { get; set; } /// /// Gets or sets a value indicating whether the IP address of the request's sender should be included in traces. /// - public bool AddRemoteAddress { get; set; } + public bool IncludeRemoteAddress { get; set; } /// /// Gets or sets a value indicating whether the user's session ID should be included in traces. /// - public bool AddSessionId { get; set; } + public bool IncludeSessionId { get; set; } /// /// Gets or sets a value indicating whether the time taken to process the request should be included in traces. /// - public bool AddTimeTaken { get; set; } = true; + public bool IncludeTimeTaken { get; set; } = true; /// /// Gets request header values that are allowed to be logged. /// - /// If a request header is not present in the , the header name will be logged with a redacted value. Request headers can + /// If a request header is not present in the , the header name will be logged with a redacted value. Request headers can /// contain authentication tokens, or private information which may have regulatory concerns under GDPR and other laws. Arbitrary request headers should /// not be logged unless logs are secure and access controlled and the privacy impact assessed. /// @@ -74,7 +74,7 @@ public sealed class HttpExchangesEndpointOptions : EndpointOptions /// /// Gets response header values that are allowed to be logged. /// - /// If a response header is not present in the , the header name will be logged with a redacted value. + /// If a response header is not present in the , the header name will be logged with a redacted value. /// /// public IList ResponseHeaders { get; private set; } = new List(); diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesResult.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesResult.cs index 7af25f437..9f2d5cdf6 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesResult.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesResult.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using System.Text.Json.Serialization; using Steeltoe.Common; namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; @@ -11,7 +10,6 @@ public sealed class HttpExchangesResult { internal MediaTypeVersion CurrentVersion { get; set; } - [JsonPropertyName("traces")] public IList Exchanges { get; } public HttpExchangesResult(IList exchanges) diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceResult.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceResult.cs deleted file mode 100644 index 67f249004..000000000 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/TraceResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using System.Text.Json.Serialization; - -namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; - -public sealed class TraceResult -{ - [JsonPropertyName("timestamp")] - public long TimeStamp { get; } - - [JsonPropertyName("info")] - public IDictionary Info { get; } - - public TraceResult(long timestamp, IDictionary info) - { - ArgumentNullException.ThrowIfNull(info); - - TimeStamp = timestamp; - Info = info; - } -} diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs index 3b96a073e..7da71863e 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs @@ -82,17 +82,8 @@ public Links Invoke(string baseUrl) { if (!links.Entries.ContainsKey(endpointOptions.Id)) { - if (baseUrl.EndsWith(ConfigureManagementOptions.DefaultCloudFoundryPath, StringComparison.OrdinalIgnoreCase) && - endpointOptions.Id == "httpexchanges") - { - string linkPath = $"{baseUrl.TrimEnd('/')}/httptrace"; - links.Entries.Add("httptrace", new Link(linkPath)); - } - else - { - string linkPath = $"{baseUrl.TrimEnd('/')}/{endpointOptions.Path}"; - links.Entries.Add(endpointOptions.Id, new Link(linkPath)); - } + string linkPath = $"{baseUrl.TrimEnd('/')}/{endpointOptions.Path}"; + links.Entries.Add(endpointOptions.Id, new Link(linkPath)); } else { diff --git a/src/Management/src/Endpoint/ConfigurationSchema.json b/src/Management/src/Endpoint/ConfigurationSchema.json index cc0a71185..dd5af2597 100644 --- a/src/Management/src/Endpoint/ConfigurationSchema.json +++ b/src/Management/src/Endpoint/ConfigurationSchema.json @@ -366,57 +366,57 @@ "HttpExchanges": { "type": "object", "properties": { - "AddPathInfo": { + "AllowedVerbs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets the list of HTTP verbs that are allowed for this endpoint." + }, + "Capacity": { + "type": "integer", + "description": "Gets or sets a value indicating how many traces should be stored. Default value is 100." + }, + "Enabled": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this endpoint is enabled." + }, + "Id": { + "type": "string", + "description": "Gets or sets the unique ID of this endpoint." + }, + "IncludePathInfo": { "type": "boolean", "description": "Gets or sets a value indicating whether the request path should be included in traces." }, - "AddQueryString": { + "IncludeQueryString": { "type": "boolean", "description": "Gets or sets a value indicating whether the request querystring should be included in traces." }, - "AddRemoteAddress": { + "IncludeRemoteAddress": { "type": "boolean", "description": "Gets or sets a value indicating whether the IP address of the request's sender should be included in traces." }, - "AddRequestHeaders": { + "IncludeRequestHeaders": { "type": "boolean", "description": "Gets or sets a value indicating whether HTTP headers from the request should be included in traces.\n\nIf a request header is not present in the , the header name will be logged with a redacted value. Request headers can contain authentication tokens, or private information which may have regulatory concerns under GDPR and other laws. Arbitrary request headers should not be logged unless logs are secure and access controlled and the privacy impact assessed." }, - "AddResponseHeaders": { + "IncludeResponseHeaders": { "type": "boolean", "description": "Gets or sets a value indicating whether HTTP headers from the response should be included in traces.\n\nIf a response header is not present in the , the header name will be logged with a redacted value." }, - "AddSessionId": { + "IncludeSessionId": { "type": "boolean", "description": "Gets or sets a value indicating whether the user's session ID should be included in traces." }, - "AddTimeTaken": { + "IncludeTimeTaken": { "type": "boolean", "description": "Gets or sets a value indicating whether the time taken to process the request should be included in traces." }, - "AddUserPrincipal": { + "IncludeUserPrincipal": { "type": "boolean", "description": "Gets or sets a value indicating whether the name of the user principal making the request should be included in traces." }, - "AllowedVerbs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Gets the list of HTTP verbs that are allowed for this endpoint." - }, - "Capacity": { - "type": "integer", - "description": "Gets or sets a value indicating how many traces should be stored. Default value is 100." - }, - "Enabled": { - "type": "boolean", - "description": "Gets or sets a value indicating whether this endpoint is enabled." - }, - "Id": { - "type": "string", - "description": "Gets or sets the unique ID of this endpoint." - }, "Path": { "type": "string", "description": "Gets or sets the relative path at which this endpoint is exposed." diff --git a/src/Management/src/Endpoint/EndpointOptionsExtensions.cs b/src/Management/src/Endpoint/EndpointOptionsExtensions.cs index 65ac470ae..03752b76c 100644 --- a/src/Management/src/Endpoint/EndpointOptionsExtensions.cs +++ b/src/Management/src/Endpoint/EndpointOptionsExtensions.cs @@ -62,14 +62,7 @@ public static string GetPathMatchPattern(this EndpointOptions endpointOptions, M path += '/'; } - if (baseRequestPath == ConfigureManagementOptions.DefaultCloudFoundryPath && endpointOptions.Id == "httpexchanges") - { - path += "httptrace"; - } - else - { - path += endpointOptions.Path; - } + path += endpointOptions.Path; if (!endpointOptions.RequiresExactMatch()) { diff --git a/src/Management/src/Endpoint/PublicAPI.Unshipped.txt b/src/Management/src/Endpoint/PublicAPI.Unshipped.txt index d3ba76084..9a5eb31c0 100755 --- a/src/Management/src/Endpoint/PublicAPI.Unshipped.txt +++ b/src/Management/src/Endpoint/PublicAPI.Unshipped.txt @@ -234,61 +234,57 @@ Steeltoe.Management.Endpoint.Actuators.HeapDump.HeapDumper.HeapDumper(Microsoft. Steeltoe.Management.Endpoint.Actuators.HeapDump.IHeapDumpEndpointHandler Steeltoe.Management.Endpoint.Actuators.HttpExchanges.EndpointServiceCollectionExtensions Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.HttpExchange(Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceRequest! request, Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResponse! response, long timestamp, Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TracePrincipal? principal, Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceSession? session, double timeTaken) -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Principal.get -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TracePrincipal? -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Request.get -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceRequest! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Response.get -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResponse! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Session.get -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceSession? +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.HttpExchange(Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest! request, Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeResponse! response, long timestamp, Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangePrincipal? principal, Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeSession? session, double timeTaken) -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Principal.get -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangePrincipal? +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Request.get -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest! +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Response.get -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeResponse! +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Session.get -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeSession? Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.Timestamp.get -> long Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchange.TimeTaken.get -> long +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.Headers.get -> System.Collections.Generic.IDictionary!>! +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.HttpExchangeRequest(string! method, string! uri, System.Collections.Generic.IDictionary!>! headers, string? remoteAddress) -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.Method.get -> string! +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.RemoteAddress.get -> string? +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.Uri.get -> string! +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeResponse +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeResponse.Headers.get -> System.Collections.Generic.IDictionary!>! +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeResponse.HttpExchangeResponse(int status, System.Collections.Generic.IDictionary!>! headers) -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeResponse.Status.get -> int Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddPathInfo.get -> bool -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddPathInfo.set -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddQueryString.get -> bool -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddQueryString.set -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddRemoteAddress.get -> bool -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddRemoteAddress.set -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddRequestHeaders.get -> bool -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddRequestHeaders.set -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddResponseHeaders.get -> bool -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddResponseHeaders.set -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddSessionId.get -> bool -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddSessionId.set -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddTimeTaken.get -> bool -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddTimeTaken.set -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddUserPrincipal.get -> bool -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.AddUserPrincipal.set -> void Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.Capacity.get -> int Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.Capacity.set -> void Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.HttpExchangesEndpointOptions() -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludePathInfo.get -> bool +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludePathInfo.set -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeQueryString.get -> bool +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeQueryString.set -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeRemoteAddress.get -> bool +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeRemoteAddress.set -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeRequestHeaders.get -> bool +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeRequestHeaders.set -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeResponseHeaders.get -> bool +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeResponseHeaders.set -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeSessionId.get -> bool +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeSessionId.set -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeTimeTaken.get -> bool +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeTimeTaken.set -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeUserPrincipal.get -> bool +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.IncludeUserPrincipal.set -> void Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.RequestHeaders.get -> System.Collections.Generic.IList! Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesEndpointOptions.ResponseHeaders.get -> System.Collections.Generic.IList! +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeSession +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeSession.HttpExchangeSession(string! id) -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeSession.Id.get -> string! Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesResult Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesResult.Exchanges.get -> System.Collections.Generic.IList! Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesResult.HttpExchangesResult(System.Collections.Generic.IList! exchanges) -> void Steeltoe.Management.Endpoint.Actuators.HttpExchanges.IHttpExchangesEndpointHandler Steeltoe.Management.Endpoint.Actuators.HttpExchanges.IHttpExchangesRepository Steeltoe.Management.Endpoint.Actuators.HttpExchanges.IHttpExchangesRepository.GetHttpExchanges() -> Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangesResult! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TracePrincipal -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TracePrincipal.Name.get -> string! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TracePrincipal.TracePrincipal(string! name) -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceRequest -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceRequest.Headers.get -> System.Collections.Generic.IDictionary!>! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceRequest.Method.get -> string! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceRequest.RemoteAddress.get -> string? -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceRequest.TraceRequest(string! method, string! uri, System.Collections.Generic.IDictionary!>! headers, string? remoteAddress) -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceRequest.Uri.get -> string! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResponse -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResponse.Headers.get -> System.Collections.Generic.IDictionary!>! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResponse.Status.get -> int -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResponse.TraceResponse(int status, System.Collections.Generic.IDictionary!>! headers) -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResult -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResult.Info.get -> System.Collections.Generic.IDictionary! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResult.TimeStamp.get -> long -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceResult.TraceResult(long timestamp, System.Collections.Generic.IDictionary! info) -> void -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceSession -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceSession.Id.get -> string! -Steeltoe.Management.Endpoint.Actuators.HttpExchanges.TraceSession.TraceSession(string! id) -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangePrincipal +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangePrincipal.Name.get -> string! +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangePrincipal.HttpExchangePrincipal(string! name) -> void Steeltoe.Management.Endpoint.Actuators.Hypermedia.EndpointServiceCollectionExtensions Steeltoe.Management.Endpoint.Actuators.Hypermedia.HypermediaEndpointOptions Steeltoe.Management.Endpoint.Actuators.Hypermedia.HypermediaEndpointOptions.HypermediaEndpointOptions() -> void diff --git a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/EndpointMiddlewareTest.cs b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/EndpointMiddlewareTest.cs index 1b1ebb51b..27f21282d 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/EndpointMiddlewareTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/EndpointMiddlewareTest.cs @@ -37,7 +37,7 @@ public async Task HttpExchangesActuator_ReturnsExpectedData() response.StatusCode.Should().Be(HttpStatusCode.OK); string json = await response.Content.ReadAsStringAsync(); - json.Should().Be("{\"traces\":[]}"); + json.Should().Be("{\"exchanges\":[]}"); } [Fact] @@ -50,8 +50,13 @@ public void RoutesByPathAndVerb() endpointOptions.GetPathMatchPattern(managementOptions, managementOptions.Path).Should().Be("/actuator/httpexchanges"); endpointOptions.GetPathMatchPattern(managementOptions, ConfigureManagementOptions.DefaultCloudFoundryPath).Should() - .Be("/cloudfoundryapplication/httptrace"); + .Be("/cloudfoundryapplication/httpexchanges"); endpointOptions.AllowedVerbs.Should().Contain(verb => verb == "Get"); + + EnvironmentVariableScope _ = new("VCAP_APPLICATION", "some"); + endpointOptions = GetOptionsFromSettings(); + endpointOptions.GetPathMatchPattern(managementOptions, ConfigureManagementOptions.DefaultCloudFoundryPath).Should() + .Be("/cloudfoundryapplication/httptrace"); } } diff --git a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/EndpointServiceCollectionTest.cs b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/EndpointServiceCollectionTest.cs index ac6fac9ff..a8e382156 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/EndpointServiceCollectionTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/EndpointServiceCollectionTest.cs @@ -47,50 +47,4 @@ public async Task AddHttpExchangesActuator_AddsCorrectServices() Assert.Single(list); Assert.IsType(list[0]); } - - [Fact] - public async Task AddHttpExchangesActuator_AllowsChangingRedactionOptions() - { - var appSettings = new Dictionary - { - ["management:endpoints:httpExchanges:enabled"] = "true" - }; - - var services = new ServiceCollection(); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(appSettings); - IConfiguration configuration = configurationBuilder.Build(); - services.AddLogging(); - services.AddSingleton(configuration); - - services.AddHttpExchangesActuator(traceOptions => - { - traceOptions.RequestHeaders.Add("Header1"); - }); - - await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); - var options = serviceProvider.GetRequiredService>(); - - HttpContext context = HttpExchangesDiagnosticObserverTest.CreateRequest(); - Dictionary> result = HttpExchangesDiagnosticObserver.GetHeaders(context.Request.Headers, options.CurrentValue.RequestHeaders); - - result.Should().NotBeNull(); - - result.Should().ContainKeys([ - "accept", - "authorization", - "host", - "user-agent", - "header1", - "header2" - ]); -#pragma warning disable S4040 // Strings should be normalized to uppercase - result[HeaderNames.Accept.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("testContent"); - result[HeaderNames.Authorization.ToLower(CultureInfo.InvariantCulture)][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); - result[HeaderNames.Host.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("localhost:1111"); - result[HeaderNames.UserAgent.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("TestHost"); -#pragma warning restore S4040 // Strings should be normalized to uppercase - result["header1"][0].Should().Be("header1Value"); - result["header2"][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); - } } diff --git a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesDiagnosticObserverTest.cs b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesDiagnosticObserverTest.cs index f76dda77d..17faa3c34 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesDiagnosticObserverTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesDiagnosticObserverTest.cs @@ -4,9 +4,12 @@ using System.Diagnostics; using System.Globalization; +using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -78,17 +81,66 @@ public void GetRequestUri_ReturnsExpected() [Fact] public void GetHeaders_ReturnsExpected() { + HttpContext context = CreateRequest(); + context.Response.StatusCode = StatusCodes.Status100Continue; + + Dictionary> requestHeaders = HttpExchangesDiagnosticObserver.GetHeaders(context.Request.Headers); + + requestHeaders.Should().NotBeNull(); + + requestHeaders.Should().ContainKeys([ + "accept", + "authorization", + "host", + "user-agent", + "header1", + "header2" + ]); +#pragma warning disable S4040 // Strings should be normalized to uppercase + requestHeaders[HeaderNames.Accept.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("testContent"); + requestHeaders[HeaderNames.Authorization.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("bearer TestToken"); + requestHeaders[HeaderNames.Host.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("localhost:1111"); + requestHeaders[HeaderNames.UserAgent.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("TestHost"); + requestHeaders["header1"][0].Should().Be("header1Value"); + requestHeaders["header2"][0].Should().Be("header2Value"); + + Dictionary> responseHeaders = HttpExchangesDiagnosticObserver.GetHeaders(context.Response.Headers); + responseHeaders.Should().NotBeNull(); + + responseHeaders.Should().ContainKeys([ + "headera", + "headerb", + "set-cookie" + ]); + responseHeaders["headera"][0].Should().Be("headerAValue"); + responseHeaders["headerb"][0].Should().Be("headerBValue"); + responseHeaders[HeaderNames.SetCookie.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("some-value-that-should-be-redacted"); + } + + [Fact] + public void Header_Redaction() + { + var appSettings = new Dictionary + { + ["management:endpoints:httpExchanges:RequestHeaders:0"] = string.Empty, + ["management:endpoints:httpExchanges:ResponseHeaders:0"] = string.Empty + }; IOptionsMonitor option = - GetOptionsMonitorFromSettings(); + GetOptionsMonitorFromSettings(appSettings); + + var observer = new HttpExchangesDiagnosticObserver(option, NullLoggerFactory.Instance); HttpContext context = CreateRequest(); context.Response.StatusCode = StatusCodes.Status100Continue; + observer.RecordHttpExchange(new Activity("testRequest1"), context); - Dictionary> result = HttpExchangesDiagnosticObserver.GetHeaders(context.Request.Headers, option.CurrentValue.RequestHeaders); + var exchanges = observer.GetHttpExchanges(); - result.Should().NotBeNull(); + var requestHeaders = exchanges.Exchanges[0].Request.Headers; + + requestHeaders.Should().NotBeNull(); - result.Should().ContainKeys([ + requestHeaders.Should().ContainKeys([ "accept", "authorization", "host", @@ -96,14 +148,25 @@ public void GetHeaders_ReturnsExpected() "header1", "header2" ]); -#pragma warning disable S4040 // Strings should be normalized to uppercase - result[HeaderNames.Accept.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("testContent"); - result[HeaderNames.Authorization.ToLower(CultureInfo.InvariantCulture)][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); - result[HeaderNames.Host.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("localhost:1111"); - result[HeaderNames.UserAgent.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("TestHost"); + requestHeaders[HeaderNames.Accept.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("testContent"); + requestHeaders[HeaderNames.Authorization.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("bearer TestToken"); + requestHeaders[HeaderNames.Host.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("localhost:1111"); + requestHeaders[HeaderNames.UserAgent.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("TestHost"); + requestHeaders["header1"][0].Should().Be("header1Value"); + requestHeaders["header2"][0].Should().Be("header2Value"); + + var responseHeaders = exchanges.Exchanges[0].Response.Headers; + responseHeaders.Should().NotBeNull(); + + responseHeaders.Should().ContainKeys([ + "headera", + "headerb", + "set-cookie" + ]); + responseHeaders["headera"][0].Should().Be("headerAValue"); + responseHeaders["headerb"][0].Should().Be("headerBValue"); + responseHeaders[HeaderNames.SetCookie.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("some-value-that-should-be-redacted"); #pragma warning restore S4040 // Strings should be normalized to uppercase - result["header1"][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); - result["header2"][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); } [Fact] @@ -238,7 +301,7 @@ public void ProcessEvent_HonorsCapacity() } [Fact] - public void GetTraces_ReturnsTraces() + public void GetHttpExchanges_ReturnsHttpExchanges() { using var listener = new DiagnosticListener("test"); @@ -265,7 +328,77 @@ public void GetTraces_ReturnsTraces() observer.Queue.Count.Should().Be(option.CurrentValue.Capacity); } - internal static HttpContext CreateRequest() + [Fact] + public void GetHttpExchanges_FiltersDuringReturn() + { + var appSettings = new Dictionary + { + ["management:endpoints:httpExchanges:RequestHeaders:0"] = "header2", + ["management:endpoints:httpExchanges:ResponseHeaders:0"] = "headerb" + }; + using var listener = new DiagnosticListener("test"); + + IOptionsMonitor option = + GetOptionsMonitorFromSettings(appSettings); + + var observer = new HttpExchangesDiagnosticObserver(option, NullLoggerFactory.Instance); + var current = new Activity("Microsoft.AspNetCore.Hosting.HttpRequestIn"); + current.Start(); + + HttpContext context = CreateRequest(); + + observer.ProcessEvent("Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", new + { + HttpContext = context + }); + + observer.Queue.Count.Should().Be(1); + HttpExchange unfiltered = observer.Queue.First(); + + unfiltered.Should().NotBeNull(); + + unfiltered.Request.Headers.Should().ContainKeys([ + "accept", + "authorization", + "host", + "user-agent", + "header1", + "header2" + ]); +#pragma warning disable S4040 // Strings should be normalized to uppercase + unfiltered.Request.Headers[HeaderNames.Accept.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("testContent"); + unfiltered.Request.Headers[HeaderNames.Authorization.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("bearer TestToken"); + unfiltered.Request.Headers[HeaderNames.Host.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("localhost:1111"); + unfiltered.Request.Headers[HeaderNames.UserAgent.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("TestHost"); + unfiltered.Request.Headers["header1"][0].Should().Be("header1Value"); + unfiltered.Request.Headers["header2"][0].Should().Be("header2Value"); + unfiltered.Response.Headers["headera"][0].Should().Be("headerAValue"); + unfiltered.Response.Headers["headerb"][0].Should().Be("headerBValue"); + unfiltered.Response.Headers[HeaderNames.SetCookie.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("some-value-that-should-be-redacted"); + + var filtered = observer.GetHttpExchanges().Exchanges[0]; + filtered.Should().NotBeNull(); + filtered.Request.Headers.Should().ContainKeys([ + "accept", + "authorization", + "host", + "user-agent", + "header1", + "header2" + ]); + filtered.Request.Headers[HeaderNames.Accept.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("testContent"); + filtered.Request.Headers[HeaderNames.Authorization.ToLower(CultureInfo.InvariantCulture)][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); + filtered.Request.Headers[HeaderNames.Host.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("localhost:1111"); + filtered.Request.Headers[HeaderNames.UserAgent.ToLower(CultureInfo.InvariantCulture)][0].Should().Be("TestHost"); + filtered.Request.Headers["header1"][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); + filtered.Request.Headers["header2"][0].Should().Be("header2Value"); + filtered.Response.Headers["headera"][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); + filtered.Response.Headers["headerb"][0].Should().Be("headerBValue"); + filtered.Response.Headers[HeaderNames.SetCookie.ToLower(CultureInfo.InvariantCulture)][0].Should().Be(HttpExchangesDiagnosticObserver.Redacted); +#pragma warning restore S4040 // Strings should be normalized to uppercase + } + + private static HttpContext CreateRequest() { HttpContext context = new DefaultHttpContext { @@ -286,6 +419,7 @@ internal static HttpContext CreateRequest() context.Response.Body = new MemoryStream(); context.Response.Headers.Append("HeaderA", new StringValues("headerAValue")); context.Response.Headers.Append("HeaderB", new StringValues("headerBValue")); + context.Response.Headers.Append(HeaderNames.SetCookie, "some-value-that-should-be-redacted"); return context; } diff --git a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesEndpointOptionsTest.cs b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesEndpointOptionsTest.cs index 208f7c1b1..9c7238c8e 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesEndpointOptionsTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesEndpointOptionsTest.cs @@ -16,14 +16,14 @@ public void Constructor_InitializesWithDefaults() options.Enabled.Should().BeNull(); options.Id.Should().Be("httpexchanges"); options.Capacity.Should().Be(100); - options.AddTimeTaken.Should().BeTrue(); - options.AddRequestHeaders.Should().BeTrue(); - options.AddResponseHeaders.Should().BeTrue(); - options.AddPathInfo.Should().BeTrue(); - options.AddUserPrincipal.Should().BeFalse(); - options.AddQueryString.Should().BeFalse(); - options.AddRemoteAddress.Should().BeFalse(); - options.AddSessionId.Should().BeFalse(); + options.IncludeTimeTaken.Should().BeTrue(); + options.IncludeRequestHeaders.Should().BeTrue(); + options.IncludeResponseHeaders.Should().BeTrue(); + options.IncludePathInfo.Should().BeTrue(); + options.IncludeUserPrincipal.Should().BeFalse(); + options.IncludeQueryString.Should().BeFalse(); + options.IncludeRemoteAddress.Should().BeFalse(); + options.IncludeSessionId.Should().BeFalse(); } [Fact] @@ -36,14 +36,14 @@ public void Constructor_BindsConfigurationCorrectly() ["management:endpoints:loggers:enabled"] = "false", ["management:endpoints:httpExchanges:enabled"] = "true", ["management:endpoints:httpExchanges:capacity"] = "1000", - ["management:endpoints:httpExchanges:addTimeTaken"] = "false", - ["management:endpoints:httpExchanges:addRequestHeaders"] = "false", - ["management:endpoints:httpExchanges:addResponseHeaders"] = "false", - ["management:endpoints:httpExchanges:addPathInfo"] = "false", - ["management:endpoints:httpExchanges:addUserPrincipal"] = "true", - ["management:endpoints:httpExchanges:addQueryString"] = "true", - ["management:endpoints:httpExchanges:addRemoteAddress"] = "true", - ["management:endpoints:httpExchanges:addSessionId"] = "true", + ["management:endpoints:httpExchanges:includeTimeTaken"] = "false", + ["management:endpoints:httpExchanges:includeRequestHeaders"] = "false", + ["management:endpoints:httpExchanges:includeResponseHeaders"] = "false", + ["management:endpoints:httpExchanges:includePathInfo"] = "false", + ["management:endpoints:httpExchanges:includeUserPrincipal"] = "true", + ["management:endpoints:httpExchanges:includeQueryString"] = "true", + ["management:endpoints:httpExchanges:includeRemoteAddress"] = "true", + ["management:endpoints:httpExchanges:includeSessionId"] = "true", ["management:endpoints:cloudfoundry:enabled"] = "true" }; @@ -59,13 +59,13 @@ public void Constructor_BindsConfigurationCorrectly() endpointOptions.Id.Should().Be("httpexchanges"); endpointOptions.Path.Should().Be("httpexchanges"); endpointOptions.Capacity.Should().Be(1000); - endpointOptions.AddTimeTaken.Should().BeFalse(); - endpointOptions.AddRequestHeaders.Should().BeFalse(); - endpointOptions.AddResponseHeaders.Should().BeFalse(); - endpointOptions.AddPathInfo.Should().BeFalse(); - endpointOptions.AddUserPrincipal.Should().BeTrue(); - endpointOptions.AddQueryString.Should().BeTrue(); - endpointOptions.AddRemoteAddress.Should().BeTrue(); - endpointOptions.AddSessionId.Should().BeTrue(); + endpointOptions.IncludeTimeTaken.Should().BeFalse(); + endpointOptions.IncludeRequestHeaders.Should().BeFalse(); + endpointOptions.IncludeResponseHeaders.Should().BeFalse(); + endpointOptions.IncludePathInfo.Should().BeFalse(); + endpointOptions.IncludeUserPrincipal.Should().BeTrue(); + endpointOptions.IncludeQueryString.Should().BeTrue(); + endpointOptions.IncludeRemoteAddress.Should().BeTrue(); + endpointOptions.IncludeSessionId.Should().BeTrue(); } } diff --git a/src/Management/test/Endpoint.Test/Actuators/Trace/EndpointMiddlewareTest.cs b/src/Management/test/Endpoint.Test/Actuators/Trace/EndpointMiddlewareTest.cs deleted file mode 100644 index 8b24cbf02..000000000 --- a/src/Management/test/Endpoint.Test/Actuators/Trace/EndpointMiddlewareTest.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using System.Net; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Steeltoe.Common.TestResources; -using Steeltoe.Logging.DynamicLogger; -using Steeltoe.Management.Endpoint.Actuators.Trace; -using Steeltoe.Management.Endpoint.Configuration; - -namespace Steeltoe.Management.Endpoint.Test.Actuators.Trace; - -public sealed class EndpointMiddlewareTest : BaseTest -{ - private static readonly Dictionary AppSettings = new() - { - ["Logging:Console:IncludeScopes"] = "false", - ["Logging:LogLevel:Default"] = "Warning", - ["Logging:LogLevel:Steeltoe"] = "Information", - ["management:endpoints:enabled"] = "true", - ["management:endpoints:trace:enabled"] = "true", - ["management:endpoints:actuator:exposure:include:0"] = "httptrace" - }; - - [Fact] - public async Task TraceActuator_ReturnsExpectedData() - { - IWebHostBuilder builder = TestWebHostBuilderFactory.Create().UseStartup() - .ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(AppSettings)).ConfigureLogging( - (webHostContext, loggingBuilder) => - { - loggingBuilder.AddConfiguration(webHostContext.Configuration); - loggingBuilder.AddDynamicConsole(); - }); - - using var server = new TestServer(builder); - HttpClient client = server.CreateClient(); - HttpResponseMessage response = await client.GetAsync(new Uri("http://localhost/actuator/httptrace")); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string json = await response.Content.ReadAsStringAsync(); - Assert.NotNull(json); - } - - [Fact] - public void RoutesByPathAndVerb() - { - var endpointOptions = GetOptionsFromSettings(); - ManagementOptions managementOptions = GetOptionsMonitorFromSettings().CurrentValue; - - Assert.True(endpointOptions.RequiresExactMatch()); - Assert.Equal("/actuator/httptrace", endpointOptions.GetPathMatchPattern(managementOptions, managementOptions.Path)); - - Assert.Equal("/cloudfoundryapplication/httptrace", - endpointOptions.GetPathMatchPattern(managementOptions, ConfigureManagementOptions.DefaultCloudFoundryPath)); - - Assert.Contains("Get", endpointOptions.AllowedVerbs); - } - - [Fact] - public void RoutesByPathAndVerbTrace() - { - TraceEndpointOptions endpointOptions = GetOptionsMonitorFromSettings() - .Get(ConfigureTraceEndpointOptions.TraceEndpointOptionNames.V1.ToString()); - - ManagementOptions managementOptions = GetOptionsMonitorFromSettings().CurrentValue; - - Assert.True(endpointOptions.RequiresExactMatch()); - Assert.Equal("/actuator/trace", endpointOptions.GetPathMatchPattern(managementOptions, managementOptions.Path)); - - Assert.Equal("/cloudfoundryapplication/trace", - endpointOptions.GetPathMatchPattern(managementOptions, ConfigureManagementOptions.DefaultCloudFoundryPath)); - - Assert.Contains("Get", endpointOptions.AllowedVerbs); - } -}