From 19a70d270ced9af992bbe3b33993ee0bcb4663f2 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Wed, 19 Jul 2023 08:52:58 -0700 Subject: [PATCH 1/3] Adding RestoreRawRequestPath middlware so that the raw request path to the request context. This is an opt-in feature customers needs to explicitly opt-in. --- .../RestoreRawRequestPathMiddleware.cs | 40 +++++++++++ .../WebJobsApplicationBuilderExtension.cs | 14 ++++ .../Environment/EnvironmentSettingNames.cs | 3 + .../RestoreRawRequestPathMiddlewareTests.cs | 67 +++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 src/WebJobs.Script.WebHost/Middleware/RestoreRawRequestPathMiddleware.cs create mode 100644 test/WebJobs.Script.Tests/Middleware/RestoreRawRequestPathMiddlewareTests.cs diff --git a/src/WebJobs.Script.WebHost/Middleware/RestoreRawRequestPathMiddleware.cs b/src/WebJobs.Script.WebHost/Middleware/RestoreRawRequestPathMiddleware.cs new file mode 100644 index 0000000000..9c3e255301 --- /dev/null +++ b/src/WebJobs.Script.WebHost/Middleware/RestoreRawRequestPathMiddleware.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Azure.WebJobs.Script.WebHost.Middleware +{ + /// + /// Middleware to restore the raw request URL path received, in the current request URL. + /// App service front end decodes encoded strings in the request URL path. + /// This causes routing to fail if the route param value has %2F in it. + /// Refer below issues for more context: + /// https://github.com/dotnet/aspnetcore/issues/40532#issuecomment-1083562919 + /// https://github.com/Azure/azure-functions-host/issues/9290. + /// + internal class RestoreRawRequestPathMiddleware + { + private readonly RequestDelegate _next; + internal const string UnEncodedUrlPathHeaderName = "X-Waws-Unencoded-Url"; + + public RestoreRawRequestPathMiddleware(RequestDelegate next) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } + + public async Task Invoke(HttpContext context) + { + if (context.Request.Headers.TryGetValue(UnEncodedUrlPathHeaderName, out var unencodedUrlValue) && + unencodedUrlValue.Count > 0) + { + context.Request.Path = new PathString(unencodedUrlValue.First()); + } + + await _next(context); + } + } +} \ No newline at end of file diff --git a/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs b/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs index db7ff7763e..a568bec28d 100644 --- a/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs +++ b/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs @@ -35,6 +35,20 @@ public static IApplicationBuilder UseWebJobsScriptHost(this IApplicationBuilder // Ensure the ClrOptimizationMiddleware is registered before all middleware builder.UseMiddleware(); + + // Update the request URL path(which was altered by App service FE) to raw request path + // only if customer has opted in using the app setting. + builder.UseWhen( + _ => string.Equals( + environment.GetEnvironmentVariable(EnvironmentSettingNames.RestoreRawRequestPathEnabled), "1", + StringComparison.Ordinal), config => + { + config.UseMiddleware(); + // We need to re-add routing middleware after any updates to request path. + config.UseRouting(); + config.UseEndpoints(endpoints => endpoints.MapControllers()); + }); + builder.UseMiddleware(); builder.UseMiddleware(); builder.UseMiddleware(); diff --git a/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs b/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs index 4d7d416932..d40d0bc636 100644 --- a/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs +++ b/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs @@ -138,5 +138,8 @@ public static class EnvironmentSettingNames public const string AppKind = "APP_KIND"; public const string DrainOnApplicationStopping = "FUNCTIONS_ENABLE_DRAIN_ON_APP_STOPPING"; + + // Restore the raw request path from wire to the current request. + public const string RestoreRawRequestPathEnabled = "WEBSITE_RESTORE_RAW_REQUEST_PATH"; } } diff --git a/test/WebJobs.Script.Tests/Middleware/RestoreRawRequestPathMiddlewareTests.cs b/test/WebJobs.Script.Tests/Middleware/RestoreRawRequestPathMiddlewareTests.cs new file mode 100644 index 0000000000..c719758d67 --- /dev/null +++ b/test/WebJobs.Script.Tests/Middleware/RestoreRawRequestPathMiddlewareTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Script.WebHost.Middleware; +using Moq; +using Xunit; + +public class RestoreRawRequestPathMiddlewareTests +{ + [Fact] + public async Task Invoke_WithUnencodedHeaderValue_SetsRequestPath() + { + // Arrange + const string unEncodedHeaderValue = "/cloud%2Fdevdiv"; + const string requestPathValue = "/cloud/devdiv"; + var context = CreateHttpContextWithHeader(requestPathValue, unEncodedHeaderValue); + var mockNext = new Mock(); + var middleware = new RestoreRawRequestPathMiddleware(mockNext.Object); + var originalPath = context.Request.Path; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.NotEqual(new PathString(originalPath), context.Request.Path); + Assert.Equal(new PathString(unEncodedHeaderValue), context.Request.Path); + mockNext.Verify(next => next(context), Times.Once()); + } + + [Fact] + public async Task Invoke_WithoutUnencodedUrlHeader_DoesNotChangeRequestPath() + { + // Arrange + const string requestPathValue = "/cloud/devdiv"; + var context = CreateHttpContextWithHeader(requestPathValue); + var mockNext = new Mock(); + var middleware = new RestoreRawRequestPathMiddleware(mockNext.Object); + var originalPath = context.Request.Path; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(originalPath, context.Request.Path); + mockNext.Verify(next => next(context), Times.Once()); + } + + private static HttpContext CreateHttpContextWithHeader(string requestPathValue, string unEncodedHeaderValue = null) + { + var context = new DefaultHttpContext + { + Request = + { + Path = requestPathValue + } + }; + + if (unEncodedHeaderValue is not null) + { + context.Request.Headers[RestoreRawRequestPathMiddleware.UnEncodedUrlPathHeaderName] = unEncodedHeaderValue; + } + + return context; + } +} From 7e9923f832993355a7b1d6e1b3d30a1d27492e90 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Wed, 19 Jul 2023 09:24:29 -0700 Subject: [PATCH 2/3] Updated release note. --- release_notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release_notes.md b/release_notes.md index 33b74629b8..d4d38a4eb7 100644 --- a/release_notes.md +++ b/release_notes.md @@ -16,3 +16,4 @@ - Microsoft.IdentityModel.JsonWebTokens - Microsoft.IdentityModel.Logging - Updated Grpc.AspNetCore package to 2.55.0 (https://github.com/Azure/azure-functions-host/pull/9373) +- Added RestoreRawRequestPath middleware to restore the raw un-decoded request path value to the current request (https://github.com/Azure/azure-functions-host/pull/9402) From d5c1c51a8c6bfac024b9bd314dab9586d54883c8 Mon Sep 17 00:00:00 2001 From: Lilian Kasem Date: Tue, 8 Aug 2023 12:13:21 -0700 Subject: [PATCH 3/3] Update src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs Co-authored-by: sarah <35204912+satvu@users.noreply.github.com> --- .../WebJobsApplicationBuilderExtension.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs b/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs index a568bec28d..a1cc909bc3 100644 --- a/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs +++ b/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs @@ -36,7 +36,7 @@ public static IApplicationBuilder UseWebJobsScriptHost(this IApplicationBuilder // Ensure the ClrOptimizationMiddleware is registered before all middleware builder.UseMiddleware(); - // Update the request URL path(which was altered by App service FE) to raw request path + // Update the request URL path (which was altered by App service FE) to raw request path // only if customer has opted in using the app setting. builder.UseWhen( _ => string.Equals(