-
Notifications
You must be signed in to change notification settings - Fork 165
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Restore aspnet-request-posted-body with middleware (#781)
Co-authored-by: Burak Akgerman <burak.akgerman@huntington.com>
- Loading branch information
Showing
6 changed files
with
629 additions
and
0 deletions.
There are no files selected for viewing
136 changes: 136 additions & 0 deletions
136
src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<NLogRequestPostBodyMiddleware>(); 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
51
src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddlewareOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 <= 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 <= 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 <= 30KB | ||
/// </summary> | ||
private bool DefaultCapture(HttpContext context) | ||
{ | ||
return context?.Request?.ContentLength != null && context?.Request?.ContentLength <= MaximumRequestSize; | ||
} | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
src/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRenderer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
tests/NLog.Web.AspNetCore.Tests/NLogRequestPostedBodyMiddlewareOptionsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} | ||
} |
Oops, something went wrong.