Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore aspnet-request-posted-body with middleware #781

Merged
merged 25 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2764db7
Hopefully restore aspnet-request-posted-body using middleware approac…
May 24, 2022
fdd2a1c
Increase unit test coverage
May 24, 2022
ecd3edb
Increase unit test coverage
May 24, 2022
5dd35ea
Put EnableBuffering() before CanSeek for NLogRequestPostedBodyMiddleware
May 24, 2022
969bcff
Increase limit from 8KB to 30KB
May 24, 2022
1c39dc2
(Hopefully) Make the IHttpModule request body capture work for .NET 3…
May 25, 2022
b324383
Fix SonarQube detected defect in RequestPostedBodyHttpModule NET35-45…
May 25, 2022
75f7b20
Allow RequestPostedBodyHttpModule to set Encoding, BufferSize, and Sh…
May 25, 2022
2d60921
Fix defect in RequestPostedBodyHttpModule Encoding property not being…
May 25, 2022
2360654
Initially set responseText in RequestPostedBodyHttpModule instead of …
May 25, 2022
e5ec5b9
Upgrade the RequestPostedBodyHttpModule to have a Configuration prope…
May 25, 2022
465f50a
Make changes according to snakefoot review.
May 28, 2022
51fd3b0
2nd commit for snakefoots recommended changes
May 28, 2022
a77dd4c
refactor the request posted body http module class
May 28, 2022
46b384a
Improve unit test coverage for request post body middleware
May 28, 2022
c7cd7ab
Added another unit test for requiest poated body middleware
May 28, 2022
1d49852
Additional unit test coverage
May 28, 2022
b367aef
Removing NLogRequestPostedBodyHttpModule and related classes.
May 29, 2022
5bf8a9c
Use different middleware injection to not construct each time for eac…
May 30, 2022
263c015
Execute code review changes. Still need to convert string key to object
May 30, 2022
a0e81b2
Convert string key to object as requested
May 30, 2022
e3e8f73
Fix an incorrect comment
May 30, 2022
b3b67ff
Added a unit test for a null HttpContext
May 31, 2022
8603c6d
Review comments changed as requested
May 31, 2022
3c8d902
Use "option" instead of "configuration" for review feedback.
Jun 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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
///
/// The following are saved in the HttpContext.Items collection
///
/// __nlog-aspnet-request-posted-body
///
/// Usage: app.UseMiddleware&lt;NLogRequestPostBodyMiddleware&gt;(); where app is an IApplicationBuilder
/// Register the NLogRequestPostBodyMiddlewareConfiguration in the IoC so that the config gets passed to the constructor
/// </summary>
public class NLogRequestPostedBodyMiddleware
{
private readonly RequestDelegate _next;

private NLogRequestPostedBodyMiddlewareConfiguration _configuration { get; }
snakefoot marked this conversation as resolved.
Show resolved Hide resolved

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

/// <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);
snakefoot marked this conversation as resolved.
Show resolved Hide resolved

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.Request == null)
snakefoot marked this conversation as resolved.
Show resolved Hide resolved
snakefoot marked this conversation as resolved.
Show resolved Hide resolved
{
InternalLogger.Debug("NLogRequestPostedBodyMiddleware: 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;
}

// 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 (_configuration.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,
_configuration.DetectEncodingFromByteOrderMark,
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using Microsoft.AspNetCore.Http;

namespace NLog.Web
{
/// <summary>
/// Contains the configuration for the NLogRequestPostedBodyMiddleware
/// </summary>
public class NLogRequestPostedBodyMiddlewareConfiguration
snakefoot marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// The default configuration
/// </summary>
public static readonly NLogRequestPostedBodyMiddlewareConfiguration Default = new NLogRequestPostedBodyMiddlewareConfiguration();
snakefoot marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Defaults to true
/// </summary>
public bool DetectEncodingFromByteOrderMark { get; set; } = true;
snakefoot marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// The maximum request size that will be captured
/// Defaults to 30KB
/// </summary>
public int MaximumRequestSize { get; set; } = 30 * 1024;
snakefoot marked this conversation as resolved.
Show resolved Hide resolved

/// <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; } = DefaultCapture;

/// <summary>
/// The default predicate for ShouldCapture
/// Returns true if content length &lt;= 30KB
/// </summary>
public static bool DefaultCapture(HttpContext context)
snakefoot marked this conversation as resolved.
Show resolved Hide resolved
{
return context?.Request?.ContentLength != null && context?.Request?.ContentLength <=
new NLogRequestPostedBodyMiddlewareConfiguration().MaximumRequestSize;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 string for the key in HttpContext.Items for the POST request body
/// </summary>
public static readonly string NLogPostedRequestBodyKey = "__nlog-aspnet-request-posted-body";
snakefoot marked this conversation as resolved.
Show resolved Hide resolved

/// <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

if (items[NLogPostedRequestBodyKey] is string)
snakefoot marked this conversation as resolved.
Show resolved Hide resolved
{
builder.Append(items[NLogPostedRequestBodyKey] as string);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;

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

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

[Fact]
public void SetByteOrderMarkTest()
{
var config = new NLogRequestPostedBodyMiddlewareConfiguration();
var bom = true;
config.DetectEncodingFromByteOrderMark = bom;

Assert.Equal(bom, config.DetectEncodingFromByteOrderMark);

bom = false;
config.DetectEncodingFromByteOrderMark = bom;

Assert.Equal(bom, config.DetectEncodingFromByteOrderMark);
}

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

Assert.NotNull(config);
}

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

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

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

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

httpContext.Request.Returns(request);

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

[Fact]
public void DefaultCaptureFalseNullContentLength()
{
var config = NLogRequestPostedBodyMiddlewareConfiguration.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 = NLogRequestPostedBodyMiddlewareConfiguration.Default;

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

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

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

httpContext.Request.Returns(request);

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