diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index bc52c55d6136..bfb716fa44fb 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -489,13 +489,13 @@ stages: $(_InternalRuntimeDownloadArgs) displayName: Run build.sh - script: git clean -xfd src/**/obj/; - ./dockerbuild.sh bionic --ci --nobl --arch x64 --build-installers --no-build-deps --no-build-nodejs + ./dockerbuild.sh bionic --ci --nobl --arch x64 --build-installers --no-build-deps --no-build-nodejs --init-nuget -p:OnlyPackPlatformSpecificPackages=true -p:BuildRuntimeArchive=false -p:LinuxInstallerType=deb $(_BuildArgs) $(_InternalRuntimeDownloadArgs) displayName: Build Debian installers - script: git clean -xfd src/**/obj/; - ./dockerbuild.sh rhel --ci --nobl --arch x64 --build-installers --no-build-deps --no-build-nodejs + ./dockerbuild.sh rhel --ci --nobl --arch x64 --build-installers --no-build-deps --no-build-nodejs --init-nuget -p:OnlyPackPlatformSpecificPackages=true -p:BuildRuntimeArchive=false -p:LinuxInstallerType=rpm -p:AssetManifestFileName=aspnetcore-Linux_x64.xml $(_BuildArgs) @@ -568,7 +568,7 @@ stages: $(_InternalRuntimeDownloadArgs) displayName: Run build.sh - script: git clean -xfd src/**/obj/; - ./dockerbuild.sh rhel --ci --nobl --arch arm64 --build-installers --no-build-deps --no-build-nodejs + ./dockerbuild.sh rhel --ci --nobl --arch arm64 --build-installers --no-build-deps --no-build-nodejs --init-nuget -p:OnlyPackPlatformSpecificPackages=true -p:BuildRuntimeArchive=false -p:LinuxInstallerType=rpm -p:AssetManifestFileName=aspnetcore-Linux_arm64.xml $(_BuildArgs) diff --git a/NuGet.config b/NuGet.config index 22df3f2caf37..ecf11c3e4032 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,12 +4,8 @@ - - - - @@ -28,12 +24,8 @@ - - - - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 3832a06e38d2..07ae1902e77d 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -302,22 +302,22 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime d099f075e45d2aa6007a22b71b45a08758559f80 - + https://github.com/dotnet/arcade - 3f3c360819c5c092d0e4505a67dfe59a33fba557 + f36ea231c234560514ede4c2747897a737ced28f - + https://github.com/dotnet/arcade - 3f3c360819c5c092d0e4505a67dfe59a33fba557 + f36ea231c234560514ede4c2747897a737ced28f - + https://github.com/dotnet/arcade - 3f3c360819c5c092d0e4505a67dfe59a33fba557 + f36ea231c234560514ede4c2747897a737ced28f - + https://github.com/dotnet/arcade - 3f3c360819c5c092d0e4505a67dfe59a33fba557 + f36ea231c234560514ede4c2747897a737ced28f diff --git a/eng/Versions.props b/eng/Versions.props index 17323361721d..da24219bb97f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -134,8 +134,8 @@ 7.0.1 7.0.1 - 7.0.0-beta.22558.4 - 7.0.0-beta.22558.4 + 7.0.0-beta.22561.2 + 7.0.0-beta.22561.2 7.0.0-alpha.1.22505.1 diff --git a/eng/build.sh b/eng/build.sh index a6b6ba99be44..093b04affdd4 100755 --- a/eng/build.sh +++ b/eng/build.sh @@ -33,6 +33,7 @@ target_arch='x64' configuration='' runtime_source_feed='' runtime_source_feed_key='' +init_nuget=false if [ "$(uname)" = "Darwin" ]; then target_os_name='osx' @@ -82,6 +83,8 @@ Options: --runtime-source-feed Additional feed that can be used when downloading .NET runtimes and SDKs --runtime-source-feed-key Key for feed that can be used when downloading .NET runtimes and SDKs + --init-nuget Run nuget --version. + Description: This build script installs required tools and runs an MSBuild command on this repository This script can be used to invoke various targets, such as targets to produce packages @@ -208,6 +211,9 @@ while [[ $# -gt 0 ]]; do -ci) ci=true ;; + -init-nuget) + init_nuget=true + ;; -binarylog|-bl) binary_log=true ;; @@ -359,6 +365,30 @@ export MSBUILDDEBUGPATH="$log_dir" _tmp_restore=$restore restore=true +if [[ "$init_nuget" == true ]]; then + InitializeBuildTool + + function RunBuildTool { + "$_InitializeBuildTool" "$@" || { + local exit_code=$? + # We should not Write-PipelineTaskError here because that message shows up in the build summary + # The build already logged an error, that's the reason it failed. Producing an error here only adds noise. + echo "Build failed with exit code $exit_code. Check errors above." + if [[ "$ci" == "true" ]]; then + Write-PipelineSetResult -result "Failed" -message "nuget execution failed." + # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error + # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error + ExitWithExitCode 0 + else + ExitWithExitCode $exit_code + fi + } + } + + echo 'Running dotnet nuget --version (issue: https://github.com/NuGet/Home/issues/12159#issuecomment-1278360511)' + RunBuildTool "nuget" "--version" +fi + InitializeToolset restore=$_tmp_restore= diff --git a/global.json b/global.json index 036d4a00133b..0dde90033d58 100644 --- a/global.json +++ b/global.json @@ -27,7 +27,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.22.10", - "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.22558.4", - "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.22558.4" + "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.22561.2", + "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.22561.2" } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index d0fb349f324f..030f2bcba959 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -58,6 +58,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)) @@ -65,32 +66,98 @@ internal Request(RequestContext requestContext) PathBase = string.Empty; Path = string.Empty; } - else + // Prefix may be null if the requested 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 (requestContext.Server.Options.UrlPrefixes.TryMatchLongestPrefix(IsHttps, cookedUrl.GetHost()!, originalPath, out var pathBase, out var path)) + else if (originalPath.Equals(pathBase, StringComparison.OrdinalIgnoreCase)) { + // Preserve the user input casing + PathBase = originalPath; + Path = string.Empty; + } + 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//../bat//path1//path2", reduced to "/base/call/bat//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 0b6b6d5b5eb0..79e1c361380d 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 c79868361e46..defbee588b18 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 e94cd37896cc..a5e7eeee9421 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -143,19 +143,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//../bat//path1//path2", reduced to "/base/call/bat//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 b5ff9c892ac5..44fff105615d 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -27,6 +27,7 @@ internal sealed 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; @@ -66,6 +67,8 @@ ILogger logger _logger = logger; _options = options.Value; _serverAddressesFeature = new ServerAddressesFeature(); + var iisConfigData = NativeMethods.HttpGetApplicationProperties(); + _virtualPath = iisConfigData.pwzVirtualApplicationPath; if (_options.ForwardWindowsAuthentication) { @@ -80,6 +83,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 index 4dd107b88272..97a0f0e3b481 100644 --- a/src/Servers/IIS/IIS/src/Core/IISServerSetupFilter.cs +++ b/src/Servers/IIS/IIS/src/Core/IISServerSetupFilter.cs @@ -10,13 +10,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core; internal sealed class IISServerSetupFilter : IStartupFilter { - private readonly string _virtualPath; - - public IISServerSetupFilter(string virtualPath) - { - _virtualPath = virtualPath; - } - public Action Configure(Action next) { return app => @@ -27,7 +20,6 @@ public Action Configure(Action next) 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 dbaef9307585..8df9cc472aaa 100644 --- a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs +++ b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs @@ -40,7 +40,7 @@ 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.AddSingleton(); 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..252459a7a36f --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IIS.SubApp.config @@ -0,0 +1,742 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..66883e6e2e41 --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IISSubAppSiteCollection.cs @@ -0,0 +1,12 @@ +// 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); +} \ No newline at end of file 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..9633bf22d174 --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/IISSubAppSiteFixture.cs @@ -0,0 +1,27 @@ +// 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..fbc241469acd --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/RequestPathBaseTests.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text; +using Microsoft.AspNetCore.Testing; + +#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 d394828b9ece..1d46e956edf7 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -231,6 +231,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..ffc83b434151 --- /dev/null +++ b/src/Servers/IIS/IntegrationTesting.IIS/src/Http.SubApp.config @@ -0,0 +1,1029 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 e6c3f4276dbd..e5f49181de20 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 @@ +