Merge branch 'master' into merge/release/3.0-to-master

This commit is contained in:
Pranav K 2019-08-13 13:03:17 -07:00 committed by GitHub
commit 44cfe01a89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 377 additions and 155 deletions

2
.github/CODEOWNERS vendored
View File

@ -14,7 +14,7 @@
/src/Hosting/ @tratcher @anurse /src/Hosting/ @tratcher @anurse
/src/Http/ @tratcher @jkotalik @anurse /src/Http/ @tratcher @jkotalik @anurse
/src/Middleware/ @tratcher @anurse /src/Middleware/ @tratcher @anurse
/src/ProjectTemplates/ @ryanbrandenburg # /src/ProjectTemplates/ @ryanbrandenburg
/src/Security/ @tratcher @anurse /src/Security/ @tratcher @anurse
/src/Servers/ @tratcher @jkotalik @anurse @halter73 /src/Servers/ @tratcher @jkotalik @anurse @halter73
/src/Middleware/Rewrite @jkotalik @anurse /src/Middleware/Rewrite @jkotalik @anurse

View File

@ -43,7 +43,7 @@
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<DefaultNetCoreTargetFramework>netcoreapp$(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion)</DefaultNetCoreTargetFramework> <DefaultNetCoreTargetFramework>netcoreapp$(TFMNetCoreMajorVersion).$(TFMNetCoreMinorVersion)</DefaultNetCoreTargetFramework>
</PropertyGroup> </PropertyGroup>
<!-- Warnings and errors --> <!-- Warnings and errors -->

View File

@ -9,25 +9,25 @@
--> -->
<Dependencies> <Dependencies>
<ProductDependencies> <ProductDependencies>
<Dependency Name="Microsoft.AspNetCore.Blazor.Mono" Version="3.0.0-preview9.19405.1"> <Dependency Name="Microsoft.AspNetCore.Blazor.Mono" Version="5.0.0-alpha1.19405.2">
<Uri>https://github.com/aspnet/Blazor</Uri> <Uri>https://github.com/aspnet/Blazor</Uri>
<Sha>498a843f7854f9a2a5820e55cd2c7d7654429682</Sha> <Sha>b2c48dd8c9099f71908fac26089cbea2c76d06a1</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.AspNetCore.Razor.Language" Version="3.0.0-preview9.19412.2"> <Dependency Name="Microsoft.AspNetCore.Razor.Language" Version="5.0.0-alpha1.19407.1">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri> <Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
<Sha>3e788675fd3dab5fddd7b9aba9225e5b5ebbd946</Sha> <Sha>448a88e86d20fd9315901f663318d64c9c6841bf</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="3.0.0-preview9.19412.2"> <Dependency Name="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="5.0.0-alpha1.19407.1">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri> <Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
<Sha>3e788675fd3dab5fddd7b9aba9225e5b5ebbd946</Sha> <Sha>448a88e86d20fd9315901f663318d64c9c6841bf</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.CodeAnalysis.Razor" Version="3.0.0-preview9.19412.2"> <Dependency Name="Microsoft.CodeAnalysis.Razor" Version="5.0.0-alpha1.19407.1">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri> <Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
<Sha>3e788675fd3dab5fddd7b9aba9225e5b5ebbd946</Sha> <Sha>448a88e86d20fd9315901f663318d64c9c6841bf</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.NET.Sdk.Razor" Version="3.0.0-preview9.19412.2"> <Dependency Name="Microsoft.NET.Sdk.Razor" Version="5.0.0-alpha1.19407.1">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri> <Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
<Sha>3e788675fd3dab5fddd7b9aba9225e5b5ebbd946</Sha> <Sha>448a88e86d20fd9315901f663318d64c9c6841bf</Sha>
</Dependency> </Dependency>
<Dependency Name="dotnet-ef" Version="3.0.0-preview9.19412.3"> <Dependency Name="dotnet-ef" Version="3.0.0-preview9.19412.3">
<Uri>https://github.com/aspnet/EntityFrameworkCore</Uri> <Uri>https://github.com/aspnet/EntityFrameworkCore</Uri>

View File

@ -6,12 +6,12 @@
--> -->
<Project> <Project>
<PropertyGroup Label="Version settings"> <PropertyGroup Label="Version settings">
<AspNetCoreMajorVersion>3</AspNetCoreMajorVersion> <AspNetCoreMajorVersion>5</AspNetCoreMajorVersion>
<AspNetCoreMinorVersion>0</AspNetCoreMinorVersion> <AspNetCoreMinorVersion>0</AspNetCoreMinorVersion>
<AspNetCorePatchVersion>0</AspNetCorePatchVersion> <AspNetCorePatchVersion>0</AspNetCorePatchVersion>
<PreReleasePreviewNumber>9</PreReleasePreviewNumber> <PreReleasePreviewNumber>1</PreReleasePreviewNumber>
<PreReleaseVersionLabel>preview$(PreReleasePreviewNumber)</PreReleaseVersionLabel> <PreReleaseVersionLabel>alpha$(PreReleasePreviewNumber)</PreReleaseVersionLabel>
<PreReleaseBrandingLabel>Preview $(PreReleasePreviewNumber)</PreReleaseBrandingLabel> <PreReleaseBrandingLabel>Alpha $(PreReleasePreviewNumber)</PreReleaseBrandingLabel>
<AspNetCoreMajorMinorVersion>$(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion)</AspNetCoreMajorMinorVersion> <AspNetCoreMajorMinorVersion>$(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion)</AspNetCoreMajorMinorVersion>
<!-- Additional assembly attributes are already configured to include the source revision ID. --> <!-- Additional assembly attributes are already configured to include the source revision ID. -->
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
@ -38,6 +38,11 @@
<!-- Disable XLIFF tasks --> <!-- Disable XLIFF tasks -->
<UsingToolXliff>false</UsingToolXliff> <UsingToolXliff>false</UsingToolXliff>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Version workaround">
<!-- Updating our branding to 5.0 but we'll still target netcoreapp3.0 for now. -->
<TFMNetCoreMajorVersion>3</TFMNetCoreMajorVersion>
<TFMNetCoreMinorVersion>0</TFMNetCoreMinorVersion>
</PropertyGroup>
<!-- <!--
These versions should ONLY be updated by automation. These versions should ONLY be updated by automation.
@ -85,7 +90,7 @@
<!-- Only listed explicitly to workaround https://github.com/dotnet/cli/issues/10528 --> <!-- Only listed explicitly to workaround https://github.com/dotnet/cli/issues/10528 -->
<MicrosoftNETCorePlatformsPackageVersion>3.0.0-preview9.19409.17</MicrosoftNETCorePlatformsPackageVersion> <MicrosoftNETCorePlatformsPackageVersion>3.0.0-preview9.19409.17</MicrosoftNETCorePlatformsPackageVersion>
<!-- Packages from aspnet/Blazor --> <!-- Packages from aspnet/Blazor -->
<MicrosoftAspNetCoreBlazorMonoPackageVersion>3.0.0-preview9.19405.1</MicrosoftAspNetCoreBlazorMonoPackageVersion> <MicrosoftAspNetCoreBlazorMonoPackageVersion>5.0.0-alpha1.19405.2</MicrosoftAspNetCoreBlazorMonoPackageVersion>
<!-- Packages from aspnet/Extensions --> <!-- Packages from aspnet/Extensions -->
<InternalAspNetCoreAnalyzersPackageVersion>3.0.0-preview9.19411.2</InternalAspNetCoreAnalyzersPackageVersion> <InternalAspNetCoreAnalyzersPackageVersion>3.0.0-preview9.19411.2</InternalAspNetCoreAnalyzersPackageVersion>
<MicrosoftAspNetCoreAnalyzerTestingPackageVersion>3.0.0-preview9.19411.2</MicrosoftAspNetCoreAnalyzerTestingPackageVersion> <MicrosoftAspNetCoreAnalyzerTestingPackageVersion>3.0.0-preview9.19411.2</MicrosoftAspNetCoreAnalyzerTestingPackageVersion>
@ -157,10 +162,10 @@
<MicrosoftEntityFrameworkCoreToolsPackageVersion>3.0.0-preview9.19412.3</MicrosoftEntityFrameworkCoreToolsPackageVersion> <MicrosoftEntityFrameworkCoreToolsPackageVersion>3.0.0-preview9.19412.3</MicrosoftEntityFrameworkCoreToolsPackageVersion>
<MicrosoftEntityFrameworkCorePackageVersion>3.0.0-preview9.19412.3</MicrosoftEntityFrameworkCorePackageVersion> <MicrosoftEntityFrameworkCorePackageVersion>3.0.0-preview9.19412.3</MicrosoftEntityFrameworkCorePackageVersion>
<!-- Packages from aspnet/AspNetCore-Tooling --> <!-- Packages from aspnet/AspNetCore-Tooling -->
<MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>3.0.0-preview9.19412.2</MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion> <MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>5.0.0-alpha1.19407.1</MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>
<MicrosoftAspNetCoreRazorLanguagePackageVersion>3.0.0-preview9.19412.2</MicrosoftAspNetCoreRazorLanguagePackageVersion> <MicrosoftAspNetCoreRazorLanguagePackageVersion>5.0.0-alpha1.19407.1</MicrosoftAspNetCoreRazorLanguagePackageVersion>
<MicrosoftCodeAnalysisRazorPackageVersion>3.0.0-preview9.19412.2</MicrosoftCodeAnalysisRazorPackageVersion> <MicrosoftCodeAnalysisRazorPackageVersion>5.0.0-alpha1.19407.1</MicrosoftCodeAnalysisRazorPackageVersion>
<MicrosoftNETSdkRazorPackageVersion>3.0.0-preview9.19412.2</MicrosoftNETSdkRazorPackageVersion> <MicrosoftNETSdkRazorPackageVersion>5.0.0-alpha1.19407.1</MicrosoftNETSdkRazorPackageVersion>
</PropertyGroup> </PropertyGroup>
<!-- <!--

View File

@ -436,9 +436,15 @@
<!-- Clear the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker --> <!-- Clear the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker -->
<Delete Files="$(BlazorIntermediateLinkerOutputPath)*.dll" /> <Delete Files="$(BlazorIntermediateLinkerOutputPath)*.dll" />
<PropertyGroup>
<_MonoLinkerDotNetPath>$(DOTNET_HOST_PATH)</_MonoLinkerDotNetPath>
<_MonoLinkerDotNetPath Condition="'$(_MonoLinkerDotNetPath)' == ''">dotnet</_MonoLinkerDotNetPath>
</PropertyGroup>
<!-- Run the linker and put the results in /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker --> <!-- Run the linker and put the results in /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker -->
<Exec Command="dotnet &quot;$(MonoLinkerPath)&quot; $(_BlazorLinkerAdditionalOptions) @(_BlazorFolderLookupPaths, ' ') -o &quot;$(BlazorIntermediateLinkerOutputPath)&quot; @(_BlazorAssemblyDescriptorFiles, ' ') @(_BlazorAssembliesToLink, ' ')" /> <Exec Command="$(_MonoLinkerDotNetPath) &quot;$(MonoLinkerPath)&quot; $(_BlazorLinkerAdditionalOptions) @(_BlazorFolderLookupPaths, ' ') -o &quot;$(BlazorIntermediateLinkerOutputPath)&quot; @(_BlazorAssemblyDescriptorFiles, ' ') @(_BlazorAssembliesToLink, ' ')" />
<!-- Collect the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker/ --> <!-- Collect the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker/ -->
<ItemGroup> <ItemGroup>

View File

@ -14,7 +14,7 @@
<Reference Include="Microsoft.Azure.Storage.Blob" /> <Reference Include="Microsoft.Azure.Storage.Blob" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.0'"> <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'">
<!-- This dependency was replaced by Microsoft.Azure.Storage.Blob between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <!-- This dependency was replaced by Microsoft.Azure.Storage.Blob between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. -->
<SuppressBaselineReference Include="WindowsAzure.Storage" /> <SuppressBaselineReference Include="WindowsAzure.Storage" />
</ItemGroup> </ItemGroup>

View File

@ -5,6 +5,8 @@
<RootNamespace>Microsoft.AspNetCore</RootNamespace> <RootNamespace>Microsoft.AspNetCore</RootNamespace>
<!-- https://github.com/aspnet/AspNetCore/issues/7939: This unit test requires the shared framework be available in Helix. --> <!-- https://github.com/aspnet/AspNetCore/issues/7939: This unit test requires the shared framework be available in Helix. -->
<BuildHelixPayload>false</BuildHelixPayload> <BuildHelixPayload>false</BuildHelixPayload>
<!-- Test logic needs to be updated to handle when SharedFx version and TFM are not the same -->
<SkipTests>true</SkipTests>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -41,6 +41,7 @@ namespace Microsoft.AspNetCore.SpaServices
public SpaOptions() { } public SpaOptions() { }
public Microsoft.AspNetCore.Http.PathString DefaultPage { get { throw null; } set { } } public Microsoft.AspNetCore.Http.PathString DefaultPage { get { throw null; } set { } }
public Microsoft.AspNetCore.Builder.StaticFileOptions DefaultPageStaticFileOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Builder.StaticFileOptions DefaultPageStaticFileOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string PackageManagerCommand { get { throw null; } set { } }
public string SourcePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public string SourcePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.TimeSpan StartupTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.TimeSpan StartupTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
} }

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
{ {
private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine
private readonly string _npmScriptName; private readonly string _scriptName;
/// <summary> /// <summary>
/// Constructs an instance of <see cref="AngularCliBuilder"/>. /// Constructs an instance of <see cref="AngularCliBuilder"/>.
@ -35,12 +35,13 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
throw new ArgumentException("Cannot be null or empty.", nameof(npmScript)); throw new ArgumentException("Cannot be null or empty.", nameof(npmScript));
} }
_npmScriptName = npmScript; _scriptName = npmScript;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task Build(ISpaBuilder spaBuilder) public async Task Build(ISpaBuilder spaBuilder)
{ {
var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand;
var sourcePath = spaBuilder.Options.SourcePath; var sourcePath = spaBuilder.Options.SourcePath;
if (string.IsNullOrEmpty(sourcePath)) if (string.IsNullOrEmpty(sourcePath))
{ {
@ -50,32 +51,33 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
var logger = LoggerFinder.GetOrCreateLogger( var logger = LoggerFinder.GetOrCreateLogger(
spaBuilder.ApplicationBuilder, spaBuilder.ApplicationBuilder,
nameof(AngularCliBuilder)); nameof(AngularCliBuilder));
var npmScriptRunner = new NpmScriptRunner( var scriptRunner = new NodeScriptRunner(
sourcePath, sourcePath,
_npmScriptName, _scriptName,
"--watch", "--watch",
null); null,
npmScriptRunner.AttachToLogger(logger); pkgManagerCommand);
scriptRunner.AttachToLogger(logger);
using (var stdOutReader = new EventedStreamStringReader(npmScriptRunner.StdOut)) using (var stdOutReader = new EventedStreamStringReader(scriptRunner.StdOut))
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr))
{ {
try try
{ {
await npmScriptRunner.StdOut.WaitForMatch( await scriptRunner.StdOut.WaitForMatch(
new Regex("Date", RegexOptions.None, RegexMatchTimeout)); new Regex("Date", RegexOptions.None, RegexMatchTimeout));
} }
catch (EndOfStreamException ex) catch (EndOfStreamException ex)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"The NPM script '{_npmScriptName}' exited without indicating success.\n" + $"The {pkgManagerCommand} script '{_scriptName}' exited without indicating success.\n" +
$"Output was: {stdOutReader.ReadAsString()}\n" + $"Output was: {stdOutReader.ReadAsString()}\n" +
$"Error output was: {stdErrReader.ReadAsString()}", ex); $"Error output was: {stdErrReader.ReadAsString()}", ex);
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"The NPM script '{_npmScriptName}' timed out without indicating success. " + $"The {pkgManagerCommand} script '{_scriptName}' timed out without indicating success. " +
$"Output was: {stdOutReader.ReadAsString()}\n" + $"Output was: {stdOutReader.ReadAsString()}\n" +
$"Error output was: {stdErrReader.ReadAsString()}", ex); $"Error output was: {stdErrReader.ReadAsString()}", ex);
} }

View File

@ -23,23 +23,24 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
public static void Attach( public static void Attach(
ISpaBuilder spaBuilder, ISpaBuilder spaBuilder,
string npmScriptName) string scriptName)
{ {
var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand;
var sourcePath = spaBuilder.Options.SourcePath; var sourcePath = spaBuilder.Options.SourcePath;
if (string.IsNullOrEmpty(sourcePath)) if (string.IsNullOrEmpty(sourcePath))
{ {
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath)); throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
} }
if (string.IsNullOrEmpty(npmScriptName)) if (string.IsNullOrEmpty(scriptName))
{ {
throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName)); throw new ArgumentException("Cannot be null or empty", nameof(scriptName));
} }
// Start Angular CLI and attach to middleware pipeline // Start Angular CLI and attach to middleware pipeline
var appBuilder = spaBuilder.ApplicationBuilder; var appBuilder = spaBuilder.ApplicationBuilder;
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, logger); var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerCommand, logger);
// Everything we proxy is hardcoded to target http://localhost because: // Everything we proxy is hardcoded to target http://localhost because:
// - the requests are always from the local machine (we're not accepting remote // - the requests are always from the local machine (we're not accepting remote
@ -62,27 +63,27 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
} }
private static async Task<AngularCliServerInfo> StartAngularCliServerAsync( private static async Task<AngularCliServerInfo> StartAngularCliServerAsync(
string sourcePath, string npmScriptName, ILogger logger) string sourcePath, string scriptName, string pkgManagerCommand, ILogger logger)
{ {
var portNumber = TcpPortFinder.FindAvailablePort(); var portNumber = TcpPortFinder.FindAvailablePort();
logger.LogInformation($"Starting @angular/cli on port {portNumber}..."); logger.LogInformation($"Starting @angular/cli on port {portNumber}...");
var npmScriptRunner = new NpmScriptRunner( var scriptRunner = new NodeScriptRunner(
sourcePath, npmScriptName, $"--port {portNumber}", null); sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerCommand);
npmScriptRunner.AttachToLogger(logger); scriptRunner.AttachToLogger(logger);
Match openBrowserLine; Match openBrowserLine;
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr))
{ {
try try
{ {
openBrowserLine = await npmScriptRunner.StdOut.WaitForMatch( openBrowserLine = await scriptRunner.StdOut.WaitForMatch(
new Regex("open your browser on (http\\S+)", RegexOptions.None, RegexMatchTimeout)); new Regex("open your browser on (http\\S+)", RegexOptions.None, RegexMatchTimeout));
} }
catch (EndOfStreamException ex) catch (EndOfStreamException ex)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"The NPM script '{npmScriptName}' exited without indicating that the " + $"The {pkgManagerCommand} script '{scriptName}' exited without indicating that the " +
$"Angular CLI was listening for requests. The error output was: " + $"Angular CLI was listening for requests. The error output was: " +
$"{stdErrReader.ReadAsString()}", ex); $"{stdErrReader.ReadAsString()}", ex);
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -16,14 +16,14 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
/// Executes the <c>script</c> entries defined in a <c>package.json</c> file, /// Executes the <c>script</c> entries defined in a <c>package.json</c> file,
/// capturing any output written to stdio. /// capturing any output written to stdio.
/// </summary> /// </summary>
internal class NpmScriptRunner internal class NodeScriptRunner
{ {
public EventedStreamReader StdOut { get; } public EventedStreamReader StdOut { get; }
public EventedStreamReader StdErr { get; } public EventedStreamReader StdErr { get; }
private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1)); private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
public NpmScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars) public NodeScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars, string pkgManagerCommand)
{ {
if (string.IsNullOrEmpty(workingDirectory)) if (string.IsNullOrEmpty(workingDirectory))
{ {
@ -35,18 +35,23 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
throw new ArgumentException("Cannot be null or empty.", nameof(scriptName)); throw new ArgumentException("Cannot be null or empty.", nameof(scriptName));
} }
var npmExe = "npm"; if (string.IsNullOrEmpty(pkgManagerCommand))
{
throw new ArgumentException("Cannot be null or empty.", nameof(pkgManagerCommand));
}
var exeToRun = pkgManagerCommand;
var completeArguments = $"run {scriptName} -- {arguments ?? string.Empty}"; var completeArguments = $"run {scriptName} -- {arguments ?? string.Empty}";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
// On Windows, the NPM executable is a .cmd file, so it can't be executed // On Windows, the node executable is a .cmd file, so it can't be executed
// directly (except with UseShellExecute=true, but that's no good, because // directly (except with UseShellExecute=true, but that's no good, because
// it prevents capturing stdio). So we need to invoke it via "cmd /c". // it prevents capturing stdio). So we need to invoke it via "cmd /c".
npmExe = "cmd"; exeToRun = "cmd";
completeArguments = $"/c npm {completeArguments}"; completeArguments = $"/c {pkgManagerCommand} {completeArguments}";
} }
var processStartInfo = new ProcessStartInfo(npmExe) var processStartInfo = new ProcessStartInfo(exeToRun)
{ {
Arguments = completeArguments, Arguments = completeArguments,
UseShellExecute = false, UseShellExecute = false,
@ -64,19 +69,19 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
} }
} }
var process = LaunchNodeProcess(processStartInfo); var process = LaunchNodeProcess(processStartInfo, pkgManagerCommand);
StdOut = new EventedStreamReader(process.StandardOutput); StdOut = new EventedStreamReader(process.StandardOutput);
StdErr = new EventedStreamReader(process.StandardError); StdErr = new EventedStreamReader(process.StandardError);
} }
public void AttachToLogger(ILogger logger) public void AttachToLogger(ILogger logger)
{ {
// When the NPM task emits complete lines, pass them through to the real logger // When the node task emits complete lines, pass them through to the real logger
StdOut.OnReceivedLine += line => StdOut.OnReceivedLine += line =>
{ {
if (!string.IsNullOrWhiteSpace(line)) if (!string.IsNullOrWhiteSpace(line))
{ {
// NPM tasks commonly emit ANSI colors, but it wouldn't make sense to forward // Node tasks commonly emit ANSI colors, but it wouldn't make sense to forward
// those to loggers (because a logger isn't necessarily any kind of terminal) // those to loggers (because a logger isn't necessarily any kind of terminal)
logger.LogInformation(StripAnsiColors(line)); logger.LogInformation(StripAnsiColors(line));
} }
@ -106,7 +111,7 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
private static string StripAnsiColors(string line) private static string StripAnsiColors(string line)
=> AnsiColorRegex.Replace(line, string.Empty); => AnsiColorRegex.Replace(line, string.Empty);
private static Process LaunchNodeProcess(ProcessStartInfo startInfo) private static Process LaunchNodeProcess(ProcessStartInfo startInfo, string commandName)
{ {
try try
{ {
@ -119,8 +124,8 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
} }
catch (Exception ex) catch (Exception ex)
{ {
var message = $"Failed to start 'npm'. To resolve this:.\n\n" var message = $"Failed to start '{commandName}'. To resolve this:.\n\n"
+ "[1] Ensure that 'npm' is installed and can be found in one of the PATH directories.\n" + $"[1] Ensure that '{commandName}' is installed and can be found in one of the PATH directories.\n"
+ $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n" + $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n"
+ " Make sure the executable is in one of those directories, or update your PATH.\n\n" + " Make sure the executable is in one of those directories, or update your PATH.\n\n"
+ "[2] See the InnerException for further details of the cause."; + "[2] See the InnerException for further details of the cause.";

View File

@ -22,23 +22,24 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
public static void Attach( public static void Attach(
ISpaBuilder spaBuilder, ISpaBuilder spaBuilder,
string npmScriptName) string scriptName)
{ {
var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand;
var sourcePath = spaBuilder.Options.SourcePath; var sourcePath = spaBuilder.Options.SourcePath;
if (string.IsNullOrEmpty(sourcePath)) if (string.IsNullOrEmpty(sourcePath))
{ {
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath)); throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
} }
if (string.IsNullOrEmpty(npmScriptName)) if (string.IsNullOrEmpty(scriptName))
{ {
throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName)); throw new ArgumentException("Cannot be null or empty", nameof(scriptName));
} }
// Start create-react-app and attach to middleware pipeline // Start create-react-app and attach to middleware pipeline
var appBuilder = spaBuilder.ApplicationBuilder; var appBuilder = spaBuilder.ApplicationBuilder;
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
var portTask = StartCreateReactAppServerAsync(sourcePath, npmScriptName, logger); var portTask = StartCreateReactAppServerAsync(sourcePath, scriptName, pkgManagerCommand, logger);
// Everything we proxy is hardcoded to target http://localhost because: // Everything we proxy is hardcoded to target http://localhost because:
// - the requests are always from the local machine (we're not accepting remote // - the requests are always from the local machine (we're not accepting remote
@ -61,7 +62,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
} }
private static async Task<int> StartCreateReactAppServerAsync( private static async Task<int> StartCreateReactAppServerAsync(
string sourcePath, string npmScriptName, ILogger logger) string sourcePath, string scriptName, string pkgManagerCommand, ILogger logger)
{ {
var portNumber = TcpPortFinder.FindAvailablePort(); var portNumber = TcpPortFinder.FindAvailablePort();
logger.LogInformation($"Starting create-react-app server on port {portNumber}..."); logger.LogInformation($"Starting create-react-app server on port {portNumber}...");
@ -71,11 +72,11 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
{ "PORT", portNumber.ToString() }, { "PORT", portNumber.ToString() },
{ "BROWSER", "none" }, // We don't want create-react-app to open its own extra browser window pointing to the internal dev server port { "BROWSER", "none" }, // We don't want create-react-app to open its own extra browser window pointing to the internal dev server port
}; };
var npmScriptRunner = new NpmScriptRunner( var scriptRunner = new NodeScriptRunner(
sourcePath, npmScriptName, null, envVars); sourcePath, scriptName, null, envVars, pkgManagerCommand);
npmScriptRunner.AttachToLogger(logger); scriptRunner.AttachToLogger(logger);
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr))
{ {
try try
{ {
@ -83,13 +84,13 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
// it doesn't do so until it's finished compiling, and even then only if there were // it doesn't do so until it's finished compiling, and even then only if there were
// no compiler warnings. So instead of waiting for that, consider it ready as soon // no compiler warnings. So instead of waiting for that, consider it ready as soon
// as it starts listening for requests. // as it starts listening for requests.
await npmScriptRunner.StdOut.WaitForMatch( await scriptRunner.StdOut.WaitForMatch(
new Regex("Starting the development server", RegexOptions.None, RegexMatchTimeout)); new Regex("Starting the development server", RegexOptions.None, RegexMatchTimeout));
} }
catch (EndOfStreamException ex) catch (EndOfStreamException ex)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"The NPM script '{npmScriptName}' exited without indicating that the " + $"The {pkgManagerCommand} script '{scriptName}' exited without indicating that the " +
$"create-react-app server was listening for requests. The error output was: " + $"create-react-app server was listening for requests. The error output was: " +
$"{stdErrReader.ReadAsString()}", ex); $"{stdErrReader.ReadAsString()}", ex);
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;

View File

@ -1,11 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using System;
namespace Microsoft.AspNetCore.SpaServices namespace Microsoft.AspNetCore.SpaServices
{ {
@ -15,6 +14,7 @@ namespace Microsoft.AspNetCore.SpaServices
public class SpaOptions public class SpaOptions
{ {
private PathString _defaultPage = "/index.html"; private PathString _defaultPage = "/index.html";
private string _packageManagerCommand = "npm";
/// <summary> /// <summary>
/// Constructs a new instance of <see cref="SpaOptions"/>. /// Constructs a new instance of <see cref="SpaOptions"/>.
@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices
internal SpaOptions(SpaOptions copyFromOptions) internal SpaOptions(SpaOptions copyFromOptions)
{ {
_defaultPage = copyFromOptions.DefaultPage; _defaultPage = copyFromOptions.DefaultPage;
_packageManagerCommand = copyFromOptions.PackageManagerCommand;
DefaultPageStaticFileOptions = copyFromOptions.DefaultPageStaticFileOptions; DefaultPageStaticFileOptions = copyFromOptions.DefaultPageStaticFileOptions;
SourcePath = copyFromOptions.SourcePath; SourcePath = copyFromOptions.SourcePath;
} }
@ -69,6 +70,26 @@ namespace Microsoft.AspNetCore.SpaServices
/// </summary> /// </summary>
public string SourcePath { get; set; } public string SourcePath { get; set; }
/// <summary>
/// Gets or sets the name of the package manager executible, (e.g npm,
/// yarn) to run the SPA.
///
/// The default value is 'npm'.
/// </summary>
public string PackageManagerCommand
{
get => _packageManagerCommand;
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException($"The value for {nameof(PackageManagerCommand)} cannot be null or empty.");
}
_packageManagerCommand = value;
}
}
/// <summary> /// <summary>
/// Gets or sets the maximum duration that a request will wait for the SPA /// Gets or sets the maximum duration that a request will wait for the SPA
/// to become ready to serve to the client. /// to become ready to serve to the client.

View File

@ -120,12 +120,14 @@ namespace Microsoft.AspNetCore.StaticFiles.Infrastructure
{ {
public SharedOptions() { } public SharedOptions() { }
public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool RedirectToAppendTrailingSlash { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.PathString RequestPath { get { throw null; } set { } } public Microsoft.AspNetCore.Http.PathString RequestPath { get { throw null; } set { } }
} }
public abstract partial class SharedOptionsBase public abstract partial class SharedOptionsBase
{ {
protected SharedOptionsBase(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions sharedOptions) { } protected SharedOptionsBase(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions sharedOptions) { }
public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { get { throw null; } set { } } public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { get { throw null; } set { } }
public bool RedirectToAppendTrailingSlash { get { throw null; } set { } }
public Microsoft.AspNetCore.Http.PathString RequestPath { get { throw null; } set { } } public Microsoft.AspNetCore.Http.PathString RequestPath { get { throw null; } set { } }
protected Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions SharedOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions SharedOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
} }

View File

@ -80,17 +80,13 @@ namespace Microsoft.AspNetCore.StaticFiles
{ {
// If the path matches a directory but does not end in a slash, redirect to add the slash. // If the path matches a directory but does not end in a slash, redirect to add the slash.
// This prevents relative links from breaking. // This prevents relative links from breaking.
if (!Helpers.PathEndsInSlash(context.Request.Path)) if (!Helpers.PathEndsInSlash(context.Request.Path) && _options.RedirectToAppendTrailingSlash)
{ {
context.Response.StatusCode = StatusCodes.Status301MovedPermanently; Helpers.RedirectToPathWithSlash(context);
var request = context.Request;
var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);
context.Response.Headers[HeaderNames.Location] = redirect;
return Task.CompletedTask; return Task.CompletedTask;
} }
// Match found, re-write the url. A later middleware will actually serve the file. // Match found, re-write the url. A later middleware will actually serve the file.
context.Request.Path = new PathString(context.Request.Path.Value + defaultFile); context.Request.Path = new PathString(Helpers.GetPathValueWithSlash(context.Request.Path) + defaultFile);
break; break;
} }
} }

View File

@ -87,12 +87,9 @@ namespace Microsoft.AspNetCore.StaticFiles
{ {
// If the path matches a directory but does not end in a slash, redirect to add the slash. // If the path matches a directory but does not end in a slash, redirect to add the slash.
// This prevents relative links from breaking. // This prevents relative links from breaking.
if (!Helpers.PathEndsInSlash(context.Request.Path)) if (!Helpers.PathEndsInSlash(context.Request.Path) && _options.RedirectToAppendTrailingSlash)
{ {
context.Response.StatusCode = StatusCodes.Status301MovedPermanently; Helpers.RedirectToPathWithSlash(context);
var request = context.Request;
var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);
context.Response.Headers[HeaderNames.Location] = redirect;
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -2,9 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.StaticFiles namespace Microsoft.AspNetCore.StaticFiles
{ {
@ -12,7 +15,8 @@ namespace Microsoft.AspNetCore.StaticFiles
{ {
internal static IFileProvider ResolveFileProvider(IWebHostEnvironment hostingEnv) internal static IFileProvider ResolveFileProvider(IWebHostEnvironment hostingEnv)
{ {
if (hostingEnv.WebRootFileProvider == null) { if (hostingEnv.WebRootFileProvider == null)
{
throw new InvalidOperationException("Missing FileProvider."); throw new InvalidOperationException("Missing FileProvider.");
} }
return hostingEnv.WebRootFileProvider; return hostingEnv.WebRootFileProvider;
@ -28,6 +32,23 @@ namespace Microsoft.AspNetCore.StaticFiles
return path.Value.EndsWith("/", StringComparison.Ordinal); return path.Value.EndsWith("/", StringComparison.Ordinal);
} }
internal static string GetPathValueWithSlash(PathString path)
{
if (!PathEndsInSlash(path))
{
return path.Value + "/";
}
return path.Value;
}
internal static void RedirectToPathWithSlash(HttpContext context)
{
context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
var request = context.Request;
var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);
context.Response.Headers[HeaderNames.Location] = redirect;
}
internal static bool TryMatchPath(HttpContext context, PathString matchUrl, bool forDirectory, out PathString subpath) internal static bool TryMatchPath(HttpContext context, PathString matchUrl, bool forDirectory, out PathString subpath)
{ {
var path = context.Request.Path; var path = context.Request.Path;

View File

@ -42,5 +42,10 @@ namespace Microsoft.AspNetCore.StaticFiles.Infrastructure
/// The file system used to locate resources /// The file system used to locate resources
/// </summary> /// </summary>
public IFileProvider FileProvider { get; set; } public IFileProvider FileProvider { get; set; }
/// <summary>
/// Indicates whether to redirect to add a trailing slash at the end of path. Relative resource links may require this.
/// </summary>
public bool RedirectToAppendTrailingSlash { get; set; } = true;
} }
} }

View File

@ -48,5 +48,14 @@ namespace Microsoft.AspNetCore.StaticFiles.Infrastructure
get { return SharedOptions.FileProvider; } get { return SharedOptions.FileProvider; }
set { SharedOptions.FileProvider = value; } set { SharedOptions.FileProvider = value; }
} }
/// <summary>
/// Indicates whether to redirect to add a trailing slash at the end of path. Relative resource links may require this.
/// </summary>
public bool RedirectToAppendTrailingSlash
{
get { return SharedOptions.RedirectToAppendTrailingSlash; }
set { SharedOptions.RedirectToAppendTrailingSlash = value; }
}
} }
} }

View File

@ -38,9 +38,14 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("/subdir", @".", "/subdir/missing.dir")] [InlineData("/subdir", @".", "/subdir/missing.dir")]
[InlineData("/subdir", @".", "/subdir/missing.dir/")] [InlineData("/subdir", @".", "/subdir/missing.dir/")]
[InlineData("", @"./", "/missing.dir")] [InlineData("", @"./", "/missing.dir")]
public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".", "/missing.dir", false)]
[InlineData("", @".", "/missing.dir/", false)]
[InlineData("/subdir", @".", "/subdir/missing.dir", false)]
[InlineData("/subdir", @".", "/subdir/missing.dir/", false)]
[InlineData("", @"./", "/missing.dir", false)]
public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
[ConditionalTheory] [ConditionalTheory]
@ -48,12 +53,14 @@ namespace Microsoft.AspNetCore.StaticFiles
[OSSkipCondition(OperatingSystems.MacOSX)] [OSSkipCondition(OperatingSystems.MacOSX)]
[InlineData("", @".\", "/missing.dir")] [InlineData("", @".\", "/missing.dir")]
[InlineData("", @".\", "/Missing.dir")] [InlineData("", @".\", "/Missing.dir")]
public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".\", "/missing.dir", false)]
[InlineData("", @".\", "/Missing.dir", false)]
public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
{ {
@ -62,7 +69,8 @@ namespace Microsoft.AspNetCore.StaticFiles
app.UseDefaultFiles(new DefaultFilesOptions app.UseDefaultFiles(new DefaultFilesOptions
{ {
RequestPath = new PathString(baseUrl), RequestPath = new PathString(baseUrl),
FileProvider = fileProvider FileProvider = fileProvider,
RedirectToAppendTrailingSlash = appendTrailingSlash
}); });
app.Run(context => context.Response.WriteAsync(context.Request.Path.Value)); app.Run(context => context.Response.WriteAsync(context.Request.Path.Value));
}); });
@ -102,7 +110,7 @@ namespace Microsoft.AspNetCore.StaticFiles
FileProvider = fileProvider FileProvider = fileProvider
}); });
app.UseEndpoints(endpoints => {}); app.UseEndpoints(endpoints => { });
}, },
services => { services.AddDirectoryBrowser(); services.AddRouting(); }); services => { services.AddDirectoryBrowser(); services.AddRouting(); });
@ -118,9 +126,19 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("", @"./SubFolder", "/")] [InlineData("", @"./SubFolder", "/")]
[InlineData("", @"./SubFolder", "/你好/")] [InlineData("", @"./SubFolder", "/你好/")]
[InlineData("", @"./SubFolder", "/你好/世界/")] [InlineData("", @"./SubFolder", "/你好/世界/")]
public async Task FoundDirectoryWithDefaultFile_PathModified_All(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".", "/SubFolder/", false)]
[InlineData("", @"./", "/SubFolder/", false)]
[InlineData("", @"./SubFolder", "/", false)]
[InlineData("", @"./SubFolder", "/你好/", false)]
[InlineData("", @"./SubFolder", "/你好/世界/", false)]
[InlineData("", @".", "/SubFolder", false)]
[InlineData("", @"./", "/SubFolder", false)]
[InlineData("", @"./SubFolder", "", false)]
[InlineData("", @"./SubFolder", "/你好", false)]
[InlineData("", @"./SubFolder", "/你好/世界", false)]
public async Task FoundDirectoryWithDefaultFile_PathModified_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl); await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
[ConditionalTheory] [ConditionalTheory]
@ -130,12 +148,20 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("", @".\subFolder", "/")] [InlineData("", @".\subFolder", "/")]
[InlineData("", @".\SubFolder", "/你好/")] [InlineData("", @".\SubFolder", "/你好/")]
[InlineData("", @".\SubFolder", "/你好/世界/")] [InlineData("", @".\SubFolder", "/你好/世界/")]
public async Task FoundDirectoryWithDefaultFile_PathModified_Windows(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".\", "/SubFolder/", false)]
[InlineData("", @".\subFolder", "/", false)]
[InlineData("", @".\SubFolder", "/你好/", false)]
[InlineData("", @".\SubFolder", "/你好/世界/", false)]
[InlineData("", @".\", "/SubFolder", false)]
[InlineData("", @".\subFolder", "", false)]
[InlineData("", @".\SubFolder", "/你好", false)]
[InlineData("", @".\SubFolder", "/你好/世界", false)]
public async Task FoundDirectoryWithDefaultFile_PathModified_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl); await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
private async Task FoundDirectoryWithDefaultFile_PathModified(string baseUrl, string baseDir, string requestUrl) private async Task FoundDirectoryWithDefaultFile_PathModified(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
{ {
@ -144,14 +170,17 @@ namespace Microsoft.AspNetCore.StaticFiles
app.UseDefaultFiles(new DefaultFilesOptions app.UseDefaultFiles(new DefaultFilesOptions
{ {
RequestPath = new PathString(baseUrl), RequestPath = new PathString(baseUrl),
FileProvider = fileProvider FileProvider = fileProvider,
RedirectToAppendTrailingSlash = appendTrailingSlash
}); });
app.Run(context => context.Response.WriteAsync(context.Request.Path.Value)); app.Run(context => context.Response.WriteAsync(context.Request.Path.Value));
}); });
var response = await server.CreateClient().GetAsync(requestUrl); var response = await server.CreateClient().GetAsync(requestUrl);
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(requestUrl + "default.html", await response.Content.ReadAsStringAsync()); // Should be modified var requestUrlWithSlash = requestUrl.EndsWith("/") ? requestUrl : requestUrl + "/";
Assert.Equal(requestUrlWithSlash + "default.html", await response.Content.ReadAsStringAsync()); // Should be modified and be valid path to file
} }
} }
@ -202,9 +231,17 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("/SubFolder", @".", "/somedir/")] [InlineData("/SubFolder", @".", "/somedir/")]
[InlineData("", @"./SubFolder", "/")] [InlineData("", @"./SubFolder", "/")]
[InlineData("", @"./SubFolder/", "/")] [InlineData("", @"./SubFolder/", "/")]
public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) [InlineData("/SubFolder", @"./", "/SubFolder/", false)]
[InlineData("/SubFolder", @".", "/somedir/", false)]
[InlineData("", @"./SubFolder", "/", false)]
[InlineData("", @"./SubFolder/", "/", false)]
[InlineData("/SubFolder", @"./", "/SubFolder", false)]
[InlineData("/SubFolder", @".", "/somedir", false)]
[InlineData("", @"./SubFolder", "", false)]
[InlineData("", @"./SubFolder/", "", false)]
public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
[ConditionalTheory] [ConditionalTheory]
@ -213,24 +250,37 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("/SubFolder", @".\", "/SubFolder/")] [InlineData("/SubFolder", @".\", "/SubFolder/")]
[InlineData("", @".\SubFolder", "/")] [InlineData("", @".\SubFolder", "/")]
[InlineData("", @".\SubFolder\", "/")] [InlineData("", @".\SubFolder\", "/")]
public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) [InlineData("/SubFolder", @".\", "/SubFolder/", false)]
[InlineData("", @".\SubFolder", "/", false)]
[InlineData("", @".\SubFolder\", "/", false)]
[InlineData("/SubFolder", @".\", "/SubFolder", false)]
[InlineData("", @".\SubFolder", "", false)]
[InlineData("", @".\SubFolder\", "", false)]
public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl) private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
{ {
var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions
{ {
RequestPath = new PathString(baseUrl), RequestPath = new PathString(baseUrl),
FileProvider = fileProvider FileProvider = fileProvider,
RedirectToAppendTrailingSlash = appendTrailingSlash
})); }));
var response = await server.CreateRequest(requestUrl).GetAsync(); var response = await server.CreateRequest(requestUrl).GetAsync();
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); // Passed through Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); // Passed through
} }
} }
[Fact]
public void Options_AppendTrailingSlashByDefault()
{
Assert.True(new DefaultFilesOptions().RedirectToAppendTrailingSlash);
}
} }
} }

View File

@ -56,9 +56,14 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("/subdir", @".", "/subdir/missing.dir")] [InlineData("/subdir", @".", "/subdir/missing.dir")]
[InlineData("/subdir", @".", "/subdir/missing.dir/")] [InlineData("/subdir", @".", "/subdir/missing.dir/")]
[InlineData("", @"./", "/missing.dir")] [InlineData("", @"./", "/missing.dir")]
public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".", "/missing.dir", false)]
[InlineData("", @".", "/missing.dir/", false)]
[InlineData("/subdir", @".", "/subdir/missing.dir", false)]
[InlineData("/subdir", @".", "/subdir/missing.dir/", false)]
[InlineData("", @"./", "/missing.dir", false)]
public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
[ConditionalTheory] [ConditionalTheory]
@ -66,12 +71,14 @@ namespace Microsoft.AspNetCore.StaticFiles
[OSSkipCondition(OperatingSystems.MacOSX)] [OSSkipCondition(OperatingSystems.MacOSX)]
[InlineData("", @".\", "/missing.dir")] [InlineData("", @".\", "/missing.dir")]
[InlineData("", @".\", "/Missing.dir")] [InlineData("", @".\", "/Missing.dir")]
public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".\", "/missing.dir", false)]
[InlineData("", @".\", "/Missing.dir", false)]
public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
{ {
@ -79,7 +86,8 @@ namespace Microsoft.AspNetCore.StaticFiles
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
{ {
RequestPath = new PathString(baseUrl), RequestPath = new PathString(baseUrl),
FileProvider = fileProvider FileProvider = fileProvider,
RedirectToAppendTrailingSlash = appendTrailingSlash
}), }),
services => services.AddDirectoryBrowser()); services => services.AddDirectoryBrowser());
var response = await server.CreateRequest(requestUrl).GetAsync(); var response = await server.CreateRequest(requestUrl).GetAsync();
@ -117,7 +125,7 @@ namespace Microsoft.AspNetCore.StaticFiles
FileProvider = fileProvider FileProvider = fileProvider
}); });
app.UseEndpoints(endpoints => {}); app.UseEndpoints(endpoints => { });
}, },
services => { services.AddDirectoryBrowser(); services.AddRouting(); }); services => { services.AddDirectoryBrowser(); services.AddRouting(); });
@ -133,9 +141,19 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("/somedir", @".", "/somedir/")] [InlineData("/somedir", @".", "/somedir/")]
[InlineData("/somedir", @"./", "/somedir/")] [InlineData("/somedir", @"./", "/somedir/")]
[InlineData("/somedir", @".", "/somedir/SubFolder/")] [InlineData("/somedir", @".", "/somedir/SubFolder/")]
public async Task FoundDirectory_Served_All(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".", "/", false)]
[InlineData("", @".", "/SubFolder/", false)]
[InlineData("/somedir", @".", "/somedir/", false)]
[InlineData("/somedir", @"./", "/somedir/", false)]
[InlineData("/somedir", @".", "/somedir/SubFolder/", false)]
[InlineData("", @".", "", false)]
[InlineData("", @".", "/SubFolder", false)]
[InlineData("/somedir", @".", "/somedir", false)]
[InlineData("/somedir", @"./", "/somedir", false)]
[InlineData("/somedir", @".", "/somedir/SubFolder", false)]
public async Task FoundDirectory_Served_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await FoundDirectory_Served(baseUrl, baseDir, requestUrl); await FoundDirectory_Served(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
[ConditionalTheory] [ConditionalTheory]
@ -143,12 +161,16 @@ namespace Microsoft.AspNetCore.StaticFiles
[OSSkipCondition(OperatingSystems.MacOSX)] [OSSkipCondition(OperatingSystems.MacOSX)]
[InlineData("/somedir", @".\", "/somedir/")] [InlineData("/somedir", @".\", "/somedir/")]
[InlineData("/somedir", @".", "/somedir/subFolder/")] [InlineData("/somedir", @".", "/somedir/subFolder/")]
public async Task FoundDirectory_Served_Windows(string baseUrl, string baseDir, string requestUrl) [InlineData("/somedir", @".\", "/somedir/", false)]
[InlineData("/somedir", @".", "/somedir/subFolder/", false)]
[InlineData("/somedir", @".\", "/somedir", false)]
[InlineData("/somedir", @".", "/somedir/subFolder", false)]
public async Task FoundDirectory_Served_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await FoundDirectory_Served(baseUrl, baseDir, requestUrl); await FoundDirectory_Served(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
private async Task FoundDirectory_Served(string baseUrl, string baseDir, string requestUrl) private async Task FoundDirectory_Served(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
{ {
@ -156,7 +178,8 @@ namespace Microsoft.AspNetCore.StaticFiles
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
{ {
RequestPath = new PathString(baseUrl), RequestPath = new PathString(baseUrl),
FileProvider = fileProvider FileProvider = fileProvider,
RedirectToAppendTrailingSlash = appendTrailingSlash,
}), }),
services => services.AddDirectoryBrowser()); services => services.AddDirectoryBrowser());
var response = await server.CreateRequest(requestUrl).GetAsync(); var response = await server.CreateRequest(requestUrl).GetAsync();
@ -215,21 +238,31 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("", @".", "/SubFolder/")] [InlineData("", @".", "/SubFolder/")]
[InlineData("/somedir", @".", "/somedir/")] [InlineData("/somedir", @".", "/somedir/")]
[InlineData("/somedir", @".", "/somedir/SubFolder/")] [InlineData("/somedir", @".", "/somedir/SubFolder/")]
public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".", "/", false)]
[InlineData("", @".", "/SubFolder/", false)]
[InlineData("/somedir", @".", "/somedir/", false)]
[InlineData("/somedir", @".", "/somedir/SubFolder/", false)]
[InlineData("", @".", "", false)]
[InlineData("", @".", "/SubFolder", false)]
[InlineData("/somedir", @".", "/somedir", false)]
[InlineData("/somedir", @".", "/somedir/SubFolder", false)]
public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
[ConditionalTheory] [ConditionalTheory]
[OSSkipCondition(OperatingSystems.Linux)] [OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)] [OSSkipCondition(OperatingSystems.MacOSX)]
[InlineData("/somedir", @".", "/somedir/subFolder/")] [InlineData("/somedir", @".", "/somedir/subFolder/")]
public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) [InlineData("/somedir", @".", "/somedir/subFolder/", false)]
[InlineData("/somedir", @".", "/somedir/subFolder", false)]
public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl) private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
{ {
@ -237,7 +270,8 @@ namespace Microsoft.AspNetCore.StaticFiles
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
{ {
RequestPath = new PathString(baseUrl), RequestPath = new PathString(baseUrl),
FileProvider = fileProvider FileProvider = fileProvider,
RedirectToAppendTrailingSlash = appendTrailingSlash
}), }),
services => services.AddDirectoryBrowser()); services => services.AddDirectoryBrowser());
@ -251,21 +285,31 @@ namespace Microsoft.AspNetCore.StaticFiles
[InlineData("", @".", "/SubFolder/")] [InlineData("", @".", "/SubFolder/")]
[InlineData("/somedir", @".", "/somedir/")] [InlineData("/somedir", @".", "/somedir/")]
[InlineData("/somedir", @".", "/somedir/SubFolder/")] [InlineData("/somedir", @".", "/somedir/SubFolder/")]
public async Task HeadDirectory_HeadersButNotBodyServed_All(string baseUrl, string baseDir, string requestUrl) [InlineData("", @".", "/", false)]
[InlineData("", @".", "/SubFolder/", false)]
[InlineData("/somedir", @".", "/somedir/", false)]
[InlineData("/somedir", @".", "/somedir/SubFolder/", false)]
[InlineData("", @".", "", false)]
[InlineData("", @".", "/SubFolder", false)]
[InlineData("/somedir", @".", "/somedir", false)]
[InlineData("/somedir", @".", "/somedir/SubFolder", false)]
public async Task HeadDirectory_HeadersButNotBodyServed_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl); await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
[ConditionalTheory] [ConditionalTheory]
[OSSkipCondition(OperatingSystems.Linux)] [OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)] [OSSkipCondition(OperatingSystems.MacOSX)]
[InlineData("/somedir", @".", "/somedir/subFolder/")] [InlineData("/somedir", @".", "/somedir/subFolder/")]
public async Task HeadDirectory_HeadersButNotBodyServed_Windows(string baseUrl, string baseDir, string requestUrl) [InlineData("/somedir", @".", "/somedir/subFolder/", false)]
[InlineData("/somedir", @".", "/somedir/subFolder", false)]
public async Task HeadDirectory_HeadersButNotBodyServed_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl); await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl, appendTrailingSlash);
} }
private async Task HeadDirectory_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl) private async Task HeadDirectory_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true)
{ {
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
{ {
@ -273,7 +317,8 @@ namespace Microsoft.AspNetCore.StaticFiles
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
{ {
RequestPath = new PathString(baseUrl), RequestPath = new PathString(baseUrl),
FileProvider = fileProvider FileProvider = fileProvider,
RedirectToAppendTrailingSlash = appendTrailingSlash
}), }),
services => services.AddDirectoryBrowser()); services => services.AddDirectoryBrowser());
@ -285,5 +330,11 @@ namespace Microsoft.AspNetCore.StaticFiles
Assert.Empty((await response.Content.ReadAsByteArrayAsync())); Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
} }
} }
[Fact]
public void Options_AppendTrailingSlashByDefault()
{
Assert.True(new DirectoryBrowserOptions().RedirectToAppendTrailingSlash);
}
} }
} }

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(Resources.ArgumentCannotBeNullOrEmpty, (nameof(context))); throw new ArgumentNullException(nameof(context));
} }
var httpContext = context.HttpContext; var httpContext = context.HttpContext;
@ -69,4 +69,4 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return urlHelper; return urlHelper;
} }
} }
} }

View File

@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1 . $PSScriptRoot\Test-Template.ps1
Test-Template "angular" "angular" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.0.3.0.0-dev.nupkg" $true Test-Template "angular" "angular" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true

View File

@ -10,4 +10,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1 . $PSScriptRoot\Test-Template.ps1
Test-Template "blazorserver" "blazorserver" "Microsoft.DotNet.Web.ProjectTemplates.3.0.3.0.0-dev.nupkg" $false Test-Template "blazorserver" "blazorserver" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false

View File

@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1 . $PSScriptRoot\Test-Template.ps1
Test-Template "web" "web" "Microsoft.DotNet.Web.ProjectTemplates.3.0.3.0.0-dev.nupkg" $false Test-Template "web" "web" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false

View File

@ -6,4 +6,4 @@ param()
. $PSScriptRoot\Test-Template.ps1 . $PSScriptRoot\Test-Template.ps1
Test-Template "webapp" "webapp -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.3.0.3.0.0-dev.nupkg" $false Test-Template "webapp" "webapp -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false

View File

@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1 . $PSScriptRoot\Test-Template.ps1
Test-Template "react" "react" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.0.3.0.0-dev.nupkg" $true Test-Template "react" "react" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true

View File

@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1 . $PSScriptRoot\Test-Template.ps1
Test-Template "reactredux" "reactredux" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.0.3.0.0-dev.nupkg" $true Test-Template "reactredux" "reactredux" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true

View File

@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1 . $PSScriptRoot\Test-Template.ps1
Test-Template "mvc" "mvc -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.3.0.3.0.0-dev.nupkg" $false Test-Template "mvc" "mvc -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false

View File

@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1 . $PSScriptRoot\Test-Template.ps1
Test-Template "worker" "worker" "Microsoft.DotNet.Web.ProjectTemplates.3.0.3.0.0-dev.nupkg" $false Test-Template "worker" "worker" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false

View File

@ -31,9 +31,11 @@ namespace Templates.Test.Helpers
"Microsoft.DotNet.Web.ProjectTemplates.2.1", "Microsoft.DotNet.Web.ProjectTemplates.2.1",
"Microsoft.DotNet.Web.ProjectTemplates.2.2", "Microsoft.DotNet.Web.ProjectTemplates.2.2",
"Microsoft.DotNet.Web.ProjectTemplates.3.0", "Microsoft.DotNet.Web.ProjectTemplates.3.0",
"Microsoft.DotNet.Web.ProjectTemplates.5.0",
"Microsoft.DotNet.Web.Spa.ProjectTemplates.2.1", "Microsoft.DotNet.Web.Spa.ProjectTemplates.2.1",
"Microsoft.DotNet.Web.Spa.ProjectTemplates.2.2", "Microsoft.DotNet.Web.Spa.ProjectTemplates.2.2",
"Microsoft.DotNet.Web.Spa.ProjectTemplates.3.0", "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.0",
"Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0",
"Microsoft.DotNet.Web.Spa.ProjectTemplates" "Microsoft.DotNet.Web.Spa.ProjectTemplates"
}; };

View File

@ -139,6 +139,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
BadHttpRequestException ex; BadHttpRequestException ex;
switch (reason) switch (reason)
{ {
case RequestRejectionReason.TlsOverHttpError:
ex = new BadHttpRequestException(CoreStrings.HttpParserTlsOverHttpError, StatusCodes.Status400BadRequest, reason);
break;
case RequestRejectionReason.InvalidRequestLine: case RequestRejectionReason.InvalidRequestLine:
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(detail), StatusCodes.Status400BadRequest, reason); ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(detail), StatusCodes.Status400BadRequest, reason);
break; break;

View File

@ -617,4 +617,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http2TellClientToCalmDown" xml:space="preserve"> <data name="Http2TellClientToCalmDown" xml:space="preserve">
<value>A new stream was refused because this connection has too many streams that haven't finished processing. This may happen if many streams are aborted but not yet cleaned up.</value> <value>A new stream was refused because this connection has too many streams that haven't finished processing. This may happen if many streams are aborted but not yet cleaned up.</value>
</data> </data>
</root> <data name="HttpParserTlsOverHttpError" xml:space="preserve">
<value>Detected a TLS handshake to an endpoint that does not have TLS enabled.</value>
</data>
</root>

View File

@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private const byte ByteTab = (byte)'\t'; private const byte ByteTab = (byte)'\t';
private const byte ByteQuestionMark = (byte)'?'; private const byte ByteQuestionMark = (byte)'?';
private const byte BytePercentage = (byte)'%'; private const byte BytePercentage = (byte)'%';
private const int MinTlsRequestSize = 1; // We need at least 1 byte to check for a proper TLS request line
public unsafe bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined) public unsafe bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
{ {
@ -415,9 +416,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return new Span<byte>(data, methodLength); return new Span<byte>(data, methodLength);
} }
private unsafe bool IsTlsHandshake(byte* data, int length)
{
const byte SslRecordTypeHandshake = (byte)0x16;
// Make sure we can check at least for the existence of a TLS handshake - we check the first byte
// See https://serializethoughts.com/2014/07/27/dissecting-tls-client-hello-message/
return (length >= MinTlsRequestSize && data[0] == SslRecordTypeHandshake);
}
[StackTraceHidden] [StackTraceHidden]
private unsafe void RejectRequestLine(byte* requestLine, int length) private unsafe void RejectRequestLine(byte* requestLine, int length)
=> throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length); {
// Check for incoming TLS handshake over HTTP
if (IsTlsHandshake(requestLine, length))
{
throw GetInvalidRequestException(RequestRejectionReason.TlsOverHttpError, requestLine, length);
}
else
{
throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length);
}
}
[StackTraceHidden] [StackTraceHidden]
private unsafe void RejectRequestHeader(byte* headerLine, int length) private unsafe void RejectRequestHeader(byte* headerLine, int length)

View File

@ -1,10 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
internal enum RequestRejectionReason internal enum RequestRejectionReason
{ {
TlsOverHttpError,
UnrecognizedHTTPVersion, UnrecognizedHTTPVersion,
InvalidRequestLine, InvalidRequestLine,
InvalidRequestHeader, InvalidRequestHeader,

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -394,6 +394,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(buffer.End, examined); Assert.Equal(buffer.End, examined);
} }
[Fact]
public void ParseRequestLineTlsOverHttp()
{
var parser = CreateParser(_nullTrace);
var buffer = ReadOnlySequenceFactory.CreateSegments(new byte[] { 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0xfc, 0x03, 0x03, 0x03, 0xca, 0xe0, 0xfd, 0x0a });
var requestHandler = new RequestHandler();
var badHttpRequestException = Assert.Throws<BadHttpRequestException>(() =>
{
parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined);
});
Assert.Equal(badHttpRequestException.StatusCode, StatusCodes.Status400BadRequest);
Assert.Equal(RequestRejectionReason.TlsOverHttpError, badHttpRequestException.Reason);
}
[Fact] [Fact]
public void ParseHeadersWithGratuitouslySplitBuffers() public void ParseHeadersWithGratuitouslySplitBuffers()
{ {

View File

@ -32,7 +32,7 @@
<Reference Include="Microsoft.Bcl.AsyncInterfaces" /> <Reference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.0'"> <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'">
<!-- This dependency was replaced by Protocols.NewtonsoftJson between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <!-- This dependency was replaced by Protocols.NewtonsoftJson between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. -->
<SuppressBaselineReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" /> <SuppressBaselineReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,6 @@
{ {
"name": "@microsoft/signalr-protocol-msgpack", "name": "@microsoft/signalr-protocol-msgpack",
"version": "3.0.0-dev", "version": "5.0.0-dev",
"description": "MsgPack Protocol support for ASP.NET Core SignalR", "description": "MsgPack Protocol support for ASP.NET Core SignalR",
"main": "./dist/cjs/index.js", "main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js", "module": "./dist/esm/index.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "@microsoft/signalr", "name": "@microsoft/signalr",
"version": "3.0.0-dev", "version": "5.0.0-dev",
"description": "ASP.NET Core SignalR Client", "description": "ASP.NET Core SignalR Client",
"main": "./dist/cjs/index.js", "main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js", "module": "./dist/esm/index.js",

View File

@ -24,7 +24,7 @@
<Reference Include="System.Text.Json" /> <Reference Include="System.Text.Json" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.0'"> <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'">
<!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. -->
<SuppressBaselineReference Include="Newtonsoft.Json" /> <SuppressBaselineReference Include="Newtonsoft.Json" />

View File

@ -23,7 +23,7 @@
<Reference Include="Microsoft.AspNetCore.SignalR.Common" /> <Reference Include="Microsoft.AspNetCore.SignalR.Common" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.0'"> <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'">
<!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. -->
<SuppressBaselineReference Include="Newtonsoft.Json" /> <SuppressBaselineReference Include="Newtonsoft.Json" />
</ItemGroup> </ItemGroup>

View File

@ -29,7 +29,7 @@
<Reference Include="System.Text.Json" /> <Reference Include="System.Text.Json" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.0'"> <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'">
<!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. -->
<SuppressBaselineReference Include="Newtonsoft.Json" /> <SuppressBaselineReference Include="Newtonsoft.Json" />

View File

@ -23,8 +23,8 @@
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.2.1" Version="$(MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion)" PrivateAssets="All" /> <Reference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.2.1" Version="$(MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion)" PrivateAssets="All" />
<Reference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.2.2" Version="$(MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion)" PrivateAssets="All" /> <Reference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.2.2" Version="$(MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.3.0.x86" Version="$(PackageVersion)" PrivateAssets="All" /> <PackageReference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.5.0.x86" Version="$(PackageVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.3.0.x64" Version="$(PackageVersion)" PrivateAssets="All" /> <PackageReference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.5.0.x64" Version="$(PackageVersion)" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>