diff --git a/src/NLog.Web.AspNetCore/LayoutRenderers/AspNetRequestConnectionIdLayoutRenderer.cs b/src/NLog.Web.AspNetCore/LayoutRenderers/AspNetRequestConnectionIdLayoutRenderer.cs new file mode 100644 index 00000000..7ec94dea --- /dev/null +++ b/src/NLog.Web.AspNetCore/LayoutRenderers/AspNetRequestConnectionIdLayoutRenderer.cs @@ -0,0 +1,36 @@ +using NLog.LayoutRenderers; +using NLog.Web.Internal; +using System.Text; + +namespace NLog.Web.LayoutRenderers +{ + /// + /// ASP.NET request connection id + /// + /// + /// ${aspnet-request-connection-id} + /// + [LayoutRenderer("aspnet-request-connection-id")] + public class AspNetRequestConnectionIdLayoutRenderer : AspNetLayoutRendererBase + { + /// + /// Renders the ASP.NET connection ID + /// + /// The to append the rendered data to. + /// Logging event. + protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) + { + var connection = HttpContextAccessor.HttpContext.TryGetConnection(); + if (connection == null) + { + return; + } + + var id = connection.Id; + if(!string.IsNullOrEmpty(id)) + { + builder.Append(id); + } + } + } +} diff --git a/src/Shared/Internal/HttpContextExtensions.cs b/src/Shared/Internal/HttpContextExtensions.cs index 234999fc..b500f6e9 100644 --- a/src/Shared/Internal/HttpContextExtensions.cs +++ b/src/Shared/Internal/HttpContextExtensions.cs @@ -46,6 +46,22 @@ internal static HttpResponseBase TryGetResponse(this HttpContextBase context) } } #else + internal static WebSocketManager TryGetWebSocket(this HttpContext context) + { + var websocket = context?.WebSockets; + if (websocket == null) + InternalLogger.Debug("HttpContext WebSocket Lookup returned null"); + return websocket; + } + + internal static ConnectionInfo TryGetConnection(this HttpContext context) + { + var connection = context?.Connection; + if (connection == null) + InternalLogger.Debug("HttpContext Connection Lookup returned null"); + return connection; + } + internal static HttpRequest TryGetRequest(this HttpContext context) { var request = context?.Request; diff --git a/src/Shared/LayoutRenderers/AspNetRequestClientCertificateLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestClientCertificateLayoutRenderer.cs index 52566e38..c3462e4a 100644 --- a/src/Shared/LayoutRenderers/AspNetRequestClientCertificateLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetRequestClientCertificateLayoutRenderer.cs @@ -13,11 +13,17 @@ namespace NLog.Web.LayoutRenderers /// ASP.NET Client Certificate of the Connection /// /// - /// ${aspnet-request-client certificate} + /// ${aspnet-request-client-certificate} + /// ${aspnet-request-client-certificate:Verbose=True} /// [LayoutRenderer("aspnet-request-client-certificate")] public class AspNetRequestClientCertificateLayoutRenderer : AspNetLayoutRendererBase { + /// + /// This is passed to the X509Certificate2.ToString(bool) method + /// + public bool Verbose { get; set; } + /// /// Render Remote Port /// @@ -30,7 +36,7 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) { return; } - builder.Append(connection.ClientCertificate); + builder.Append(connection.ClientCertificate?.ToString(Verbose)); #else var certificate = httpContext.Request.ClientCertificate; if (certificate == null) @@ -39,7 +45,7 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) } // Convert to an X509Certificate2, which does have the proper overridden ToString() method. // HttpClientCertificate class only use object.ToString() which is useless. - builder.Append(new X509Certificate2(certificate.Certificate)); + builder.Append(new X509Certificate2(certificate.Certificate)?.ToString(Verbose)); #endif } } diff --git a/src/Shared/LayoutRenderers/AspNetRequestIsWebSocketLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestIsWebSocketLayoutRenderer.cs index a2fd3350..b33caf45 100644 --- a/src/Shared/LayoutRenderers/AspNetRequestIsWebSocketLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetRequestIsWebSocketLayoutRenderer.cs @@ -1,5 +1,6 @@ using System.Text; using NLog.LayoutRenderers; +using NLog.Web.Internal; namespace NLog.Web.LayoutRenderers { @@ -21,7 +22,7 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) { // Not available on .NET 3.5 #if ASP_NET_CORE - var websockets = HttpContextAccessor.HttpContext?.WebSockets; + var websockets = HttpContextAccessor.HttpContext.TryGetWebSocket(); builder.Append(websockets?.IsWebSocketRequest == true ? '1' : '0'); #elif NET46_OR_GREATER var httpContext = HttpContextAccessor.HttpContext; diff --git a/src/Shared/LayoutRenderers/AspNetRequestLocalIpLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestLocalIpLayoutRenderer.cs index 645ccfae..d8481086 100644 --- a/src/Shared/LayoutRenderers/AspNetRequestLocalIpLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetRequestLocalIpLayoutRenderer.cs @@ -26,7 +26,7 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) var httpContext = HttpContextAccessor.HttpContext; #if ASP_NET_CORE - var connection = httpContext.Connection; + var connection = httpContext.TryGetConnection(); if (connection == null) { return; diff --git a/src/Shared/LayoutRenderers/AspNetRequestLocalPortLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestLocalPortLayoutRenderer.cs index e1103bf2..37ff62ea 100644 --- a/src/Shared/LayoutRenderers/AspNetRequestLocalPortLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetRequestLocalPortLayoutRenderer.cs @@ -25,7 +25,7 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) { var httpContext = HttpContextAccessor.HttpContext; #if ASP_NET_CORE - var connection = httpContext.Connection; + var connection = httpContext.TryGetConnection(); if (connection == null) { return; diff --git a/src/Shared/LayoutRenderers/AspNetRequestRemotePortLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestRemotePortLayoutRenderer.cs index e2478a47..fa46aa63 100644 --- a/src/Shared/LayoutRenderers/AspNetRequestRemotePortLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetRequestRemotePortLayoutRenderer.cs @@ -26,7 +26,7 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) var httpContext = HttpContextAccessor.HttpContext; #if ASP_NET_CORE - var connection = httpContext.Connection; + var connection = httpContext.TryGetConnection(); if (connection == null) { return; diff --git a/src/Shared/LayoutRenderers/AspNetRequestWebSocketRequestedProtocolsLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestWebSocketRequestedProtocolsLayoutRenderer.cs index e64f0558..dd0c154f 100644 --- a/src/Shared/LayoutRenderers/AspNetRequestWebSocketRequestedProtocolsLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetRequestWebSocketRequestedProtocolsLayoutRenderer.cs @@ -1,5 +1,6 @@ using System.Text; using NLog.LayoutRenderers; +using NLog.Web.Internal; namespace NLog.Web.LayoutRenderers { @@ -28,7 +29,7 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) { // Not available on .NET 3.5 #if ASP_NET_CORE - var websockets = HttpContextAccessor.HttpContext?.WebSockets; + var websockets = HttpContextAccessor.HttpContext.TryGetWebSocket(); if (websockets == null) { return; diff --git a/src/Shared/LayoutRenderers/AspNetResponseHasStartedLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetResponseHasStartedLayoutRenderer.cs new file mode 100644 index 00000000..ca7a8ccd --- /dev/null +++ b/src/Shared/LayoutRenderers/AspNetResponseHasStartedLayoutRenderer.cs @@ -0,0 +1,31 @@ +using NLog.LayoutRenderers; +using NLog.Web.Internal; +using System.Text; + +namespace NLog.Web.LayoutRenderers +{ + /// + /// ASP.NET response headers already sent, in other words the response has started + /// + /// + /// ${aspnet-response-has-started} + /// + [LayoutRenderer("aspnet-response-has-started")] + public class AspNetResponseHasStartedLayoutRenderer : AspNetLayoutRendererBase + { + /// + /// Renders the ASP.NET HttpResponse HasStarted property in ASP.NET Core and the HttpResponse HeadersWritten in .NET Framework + /// + /// The to append the rendered data to. + /// Logging event. + protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) + { + var response = HttpContextAccessor.HttpContext.TryGetResponse(); +#if ASP_NET_CORE + builder.Append(response?.HasStarted == true ? '1' : '0'); +#elif NET46 + builder.Append(response?.HeadersWritten == true ? '1' : '0'); +#endif + } + } +} diff --git a/tests/NLog.Web.AspNetCore.Tests/LayoutRenderers/AspNetRequestConnectionIdLayoutRendererTests.cs b/tests/NLog.Web.AspNetCore.Tests/LayoutRenderers/AspNetRequestConnectionIdLayoutRendererTests.cs new file mode 100644 index 00000000..12eac80f --- /dev/null +++ b/tests/NLog.Web.AspNetCore.Tests/LayoutRenderers/AspNetRequestConnectionIdLayoutRendererTests.cs @@ -0,0 +1,52 @@ +using NLog.Web.LayoutRenderers; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace NLog.Web.Tests.LayoutRenderers +{ + public class AspNetRequestConnectionIdLayoutRendererTests : LayoutRenderersTestBase + { + [Fact] + public void SuccessTest() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + + httpContext.Connection.Id.Returns("My Connection Id"); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("My Connection Id", result); + } + + [Fact] + public void EmptyTest() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + + httpContext.Connection.Id.Returns(""); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("", result); + } + + [Fact] + public void NullTest() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + + httpContext.Connection.Id.ReturnsNull(); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("", result); + } + } +} diff --git a/tests/Shared/LayoutRenderers/AspNetRequestClientCertificateRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetRequestClientCertificateRendererTests.cs index db416df8..801fc58a 100644 --- a/tests/Shared/LayoutRenderers/AspNetRequestClientCertificateRendererTests.cs +++ b/tests/Shared/LayoutRenderers/AspNetRequestClientCertificateRendererTests.cs @@ -28,6 +28,25 @@ public void SuccessTest() Assert.Equal(certificate.ToString(), result); } + [Fact] + public void SuccessVerboseTest() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + renderer.Verbose = true; + + var certificate = new X509Certificate2( + Convert.FromBase64String( +"MIIC7DCCAdSgAwIBAgIQJq2oGnSgP79FVWGrezYIBDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMjIwMzExMDQyOTI4WhcNMjcwMzEwMDAwMDAwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfSpPySBJetaQdagcgNMs6owXj6QwDv4BKB/qRG4AM5myL3T+7cTiUUOaHywIR+79k2K+LVOLKzfXLJvrzYZSj3yd02ScmM4BEJEcbauY7wCYjgFfB6K9Klh87UAP6+gUUHjIVfzI/Go3883c9D29S3PbO3z5Yz1hTVS0Hes5ZE0d7TDevVSXm2ZpUZSPz7W50+FBq2z3uI3pSBg2oZYHhUvbFIhMI0VIFAPSiyU9XIo+RCv3eN27Fq8g3Qo0z+8wnk7zSldncVEZko5WGKNL781U/TDuhBigkvEme9goaRPRW8oNjm//v+vyJwo/WDzghSwc6jCIdjTXUIqxw4THBAgMBAAGjOjA4MAsGA1UdDwQEAwIEsDATBgNVHSUEDDAKBggrBgEFBQcDATAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAEqAzHpQgeiW1abdOj4LClM+8uU813tvFhepneLL59yvtZ2NT6ruSd7Fa15CT8bIKACgxzUOaB7N/KseORqLFrNiwvu2A1vvEkhueH0TLC2KJt7SvlEKw5QooiLdOIfNBaL8h/YE/TK9hXCdTgmsPuXuc47vxSyLUUzMGFlW4olFkOEHOP0havplJvPabLY/WpPTV0+mKUe+CKKEKNduH8il9heXguak06XGufP8UQP1fE+GVDFJDqX0S2TMcaoohxL2lV4VqNnadGJ/VA97ZDTWKFteDdTNwZYyb0KvxLtUCc6cHak9ZRs1E7+SZyNx/pcB4vgpnWPXKX8WDr3VGw0=")); + + httpContext.Connection.ClientCertificate.Returns(certificate); + + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal(certificate.ToString(true), result); + } + [Fact] public void NullTest() { @@ -77,6 +96,45 @@ public void SuccessTest() //Assert.Equal(certificate.ToString(), result); Assert.Equal(string.Empty, result); } + + [Fact] + public void SuccessVerboseTest() + { + var httpContext = Substitute.For(); + var renderer = new AspNetRequestClientCertificateLayoutRenderer(); + renderer.Verbose = true; + renderer.HttpContextAccessor = new FakeHttpContextAccessor(httpContext); + + // Arrange + var byteArray = Convert.FromBase64String( + "MIIC7DCCAdSgAwIBAgIQJq2oGnSgP79FVWGrezYIBDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMjIwMzExMDQyOTI4WhcNMjcwMzEwMDAwMDAwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfSpPySBJetaQdagcgNMs6owXj6QwDv4BKB/qRG4AM5myL3T+7cTiUUOaHywIR+79k2K+LVOLKzfXLJvrzYZSj3yd02ScmM4BEJEcbauY7wCYjgFfB6K9Klh87UAP6+gUUHjIVfzI/Go3883c9D29S3PbO3z5Yz1hTVS0Hes5ZE0d7TDevVSXm2ZpUZSPz7W50+FBq2z3uI3pSBg2oZYHhUvbFIhMI0VIFAPSiyU9XIo+RCv3eN27Fq8g3Qo0z+8wnk7zSldncVEZko5WGKNL781U/TDuhBigkvEme9goaRPRW8oNjm//v+vyJwo/WDzghSwc6jCIdjTXUIqxw4THBAgMBAAGjOjA4MAsGA1UdDwQEAwIEsDATBgNVHSUEDDAKBggrBgEFBQcDATAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAEqAzHpQgeiW1abdOj4LClM+8uU813tvFhepneLL59yvtZ2NT6ruSd7Fa15CT8bIKACgxzUOaB7N/KseORqLFrNiwvu2A1vvEkhueH0TLC2KJt7SvlEKw5QooiLdOIfNBaL8h/YE/TK9hXCdTgmsPuXuc47vxSyLUUzMGFlW4olFkOEHOP0havplJvPabLY/WpPTV0+mKUe+CKKEKNduH8il9heXguak06XGufP8UQP1fE+GVDFJDqX0S2TMcaoohxL2lV4VqNnadGJ/VA97ZDTWKFteDdTNwZYyb0KvxLtUCc6cHak9ZRs1E7+SZyNx/pcB4vgpnWPXKX8WDr3VGw0="); + + var certificate = new X509Certificate2(byteArray); + + // We need Microsoft Fakes here, but we only have Visual Studio 2022 Community, not Enterprise + // and we also cannot require NLog.Web community of developer to have Enterprise edition + //httpContext.Request.ClientCertificate = new HttpClientCertificate(byteArray); + + // This throws NullReferenceException + //httpContext.Request.ClientCertificate.Certificate.Returns(byteArray); + + var httpRequest = Substitute.For(); + httpContext.Request.Returns(httpRequest); + + // This throws NotSupportedException due to no public constructors, from Castle.DynamicProxy.Generators + //var clientCertificate = Substitute.For(); + + //clientCertificate.Certificate.Returns(byteArray); + //httpRequest.ClientCertificate.Returns(clientCertificate); + + // Act + string result = renderer.Render(new LogEventInfo()); + + // Assert + //Assert.Equal(certificate.ToString(true), result); + Assert.Equal(string.Empty, result); + } + [Fact] public void NullTest() { diff --git a/tests/Shared/LayoutRenderers/AspNetResponseHasStartedLayoutRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetResponseHasStartedLayoutRendererTests.cs new file mode 100644 index 00000000..5562cb5d --- /dev/null +++ b/tests/Shared/LayoutRenderers/AspNetResponseHasStartedLayoutRendererTests.cs @@ -0,0 +1,106 @@ +using NLog.Web.LayoutRenderers; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace NLog.Web.Tests.LayoutRenderers +{ + public class AspNetResponseHasStartedLayoutRendererTests : LayoutRenderersTestBase + { +#if !ASP_NET_CORE && NET46_OR_GREATER + [Fact] + public void TrueCase() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + httpContext.Response.HeadersWritten.Returns(true); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("1", result); + } + + [Fact] + public void FalseCase() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + + httpContext.Response.HeadersWritten.Returns(false); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("0", result); + } + + [Fact] + public void NullCase() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + + httpContext.Response.ReturnsNull(); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("0", result); + } + +#endif + + +#if ASP_NET_CORE + [Fact] + public void TrueCase() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + httpContext.Response.HasStarted.Returns(true); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("1", result); + } + + [Fact] + public void FalseCase() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + + httpContext.Response.HasStarted.Returns(false); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("0", result); + } + + [Fact] + public void NullCase() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + + httpContext.Response.ReturnsNull(); + // Act + string result = renderer.Render(new LogEventInfo()); + // Assert + Assert.Equal("0", result); + } +#endif + +#if ASP_NET_CORE || NET46_OR_GREATER + protected override void NullRendersEmptyString() + { + // Arrange + var (renderer, _) = CreateWithHttpContext(); + + // Act + string result = renderer.Render(LogEventInfo.CreateNullEvent()); + + // Assert + Assert.Equal("0", result); + } +#endif + } +}