Skip to content

Commit

Permalink
Restore aspnet-request-posted-body with middleware (#781)
Browse files Browse the repository at this point in the history
Co-authored-by: Burak Akgerman <burak.akgerman@huntington.com>
  • Loading branch information
bakgerman and Burak Akgerman authored Jun 1, 2022
1 parent 6bb5747 commit 3aee9bb
Show file tree
Hide file tree
Showing 6 changed files with 629 additions and 0 deletions.
136 changes: 136 additions & 0 deletions src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using NLog.Common;
using NLog.Web.LayoutRenderers;

namespace NLog.Web
{
/// <summary>
/// This class is to intercept the HTTP pipeline and to allow additional logging of the following
///
/// POST request body
///
/// Usage: app.UseMiddleware&lt;NLogRequestPostBodyMiddleware&gt;(); where app is an IApplicationBuilder
/// Register the NLogRequestPostBodyMiddlewareOption in the IoC so that the config gets passed to the constructor
/// </summary>
public class NLogRequestPostedBodyMiddleware
{
private readonly RequestDelegate _next;

private readonly NLogRequestPostedBodyMiddlewareOptions _options;

/// <summary>
/// Constructor that takes a configuration
/// </summary>
/// <param name="next"></param>
/// <param name="options"></param>
public NLogRequestPostedBodyMiddleware(RequestDelegate next, NLogRequestPostedBodyMiddlewareOptions options)
{
_next = next;
_options = options;
}

/// <summary>
/// This allows interception of the HTTP pipeline for logging purposes
/// </summary>
/// <param name="context">The HttpContext</param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
if (ShouldCaptureRequestBody(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))
{
context.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey] = requestBody;
}
}

// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
await _next(context).ConfigureAwait(false);
}

private bool ShouldCaptureRequestBody(HttpContext context)
{
// Perform null checking
if (context == null)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext is null");
// Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
return false;
}

if (context.Request == null)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request 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.Body == null)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: 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 (!context.Request.Body.CanRead)
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: 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;
}

return (_options.ShouldCapture(context));
}

/// <summary>
/// Convert the stream to a String for logging.
/// If the stream is binary please do not utilize this middleware
/// Arguably, logging a byte array in a sensible format is simply not possible.
/// </summary>
/// <param name="stream"></param>
/// <returns>The contents of the Stream read fully from start to end as a String</returns>
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))
{
// 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;

// Return the string of the body
return responseText;
}
}
}
51 changes: 51 additions & 0 deletions src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddlewareOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using Microsoft.AspNetCore.Http;

namespace NLog.Web
{
/// <summary>
/// Contains the configuration for the NLogRequestPostedBodyMiddleware
/// </summary>
public class NLogRequestPostedBodyMiddlewareOptions
{
/// <summary>
/// The default configuration
/// </summary>
internal static readonly NLogRequestPostedBodyMiddlewareOptions Default = new NLogRequestPostedBodyMiddlewareOptions();

/// <summary>
/// The default constructor
/// </summary>
public NLogRequestPostedBodyMiddlewareOptions()
{
ShouldCapture = DefaultCapture;
}

/// <summary>
/// The maximum request size that will be captured
/// Defaults to 30KB. This checks against the ContentLength.
/// HttpRequest.EnableBuffer() writes the request to TEMP files on disk if the request ContentLength is > 30KB
/// but uses memory otherwise if &lt;= 30KB, so we should protect against "very large"
/// request post body payloads.
/// </summary>
public int MaximumRequestSize { get; set; } = 30 * 1024;

/// <summary>
/// If this returns true, the post request body will be captured
/// Defaults to true if content length &lt;= 30KB
/// This can be used to capture only certain content types,
/// only certain hosts, only below a certain request body size, and so forth
/// </summary>
/// <returns></returns>
public Predicate<HttpContext> ShouldCapture { get; set; }

/// <summary>
/// The default predicate for ShouldCapture
/// Returns true if content length &lt;= 30KB
/// </summary>
private bool DefaultCapture(HttpContext context)
{
return context?.Request?.ContentLength != null && context?.Request?.ContentLength <= MaximumRequestSize;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Text;
using NLog.LayoutRenderers;
#if ASP_NET_CORE
using Microsoft.AspNetCore.Http;
#else
using System.Web;
#endif
namespace NLog.Web.LayoutRenderers
{
/// <summary>
/// ASP.NET posted body, e.g. FORM or Ajax POST
/// </summary>
/// <para>Example usage of ${aspnet-request-posted-body}:</para>
/// <example>
/// <code lang="NLog Layout Renderer">
/// ${aspnet-request-posted-body} - Produces - {username:xyz,password:xyz}
/// </code>
/// </example>
[LayoutRenderer("aspnet-request-posted-body")]
public class AspNetRequestPostedBodyLayoutRenderer : AspNetLayoutRendererBase
{

/// <summary>
/// The object for the key in HttpContext.Items for the POST request body
/// </summary>
internal static readonly object NLogPostedRequestBodyKey = new object();

/// <summary>Renders the ASP.NET posted body</summary>
/// <param name="builder">The <see cref="T:System.Text.StringBuilder" /> to append the rendered data to.</param>
/// <param name="logEvent">Logging event.</param>
protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
{
var httpContext = HttpContextAccessor.HttpContext;
if (httpContext == null)
{
return;
}

var items = httpContext.Items;
if (items == null)
{
return;
}

if (httpContext.Items.Count == 0)
{
return;
}

#if !ASP_NET_CORE
if (!items.Contains(NLogPostedRequestBodyKey))
{
return;
}
#else
if (!items.ContainsKey(NLogPostedRequestBodyKey))
{
return;
}
#endif

builder.Append(items[NLogPostedRequestBodyKey] as string);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;

namespace NLog.Web.Tests
{
public class NLogRequestPostedBodyMiddlewareOptionsTests
{
[Fact]
public void SetMaximumRequestSizeTest()
{
var config = new NLogRequestPostedBodyMiddlewareOptions();
var size = new Random().Next();
config.MaximumRequestSize = size;

Assert.Equal(size, config.MaximumRequestSize);
}

[Fact]
public void GetDefault()
{
var config = NLogRequestPostedBodyMiddlewareOptions.Default;

Assert.NotNull(config);
}

[Fact]
public void DefaultCaptureTrue()
{
var config = NLogRequestPostedBodyMiddlewareOptions.Default;

HttpContext httpContext = Substitute.For<HttpContext>();

HttpRequest request = Substitute.For<HttpRequest>();

request.ContentLength.Returns(NLogRequestPostedBodyMiddlewareOptions.Default.MaximumRequestSize - 1);

httpContext.Request.Returns(request);

Assert.True(config.ShouldCapture(httpContext));
}

[Fact]
public void DefaultCaptureFalseNullContentLength()
{
var config = NLogRequestPostedBodyMiddlewareOptions.Default;

HttpContext httpContext = Substitute.For<HttpContext>();

HttpRequest request = Substitute.For<HttpRequest>();

request.ContentLength.Returns((long?)null);

httpContext.Request.Returns(request);

Assert.False(config.ShouldCapture(httpContext));
}

[Fact]
public void DefaultCaptureExcessiveContentLength()
{
var config = NLogRequestPostedBodyMiddlewareOptions.Default;

HttpContext httpContext = Substitute.For<HttpContext>();

HttpRequest request = Substitute.For<HttpRequest>();

request.ContentLength.Returns(NLogRequestPostedBodyMiddlewareOptions.Default.MaximumRequestSize + 1);

httpContext.Request.Returns(request);

Assert.False(config.ShouldCapture(httpContext));
}
}
}
Loading

0 comments on commit 3aee9bb

Please sign in to comment.