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 @@
+