Skip to content

Commit

Permalink
NLogRequestPostedBodyModule - Enables ${aspnet-request-posted-body}
Browse files Browse the repository at this point in the history
  • Loading branch information
snakefoot committed Jun 4, 2022
1 parent 3aee9bb commit bc968f0
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 22 deletions.
46 changes: 25 additions & 21 deletions src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -90,7 +89,7 @@ private bool ShouldCaptureRequestBody(HttpContext context)
return false;
}

return (_options.ShouldCapture(context));
return _options.ShouldCapture(context);
}

/// <summary>
Expand All @@ -105,30 +104,35 @@ private async Task<string> 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;
}
Expand Down
150 changes: 150 additions & 0 deletions src/NLog.Web/NLogRequestPostedBodyModule.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// HttpModule that enables ${aspnet-request-posted-body}
/// </summary>
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;
}

/// <summary>
/// Reads the posted body stream into a string
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ namespace NLog.Web.LayoutRenderers
[LayoutRenderer("aspnet-request-posted-body")]
public class AspNetRequestPostedBodyLayoutRenderer : AspNetLayoutRendererBase
{

/// <summary>
/// The object for the key in HttpContext.Items for the POST request body
/// </summary>
Expand Down
68 changes: 68 additions & 0 deletions tests/NLog.Web.Tests/NLogRequestPostedBodyModuleTests.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}

0 comments on commit bc968f0

Please sign in to comment.