diff --git a/src/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRenderer.cs new file mode 100644 index 00000000..b452adc2 --- /dev/null +++ b/src/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRenderer.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Text; +using NLog.Config; +using NLog.LayoutRenderers; +using NLog.Web.Internal; +using System; +#if !ASP_NET_CORE +using System.Collections.Specialized; +using System.Linq; +#else +using Microsoft.AspNetCore.Http; +#endif + +namespace NLog.Web.LayoutRenderers +{ + /// + /// ASP.NET Request Headers + /// + /// Example usage of ${aspnet-request-headers} + /// + /// + /// ${aspnet-request-headers:OutputFormat=Flat} + /// ${aspnet-request-headers:OutputFormat=Json} + /// ${aspnet-request-headers:OutputFormat=Json:HeaderNames=username} + /// ${aspnet-request-headers:OutputFormat=Json:Exclude=access_token} + /// + /// + [LayoutRenderer("aspnet-request-headers")] + [ThreadSafe] + public class AspNetRequestHeadersLayoutRenderer : AspNetLayoutMultiValueRendererBase + { + /// + /// Header names to be rendered. + /// If null or empty array, all headers will be rendered. + /// + public List HeaderNames { get; set; } + + /// + /// Gets or sets the keys to exclude from the output. If omitted, none are excluded. + /// + /// +#if ASP_NET_CORE + public ISet Exclude { get; set; } +#else + public HashSet Exclude { get; set; } +#endif + + /// + /// Initializes a new instance of the class. + /// + public AspNetRequestHeadersLayoutRenderer() + { + Exclude = new HashSet(new[] { "ALL_HTTP", "ALL_RAW", "AUTH_PASSWORD" }, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Renders the ASP.NET Headers appends it to the specified . + /// + /// The to append the rendered data to. + /// Logging event. + protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) + { + var httpRequest = HttpContextAccessor.HttpContext.TryGetRequest(); + if (httpRequest == null) + { + return; + } + + var headers = httpRequest.Headers; + if (headers?.Count > 0) + { + bool checkForExclude = (HeaderNames == null || HeaderNames.Count == 0) && Exclude?.Count > 0; + var headerValues = GetHeaderValues(headers, checkForExclude); + SerializePairs(headerValues, builder, logEvent); + } + } + +#if !ASP_NET_CORE + private IEnumerable> GetHeaderValues(NameValueCollection headers, bool checkForExclude) + { + var headerNames = HeaderNames?.Count > 0 ? HeaderNames : headers.Keys.Cast().ToList(); + foreach (var headerName in headerNames) + { + if (checkForExclude && Exclude.Contains(headerName)) + continue; + + var headerValue = headers[headerName]; + if (headerValue == null) + { + continue; + } + + yield return new KeyValuePair(headerName, headerValue); + } + } +#else + private IEnumerable> GetHeaderValues(IHeaderDictionary headers, bool checkForExclude) + { + var headerNames = HeaderNames?.Count > 0 ? HeaderNames : headers.Keys; + foreach (var headerName in headerNames) + { + if (checkForExclude && Exclude.Contains(headerName)) + continue; + + if (!headers.TryGetValue(headerName, out var headerValue)) + { + continue; + } + + yield return new KeyValuePair(headerName, headerValue); + } + } +#endif + } +} \ No newline at end of file diff --git a/tests/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRendererTests.cs new file mode 100644 index 00000000..e278e651 --- /dev/null +++ b/tests/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRendererTests.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using NLog.Web.LayoutRenderers; +using NLog.Web.Enums; +using Xunit; +using System.Collections.Specialized; + +#if !ASP_NET_CORE +using NSubstitute; +using System.Web; +#else +using Microsoft.Extensions.Primitives; +#endif + +namespace NLog.Web.Tests.LayoutRenderers +{ + public class AspNetRequestHeadersLayoutRendererTests : TestInvolvingAspNetHttpContext + { + [Fact] + public void NullKeyRendersAllHeaders() + { +#if ASP_NET_CORE + var expectedResult = "Host=stackoverflow.com,key=TEST,Key1=TEST1"; // ASP.NET Core automatically includes host of request URL as part of headers +#else + var expectedResult = "key=TEST,Key1=TEST1"; +#endif + var renderer = CreateRenderer(); + renderer.HeaderNames = null; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void NullKeyRendersAllHeadersExceptExcluded() + { +#if ASP_NET_CORE + var expectedResult = "Host=stackoverflow.com,Key1=TEST1"; // ASP.NET Core automatically includes host of request URL as part of headers +#else + var expectedResult = "Key1=TEST1"; +#endif + var renderer = CreateRenderer(); + renderer.HeaderNames = null; + renderer.Exclude.Add("key"); + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyNotFoundRendersEmptyString_Flat_Formatting() + { + var renderer = CreateRenderer(); + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Flat; + renderer.HeaderNames = new List { "notfound" }; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Empty(result); + } + + [Fact] + public void KeyNotFoundRendersEmptyString_Json_Formatting() + { + var renderer = CreateRenderer(); + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json; + renderer.HeaderNames = new List { "notfound" }; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Empty(result); + } + + [Fact] + public void KeyFoundRendersValue_Multiple_Headers_Flat_Formatting() + { + var expectedResult = "key=TEST,Key1=TEST1"; + + var renderer = CreateRenderer(); + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Multiple_Headers_Flat_Formatting_separators() + { + var expectedResult = "key:TEST|Key1:TEST1"; + + var renderer = CreateRenderer(); + renderer.ValueSeparator = ":"; + renderer.ItemSeparator = "|"; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Multiple_Headers_Flat_Formatting_separators_layouts() + { + var expectedResult = "key>TEST" + Environment.NewLine + "Key1>TEST1"; + + var renderer = CreateRenderer(); + renderer.ValueSeparator = "${event-properties:valueSeparator1}"; + renderer.ItemSeparator = "${newline}"; + + var logEventInfo = new LogEventInfo(); + logEventInfo.Properties["valueSeparator1"] = ">"; + + // Act + string result = renderer.Render(logEventInfo); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Single_Header_Flat_Formatting() + { + var expectedResult = "key=TEST"; + + var renderer = CreateRenderer(addSecondHeader: false); + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Single_Header_Json_Formatting() + { + var expectedResult = "[{\"key\":\"TEST\"}]"; + + var renderer = CreateRenderer(addSecondHeader: false); + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Single_Header_Json_Formatting_no_array() + { + var expectedResult = "{\"key\":\"TEST\"}"; + + var renderer = CreateRenderer(addSecondHeader: false); + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json; + renderer.SingleAsArray = false; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void KeyFoundRendersValue_Multiple_Headers_Json_Formatting(bool singleAsArray) + { + var expectedResult = "[{\"key\":\"TEST\"},{\"Key1\":\"TEST1\"}]"; + + var renderer = CreateRenderer(); + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json; + renderer.SingleAsArray = singleAsArray; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyNotFoundRendersEmptyString_Flat_Formatting_ValuesOnly() + { + var renderer = CreateRenderer(); + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Flat; + renderer.HeaderNames = new List { "notfound" }; + renderer.ValuesOnly = true; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Empty(result); + } + + [Fact] + public void KeyNotFoundRendersEmptyString_Json_Formatting_ValuesOnly() + { + var renderer = CreateRenderer(); + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json; + renderer.HeaderNames = new List { "notfound" }; + renderer.ValuesOnly = true; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Empty(result); + } + + [Fact] + public void KeyFoundRendersValue_Header_Multiple_Items_Flat_Formatting_ValuesOnly() + { + var expectedResult = "TEST,TEST1"; + + var renderer = CreateRenderer(); + renderer.ValuesOnly = true; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Header_Multiple_Items_Flat_Formatting_separators_ValuesOnly() + { + var expectedResult = "TEST|TEST1"; + + var renderer = CreateRenderer(); + renderer.ValueSeparator = ":"; + renderer.ItemSeparator = "|"; + renderer.ValuesOnly = true; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Single_Item_Flat_Formatting_ValuesOnly() + { + var expectedResult = "TEST"; + + var renderer = CreateRenderer(addSecondHeader: false); + renderer.ValuesOnly = true; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Single_Item_Json_Formatting_ValuesOnly() + { + var expectedResult = "[\"TEST\"]"; + + var renderer = CreateRenderer(addSecondHeader: false); + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json; + renderer.ValuesOnly = true; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void KeyFoundRendersValue_Single_Item_Json_Formatting_no_array_ValuesOnly() + { + // With ValuesOnly enabled, only arrays are valid + var expectedResult = "[\"TEST\"]"; + + var renderer = CreateRenderer(addSecondHeader: false); + + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json; + renderer.SingleAsArray = false; + renderer.ValuesOnly = true; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void KeyFoundRendersValue_Header_Multiple_Items_Json_Formatting_ValuesOnly(bool singleAsArray) + { + var expectedResult = "[\"TEST\",\"TEST1\"]"; + + var renderer = CreateRenderer(); + + renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json; + renderer.SingleAsArray = singleAsArray; + renderer.ValuesOnly = true; + + string result = renderer.Render(new LogEventInfo()); + + Assert.Equal(expectedResult, result); + } + + /// + /// Create headers renderer with mocked HTTP context + /// + /// Add second header + /// Created headers layout renderer + private AspNetRequestHeadersLayoutRenderer CreateRenderer(bool addSecondHeader = true) + { + var headerNames = new List(); +#if ASP_NET_CORE + var httpContext = HttpContext; +#else + var httpContext = Substitute.For(); +#endif + +#if ASP_NET_CORE + headerNames.Add("key"); + httpContext.Request.Headers.Add("key", new StringValues("TEST")); + + if (addSecondHeader) + { + headerNames.Add("Key1"); + httpContext.Request.Headers.Add("Key1", new StringValues("TEST1")); + } +#else + var headers = new NameValueCollection(); + headers.Add("key", "TEST"); + headerNames.Add("key"); + + if (addSecondHeader) + { + headers.Add("Key1", "TEST1"); + headerNames.Add("Key1"); + } + + httpContext.Request.Headers.Returns(headers); +#endif + + var renderer = new AspNetRequestHeadersLayoutRenderer(); + renderer.HttpContextAccessor = new FakeHttpContextAccessor(httpContext); + renderer.HeaderNames = headerNames; + return renderer; + } + } +}