diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 880beebf562d..210fef9d652f 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -63,6 +63,7 @@ internal Request(RequestContext requestContext) PathBase = string.Empty; Path = originalPath; + var prefix = requestContext.Server.Options.UrlPrefixes.GetPrefix((int)requestContext.UrlContext); // 'OPTIONS * HTTP/1.1' if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawUrl, "*", StringComparison.Ordinal)) @@ -70,31 +71,96 @@ internal Request(RequestContext requestContext) PathBase = string.Empty; Path = string.Empty; } - else + // Prefix may be null if the request has been transfered to our queue + else if (prefix is not null) { - var prefix = requestContext.Server.Options.UrlPrefixes.GetPrefix((int)requestContext.UrlContext); - // Prefix may be null if the requested has been transfered to our queue - if (!(prefix is null)) + var pathBase = prefix.PathWithoutTrailingSlash; + // url: /base/path, prefix: /base/, base: /base, path: /path + // url: /, prefix: /, base: , path: / + if (originalPath.Equals(pathBase, StringComparison.Ordinal)) { - if (originalPath.Length == prefix.PathWithoutTrailingSlash.Length) - { - // They matched exactly except for the trailing slash. - PathBase = originalPath; - Path = string.Empty; - } - else - { - // url: /base/path, prefix: /base/, base: /base, path: /path - // url: /, prefix: /, base: , path: / - PathBase = originalPath.Substring(0, prefix.PathWithoutTrailingSlash.Length); // Preserve the user input casing - Path = originalPath.Substring(prefix.PathWithoutTrailingSlash.Length); - } + // Exact match, no need to preserve the casing + PathBase = pathBase; + Path = string.Empty; + } + else if (originalPath.Equals(pathBase, StringComparison.OrdinalIgnoreCase)) + { + // Preserve the user input casing + PathBase = originalPath; + Path = string.Empty; } - else if (requestContext.Server.Options.UrlPrefixes.TryMatchLongestPrefix(IsHttps, cookedUrl.GetHost()!, originalPath, out var pathBase, out var path)) + else if (originalPath.StartsWith(prefix.Path, StringComparison.Ordinal)) { + // Exact match, no need to preserve the casing PathBase = pathBase; - Path = path; + Path = originalPath[pathBase.Length..]; } + else if (originalPath.StartsWith(prefix.Path, StringComparison.OrdinalIgnoreCase)) + { + // Preserve the user input casing + PathBase = originalPath[..pathBase.Length]; + Path = originalPath[pathBase.Length..]; + } + else + { + // Http.Sys path base matching is based on the cooked url which applies some non-standard normalizations that we don't use + // like collapsing duplicate slashes "//", converting '\' to '/', and un-escaping "%2F" to '/'. Find the right split and + // ignore the normalizations. + var originalOffset = 0; + var baseOffset = 0; + while (originalOffset < originalPath.Length && baseOffset < pathBase.Length) + { + var baseValue = pathBase[baseOffset]; + var offsetValue = originalPath[originalOffset]; + if (baseValue == offsetValue + || char.ToUpperInvariant(baseValue) == char.ToUpperInvariant(offsetValue)) + { + // case-insensitive match, continue + originalOffset++; + baseOffset++; + } + else if (baseValue == '/' && offsetValue == '\\') + { + // Http.Sys considers these equivalent + originalOffset++; + baseOffset++; + } + else if (baseValue == '/' && originalPath.AsSpan(originalOffset).StartsWith("%2F", StringComparison.OrdinalIgnoreCase)) + { + // Http.Sys un-escapes this + originalOffset += 3; + baseOffset++; + } + else if (baseOffset > 0 && pathBase[baseOffset - 1] == '/' + && (offsetValue == '/' || offsetValue == '\\')) + { + // Duplicate slash, skip + originalOffset++; + } + else if (baseOffset > 0 && pathBase[baseOffset - 1] == '/' + && originalPath.AsSpan(originalOffset).StartsWith("%2F", StringComparison.OrdinalIgnoreCase)) + { + // Duplicate slash equivalent, skip + originalOffset += 3; + } + else + { + // Mismatch, fall back + // The failing test case here is "/base/call//../ball//path1//path2", reduced to "/base/call/ball//path1//path2", + // where http.sys collapses "//" before "../", but we do "../" first. We've lost the context that there were dot segments, + // or duplicate slashes, how do we figure out that "call/" can be eliminated? + originalOffset = 0; + break; + } + } + PathBase = originalPath[..originalOffset]; + Path = originalPath[originalOffset..]; + } + } + else if (requestContext.Server.Options.UrlPrefixes.TryMatchLongestPrefix(IsHttps, cookedUrl.GetHost()!, originalPath, out var pathBase, out var path)) + { + PathBase = pathBase; + Path = path; } ProtocolVersion = RequestContext.GetVersion(); diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/RequestTests.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/RequestTests.cs index 211cae502d47..be57b06b5c3d 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Listener/RequestTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/RequestTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -138,6 +138,42 @@ public async Task Request_OverlongUTF8Path(string requestPath, string expectedPa } } + [ConditionalTheory] + [InlineData("/", "/", "", "/")] + [InlineData("/base", "/base", "/base", "")] + [InlineData("/base", "/baSe", "/baSe", "")] + [InlineData("/base", "/base/path", "/base", "/path")] + [InlineData("/base", "///base/path1/path2", "///base", "/path1/path2")] + [InlineData("/base/ball", @"/baSe\ball//path1//path2", @"/baSe\ball", "//path1//path2")] + [InlineData("/base/ball", @"/base%2fball//path1//path2", @"/base%2fball", "//path1//path2")] + [InlineData("/base/ball", @"/base%2Fball//path1//path2", @"/base%2Fball", "//path1//path2")] + [InlineData("/base/ball", @"/base%5cball//path1//path2", @"/base\ball", "//path1//path2")] + [InlineData("/base/ball", @"/base%5Cball//path1//path2", @"/base\ball", "//path1//path2")] + [InlineData("/base/ball", "///baSe//ball//path1//path2", "///baSe//ball", "//path1//path2")] + [InlineData("/base/ball", @"/base/\ball//path1//path2", @"/base/\ball", "//path1//path2")] + [InlineData("/base/ball", @"/base/%2fball//path1//path2", @"/base/%2fball", "//path1//path2")] + [InlineData("/base/ball", @"/base/%2Fball//path1//path2", @"/base/%2Fball", "//path1//path2")] + [InlineData("/base/ball", @"/base/%5cball//path1//path2", @"/base/\ball", "//path1//path2")] + [InlineData("/base/ball", @"/base/%5Cball//path1//path2", @"/base/\ball", "//path1//path2")] + [InlineData("/base/ball", @"/base/call/../ball//path1//path2", @"/base/ball", "//path1//path2")] + // The results should be "/base/ball", "//path1//path2", but Http.Sys collapses the "//" before the "../" + // and we don't have a good way of emulating that. + [InlineData("/base/ball", @"/base/call//../ball//path1//path2", @"", "/base/call/ball//path1//path2")] + [InlineData("/base/ball", @"/base/call/.%2e/ball//path1//path2", @"/base/ball", "//path1//path2")] + [InlineData("/base/ball", @"/base/call/.%2E/ball//path1//path2", @"/base/ball", "//path1//path2")] + public async Task Request_WithPathBase(string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + using var server = Utilities.CreateHttpServerReturnRoot(pathBase, out var root); + var responseTask = SendSocketRequestAsync(root, requestPath); + var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal(expectedPathBase, context.Request.PathBase); + Assert.Equal(expectedPath, context.Request.Path); + context.Dispose(); + + var response = await responseTask; + Assert.Equal("200", response.Substring(9)); + } + private async Task SendSocketRequestAsync(string address, string path, string method = "GET") { var uri = new Uri(address); diff --git a/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs b/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs index b40df9a02f41..605298947fe9 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs @@ -208,6 +208,7 @@ public async Task Request_FieldsCanBeSetToNull_Set() [InlineData("/base path/", "/base%20path/sub%20path", "/base path", "/sub path")] [InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")] [InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")] + [InlineData("/base", "///base/path1/path2", "///base", "/path1/path2")] public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath) { string root; diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index 8405bbbbede7..4a4f77a84669 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -149,19 +149,105 @@ protected void InitializeContext() KnownMethod = VerbId; StatusCode = 200; - var originalPath = GetOriginalPath(); + var originalPath = GetOriginalPath() ?? string.Empty; + var pathBase = _server.VirtualPath ?? string.Empty; + if (pathBase.Length > 1 && pathBase[^1] == '/') + { + pathBase = pathBase[..^1]; + } if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal)) { PathBase = string.Empty; Path = string.Empty; } - else + else if (string.IsNullOrEmpty(pathBase) || pathBase == "/") { - // Path and pathbase are unescaped by RequestUriBuilder - // The UsePathBase middleware will modify the pathbase and path correctly PathBase = string.Empty; - Path = originalPath ?? string.Empty; + Path = originalPath; + } + else if (originalPath.Equals(pathBase, StringComparison.Ordinal)) + { + // Exact match, no need to preserve the casing + PathBase = pathBase; + Path = string.Empty; + } + else if (originalPath.Equals(pathBase, StringComparison.OrdinalIgnoreCase)) + { + // Preserve the user input casing + PathBase = originalPath; + Path = string.Empty; + } + else if (originalPath.Length == pathBase.Length + 1 + && originalPath[^1] == '/' + && originalPath.StartsWith(pathBase, StringComparison.Ordinal)) + { + // Exact match, no need to preserve the casing + PathBase = pathBase; + Path = "/"; + } + else if (originalPath.Length == pathBase.Length + 1 + && originalPath[^1] == '/' + && originalPath.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + { + // Preserve the user input casing + PathBase = originalPath[..pathBase.Length]; + Path = "/"; + } + else + { + // Http.Sys path base matching is based on the cooked url which applies some non-standard normalizations that we don't use + // like collapsing duplicate slashes "//", converting '\' to '/', and un-escaping "%2F" to '/'. Find the right split and + // ignore the normalizations. + var originalOffset = 0; + var baseOffset = 0; + while (originalOffset < originalPath.Length && baseOffset < pathBase.Length) + { + var baseValue = pathBase[baseOffset]; + var offsetValue = originalPath[originalOffset]; + if (baseValue == offsetValue + || char.ToUpperInvariant(baseValue) == char.ToUpperInvariant(offsetValue)) + { + // case-insensitive match, continue + originalOffset++; + baseOffset++; + } + else if (baseValue == '/' && offsetValue == '\\') + { + // Http.Sys considers these equivalent + originalOffset++; + baseOffset++; + } + else if (baseValue == '/' && originalPath.AsSpan(originalOffset).StartsWith("%2F", StringComparison.OrdinalIgnoreCase)) + { + // Http.Sys un-escapes this + originalOffset += 3; + baseOffset++; + } + else if (baseOffset > 0 && pathBase[baseOffset - 1] == '/' + && (offsetValue == '/' || offsetValue == '\\')) + { + // Duplicate slash, skip + originalOffset++; + } + else if (baseOffset > 0 && pathBase[baseOffset - 1] == '/' + && originalPath.AsSpan(originalOffset).StartsWith("%2F", StringComparison.OrdinalIgnoreCase)) + { + // Duplicate slash equivalent, skip + originalOffset += 3; + } + else + { + // Mismatch, fall back + // The failing test case here is "/base/call//../ball//path1//path2", reduced to "/base/call/ball//path1//path2", + // where http.sys collapses "//" before "../", but we do "../" first. We've lost the context that there were dot segments, + // or duplicate slashes, how do we figure out that "call/" can be eliminated? + originalOffset = 0; + break; + } + } + PathBase = originalPath[..originalOffset]; + Path = originalPath[originalOffset..]; } var cookedUrl = GetCookedUrl(); diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs index 4075bd897fed..e5a8c735b0f2 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -30,6 +30,7 @@ internal class IISHttpServer : IServer private readonly IISServerOptions _options; private readonly IISNativeApplication _nativeApplication; private readonly ServerAddressesFeature _serverAddressesFeature; + private readonly string? _virtualPath; private readonly TaskCompletionSource _shutdownSignal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private bool? _websocketAvailable; @@ -69,6 +70,8 @@ ILogger logger _logger = logger; _options = options.Value; _serverAddressesFeature = new ServerAddressesFeature(); + var iisConfigData = NativeMethods.HttpGetApplicationProperties(); + _virtualPath = iisConfigData.pwzVirtualApplicationPath; if (_options.ForwardWindowsAuthentication) { @@ -83,6 +86,8 @@ ILogger logger } } + public string? VirtualPath => _virtualPath; + public unsafe Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext : notnull { _httpServerHandle = GCHandle.Alloc(this); diff --git a/src/Servers/IIS/IIS/src/Core/IISServerSetupFilter.cs b/src/Servers/IIS/IIS/src/Core/IISServerSetupFilter.cs deleted file mode 100644 index 86782a4d39ac..000000000000 --- a/src/Servers/IIS/IIS/src/Core/IISServerSetupFilter.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Server.IIS.Core -{ - internal class IISServerSetupFilter : IStartupFilter - { - private readonly string _virtualPath; - - public IISServerSetupFilter(string virtualPath) - { - _virtualPath = virtualPath; - } - - public Action Configure(Action next) - { - return app => - { - var server = app.ApplicationServices.GetService(); - if (server?.GetType() != typeof(IISHttpServer)) - { - throw new InvalidOperationException("Application is running inside IIS process but is not configured to use IIS server."); - } - - app.UsePathBase(_virtualPath); - next(app); - }; - } - } -} diff --git a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs index 1d0bd3894fca..0e854519b0a5 100644 --- a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs +++ b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs @@ -42,7 +42,6 @@ public static IWebHostBuilder UseIIS(this IWebHostBuilder hostBuilder) services.AddSingleton(new IISNativeApplication(new NativeSafeHandle(iisConfigData.pNativeApplication))); services.AddSingleton(); services.AddTransient(); - services.AddSingleton(new IISServerSetupFilter(iisConfigData.pwzVirtualApplicationPath)); services.AddAuthenticationCore(); services.AddSingleton(_ => new ServerIntegratedAuth() { diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IIS.SubApp.config b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IIS.SubApp.config new file mode 100644 index 000000000000..a4a5587cc9cc --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IIS.SubApp.config @@ -0,0 +1,742 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IISSubAppSiteCollection.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IISSubAppSiteCollection.cs new file mode 100644 index 000000000000..3d7769d37432 --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IISSubAppSiteCollection.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + [CollectionDefinition(Name)] + public class IISSubAppSiteCollection : ICollectionFixture + { + public const string Name = nameof(IISSubAppSiteCollection); + } +} diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IISSubAppSiteFixture.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IISSubAppSiteFixture.cs new file mode 100644 index 000000000000..4e8a5fdec60c --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IISSubAppSiteFixture.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + public class IISSubAppSiteFixture : IISTestSiteFixture + { + public IISSubAppSiteFixture() : base(Configure) + { + } + + private static void Configure(IISDeploymentParameters deploymentParameters) + { + if (deploymentParameters.ServerType == IntegrationTesting.ServerType.IIS) + { + deploymentParameters.ServerConfigTemplateContent = File.ReadAllText("IIS.SubApp.Config"); + } + else // IIS Express + { + using var stream = typeof(IISExpressDeployer).Assembly.GetManifestResourceStream("Microsoft.AspNetCore.Server.IntegrationTesting.IIS.Http.SubApp.config"); + using var reader = new StreamReader(stream); + deploymentParameters.ServerConfigTemplateContent = reader.ReadToEnd(); + } + } + } +} diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/RequestPathBaseTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/RequestPathBaseTests.cs new file mode 100644 index 000000000000..626e1eb1ab23 --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/RequestPathBaseTests.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing; +using Xunit; + +#if !IIS_FUNCTIONALS +using Microsoft.AspNetCore.Server.IIS.FunctionalTests; + +#if IISEXPRESS_FUNCTIONALS +namespace Microsoft.AspNetCore.Server.IIS.IISExpress.FunctionalTests +#elif NEWHANDLER_FUNCTIONALS +namespace Microsoft.AspNetCore.Server.IIS.NewHandler.FunctionalTests +#elif NEWSHIM_FUNCTIONALS +namespace Microsoft.AspNetCore.Server.IIS.NewShim.FunctionalTests +#endif + +#else +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +#endif +{ + [Collection(IISSubAppSiteCollection.Name)] + public class RequestPathBaseTests : FixtureLoggedTest + { + private readonly IISSubAppSiteFixture _fixture; + + public RequestPathBaseTests(IISSubAppSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [RequiresNewHandler] + [InlineData("/Sub/App/PathAndPathBase", "/Sub/App/PathAndPathBase", "")] + [InlineData("/SUb/APp/PathAndPAthBase", "/SUb/APp/PathAndPAthBase", "")] + [InlineData(@"/Sub\App/PathAndPathBase/", @"/Sub\App/PathAndPathBase", "/")] + [InlineData("/Sub%2FApp/PathAndPathBase/", "/Sub%2FApp/PathAndPathBase", "/")] + [InlineData("/Sub%2fApp/PathAndPathBase/", "/Sub%2fApp/PathAndPathBase", "/")] + [InlineData("/Sub%5cApp/PathAndPathBase/", @"/Sub\App/PathAndPathBase", "/")] + [InlineData("/Sub%5CApp/PathAndPathBase/", @"/Sub\App/PathAndPathBase", "/")] + [InlineData("/Sub/App/PathAndPathBase/Path", "/Sub/App/PathAndPathBase", "/Path")] + [InlineData("/Sub/App/PathANDPathBase/PATH", "/Sub/App/PathANDPathBase", "/PATH")] + public async Task RequestPathBase_Split(string url, string expectedPathBase, string expectedPath) + { + // The test app trims the test name off of the request path and puts it on the PathBase. + // /AppName/TestName/Path + var (status, body) = await SendSocketRequestAsync(url); + Assert.Equal(200, status); + Assert.Equal($"PathBase: {expectedPathBase}; Path: {expectedPath}", body); + } + + [ConditionalTheory] + [RequiresNewHandler] + [InlineData("//Sub/App/PathAndPathBase", "//Sub/App/PathAndPathBase", "")] + [InlineData(@"/\Sub/App/PathAndPathBase/", @"/\Sub/App/PathAndPathBase", "/")] + [InlineData(@"/Sub/\App/PathAndPathBase//path", @"/Sub/\App/PathAndPathBase", "//path")] + [InlineData("/%2FSub/App/PathAndPathBase/", "/%2FSub/App/PathAndPathBase", "/")] + [InlineData("/%5CSub/App/PathAndPathBase/", @"/\Sub/App/PathAndPathBase", "/")] + [InlineData("///Sub/App/PathAndPathBase/path1/path2", "///Sub/App/PathAndPathBase", "/path1/path2")] + [InlineData("/Sub%2F/App/PathAndPathBase/%2FPath", "/Sub%2F/App/PathAndPathBase", "/%2FPath")] + [InlineData(@"/%2F\/Sub/App/PathAndPathBase/Path", @"/%2F\/Sub/App/PathAndPathBase", "/Path")] + [InlineData(@"/Sub/App/PathANDPathBase/PATH", @"/Sub/App/PathANDPathBase", "/PATH")] + [InlineData("/Sub/%5cApp/PathAndPathBase/", @"/Sub/\App/PathAndPathBase", "/")] + [InlineData("//Sub//App/PathAndPathBase//Path", "//Sub//App/PathAndPathBase", "//Path")] + [InlineData(@"/Sub/ball/../App/PathAndPathBase/path1//path2", @"/Sub/App/PathAndPathBase", "/path1//path2")] + [InlineData(@"/Sub//ball/../App/PathAndPathBase/path1//path2", @"/Sub//App/PathAndPathBase", "/path1//path2")] + // The results should be "/Sub//App/PathAndPathBase", "//path1//path2", but Http.Sys collapses the "//" before the "../" + // and we don't have a good way of emulating that. + // [InlineData(@"/Sub/call//../App/PathAndPathBase//path1//path2", @"", "/Sub/call/App/PathAndPathBase//path1//path2")] + [InlineData(@"/Sub/call/.%2e/App/PathAndPathBase//path1//path2", @"/Sub/App/PathAndPathBase", "//path1//path2")] + [InlineData(@"/Sub/call/.%2E/App/PathAndPathBase//path1//path2", @"/Sub/App/PathAndPathBase", "//path1//path2")] + public async Task RequestPathBase_WithDoubleSlashes_Split(string url, string expectedPathBase, string expectedPath) + { + // The test app trims the test name off of the request path and puts it on the PathBase. + // /AppName/TestName/Path + var (status, body) = await SendSocketRequestAsync(url); + Assert.Equal(200, status); + Assert.Equal($"PathBase: {expectedPathBase}; Path: {expectedPath}", body); + } + + + private async Task<(int Status, string Body)> SendSocketRequestAsync(string path) + { + using (var connection = _fixture.CreateTestConnection()) + { + await connection.Send( + "GET " + path + " HTTP/1.1", + "Host: " + _fixture.Client.BaseAddress.Authority, + "", + ""); + var headers = await connection.ReceiveHeaders(); + var status = int.Parse(headers[0].Substring(9, 3), CultureInfo.InvariantCulture); + if (headers.Contains("Transfer-Encoding: chunked")) + { + var bytes0 = await connection.ReceiveChunk(); + return (status, Encoding.UTF8.GetString(bytes0.Span)); + } + var length = int.Parse(headers.Single(h => h.StartsWith("Content-Length: ", StringComparison.Ordinal))["Content-Length: ".Length..], CultureInfo.InvariantCulture); + var bytes1 = await connection.Receive(length); + return (status, Encoding.ASCII.GetString(bytes1.Span)); + } + } + } +} diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs index 2e3a90029527..b8cc6d483008 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -240,6 +240,11 @@ private async Task AuthenticationRestrictedNTLM(HttpContext ctx) } } + private Task PathAndPathBase(HttpContext ctx) + { + return ctx.Response.WriteAsync($"PathBase: {ctx.Request.PathBase.Value}; Path: {ctx.Request.Path.Value}"); + } + private async Task FeatureCollectionSetRequestFeatures(HttpContext ctx) { try diff --git a/src/Servers/IIS/IntegrationTesting.IIS/src/Http.SubApp.config b/src/Servers/IIS/IntegrationTesting.IIS/src/Http.SubApp.config new file mode 100644 index 000000000000..15db41e15d16 --- /dev/null +++ b/src/Servers/IIS/IntegrationTesting.IIS/src/Http.SubApp.config @@ -0,0 +1,1029 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Servers/IIS/IntegrationTesting.IIS/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj b/src/Servers/IIS/IntegrationTesting.IIS/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj index ed716f84d993..c5356da9bb55 100644 --- a/src/Servers/IIS/IntegrationTesting.IIS/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj +++ b/src/Servers/IIS/IntegrationTesting.IIS/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj @@ -26,6 +26,7 @@ +