diff --git a/src/Shared/LayoutRenderers/AspNetRequestIpLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestIpLayoutRenderer.cs index df7a843d..9731fc6f 100644 --- a/src/Shared/LayoutRenderers/AspNetRequestIpLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetRequestIpLayoutRenderer.cs @@ -1,7 +1,5 @@ -using System; -using System.ComponentModel; +using System.ComponentModel; using System.Text; -using NLog.Config; using NLog.LayoutRenderers; using NLog.Layouts; using NLog.Web.Internal; @@ -17,7 +15,11 @@ namespace NLog.Web.LayoutRenderers /// ASP.NET Request IP address of the remote client /// /// - /// ${aspnet-request-ip} + /// ${aspnet-request-ip} to return the Remote IP + /// ${aspnet-request-ip:CheckForwardedForHeader=true} to return first element in the X-Forwarded-For header + /// ${aspnet-request-ip:CheckForwardedForHeaderOffset=1} to return second element in the X-Forwarded-For header + /// ${aspnet-request-ip:CheckForwardedForHeaderOffset=-1} to return last element in the X-Forwarded-For header + /// ${aspnet-request-ip:CheckForwardedForHeaderOffset=1:ForwardedForHeader=myHeader} to return second element in the myHeader header /// /// Documentation on NLog Wiki [LayoutRenderer("aspnet-request-ip")] @@ -35,6 +37,24 @@ public class AspNetRequestIpLayoutRenderer : AspNetLayoutRendererBase /// public bool CheckForwardedForHeader { get; set; } + /// + /// Gets or sets the array index of the X-Forwarded-For header to use, if the desired client IP is not at + /// the zeroth index. Defaults to zero. If the index is too large the last array element is returned instead. + /// If a negative index is used, this is used as the position from the end of the array. + /// Minus one will indicate the last element in the array. If the negative index is too large the first index + /// of the array is returned instead. + /// + public int CheckForwardedForHeaderOffset + { + get => _checkForwardedForHeaderOffset; + set + { + _checkForwardedForHeaderOffset = value; + CheckForwardedForHeader = true; + } + } + private int _checkForwardedForHeaderOffset; + /// protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) { @@ -60,6 +80,25 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) builder.Append(ip); } + private int CalculatePosition(string[] headerContents) + { + var position = CheckForwardedForHeaderOffset; + + if (position < 0) + { + position = headerContents.Length + position; + } + if (position < 0) + { + position = 0; + } + if (position >= headerContents.Length) + { + position = headerContents.Length - 1; + } + return position; + } + #if !ASP_NET_CORE string TryLookupForwardHeader(HttpRequestBase httpRequest, LogEventInfo logEvent) { @@ -71,7 +110,8 @@ string TryLookupForwardHeader(HttpRequestBase httpRequest, LogEventInfo logEvent var addresses = forwardedHeader.Split(','); if (addresses.Length > 0) { - return addresses[0]; + var position = CalculatePosition(addresses); + return addresses[position]?.Trim(); } } @@ -86,7 +126,8 @@ private string TryLookupForwardHeader(HttpRequest httpRequest, LogEventInfo logE var forwardedHeaders = httpRequest.Headers.GetCommaSeparatedValues(headerName); if (forwardedHeaders.Length > 0) { - return forwardedHeaders[0]; + var position = CalculatePosition(forwardedHeaders); + return forwardedHeaders[position]?.Trim(); } } diff --git a/tests/Shared/LayoutRenderers/AspNetRequestIpLayoutRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetRequestIpLayoutRendererTests.cs index ea342000..9d33dc8c 100644 --- a/tests/Shared/LayoutRenderers/AspNetRequestIpLayoutRendererTests.cs +++ b/tests/Shared/LayoutRenderers/AspNetRequestIpLayoutRendererTests.cs @@ -116,5 +116,117 @@ public void ForwardedForHeaderPresentWithCustomRenderForwardedValue() // Assert Assert.Equal("127.0.0.1", result); } + + [Fact] + public void ForwardedForHeaderContainsMultipleEntriesRendersIndexValue() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + +#if !ASP_NET_CORE + httpContext.Request.ServerVariables.Returns(new NameValueCollection {{"REMOTE_ADDR", "192.0.0.0"}}); + httpContext.Request.Headers.Returns( + new NameValueCollection {{ForwardedForHeader, "192.168.1.1, 127.0.0.1"}}); +#else + var headers = new HeaderDict(); + headers.Add(ForwardedForHeader, new StringValues("192.168.1.1, 127.0.0.1")); + httpContext.Request.Headers.Returns(callinfo => headers); +#endif + renderer.CheckForwardedForHeaderOffset = 1; + + // Act + string result = renderer.Render(new LogEventInfo()); + + // Assert + Assert.Equal("127.0.0.1", result); + } + + [Fact] + public void ForwardedForHeaderContainsMultipleEntriesRendersLastValue() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + +#if !ASP_NET_CORE + httpContext.Request.ServerVariables.Returns(new NameValueCollection {{"REMOTE_ADDR", "192.0.0.0"}}); + httpContext.Request.Headers.Returns( + new NameValueCollection {{ForwardedForHeader, "192.168.1.1, 127.0.0.1"}}); +#else + var headers = new HeaderDict(); + headers.Add(ForwardedForHeader, new StringValues("192.168.1.1, 127.0.0.1")); + httpContext.Request.Headers.Returns(callinfo => headers); +#endif + renderer.CheckForwardedForHeaderOffset = -1; + + // Act + string result = renderer.Render(new LogEventInfo()); + + // Assert + Assert.Equal("127.0.0.1", result); + } + + [Fact] + public void ForwardedForHeaderContainsMultipleEntriesExcessiveIndexRendersLastValue() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + +#if !ASP_NET_CORE + httpContext.Request.ServerVariables.Returns(new NameValueCollection {{"REMOTE_ADDR", "192.0.0.0"}}); + httpContext.Request.Headers.Returns( + new NameValueCollection {{ForwardedForHeader, "192.168.1.1, 127.0.0.1"}}); +#else + var headers = new HeaderDict(); + headers.Add(ForwardedForHeader, new StringValues("192.168.1.1, 127.0.0.1")); + httpContext.Request.Headers.Returns(callinfo => headers); +#endif + renderer.CheckForwardedForHeaderOffset = 2; + + // Act + string result = renderer.Render(new LogEventInfo()); + + // Assert + Assert.Equal("127.0.0.1", result); + + renderer.CheckForwardedForHeaderOffset = 3; + + // Act + result = renderer.Render(new LogEventInfo()); + + // Assert + Assert.Equal("127.0.0.1", result); + } + + [Fact] + public void ForwardedForHeaderContainsMultipleEntriesExcessiveNegativeIndexRendersFirstValue() + { + // Arrange + var (renderer, httpContext) = CreateWithHttpContext(); + +#if !ASP_NET_CORE + httpContext.Request.ServerVariables.Returns(new NameValueCollection {{"REMOTE_ADDR", "192.0.0.0"}}); + httpContext.Request.Headers.Returns( + new NameValueCollection {{ForwardedForHeader, "127.0.0.1, 192.168.1.1"}}); +#else + var headers = new HeaderDict(); + headers.Add(ForwardedForHeader, new StringValues("127.0.0.1, 192.168.1.1")); + httpContext.Request.Headers.Returns(callinfo => headers); +#endif + renderer.CheckForwardedForHeaderOffset = -3; + + // Act + string result = renderer.Render(new LogEventInfo()); + + // Assert + Assert.Equal("127.0.0.1", result); + + renderer.CheckForwardedForHeaderOffset = -4; + + // Act + result = renderer.Render(new LogEventInfo()); + + // Assert + Assert.Equal("127.0.0.1", result); + } } } \ No newline at end of file