diff --git a/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs b/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs index ec471dd1..9b407642 100644 --- a/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs +++ b/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs @@ -44,7 +44,6 @@ public async Task Invoke(HttpContext context) // This is required, otherwise reading the request will destructively read the request context.Request.EnableBuffering(); - // Save the POST request body in HttpContext.Items with a key of '__nlog-aspnet-request-posted-body' var requestBody = await GetString(context.Request.Body).ConfigureAwait(false); if (!string.IsNullOrEmpty(requestBody)) @@ -90,7 +89,7 @@ private bool ShouldCaptureRequestBody(HttpContext context) return false; } - return (_options.ShouldCapture(context)); + return _options.ShouldCapture(context); } /// @@ -105,30 +104,35 @@ private async Task GetString(Stream stream) // Save away the original stream position var originalPosition = stream.Position; - // This is required to reset the stream position to the beginning in order to properly read all of the stream. - stream.Position = 0; - string responseText = null; - // The last argument, leaveOpen, is set to true, so that the stream is not pre-maturely closed - // therefore preventing the next reader from reading the stream. - // The middle three arguments are from the configuration instance - // These default to UTF-8, true, and 1024. - using (var streamReader = new StreamReader( - stream, - Encoding.UTF8, - true, - 1024, - leaveOpen: true)) + try + { + // This is required to reset the stream position to the beginning in order to properly read all of the stream. + stream.Position = 0; + + // The last argument, leaveOpen, is set to true, so that the stream is not pre-maturely closed + // therefore preventing the next reader from reading the stream. + // The middle three arguments are from the configuration instance + // These default to UTF-8, true, and 1024. + using (var streamReader = new StreamReader( + stream, + Encoding.UTF8, + true, + 1024, + leaveOpen: true)) + { + // This is the most straight forward logic to read the entire body + responseText = await streamReader.ReadToEndAsync().ConfigureAwait(false); + } + } + finally { - // This is the most straight forward logic to read the entire body - responseText = await streamReader.ReadToEndAsync().ConfigureAwait(false); + // This is required to reset the stream position to the original, in order to + // properly let the next reader process the stream from the original point + stream.Position = originalPosition; } - // This is required to reset the stream position to the original, in order to - // properly let the next reader process the stream from the original point - stream.Position = originalPosition; - // Return the string of the body return responseText; } diff --git a/src/NLog.Web/NLogRequestPostedBodyModule.cs b/src/NLog.Web/NLogRequestPostedBodyModule.cs new file mode 100644 index 00000000..ab379d76 --- /dev/null +++ b/src/NLog.Web/NLogRequestPostedBodyModule.cs @@ -0,0 +1,150 @@ +using System; +using System.IO; +using System.Text; +using System.Web; +using NLog.Common; +using NLog.Web.LayoutRenderers; + +namespace NLog.Web +{ + /// + /// HttpModule that enables ${aspnet-request-posted-body} + /// + public class NLogRequestPostedBodyModule : IHttpModule + { + void IHttpModule.Init(HttpApplication context) + { + context.BeginRequest += (sender, args) => OnBeginRequest((sender as HttpApplication)?.Context); + } + + internal void OnBeginRequest(HttpContext context) + { + if (ShouldCaptureRequestBody(context)) + { + var requestBody = GetString(context.Request.InputStream); + + if (!string.IsNullOrEmpty(requestBody)) + { + context.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey] = requestBody; + } + } + } + + private bool ShouldCaptureRequestBody(HttpContext context) + { + // Perform null checking + if (context == null) + { + InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext is null"); + // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler + return false; + } + + // Perform null checking + if (context.Request == null) + { + InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext.Request stream is null"); + // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler + return false; + } + + var stream = context.Request.InputStream; + if (stream == null) + { + InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext.Request.Body stream is null"); + // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler + return false; + } + + // If we cannot read the stream we cannot capture the body + if (!stream.CanRead) + { + InternalLogger.Debug("NLogRequestPostedBodyModule: HttpContext.Request.Body stream is non-readable"); + // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler + return false; + } + + // If we cannot seek the stream we cannot capture the body + if (!stream.CanSeek) + { + InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpApplication.HttpContext.Request.Body stream is non-seekable"); + // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler + return false; + } + + return true; + } + + /// + /// Reads the posted body stream into a string + /// + /// + /// + private string GetString(Stream stream) + { + string responseText = null; + + if (stream.Length == 0) + return responseText; + + // Save away the original stream position + var originalPosition = stream.Position; + + try + { + // This is required to reset the stream position to the beginning in order to properly read all of the stream. + stream.Position = 0; + +#if NET46_OR_GREATER + //This required 5 argument constructor with leaveOpen available was added in 4.5, + //but the project and its unit test project are built for 4.6 + //and we should not change the csproj file just for this single class + + //If the 4 argument constructor with leaveOpen missing is used, the stream is closed after the + //ReadToEnd() operation completes and the request stream is no longer open for the actual consumer + //This causes the unit test to fail and should cause a failure during actual usage. + + using (var streamReader = new StreamReader( + stream, + Encoding.UTF8, + true, + bufferSize: 1024, + leaveOpen: true)) + { + // This is the most straight forward logic to read the entire body + responseText = streamReader.ReadToEnd(); + } + +#else + byte[] byteArray = new byte[Math.Min(stream.Length, 1024)]; + + using (var ms = new MemoryStream()) + { + int read = 0; + + while ((read = stream.Read(byteArray, 0, byteArray.Length)) > 0) + { + ms.Write(byteArray, 0, read); + } + + responseText = Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length); + } +#endif + } + finally + { + // This is required to reset the stream position to the original, in order to + // properly let the next reader process the stream from the original point + stream.Position = originalPosition; + } + + // Return the string of the body + return responseText; + } + + void IHttpModule.Dispose() + { + // Nothing here to do + } + } +} diff --git a/src/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRenderer.cs index c0d8f9f1..f86a91f5 100644 --- a/src/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRenderer.cs @@ -19,7 +19,6 @@ namespace NLog.Web.LayoutRenderers [LayoutRenderer("aspnet-request-posted-body")] public class AspNetRequestPostedBodyLayoutRenderer : AspNetLayoutRendererBase { - /// /// The object for the key in HttpContext.Items for the POST request body /// diff --git a/tests/NLog.Web.Tests/NLogRequestPostedBodyModuleTests.cs b/tests/NLog.Web.Tests/NLogRequestPostedBodyModuleTests.cs new file mode 100644 index 00000000..5ab76ae9 --- /dev/null +++ b/tests/NLog.Web.Tests/NLogRequestPostedBodyModuleTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Web; +using System.Web.Hosting; +using NLog.Web.LayoutRenderers; +using NLog.Web.Tests.LayoutRenderers; +using Xunit; + +namespace NLog.Web.Tests +{ + public class NLogRequestPostedBodyModuleTests : TestInvolvingAspNetHttpContext + { + [Fact] + public void HttpRequestNoBodyTest() + { + // Arrange + var httpContext = SetUpFakeHttpContext(); + + // Act + var httpModule = new NLogRequestPostedBodyModule(); + httpModule.OnBeginRequest(httpContext); + + // Assert + Assert.NotNull(httpContext.Items); + Assert.Empty(httpContext.Items); + } + + [Fact] + public void HttpRequestBodyTest() + { + // Arrange + var expectedMessage = "Expected message"; + MyWorkerRequest myRequest = new MyWorkerRequest(expectedMessage); + HttpContext httpContext = new HttpContext(myRequest); + + // Act + var httpModule = new NLogRequestPostedBodyModule(); + httpModule.OnBeginRequest(httpContext); + + // Assert + Assert.NotNull(httpContext.Items); + Assert.Single(httpContext.Items); + Assert.NotNull(httpContext.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey]); + Assert.True(httpContext.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey] is string); + Assert.Equal(expectedMessage, httpContext.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey] as string); + } + + public class MyWorkerRequest : SimpleWorkerRequest + { + private readonly MemoryStream _entityBody; + + public MyWorkerRequest(string entityBody) + :base("/", "/", "/", "", new StringWriter(CultureInfo.InvariantCulture)) + { + _entityBody = new MemoryStream(); + StreamWriter sw = new StreamWriter(_entityBody); + sw.Write(entityBody); + sw.Flush(); + _entityBody.Position = 0; + } + + public override bool IsEntireEntityBodyIsPreloaded() => true; + public override byte[] GetPreloadedEntityBody() => _entityBody.ToArray(); + } + } +}