Merge branch 'merge/release/3.1-to-master' of https://github.com/dotnet-maestro-bot/AspNetCore

This commit is contained in:
BrennanConroy 2019-10-31 20:25:42 -07:00
commit 35403fe669
85 changed files with 9323 additions and 5295 deletions

View File

@ -65,7 +65,10 @@ variables:
valule: test valule: test
- name: _PublishArgs - name: _PublishArgs
value: '' value: ''
# used for post-build phases, internal builds only
- ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:
- group: DotNet-AspNet-SDLValidation-Params
stages: stages:
- stage: build - stage: build
displayName: Build displayName: Build
@ -654,3 +657,17 @@ stages:
enableSymbolValidation: false enableSymbolValidation: false
enableSigningValidation: false enableSigningValidation: false
publishInstallersAndChecksums: true publishInstallersAndChecksums: true
# This is to enable SDL runs part of Post-Build Validation Stage
SDLValidationParameters:
enable: true
continueOnError: false
params: ' -SourceToolsList @("policheck","credscan")
-TsaInstanceURL $(_TsaInstanceURL)
-TsaProjectName $(_TsaProjectName)
-TsaNotificationEmail $(_TsaNotificationEmail)
-TsaCodebaseAdmin $(_TsaCodebaseAdmin)
-TsaBugAreaPath $(_TsaBugAreaPath)
-TsaIterationPath $(_TsaIterationPath)
-TsaRepositoryName "AspNetCore"
-TsaCodebaseName "AspNetCore"
-TsaPublish $True'

View File

@ -9,9 +9,9 @@
--> -->
<Dependencies> <Dependencies>
<ProductDependencies> <ProductDependencies>
<Dependency Name="Microsoft.AspNetCore.Blazor.Mono" Version="5.0.0-alpha1.19528.2"> <Dependency Name="Microsoft.AspNetCore.Blazor.Mono" Version="5.0.0-alpha1.19531.2">
<Uri>https://github.com/aspnet/Blazor</Uri> <Uri>https://github.com/aspnet/Blazor</Uri>
<Sha>109766852cdedfd07babca70c56b18a508228e1d</Sha> <Sha>67c011629f56585c6a561dc4bdca70efc43579dd</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.AspNetCore.Razor.Language" Version="5.0.0-alpha1.19524.8"> <Dependency Name="Microsoft.AspNetCore.Razor.Language" Version="5.0.0-alpha1.19524.8">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri> <Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>

View File

@ -94,7 +94,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>5.0.0-alpha1.19520.7</MicrosoftNETCorePlatformsPackageVersion> <MicrosoftNETCorePlatformsPackageVersion>5.0.0-alpha1.19520.7</MicrosoftNETCorePlatformsPackageVersion>
<!-- Packages from aspnet/Blazor --> <!-- Packages from aspnet/Blazor -->
<MicrosoftAspNetCoreBlazorMonoPackageVersion>5.0.0-alpha1.19528.2</MicrosoftAspNetCoreBlazorMonoPackageVersion> <MicrosoftAspNetCoreBlazorMonoPackageVersion>5.0.0-alpha1.19531.2</MicrosoftAspNetCoreBlazorMonoPackageVersion>
<!-- Packages from aspnet/Extensions --> <!-- Packages from aspnet/Extensions -->
<InternalAspNetCoreAnalyzersPackageVersion>5.0.0-alpha1.19530.2</InternalAspNetCoreAnalyzersPackageVersion> <InternalAspNetCoreAnalyzersPackageVersion>5.0.0-alpha1.19530.2</InternalAspNetCoreAnalyzersPackageVersion>
<MicrosoftAspNetCoreAnalyzerTestingPackageVersion>5.0.0-alpha1.19530.2</MicrosoftAspNetCoreAnalyzerTestingPackageVersion> <MicrosoftAspNetCoreAnalyzerTestingPackageVersion>5.0.0-alpha1.19530.2</MicrosoftAspNetCoreAnalyzerTestingPackageVersion>

View File

@ -2,6 +2,7 @@
// 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.Diagnostics; using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
@ -27,6 +28,7 @@ namespace Microsoft.AspNetCore.Analyzers
{ {
MiddlewareItem? useAuthorizationItem = default; MiddlewareItem? useAuthorizationItem = default;
MiddlewareItem? useRoutingItem = default; MiddlewareItem? useRoutingItem = default;
MiddlewareItem? useEndpoint = default;
var length = middlewareAnalysis.Middleware.Length; var length = middlewareAnalysis.Middleware.Length;
for (var i = length - 1; i >= 0; i-- ) for (var i = length - 1; i >= 0; i-- )
@ -70,9 +72,24 @@ namespace Microsoft.AspNetCore.Analyzers
useAuthorizationItem.Operation.Syntax.GetLocation(), useAuthorizationItem.Operation.Syntax.GetLocation(),
middlewareItem.UseMethod.Name)); middlewareItem.UseMethod.Name));
} }
useEndpoint = middlewareItem;
} }
else if (middleware == "UseRouting") else if (middleware == "UseRouting")
{ {
if (useEndpoint is null)
{
// We're likely here because the middleware uses an expression chain e.g.
// app.UseRouting()
// .UseAuthorization()
// .UseEndpoints(..));
// This analyzer expects MiddlewareItem instances to appear in the order in which they appear in source
// which unfortunately isn't true for chained calls (the operations appear in reverse order).
// We'll avoid doing any analysis in this event and rely on the runtime guardrails.
// We'll use https://github.com/aspnet/AspNetCore/issues/16648 to track addressing this in a future milestone
return;
}
useRoutingItem = middlewareItem; useRoutingItem = middlewareItem;
} }
} }

View File

@ -244,6 +244,22 @@ namespace Microsoft.AspNetCore.Analyzers
Assert.Empty(diagnostics); Assert.Empty(diagnostics);
} }
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredAsAChain_ReportsNoDiagnostics()
{
// Regression test for https://github.com/aspnet/AspNetCore/issues/15203
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthConfiguredCorrectlyChained));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
[Fact] [Fact]
public async Task StartupAnalyzer_UseAuthorizationInvokedMultipleTimesInEndpointRoutingBlock_ReportsNoDiagnostics() public async Task StartupAnalyzer_UseAuthorizationInvokedMultipleTimesInEndpointRoutingBlock_ReportsNoDiagnostics()
{ {
@ -279,6 +295,23 @@ namespace Microsoft.AspNetCore.Analyzers
}); });
} }
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredBeforeUseRoutingChained_ReportsDiagnostics()
{
// This one asserts a false negative for https://github.com/aspnet/AspNetCore/issues/15203.
// We don't correctly identify chained calls, this test verifies the behavior.
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthBeforeUseRoutingChained));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
[Fact] [Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredAfterUseEndpoints_ReportsDiagnostics() public async Task StartupAnalyzer_UseAuthorizationConfiguredAfterUseEndpoints_ReportsDiagnostics()
{ {

View File

@ -0,0 +1,17 @@
// 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.
using Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest {
public class UseAuthBeforeUseRoutingChained
{
public void Configure(IApplicationBuilder app)
{
app.UseFileServer()
.UseAuthorization()
.UseRouting()
.UseEndpoints(r => { });
}
}
}

View File

@ -0,0 +1,16 @@
// 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.
using Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest {
public class UseAuthConfiguredCorrectlyChained
{
public void Configure(IApplicationBuilder app)
{
app.UseRouting()
.UseAuthorization()
.UseEndpoints(r => { });
}
}
}

View File

@ -54,13 +54,9 @@ namespace Microsoft.AspNetCore.Blazor.Http
SameOrigin = 1, SameOrigin = 1,
Include = 2, Include = 2,
} }
public partial class WebAssemblyHttpMessageHandler : System.Net.Http.HttpMessageHandler public static partial class WebAssemblyHttpMessageHandlerOptions
{ {
public const string FetchArgs = "WebAssemblyHttpMessageHandler.FetchArgs"; public static Microsoft.AspNetCore.Blazor.Http.FetchCredentialsOption DefaultCredentials { get { throw null; } set { } }
public WebAssemblyHttpMessageHandler() { }
public static Microsoft.AspNetCore.Blazor.Http.FetchCredentialsOption DefaultCredentials { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
[System.Diagnostics.DebuggerStepThroughAttribute]
protected override System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; }
} }
} }
namespace Microsoft.AspNetCore.Blazor.Rendering namespace Microsoft.AspNetCore.Blazor.Rendering

View File

@ -2,11 +2,8 @@
// 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.Net.Http;
using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Http;
using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@ -30,16 +27,6 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
public Task StartAsync(CancellationToken cancellationToken = default) public Task StartAsync(CancellationToken cancellationToken = default)
{ {
// We need to do this as early as possible, it eliminates a bunch of problems. Note that what we do
// is a bit fragile. If you see things breaking because JSRuntime.Current isn't set, then it's likely
// that something on the startup path went wrong.
//
// We want to the JSRuntime created here to be the 'ambient' runtime when JS calls back into .NET. When
// this happens in the browser it will be a direct call from Mono. We effectively needs to set the
// JSRuntime in the 'root' execution context which implies that we want to do as part of a direct
// call from Program.Main, and before any 'awaits'.
SetBrowserHttpMessageHandlerAsDefault();
return StartAsyncAwaited(); return StartAsyncAwaited();
} }
@ -102,35 +89,5 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
{ {
(Services as IDisposable)?.Dispose(); (Services as IDisposable)?.Dispose();
} }
private static void SetBrowserHttpMessageHandlerAsDefault()
{
// Within the Mono WebAssembly BCL, this is a special private static field
// that can be assigned to override the default handler
const string getHttpMessageHandlerFieldName = "GetHttpMessageHandler";
var getHttpMessageHandlerField = typeof(HttpClient).GetField(
getHttpMessageHandlerFieldName,
BindingFlags.Static | BindingFlags.NonPublic);
// getHttpMessageHandlerField will be null in tests, but nonnull when actually
// running under Mono WebAssembly
if (getHttpMessageHandlerField != null)
{
// Just in case you're not actually using HttpClient, defer the construction
// of the WebAssemblyHttpMessageHandler
var handlerSingleton = new Lazy<HttpMessageHandler>(
() => new WebAssemblyHttpMessageHandler());
Func<HttpMessageHandler> handlerFactory = () => handlerSingleton.Value;
getHttpMessageHandlerField.SetValue(null, handlerFactory);
}
else
{
// We log a warning in case this ever happens at runtime (even though there's
// no obvious way it could be possible), but don't actually throw because that
// would break unit tests
Console.WriteLine("WARNING: Could not set default HttpMessageHandler because " +
$"'{getHttpMessageHandlerFieldName}' was not found on '{typeof(HttpClient).FullName}'.");
}
}
} }
} }

View File

@ -1,204 +0,0 @@
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Blazor.Http
{
/// <summary>
/// A browser-compatible implementation of <see cref="HttpMessageHandler"/>
/// </summary>
public class WebAssemblyHttpMessageHandler : HttpMessageHandler
{
/// <summary>
/// Gets or sets the default value of the 'credentials' option on outbound HTTP requests.
/// Defaults to <see cref="FetchCredentialsOption.SameOrigin"/>.
/// </summary>
public static FetchCredentialsOption DefaultCredentials { get; set; }
= FetchCredentialsOption.SameOrigin;
private static readonly object _idLock = new object();
private static readonly IDictionary<int, TaskCompletionSource<HttpResponseMessage>> _pendingRequests
= new Dictionary<int, TaskCompletionSource<HttpResponseMessage>>();
private static int _nextRequestId = 0;
/// <summary>
/// The name of a well-known property that can be added to <see cref="HttpRequestMessage.Properties"/>
/// to control the arguments passed to the underlying JavaScript <code>fetch</code> API.
/// </summary>
public const string FetchArgs = "WebAssemblyHttpMessageHandler.FetchArgs";
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<HttpResponseMessage>();
cancellationToken.Register(() => tcs.TrySetCanceled());
int id;
lock (_idLock)
{
id = _nextRequestId++;
_pendingRequests.Add(id, tcs);
}
var options = new FetchOptions();
if (request.Properties.TryGetValue(FetchArgs, out var fetchArgs))
{
options.RequestInitOverrides = fetchArgs;
}
options.RequestInit = new RequestInit
{
Credentials = GetDefaultCredentialsString(),
Headers = GetHeaders(request),
Method = request.Method.Method
};
options.RequestUri = request.RequestUri.ToString();
WebAssemblyJSRuntime.Instance.InvokeUnmarshalled<int, byte[], string, object>(
"Blazor._internal.http.sendAsync",
id,
request.Content == null ? null : await request.Content.ReadAsByteArrayAsync(),
JsonSerializer.Serialize(options, JsonSerializerOptionsProvider.Options));
return await tcs.Task;
}
/// <remarks>
/// While it may be tempting to remove this method because it appears to be unused,
/// this method is referenced by client code and must persist.
/// </remarks>
#pragma warning disable IDE0051 // Remove unused private members
private static void ReceiveResponse(
#pragma warning restore IDE0051 // Remove unused private members
string id,
string responseDescriptorJson,
byte[] responseBodyData,
string errorText)
{
TaskCompletionSource<HttpResponseMessage> tcs;
var idVal = int.Parse(id);
lock (_idLock)
{
tcs = _pendingRequests[idVal];
_pendingRequests.Remove(idVal);
}
if (errorText != null)
{
tcs.SetException(new HttpRequestException(errorText));
}
else
{
var responseDescriptor = JsonSerializer.Deserialize<ResponseDescriptor>(responseDescriptorJson, JsonSerializerOptionsProvider.Options);
var responseContent = responseBodyData == null ? null : new ByteArrayContent(responseBodyData);
var responseMessage = responseDescriptor.ToResponseMessage(responseContent);
tcs.SetResult(responseMessage);
}
}
/// <remarks>
/// While it may be tempting to remove this method because it appears to be unused,
/// this method is referenced by client code and must persist.
/// </remarks>
#pragma warning disable IDE0051 // Remove unused private members
private static byte[] AllocateArray(string length) => new byte[int.Parse(length)];
#pragma warning restore IDE0051 // Remove unused private members
private static IReadOnlyList<Header> GetHeaders(HttpRequestMessage request)
{
var requestHeaders = request.Headers.AsEnumerable();
if (request.Content?.Headers != null)
{
requestHeaders = requestHeaders.Concat(request.Content.Headers);
}
var headers = new List<Header>();
foreach (var item in requestHeaders)
{
foreach (var headerValue in item.Value)
{
headers.Add(new Header { Name = item.Key, Value = headerValue });
}
}
return headers;
}
private static string GetDefaultCredentialsString()
{
// See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials for
// standard values and meanings
switch (DefaultCredentials)
{
case FetchCredentialsOption.Omit:
return "omit";
case FetchCredentialsOption.SameOrigin:
return "same-origin";
case FetchCredentialsOption.Include:
return "include";
default:
throw new ArgumentException($"Unknown credentials option '{DefaultCredentials}'.");
}
}
// Keep these in sync with TypeScript class in Http.ts
private class FetchOptions
{
public string RequestUri { get; set; }
public RequestInit RequestInit { get; set; }
public object RequestInitOverrides { get; set; }
}
private class RequestInit
{
public string Credentials { get; set; }
public IReadOnlyList<Header> Headers { get; set; }
public string Method { get; set; }
}
private class ResponseDescriptor
{
#pragma warning disable 0649
public int StatusCode { get; set; }
public string StatusText { get; set; }
public IReadOnlyList<Header> Headers { get; set; }
#pragma warning restore 0649
public HttpResponseMessage ToResponseMessage(HttpContent content)
{
var result = new HttpResponseMessage((HttpStatusCode)StatusCode);
result.ReasonPhrase = StatusText;
result.Content = content;
var headers = result.Headers;
var contentHeaders = result.Content?.Headers;
foreach (var pair in Headers)
{
if (!headers.TryAddWithoutValidation(pair.Name, pair.Value))
{
contentHeaders?.TryAddWithoutValidation(pair.Name, pair.Value);
}
}
return result;
}
}
private class Header
{
public string Name { get; set; }
public string Value { get; set; }
}
}
}

View File

@ -0,0 +1,59 @@
// 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.
using System;
using System.Reflection;
namespace Microsoft.AspNetCore.Blazor.Http
{
/// <summary>
/// Configures options for the WebAssembly HTTP message handler.
/// </summary>
public static class WebAssemblyHttpMessageHandlerOptions
{
/// <summary>
/// Gets or sets the default value of the 'credentials' option on outbound HTTP requests.
/// Defaults to <see cref="FetchCredentialsOption.SameOrigin"/>.
/// </summary>
public static FetchCredentialsOption DefaultCredentials
{
get
{
var valueString = MonoDefaultCredentialsGetter.Value();
var result = default(FetchCredentialsOption);
if (valueString != null)
{
Enum.TryParse(valueString, out result);
}
return result;
}
set
{
MonoDefaultCredentialsSetter.Value(value.ToString());
}
}
static Func<Type> MonoWasmHttpMessageHandlerType = ()
=> Assembly.Load("WebAssembly.Net.Http")
.GetType("WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler");
static Func<Type> MonoFetchCredentialsOptionType = ()
=> Assembly.Load("WebAssembly.Net.Http")
.GetType("WebAssembly.Net.Http.HttpClient.FetchCredentialsOption");
static Lazy<PropertyInfo> MonoDefaultCredentialsProperty = new Lazy<PropertyInfo>(
() => MonoWasmHttpMessageHandlerType()?.GetProperty("DefaultCredentials", BindingFlags.Public | BindingFlags.Static));
static Lazy<Func<string>> MonoDefaultCredentialsGetter = new Lazy<Func<string>>(() =>
{
return () => MonoDefaultCredentialsProperty.Value?.GetValue(null).ToString();
});
static Lazy<Action<string>> MonoDefaultCredentialsSetter = new Lazy<Action<string>>(() =>
{
var fetchCredentialsOptionsType = MonoFetchCredentialsOptionType();
return value => MonoDefaultCredentialsProperty.Value?.SetValue(null, Enum.Parse(fetchCredentialsOptionsType, value));
});
}
}

View File

@ -422,7 +422,7 @@
4) Add the file we just created to the list of file writes, to support incremental builds. 4) Add the file we just created to the list of file writes, to support incremental builds.
--> -->
<ItemGroup> <ItemGroup>
<_MonoBaseClassLibraryFolder Include="$(MonoBaseClassLibraryPath);$(MonoBaseClassLibraryFacadesPath)" /> <_MonoBaseClassLibraryFolder Include="$(MonoBaseClassLibraryPath);$(MonoBaseClassLibraryFacadesPath);$(MonoWasmFrameworkPath)" />
<_BlazorAssembliesToLink Include="@(_BlazorDependencyInput->'-a &quot;%(Identity)&quot;')" /> <_BlazorAssembliesToLink Include="@(_BlazorDependencyInput->'-a &quot;%(Identity)&quot;')" />
<_BlazorAssembliesToLink Include="@(IntermediateAssembly->'-a &quot;%(FullPath)&quot;')" /> <_BlazorAssembliesToLink Include="@(IntermediateAssembly->'-a &quot;%(FullPath)&quot;')" />
<_BlazorFolderLookupPaths Include="@(_MonoBaseClassLibraryFolder->'-d &quot;%(Identity)&quot;')" /> <_BlazorFolderLookupPaths Include="@(_MonoBaseClassLibraryFolder->'-d &quot;%(Identity)&quot;')" />
@ -528,7 +528,7 @@
<PropertyGroup> <PropertyGroup>
<_ReferencesArg Condition="'@(_BlazorDependencyInput)' != ''">--references &quot;$(BlazorResolveDependenciesFilePath)&quot;</_ReferencesArg> <_ReferencesArg Condition="'@(_BlazorDependencyInput)' != ''">--references &quot;$(BlazorResolveDependenciesFilePath)&quot;</_ReferencesArg>
<_BclParameter>--base-class-library &quot;$(MonoBaseClassLibraryPath)&quot; --base-class-library &quot;$(MonoBaseClassLibraryFacadesPath)&quot;</_BclParameter> <_BclParameter>--base-class-library &quot;$(MonoBaseClassLibraryPath)&quot; --base-class-library &quot;$(MonoBaseClassLibraryFacadesPath)&quot; --base-class-library &quot;$(MonoWasmFrameworkPath)&quot;</_BclParameter>
</PropertyGroup> </PropertyGroup>
<WriteLinesToFile <WriteLinesToFile

View File

@ -18,4 +18,10 @@
<type fullname="System.ComponentModel.GuidConverter" /> <type fullname="System.ComponentModel.GuidConverter" />
<type fullname="System.ComponentModel.TimeSpanConverter" /> <type fullname="System.ComponentModel.TimeSpanConverter" />
</assembly> </assembly>
<assembly fullname="WebAssembly.Net.Http">
<!-- Without this, the setter for DefaultCredentials would be removed, but we need it -->
<type fullname="WebAssembly.Net.Http.HttpClient.FetchCredentialsOption" />
<type fullname="WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler" />
</assembly>
</linker> </linker>

View File

@ -40,6 +40,7 @@
<ItemGroup> <ItemGroup>
<_BclDirectory Include="$(MonoBaseClassLibraryPath)" /> <_BclDirectory Include="$(MonoBaseClassLibraryPath)" />
<_BclDirectory Include="$(MonoBaseClassLibraryFacadesPath)" /> <_BclDirectory Include="$(MonoBaseClassLibraryFacadesPath)" />
<_BclDirectory Include="$(MonoWasmFrameworkPath)" />
</ItemGroup> </ItemGroup>
<WriteLinesToFile Lines="@(ReferencePath)" File="$(TargetDir)referenceHints.txt" WriteOnlyWhenDifferent="true" Overwrite="true" /> <WriteLinesToFile Lines="@(ReferencePath)" File="$(TargetDir)referenceHints.txt" WriteOnlyWhenDifferent="true" Overwrite="true" />

View File

@ -488,7 +488,7 @@ namespace WsProxy {
// Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously // Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously
// results in a "Memory access out of bounds", causing 'values' to be null, // results in a "Memory access out of bounds", causing 'values' to be null,
// so skip returning variable values in that case. // so skip returning variable values in that case.
while (values != null && i < var_ids.Length && i < values.Length) { while (values != null && i < vars.Length && i < values.Length) {
var value = values [i] ["value"]; var value = values [i] ["value"];
if (((string)value ["description"]) == null) if (((string)value ["description"]) == null)
value ["description"] = value ["value"]?.ToString(); value ["description"] = value ["value"]?.ToString();

View File

@ -20,7 +20,9 @@
'mscorlib.dll', 'mscorlib.dll',
'System.dll', 'System.dll',
'System.Core.dll', 'System.Core.dll',
'System.Net.Http.dll' 'System.Net.Http.dll',
'WebAssembly.Bindings.dll',
'WebAssembly.Net.Http.dll'
]); ]);
// For these tests we're using Mono's built-in mono_load_runtime_and_bcl util. // For these tests we're using Mono's built-in mono_load_runtime_and_bcl util.

View File

@ -11,14 +11,6 @@ namespace MonoSanityClient
{ {
public static class Examples public static class Examples
{ {
static Examples()
{
// We have to populate GetHttpMessageHandler with something (like the real
// Blazor web assembly host does), otherwise HttpClientHandler's constructor
// gets into an infinite loop.
FakeHttpMessageHandler.Attach();
}
public static string AddNumbers(int a, int b) public static string AddNumbers(int a, int b)
=> (a + b).ToString(); => (a + b).ToString();

View File

@ -1,26 +0,0 @@
// 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.
using System;
using System.Net.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace MonoSanityClient
{
class FakeHttpMessageHandler : HttpMessageHandler
{
public static void Attach()
{
var getHttpMessageHandlerField = typeof(HttpClient).GetField(
"GetHttpMessageHandler",
BindingFlags.Static | BindingFlags.NonPublic);
Func<HttpMessageHandler> handlerFactory = () => new FakeHttpMessageHandler();
getHttpMessageHandlerField.SetValue(null, handlerFactory);
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
=> throw new NotImplementedException($"{nameof(FakeHttpMessageHandler)} cannot {nameof(SendAsync)}.");
}
}

View File

@ -581,6 +581,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param> /// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
public void OpenRegion(int sequence) public void OpenRegion(int sequence)
{ {
// We are entering a new scope, since we track the "duplicate attributes" per
// element/component we might need to clean them up now.
if (_hasSeenAddMultipleAttributes)
{
var indexOfLastElementOrComponent = _openElementIndices.Peek();
ProcessDuplicateAttributes(first: indexOfLastElementOrComponent + 1);
}
_openElementIndices.Push(_entries.Count); _openElementIndices.Push(_entries.Count);
Append(RenderTreeFrame.Region(sequence)); Append(RenderTreeFrame.Region(sequence));
} }

View File

@ -298,6 +298,39 @@ namespace Microsoft.AspNetCore.Components.Rendering
frame => AssertFrame.Attribute(frame, "attribute7", "the end")); frame => AssertFrame.Attribute(frame, "attribute7", "the end"));
} }
[Fact]
public void CanAddMultipleAttributes_WithChildRegion()
{
// This represents bug https://github.com/aspnet/AspNetCore/issues/16570
// If a sequence of attributes is terminated by a call to builder.OpenRegion,
// then the attribute deduplication logic wasn't working correctly
// Arrange
var builder = new RenderTreeBuilder();
// Act
builder.OpenElement(0, "myelement");
builder.AddAttribute(0, "attribute1", "value1");
builder.AddMultipleAttributes(1, new Dictionary<string, object>()
{
{ "attribute1", "value2" },
});
builder.OpenRegion(2);
builder.OpenElement(3, "child");
builder.CloseElement();
builder.CloseRegion();
builder.CloseElement();
// Assert
var frames = builder.GetFrames().AsEnumerable().ToArray();
Assert.Collection(
frames,
frame => AssertFrame.Element(frame, "myelement", 4),
frame => AssertFrame.Attribute(frame, "attribute1", "value2"),
frame => AssertFrame.Region(frame, 2, 2),
frame => AssertFrame.Element(frame, "child", 1, 3));
}
[Fact] [Fact]
public void CanAddMultipleAttributes_DictionaryObject() public void CanAddMultipleAttributes_DictionaryObject()
{ {

View File

@ -122,6 +122,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// Report errors asynchronously. InitializeAsync is designed not to throw. // Report errors asynchronously. InitializeAsync is designed not to throw.
Log.InitializationFailed(_logger, ex); Log.InitializationFailed(_logger, ex);
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex), ex);
} }
}); });
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -153,7 +154,9 @@ namespace Microsoft.AspNetCore.Components.Server
string unprotected; string unprotected;
try try
{ {
unprotected = _dataProtector.Unprotect(record.Descriptor); var payload = Convert.FromBase64String(record.Descriptor);
var unprotectedBytes = _dataProtector.Unprotect(payload);
unprotected = Encoding.UTF8.GetString(unprotectedBytes);
} }
catch (Exception e) catch (Exception e)
{ {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ import { getAssemblyNameFromUrl } from './Platform/Url';
import { renderBatch } from './Rendering/Renderer'; import { renderBatch } from './Rendering/Renderer';
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch'; import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
import { Pointer } from './Platform/Platform'; import { Pointer } from './Platform/Platform';
import { fetchBootConfigAsync, loadEmbeddedResourcesAsync, shouldAutoStart } from './BootCommon'; import { shouldAutoStart } from './BootCommon';
import { setEventDispatcher } from './Rendering/RendererEventDispatcher'; import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
let started = false; let started = false;
@ -64,6 +64,46 @@ async function boot(options?: any): Promise<void> {
platform.callEntryPoint(mainAssemblyName, bootConfig.entryPoint, []); platform.callEntryPoint(mainAssemblyName, bootConfig.entryPoint, []);
} }
async function fetchBootConfigAsync() {
// Later we might make the location of this configurable (e.g., as an attribute on the <script>
// element that's importing this file), but currently there isn't a use case for that.
const bootConfigResponse = await fetch('_framework/blazor.boot.json', { method: 'Get', credentials: 'include' });
return bootConfigResponse.json() as Promise<BootJsonData>;
}
function loadEmbeddedResourcesAsync(bootConfig: BootJsonData): Promise<any> {
const cssLoadingPromises = bootConfig.cssReferences.map(cssReference => {
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.href = cssReference;
return loadResourceFromElement(linkElement);
});
const jsLoadingPromises = bootConfig.jsReferences.map(jsReference => {
const scriptElement = document.createElement('script');
scriptElement.src = jsReference;
return loadResourceFromElement(scriptElement);
});
return Promise.all(cssLoadingPromises.concat(jsLoadingPromises));
}
function loadResourceFromElement(element: HTMLElement) {
return new Promise((resolve, reject) => {
element.onload = resolve;
element.onerror = reject;
document.head!.appendChild(element);
});
}
// Keep in sync with BootJsonData in Microsoft.AspNetCore.Blazor.Build
interface BootJsonData {
main: string;
entryPoint: string;
assemblyReferences: string[];
cssReferences: string[];
jsReferences: string[];
linkerEnabled: boolean;
}
window['Blazor'].start = boot; window['Blazor'].start = boot;
if (shouldAutoStart()) { if (shouldAutoStart()) {
boot(); boot();

View File

@ -1,43 +1,3 @@
export async function fetchBootConfigAsync() {
// Later we might make the location of this configurable (e.g., as an attribute on the <script>
// element that's importing this file), but currently there isn't a use case for that.
const bootConfigResponse = await fetch('_framework/blazor.boot.json', { method: 'Get', credentials: 'include' });
return bootConfigResponse.json() as Promise<BootJsonData>;
}
export function loadEmbeddedResourcesAsync(bootConfig: BootJsonData): Promise<any> {
const cssLoadingPromises = bootConfig.cssReferences.map(cssReference => {
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.href = cssReference;
return loadResourceFromElement(linkElement);
});
const jsLoadingPromises = bootConfig.jsReferences.map(jsReference => {
const scriptElement = document.createElement('script');
scriptElement.src = jsReference;
return loadResourceFromElement(scriptElement);
});
return Promise.all(cssLoadingPromises.concat(jsLoadingPromises));
}
function loadResourceFromElement(element: HTMLElement) {
return new Promise((resolve, reject) => {
element.onload = resolve;
element.onerror = reject;
document.head!.appendChild(element);
});
}
// Keep in sync with BootJsonData in Microsoft.AspNetCore.Blazor.Build
interface BootJsonData {
main: string;
entryPoint: string;
assemblyReferences: string[];
cssReferences: string[];
jsReferences: string[];
linkerEnabled: boolean;
}
// Tells you if the script was added without <script src="..." autostart="false"></script> // Tells you if the script was added without <script src="..." autostart="false"></script>
export function shouldAutoStart() { export function shouldAutoStart() {
return !!(document && return !!(document &&

View File

@ -1,5 +1,4 @@
import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager'; import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
import { internalFunctions as httpInternalFunctions } from './Services/Http';
import { attachRootComponentToElement } from './Rendering/Renderer'; import { attachRootComponentToElement } from './Rendering/Renderer';
// Make the following APIs available in global scope for invocation from JS // Make the following APIs available in global scope for invocation from JS
@ -8,7 +7,6 @@ window['Blazor'] = {
_internal: { _internal: {
attachRootComponentToElement, attachRootComponentToElement,
http: httpInternalFunctions,
navigationManager: navigationManagerInternalFunctions, navigationManager: navigationManagerInternalFunctions,
}, },
}; };

View File

@ -21,6 +21,8 @@ const nonBubblingEvents = toLookup([
'DOMNodeRemovedFromDocument', 'DOMNodeRemovedFromDocument',
]); ]);
const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']);
export interface OnEventCallback { export interface OnEventCallback {
(event: Event, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>, eventFieldInfo: EventFieldInfo | null): void; (event: Event, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>, eventFieldInfo: EventFieldInfo | null): void;
} }
@ -107,7 +109,7 @@ export class EventDelegator {
const handlerInfos = this.getEventHandlerInfosForElement(candidateElement, false); const handlerInfos = this.getEventHandlerInfosForElement(candidateElement, false);
if (handlerInfos) { if (handlerInfos) {
const handlerInfo = handlerInfos.getHandler(evt.type); const handlerInfo = handlerInfos.getHandler(evt.type);
if (handlerInfo) { if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) {
// We are going to raise an event for this element, so prepare info needed by the .NET code // We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgs) { if (!eventArgs) {
eventArgs = EventForDotNet.fromDOMEvent(evt); eventArgs = EventForDotNet.fromDOMEvent(evt);
@ -269,3 +271,11 @@ function toLookup(items: string[]): { [key: string]: boolean } {
}); });
return result; return result;
} }
function eventIsDisabledOnElement(element: Element, eventName: string): boolean {
// We want to replicate the normal DOM event behavior that, for 'interactive' elements
// with a 'disabled' attribute, certain mouse events are suppressed
return (element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)
&& disableableEventNames.hasOwnProperty(eventName)
&& element.disabled;
}

View File

@ -1,132 +0,0 @@
import { platform } from '../Environment';
import { MethodHandle, System_String, System_Array } from '../Platform/Platform';
const httpClientAssembly = 'Microsoft.AspNetCore.Blazor';
const httpClientNamespace = `${httpClientAssembly}.Http`;
const httpClientTypeName = 'WebAssemblyHttpMessageHandler';
let receiveResponseMethod: MethodHandle;
let allocateArrayMethod: MethodHandle;
// These are the functions we're making available for invocation from .NET
export const internalFunctions = {
sendAsync,
};
async function sendAsync(id: number, body: System_Array<any>, jsonFetchArgs: System_String) {
let response: Response;
let responseData: ArrayBuffer;
const fetchOptions: FetchOptions = JSON.parse(platform.toJavaScriptString(jsonFetchArgs));
const requestInit: RequestInit = Object.assign(convertToRequestInit(fetchOptions.requestInit), fetchOptions.requestInitOverrides);
if (body) {
requestInit.body = platform.toUint8Array(body);
}
try {
response = await fetch(fetchOptions.requestUri, requestInit);
responseData = await response.arrayBuffer();
} catch (ex) {
dispatchErrorResponse(id, ex.toString());
return;
}
dispatchSuccessResponse(id, response, responseData);
}
function convertToRequestInit(blazorRequestInit: BlazorRequestInit) {
return {
credentials: blazorRequestInit.credentials,
method: blazorRequestInit.method,
headers: blazorRequestInit.headers.map(item => [item.name, item.value])
};
}
function dispatchSuccessResponse(id: number, response: Response, responseData: ArrayBuffer) {
const responseDescriptor: ResponseDescriptor = {
statusCode: response.status,
statusText: response.statusText,
headers: [],
};
response.headers.forEach((value, name) => {
responseDescriptor.headers.push({ name: name, value: value });
});
if (!allocateArrayMethod) {
allocateArrayMethod = platform.findMethod(
httpClientAssembly,
httpClientNamespace,
httpClientTypeName,
'AllocateArray'
);
}
// allocate a managed byte[] of the right size
const dotNetArray = platform.callMethod(allocateArrayMethod, null, [platform.toDotNetString(responseData.byteLength.toString())]) as System_Array<any>;
// get an Uint8Array view of it
const array = platform.toUint8Array(dotNetArray);
// copy the responseData to our managed byte[]
array.set(new Uint8Array(responseData));
dispatchResponse(
id,
platform.toDotNetString(JSON.stringify(responseDescriptor)),
dotNetArray,
/* errorMessage */ null
);
}
function dispatchErrorResponse(id: number, errorMessage: string) {
dispatchResponse(
id,
/* responseDescriptor */ null,
/* responseText */ null,
platform.toDotNetString(errorMessage)
);
}
function dispatchResponse(id: number, responseDescriptor: System_String | null, responseData: System_Array<any> | null, errorMessage: System_String | null) {
if (!receiveResponseMethod) {
receiveResponseMethod = platform.findMethod(
httpClientAssembly,
httpClientNamespace,
httpClientTypeName,
'ReceiveResponse'
);
}
platform.callMethod(receiveResponseMethod, null, [
platform.toDotNetString(id.toString()),
responseDescriptor,
responseData,
errorMessage,
]);
}
// Keep these in sync with the .NET equivalent in WebAssemblyHttpMessageHandler.cs
interface FetchOptions {
requestUri: string;
requestInit: BlazorRequestInit;
requestInitOverrides: RequestInit;
}
interface BlazorRequestInit {
credentials: string;
headers: Header[];
method: string;
}
interface ResponseDescriptor {
// We don't have BodyText in here because if we did, then in the JSON-response case (which
// is the most common case), we'd be double-encoding it, since the entire ResponseDescriptor
// also gets JSON encoded. It would work but is twice the amount of string processing.
statusCode: number;
statusText: string;
headers: Header[];
}
interface Header {
name: string;
value: string;
}

View File

@ -207,6 +207,37 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("abcdefghijklmn", () => output.Text); Browser.Equal("abcdefghijklmn", () => output.Text);
} }
[Fact]
public void NonInteractiveElementWithDisabledAttributeDoesRespondToMouseEvents()
{
Browser.MountTestComponent<EventDisablingComponent>();
var element = Browser.FindElement(By.Id("disabled-div"));
var eventLog = Browser.FindElement(By.Id("event-log"));
Browser.Equal(string.Empty, () => eventLog.GetAttribute("value"));
element.Click();
Browser.Equal("Got event on div", () => eventLog.GetAttribute("value"));
}
[Theory]
[InlineData("#disabled-button")]
[InlineData("#disabled-button span")]
[InlineData("#disabled-textarea")]
public void InteractiveElementWithDisabledAttributeDoesNotRespondToMouseEvents(string elementSelector)
{
Browser.MountTestComponent<EventDisablingComponent>();
var element = Browser.FindElement(By.CssSelector(elementSelector));
var eventLog = Browser.FindElement(By.Id("event-log"));
Browser.Equal(string.Empty, () => eventLog.GetAttribute("value"));
element.Click();
// It's no use observing that the log is still empty, since maybe the UI just hasn't updated yet
// To be sure that the preceding action has no effect, we need to trigger a different action that does have an effect
Browser.FindElement(By.Id("enabled-button")).Click();
Browser.Equal("Got event on enabled button", () => eventLog.GetAttribute("value"));
}
void SendKeysSequentially(IWebElement target, string text) void SendKeysSequentially(IWebElement target, string text)
{ {
// Calling it for each character works around some chars being skipped // Calling it for each character works around some chars being skipped

View File

@ -89,11 +89,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact] [Fact]
public void CanSendRequestHeaders() public void CanSendRequestHeaders()
{ {
AddRequestHeader("TestHeader", "Value from test"); AddRequestHeader("testheader", "Value from test");
AddRequestHeader("another-header", "Another value"); AddRequestHeader("another-header", "Another value");
IssueRequest("DELETE", "/subdir/api/person"); IssueRequest("DELETE", "/subdir/api/person");
Assert.Equal("OK", _responseStatus.Text); Assert.Equal("OK", _responseStatus.Text);
Assert.Contains("TestHeader: Value from test", _responseBody.Text); Assert.Contains("testheader: Value from test", _responseBody.Text);
Assert.Contains("another-header: Another value", _responseBody.Text); Assert.Contains("another-header: Another value", _responseBody.Text);
} }
@ -107,15 +107,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("{\"id\":123,\"name\":\"Bert\"}", _responseBody.Text); Assert.Equal("{\"id\":123,\"name\":\"Bert\"}", _responseBody.Text);
} }
[Fact]
public void CanSetRequestReferer()
{
SetValue("request-referrer", "/test-referrer");
IssueRequest("GET", "/subdir/api/person/referrer");
Assert.Equal("OK", _responseStatus.Text);
Assert.EndsWith("/test-referrer", _responseBody.Text);
}
[Fact] [Fact]
public void CanSendAndReceiveCookies() public void CanSendAndReceiveCookies()
{ {

View File

@ -0,0 +1,36 @@
<h3 id="event-disabling">Disabling events</h3>
<p>
<div @onclick="@(() => LogEvent("Got event on div"))" id="disabled-div" disabled style="border: 1px dashed red">Even though this div has a 'disabled' attribute, it still responds to mouse events, because you can't disable a div</div>
</p>
<p>
<button @onclick="@(() => LogEvent("Got event on disabled button"))" id="disabled-button" disabled>Since this button is disabled, it doesn't react to mouse events <span style="border: 1px dashed green">even on its children</span></button>
</p>
<p>
<textarea @onclick="@(() => LogEvent("Got event on disabled textarea"))" id="disabled-textarea" disabled>Since this textarea is disabled, it doesn't react to mouse events</textarea>
</p>
<p>
<button @onclick="@(() => LogEvent("Got event on enabled button"))" id="enabled-button">Since this button is enabled, it does react to mouse events <span style="border: 1px dashed green">and so do its children</span></button>
</p>
<h3>Event log</h3>
<textarea id="event-log" readonly @bind="logValue"></textarea>
<button id="clear-log" @onclick="@(() => { logValue = string.Empty; })">Clear log</button>
@code {
string logValue = string.Empty;
void LogEvent(string message)
{
if (logValue != string.Empty)
{
logValue += Environment.NewLine;
}
logValue += message;
}
}

View File

@ -1,6 +1,5 @@
@using System.Net @using System.Net
@using System.Net.Http @using System.Net.Http
@using Microsoft.AspNetCore.Blazor.Http
@inject HttpClient Http @inject HttpClient Http
<h1>HTTP request tester</h1> <h1>HTTP request tester</h1>
@ -38,11 +37,6 @@
<button id="add-header" @onclick="AddHeader">Add</button> <button id="add-header" @onclick="AddHeader">Add</button>
</p> </p>
<p>
<div>Request referrer:</div>
<input id="request-referrer" type="text" @bind=requestReferrer />
</p>
<button id="send-request" @onclick="DoRequest">Request</button> <button id="send-request" @onclick="DoRequest">Request</button>
@if (responseStatusCode.HasValue) @if (responseStatusCode.HasValue)
@ -65,7 +59,6 @@
string method = "GET"; string method = "GET";
string requestBody = ""; string requestBody = "";
List<RequestHeader> requestHeaders = new List<RequestHeader>(); List<RequestHeader> requestHeaders = new List<RequestHeader>();
string requestReferrer = "";
HttpStatusCode? responseStatusCode; HttpStatusCode? responseStatusCode;
string responseBody; string responseBody;
@ -103,14 +96,6 @@
} }
} }
if (!string.IsNullOrEmpty(requestReferrer))
{
requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
{
referrer = requestReferrer
};
}
var response = await Http.SendAsync(requestMessage); var response = await Http.SendAsync(requestMessage);
responseStatusCode = response.StatusCode; responseStatusCode = response.StatusCode;
responseBody = await response.Content.ReadAsStringAsync(); responseBody = await response.Content.ReadAsStringAsync();

View File

@ -24,6 +24,7 @@
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option> <option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option> <option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
<option value="BasicTestApp.EventCasesComponent">Event cases</option> <option value="BasicTestApp.EventCasesComponent">Event cases</option>
<option value="BasicTestApp.EventDisablingComponent">Event disabling</option>
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option> <option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
<option value="BasicTestApp.ExternalContentPackage">External content package</option> <option value="BasicTestApp.ExternalContentPackage">External content package</option>
<option value="BasicTestApp.FocusEventComponent">Focus events</option> <option value="BasicTestApp.FocusEventComponent">Focus events</option>

View File

@ -30,7 +30,7 @@ namespace BasicTestApp
{ {
// Needed because the test server runs on a different port than the client app, // Needed because the test server runs on a different port than the client app,
// and we want to test sending/receiving cookies underling this config // and we want to test sending/receiving cookies underling this config
WebAssemblyHttpMessageHandler.DefaultCredentials = FetchCredentialsOption.Include; WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
} }
app.AddComponent<Index>("root"); app.AddComponent<Index>("root");

View File

@ -2,6 +2,10 @@
// 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.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
@ -29,7 +33,7 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
public StaticWebAssetsFileProvider(string pathPrefix, string contentRoot) public StaticWebAssetsFileProvider(string pathPrefix, string contentRoot)
{ {
BasePath = new PathString(pathPrefix.StartsWith("/") ? pathPrefix : "/" + pathPrefix); BasePath = NormalizePath(pathPrefix);
InnerProvider = new PhysicalFileProvider(contentRoot); InnerProvider = new PhysicalFileProvider(contentRoot);
} }
@ -40,20 +44,30 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
/// <inheritdoc /> /// <inheritdoc />
public IDirectoryContents GetDirectoryContents(string subpath) public IDirectoryContents GetDirectoryContents(string subpath)
{ {
if (!StartsWithBasePath(subpath, out var physicalPath)) var modifiedSub = NormalizePath(subpath);
{
return NotFoundDirectoryContents.Singleton; if (StartsWithBasePath(modifiedSub, out var physicalPath))
}
else
{ {
return InnerProvider.GetDirectoryContents(physicalPath.Value); return InnerProvider.GetDirectoryContents(physicalPath.Value);
} }
else if (string.Equals(subpath, string.Empty) || string.Equals(modifiedSub, "/"))
{
return new StaticWebAssetsDirectoryRoot(BasePath);
}
else if (BasePath.StartsWithSegments(modifiedSub, FilePathComparison, out var remaining))
{
return new StaticWebAssetsDirectoryRoot(remaining);
}
return NotFoundDirectoryContents.Singleton;
} }
/// <inheritdoc /> /// <inheritdoc />
public IFileInfo GetFileInfo(string subpath) public IFileInfo GetFileInfo(string subpath)
{ {
if (!StartsWithBasePath(subpath, out var physicalPath)) var modifiedSub = NormalizePath(subpath);
if (!StartsWithBasePath(modifiedSub, out var physicalPath))
{ {
return new NotFoundFileInfo(subpath); return new NotFoundFileInfo(subpath);
} }
@ -69,9 +83,69 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
return InnerProvider.Watch(filter); return InnerProvider.Watch(filter);
} }
private static string NormalizePath(string path)
{
path = path.Replace('\\', '/');
return path != null && path.StartsWith("/") ? path : "/" + path;
}
private bool StartsWithBasePath(string subpath, out PathString rest) private bool StartsWithBasePath(string subpath, out PathString rest)
{ {
return new PathString(subpath).StartsWithSegments(BasePath, FilePathComparison, out rest); return new PathString(subpath).StartsWithSegments(BasePath, FilePathComparison, out rest);
} }
private class StaticWebAssetsDirectoryRoot : IDirectoryContents
{
private readonly string _nextSegment;
public StaticWebAssetsDirectoryRoot(PathString remainingPath)
{
// We MUST use the Value property here because it is unescaped.
_nextSegment = remainingPath.Value.Split("/", StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
}
public bool Exists => true;
public IEnumerator<IFileInfo> GetEnumerator()
{
return GenerateEnum();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GenerateEnum();
}
private IEnumerator<IFileInfo> GenerateEnum()
{
return new[] { new StaticWebAssetsFileInfo(_nextSegment) }
.Cast<IFileInfo>().GetEnumerator();
}
private class StaticWebAssetsFileInfo : IFileInfo
{
public StaticWebAssetsFileInfo(string name)
{
Name = name;
}
public bool Exists => true;
public long Length => throw new NotImplementedException();
public string PhysicalPath => throw new NotImplementedException();
public DateTimeOffset LastModified => throw new NotImplementedException();
public bool IsDirectory => true;
public string Name { get; }
public Stream CreateReadStream()
{
throw new NotImplementedException();
}
}
}
} }
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@ -9,7 +9,6 @@
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestEventListener.cs" /> <Compile Include="$(SharedSourceRoot)EventSource.Testing\TestEventListener.cs" />
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestCounterListener.cs" /> <Compile Include="$(SharedSourceRoot)EventSource.Testing\TestCounterListener.cs" />
<Content Include="testroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> <Content Include="testroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<None Remove="testroot\wwwroot\Static Web Assets.txt" />
<Content Include="Microsoft.AspNetCore.Hosting.StaticWebAssets.xml" CopyToOutputDirectory="PreserveNewest" /> <Content Include="Microsoft.AspNetCore.Hosting.StaticWebAssets.xml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>

View File

@ -37,6 +37,75 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
Assert.Equal("/_content", provider.BasePath); Assert.Equal("/_content", provider.BasePath);
} }
[Theory]
[InlineData("\\", "_content")]
[InlineData("\\_content\\RazorClassLib\\Dir", "Castle.Core.dll")]
[InlineData("", "_content")]
[InlineData("/", "_content")]
[InlineData("/_content", "RazorClassLib")]
[InlineData("/_content/RazorClassLib", "Dir")]
[InlineData("/_content/RazorClassLib/Dir", "Microsoft.AspNetCore.Hosting.Tests.dll")]
[InlineData("/_content/RazorClassLib/Dir/testroot/", "TextFile.txt")]
[InlineData("/_content/RazorClassLib/Dir/testroot/wwwroot/", "README")]
public void GetDirectoryContents_WalksUpContentRoot(string searchDir, string expected)
{
// Arrange
var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib/Dir", AppContext.BaseDirectory);
// Act
var directory = provider.GetDirectoryContents(searchDir);
// Assert
Assert.NotEmpty(directory);
Assert.Contains(directory, file => string.Equals(file.Name, expected));
}
[Fact]
public void GetDirectoryContents_DoesNotFindNonExistentFiles()
{
// Arrange
var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib/", AppContext.BaseDirectory);
// Act
var directory = provider.GetDirectoryContents("/_content/RazorClassLib/False");
// Assert
Assert.Empty(directory);
}
[Theory]
[InlineData("/False/_content/RazorClassLib/")]
[InlineData("/_content/RazorClass")]
public void GetDirectoryContents_PartialMatchFails(string requestedUrl)
{
// Arrange
var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib", AppContext.BaseDirectory);
// Act
var directory = provider.GetDirectoryContents(requestedUrl);
// Assert
Assert.Empty(directory);
}
[Fact]
public void GetDirectoryContents_HandlesWhitespaceInBase()
{
// Arrange
var provider = new StaticWebAssetsFileProvider("/_content/Static Web Assets",
Path.Combine(AppContext.BaseDirectory, "testroot", "wwwroot"));
// Act
var directory = provider.GetDirectoryContents("/_content/Static Web Assets/Static Web/");
// Assert
Assert.Collection(directory,
file =>
{
Assert.Equal("Static Web.txt", file.Name);
});
}
[Fact] [Fact]
public void StaticWebAssetsFileProvider_FindsFileWithSpaces() public void StaticWebAssetsFileProvider_FindsFileWithSpaces()
{ {

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@ -65,8 +66,33 @@ namespace Microsoft.AspNetCore.TestHost
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext); var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext);
var requestContent = request.Content ?? new StreamContent(Stream.Null); var requestContent = request.Content ?? new StreamContent(Stream.Null);
var body = await requestContent.ReadAsStreamAsync();
contextBuilder.Configure(context => // Read content from the request HttpContent into a pipe in a background task. This will allow the request
// delegate to start before the request HttpContent is complete. A background task allows duplex streaming scenarios.
contextBuilder.SendRequestStream(async writer =>
{
if (requestContent is StreamContent)
{
// This is odd but required for backwards compat. If StreamContent is passed in then seek to beginning.
// This is safe because StreamContent.ReadAsStreamAsync doesn't block. It will return the inner stream.
var body = await requestContent.ReadAsStreamAsync();
if (body.CanSeek)
{
// This body may have been consumed before, rewind it.
body.Seek(0, SeekOrigin.Begin);
}
await body.CopyToAsync(writer);
}
else
{
await requestContent.CopyToAsync(writer.AsStream());
}
await writer.CompleteAsync();
});
contextBuilder.Configure((context, reader) =>
{ {
var req = context.Request; var req = context.Request;
@ -115,12 +141,7 @@ namespace Microsoft.AspNetCore.TestHost
} }
} }
if (body.CanSeek) req.Body = new AsyncStreamWrapper(reader.AsStream(), () => contextBuilder.AllowSynchronousIO);
{
// This body may have been consumed before, rewind it.
body.Seek(0, SeekOrigin.Begin);
}
req.Body = new AsyncStreamWrapper(body, () => contextBuilder.AllowSynchronousIO);
}); });
var response = new HttpResponseMessage(); var response = new HttpResponseMessage();

View File

@ -26,7 +26,10 @@ namespace Microsoft.AspNetCore.TestHost
private bool _pipelineFinished; private bool _pipelineFinished;
private bool _returningResponse; private bool _returningResponse;
private object _testContext; private object _testContext;
private Pipe _requestPipe;
private Action<HttpContext> _responseReadCompleteCallback; private Action<HttpContext> _responseReadCompleteCallback;
private Task _sendRequestStreamTask;
internal HttpContextBuilder(ApplicationWrapper application, bool allowSynchronousIO, bool preserveExecutionContext) internal HttpContextBuilder(ApplicationWrapper application, bool allowSynchronousIO, bool preserveExecutionContext)
{ {
@ -41,9 +44,11 @@ namespace Microsoft.AspNetCore.TestHost
request.Protocol = "HTTP/1.1"; request.Protocol = "HTTP/1.1";
request.Method = HttpMethods.Get; request.Method = HttpMethods.Get;
var pipe = new Pipe(); _requestPipe = new Pipe();
_responseReaderStream = new ResponseBodyReaderStream(pipe, ClientInitiatedAbort, () => _responseReadCompleteCallback?.Invoke(_httpContext));
_responsePipeWriter = new ResponseBodyPipeWriter(pipe, ReturnResponseMessageAsync); var responsePipe = new Pipe();
_responseReaderStream = new ResponseBodyReaderStream(responsePipe, ClientInitiatedAbort, () => _responseReadCompleteCallback?.Invoke(_httpContext));
_responsePipeWriter = new ResponseBodyPipeWriter(responsePipe, ReturnResponseMessageAsync);
_responseFeature.Body = new ResponseBodyWriterStream(_responsePipeWriter, () => AllowSynchronousIO); _responseFeature.Body = new ResponseBodyWriterStream(_responsePipeWriter, () => AllowSynchronousIO);
_responseFeature.BodyWriter = _responsePipeWriter; _responseFeature.BodyWriter = _responsePipeWriter;
@ -56,14 +61,24 @@ namespace Microsoft.AspNetCore.TestHost
public bool AllowSynchronousIO { get; set; } public bool AllowSynchronousIO { get; set; }
internal void Configure(Action<HttpContext> configureContext) internal void Configure(Action<HttpContext, PipeReader> configureContext)
{ {
if (configureContext == null) if (configureContext == null)
{ {
throw new ArgumentNullException(nameof(configureContext)); throw new ArgumentNullException(nameof(configureContext));
} }
configureContext(_httpContext); configureContext(_httpContext, _requestPipe.Reader);
}
internal void SendRequestStream(Func<PipeWriter, Task> sendRequestStream)
{
if (sendRequestStream == null)
{
throw new ArgumentNullException(nameof(sendRequestStream));
}
_sendRequestStreamTask = sendRequestStream(_requestPipe.Writer);
} }
internal void RegisterResponseReadCompleteCallback(Action<HttpContext> responseReadCompleteCallback) internal void RegisterResponseReadCompleteCallback(Action<HttpContext> responseReadCompleteCallback)
@ -92,10 +107,10 @@ namespace Microsoft.AspNetCore.TestHost
// since we are now inside the Server's execution context. If it happens outside this cont // since we are now inside the Server's execution context. If it happens outside this cont
// it will be lost when we abandon the execution context. // it will be lost when we abandon the execution context.
_testContext = _application.CreateContext(_httpContext.Features); _testContext = _application.CreateContext(_httpContext.Features);
try try
{ {
await _application.ProcessRequestAsync(_testContext); await _application.ProcessRequestAsync(_testContext);
await CompleteRequestAsync();
await CompleteResponseAsync(); await CompleteResponseAsync();
_application.DisposeContext(_testContext, exception: null); _application.DisposeContext(_testContext, exception: null);
} }
@ -134,8 +149,40 @@ namespace Microsoft.AspNetCore.TestHost
// We don't want to trigger the token for already completed responses. // We don't want to trigger the token for already completed responses.
_requestLifetimeFeature.Cancel(); _requestLifetimeFeature.Cancel();
} }
// Writes will still succeed, the app will only get an error if they check the CT. // Writes will still succeed, the app will only get an error if they check the CT.
_responseReaderStream.Abort(new IOException("The client aborted the request.")); _responseReaderStream.Abort(new IOException("The client aborted the request."));
// Cancel any pending request async activity when the client aborts a duplex
// streaming scenario by disposing the HttpResponseMessage.
CancelRequestBody();
}
private async Task CompleteRequestAsync()
{
if (!_requestPipe.Reader.TryRead(out var result) || !result.IsCompleted)
{
// If request is still in progress then abort it.
CancelRequestBody();
}
else
{
// Writer was already completed in send request callback.
await _requestPipe.Reader.CompleteAsync();
}
if (_sendRequestStreamTask != null)
{
try
{
// Ensure duplex request is either completely read or has been aborted.
await _sendRequestStreamTask;
}
catch (OperationCanceledException)
{
// Request was canceled, likely because it wasn't read before the request ended.
}
}
} }
internal async Task CompleteResponseAsync() internal async Task CompleteResponseAsync()
@ -192,6 +239,13 @@ namespace Microsoft.AspNetCore.TestHost
_responseReaderStream.Abort(exception); _responseReaderStream.Abort(exception);
_requestLifetimeFeature.Cancel(); _requestLifetimeFeature.Cancel();
_responseTcs.TrySetException(exception); _responseTcs.TrySetException(exception);
CancelRequestBody();
}
private void CancelRequestBody()
{
_requestPipe.Writer.CancelPendingFlush();
_requestPipe.Reader.CancelPendingRead();
} }
void IHttpResetFeature.Reset(int errorCode) void IHttpResetFeature.Reset(int errorCode)

View File

@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.TestHost
} }
var builder = new HttpContextBuilder(Application, AllowSynchronousIO, PreserveExecutionContext); var builder = new HttpContextBuilder(Application, AllowSynchronousIO, PreserveExecutionContext);
builder.Configure(context => builder.Configure((context, reader) =>
{ {
var request = context.Request; var request = context.Request;
request.Scheme = BaseAddress.Scheme; request.Scheme = BaseAddress.Scheme;
@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.TestHost
} }
request.PathBase = pathBase; request.PathBase = pathBase;
}); });
builder.Configure(configureContext); builder.Configure((context, reader) => configureContext(context));
// TODO: Wrap the request body if any? // TODO: Wrap the request body if any?
return await builder.SendAsync(cancellationToken).ConfigureAwait(false); return await builder.SendAsync(cancellationToken).ConfigureAwait(false);
} }

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.TestHost
{ {
WebSocketFeature webSocketFeature = null; WebSocketFeature webSocketFeature = null;
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext); var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext);
contextBuilder.Configure(context => contextBuilder.Configure((context, reader) =>
{ {
var request = context.Request; var request = context.Request;
var scheme = uri.Scheme; var scheme = uri.Scheme;

View File

@ -4,6 +4,10 @@
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="..\..\..\Shared\SyncPoint\SyncPoint.cs" Link="SyncPoint.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.TestHost" /> <Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.Extensions.DiagnosticAdapter" /> <Reference Include="Microsoft.Extensions.DiagnosticAdapter" />

View File

@ -5,6 +5,7 @@ using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
@ -13,6 +14,7 @@ using System.Threading.Tasks;
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.AspNetCore.Internal;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -90,17 +92,20 @@ namespace Microsoft.AspNetCore.TestHost
{ {
// Arrange // Arrange
RequestDelegate appDelegate = async ctx => RequestDelegate appDelegate = async ctx =>
await ctx.Response.WriteAsync(await new StreamReader(ctx.Request.Body).ReadToEndAsync() + " PUT Response"); {
var content = await new StreamReader(ctx.Request.Body).ReadToEndAsync();
await ctx.Response.WriteAsync(content + " PUT Response");
};
var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate));
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); var client = server.CreateClient();
// Act // Act
var content = new StringContent("Hello world"); var content = new StringContent("Hello world");
var response = await client.PutAsync("http://localhost:12345", content); var response = await client.PutAsync("http://localhost:12345", content).WithTimeout();
// Assert // Assert
Assert.Equal("Hello world PUT Response", await response.Content.ReadAsStringAsync()); Assert.Equal("Hello world PUT Response", await response.Content.ReadAsStringAsync().WithTimeout());
} }
[Fact] [Fact]
@ -115,10 +120,10 @@ namespace Microsoft.AspNetCore.TestHost
// Act // Act
var content = new StringContent("Hello world"); var content = new StringContent("Hello world");
var response = await client.PostAsync("http://localhost:12345", content); var response = await client.PostAsync("http://localhost:12345", content).WithTimeout();
// Assert // Assert
Assert.Equal("Hello world POST Response", await response.Content.ReadAsStringAsync()); Assert.Equal("Hello world POST Response", await response.Content.ReadAsStringAsync().WithTimeout());
} }
[Fact] [Fact]
@ -163,6 +168,296 @@ namespace Microsoft.AspNetCore.TestHost
} }
} }
[Fact]
public async Task ClientStreamingWorks()
{
// Arrange
var responseStartedSyncPoint = new SyncPoint();
var requestEndingSyncPoint = new SyncPoint();
var requestStreamSyncPoint = new SyncPoint();
RequestDelegate appDelegate = async ctx =>
{
// Send headers
await ctx.Response.BodyWriter.FlushAsync();
// Ensure headers received by client
await responseStartedSyncPoint.WaitToContinue();
await ctx.Response.WriteAsync("STARTED");
// ReadToEndAsync will wait until request body is complete
var requestString = await new StreamReader(ctx.Request.Body).ReadToEndAsync();
await ctx.Response.WriteAsync(requestString + " POST Response");
await requestEndingSyncPoint.WaitToContinue();
};
Stream requestStream = null;
var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate));
var server = new TestServer(builder);
var client = server.CreateClient();
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://localhost:12345");
httpRequest.Version = new Version(2, 0);
httpRequest.Content = new PushContent(async stream =>
{
requestStream = stream;
await requestStreamSyncPoint.WaitToContinue();
});
// Act
var response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).WithTimeout();
await responseStartedSyncPoint.WaitForSyncPoint().WithTimeout();
responseStartedSyncPoint.Continue();
var responseContent = await response.Content.ReadAsStreamAsync().WithTimeout();
// Assert
// Ensure request stream has started
await requestStreamSyncPoint.WaitForSyncPoint();
byte[] buffer = new byte[1024];
var length = await responseContent.ReadAsync(buffer).AsTask().WithTimeout();
Assert.Equal("STARTED", Encoding.UTF8.GetString(buffer, 0, length));
// Send content and finish request body
await requestStream.WriteAsync(Encoding.UTF8.GetBytes("Hello world")).AsTask().WithTimeout();
await requestStream.FlushAsync().WithTimeout();
requestStreamSyncPoint.Continue();
// Ensure content is received while request is in progress
length = await responseContent.ReadAsync(buffer).AsTask().WithTimeout();
Assert.Equal("Hello world POST Response", Encoding.UTF8.GetString(buffer, 0, length));
// Request is ending
await requestEndingSyncPoint.WaitForSyncPoint().WithTimeout();
requestEndingSyncPoint.Continue();
// No more response content
length = await responseContent.ReadAsync(buffer).AsTask().WithTimeout();
Assert.Equal(0, length);
}
[Fact]
public async Task ClientStreaming_Cancellation()
{
// Arrange
var responseStartedSyncPoint = new SyncPoint();
var responseReadSyncPoint = new SyncPoint();
var responseEndingSyncPoint = new SyncPoint();
var requestStreamSyncPoint = new SyncPoint();
var readCanceled = false;
RequestDelegate appDelegate = async ctx =>
{
// Send headers
await ctx.Response.BodyWriter.FlushAsync();
// Ensure headers received by client
await responseStartedSyncPoint.WaitToContinue();
var serverBuffer = new byte[1024];
var serverLength = await ctx.Request.Body.ReadAsync(serverBuffer);
Assert.Equal("SENT", Encoding.UTF8.GetString(serverBuffer, 0, serverLength));
await responseReadSyncPoint.WaitToContinue();
try
{
await ctx.Request.Body.ReadAsync(serverBuffer);
}
catch (OperationCanceledException)
{
readCanceled = true;
}
await responseEndingSyncPoint.WaitToContinue();
};
Stream requestStream = null;
var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate));
var server = new TestServer(builder);
var client = server.CreateClient();
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://localhost:12345");
httpRequest.Version = new Version(2, 0);
httpRequest.Content = new PushContent(async stream =>
{
requestStream = stream;
await requestStreamSyncPoint.WaitToContinue();
});
// Act
var response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).WithTimeout();
await responseStartedSyncPoint.WaitForSyncPoint().WithTimeout();
responseStartedSyncPoint.Continue();
var responseContent = await response.Content.ReadAsStreamAsync().WithTimeout();
// Assert
// Ensure request stream has started
await requestStreamSyncPoint.WaitForSyncPoint();
// Write to request
await requestStream.WriteAsync(Encoding.UTF8.GetBytes("SENT")).AsTask().WithTimeout();
await requestStream.FlushAsync().WithTimeout();
await responseReadSyncPoint.WaitForSyncPoint().WithTimeout();
// Cancel request. Disposing response must be used because SendAsync has finished.
response.Dispose();
responseReadSyncPoint.Continue();
await responseEndingSyncPoint.WaitForSyncPoint().WithTimeout();
responseEndingSyncPoint.Continue();
Assert.True(readCanceled);
requestStreamSyncPoint.Continue();
}
[Fact]
public async Task ClientStreaming_ResponseCompletesWithoutReadingRequest()
{
// Arrange
var requestStreamTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var responseEndingSyncPoint = new SyncPoint();
RequestDelegate appDelegate = async ctx =>
{
await ctx.Response.WriteAsync("POST Response");
await responseEndingSyncPoint.WaitToContinue();
};
Stream requestStream = null;
var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate));
var server = new TestServer(builder);
var client = server.CreateClient();
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://localhost:12345");
httpRequest.Version = new Version(2, 0);
httpRequest.Content = new PushContent(async stream =>
{
requestStream = stream;
await requestStreamTcs.Task;
});
// Act
var response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).WithTimeout();
var responseContent = await response.Content.ReadAsStreamAsync().WithTimeout();
// Assert
// Read response
byte[] buffer = new byte[1024];
var length = await responseContent.ReadAsync(buffer).AsTask().WithTimeout();
Assert.Equal("POST Response", Encoding.UTF8.GetString(buffer, 0, length));
// Send large content and block on back pressure
var writeTask = Task.Run(async () =>
{
try
{
await requestStream.WriteAsync(Encoding.UTF8.GetBytes(new string('!', 1024 * 1024 * 50))).AsTask().WithTimeout();
requestStreamTcs.SetResult(null);
}
catch (Exception ex)
{
requestStreamTcs.SetException(ex);
}
});
responseEndingSyncPoint.Continue();
// No more response content
length = await responseContent.ReadAsync(buffer).AsTask().WithTimeout();
Assert.Equal(0, length);
await writeTask;
}
[Fact]
public async Task ClientStreaming_ServerAbort()
{
// Arrange
var requestStreamSyncPoint = new SyncPoint();
var responseEndingSyncPoint = new SyncPoint();
RequestDelegate appDelegate = async ctx =>
{
// Send headers
await ctx.Response.BodyWriter.FlushAsync();
ctx.Abort();
await responseEndingSyncPoint.WaitToContinue();
};
Stream requestStream = null;
var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate));
var server = new TestServer(builder);
var client = server.CreateClient();
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://localhost:12345");
httpRequest.Version = new Version(2, 0);
httpRequest.Content = new PushContent(async stream =>
{
requestStream = stream;
await requestStreamSyncPoint.WaitToContinue();
});
// Act
var response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).WithTimeout();
var responseContent = await response.Content.ReadAsStreamAsync().WithTimeout();
// Assert
// Ensure server has aborted
await responseEndingSyncPoint.WaitForSyncPoint();
// Ensure request stream has started
await requestStreamSyncPoint.WaitForSyncPoint();
// Send content and finish request body
await ExceptionAssert.ThrowsAsync<OperationCanceledException>(
() => requestStream.WriteAsync(Encoding.UTF8.GetBytes("Hello world")).AsTask(),
"Flush was canceled on underlying PipeWriter.").WithTimeout();
responseEndingSyncPoint.Continue();
requestStreamSyncPoint.Continue();
}
private class PushContent : HttpContent
{
private readonly Func<Stream, Task> _sendContent;
public PushContent(Func<Stream, Task> sendContent)
{
_sendContent = sendContent;
}
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
return _sendContent(stream);
}
protected override bool TryComputeLength(out long length)
{
length = -1;
return false;
}
}
[Fact] [Fact]
public async Task WebSocketWorks() public async Task WebSocketWorks()
{ {

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing;
namespace Microsoft.AspNetCore.TestHost namespace Microsoft.AspNetCore.TestHost
{ {
@ -10,20 +11,8 @@ namespace Microsoft.AspNetCore.TestHost
{ {
internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15); internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.WithTimeout(DefaultTimeout); internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);
internal static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout) internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout));
if (completedTask == task)
{
return await task;
}
else
{
throw new TimeoutException("The task has timed out.");
}
}
} }
} }

View File

@ -489,6 +489,55 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never()); testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never());
} }
[Fact]
public async Task ReadAsync_WithEnableBufferingWorks()
{
// Arrange
var formatter = GetInputFormatter();
var content = "{\"name\": \"Test\"}";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
httpContext.Request.EnableBuffering();
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
var userModel = Assert.IsType<ComplexModel>(result.Model);
Assert.Equal("Test", userModel.Name);
var requestBody = httpContext.Request.Body;
requestBody.Position = 0;
Assert.Equal(content, new StreamReader(requestBody).ReadToEnd());
}
[Fact]
public async Task ReadAsync_WithEnableBufferingWorks_WithInputStreamAtOffset()
{
// Arrange
var formatter = GetInputFormatter();
var content = "abc{\"name\": \"Test\"}";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
httpContext.Request.EnableBuffering();
var requestBody = httpContext.Request.Body;
requestBody.Position = 3;
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
var userModel = Assert.IsType<ComplexModel>(result.Model);
Assert.Equal("Test", userModel.Name);
requestBody.Position = 0;
Assert.Equal(content, new StreamReader(requestBody).ReadToEnd());
}
internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; } internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; }
internal abstract string JsonFormatter_EscapedKeys_Expected { get; } internal abstract string JsonFormatter_EscapedKeys_Expected { get; }

View File

@ -120,7 +120,17 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Stream readStream = new NonDisposableStream(request.Body); Stream readStream = new NonDisposableStream(request.Body);
var disposeReadStream = false; var disposeReadStream = false;
if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering) if (readStream.CanSeek)
{
// The most common way of getting here is the user has request buffering on.
// However, request buffering isn't eager, and consequently it will peform pass-thru synchronous
// reads as part of the deserialization.
// To avoid this, drain and reset the stream.
var position = request.Body.Position;
await readStream.DrainAsync(CancellationToken.None);
readStream.Position = position;
}
else if (!_options.SuppressInputFormatterBuffering)
{ {
// XmlDataContractSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously // XmlDataContractSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously
// read everything into a buffer, and then seek back to the beginning. // read everything into a buffer, and then seek back to the beginning.

View File

@ -101,7 +101,17 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Stream readStream = new NonDisposableStream(request.Body); Stream readStream = new NonDisposableStream(request.Body);
var disposeReadStream = false; var disposeReadStream = false;
if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering) if (readStream.CanSeek)
{
// The most common way of getting here is the user has request buffering on.
// However, request buffering isn't eager, and consequently it will peform pass-thru synchronous
// reads as part of the deserialization.
// To avoid this, drain and reset the stream.
var position = request.Body.Position;
await readStream.DrainAsync(CancellationToken.None);
readStream.Position = position;
}
else if (!_options.SuppressInputFormatterBuffering)
{ {
// XmlSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously // XmlSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously
// read everything into a buffer, and then seek back to the beginning. // read everything into a buffer, and then seek back to the beginning.

View File

@ -130,7 +130,17 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var readStream = request.Body; var readStream = request.Body;
var disposeReadStream = false; var disposeReadStream = false;
if (!request.Body.CanSeek && !suppressInputFormatterBuffering) if (readStream.CanSeek)
{
// The most common way of getting here is the user has request buffering on.
// However, request buffering isn't eager, and consequently it will peform pass-thru synchronous
// reads as part of the deserialization.
// To avoid this, drain and reset the stream.
var position = request.Body.Position;
await readStream.DrainAsync(CancellationToken.None);
readStream.Position = position;
}
else if (!suppressInputFormatterBuffering)
{ {
// JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously // JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously
// read everything into a buffer, and then seek back to the beginning. // read everything into a buffer, and then seek back to the beginning.

View File

@ -65,6 +65,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
for (var i = 0; i < candidates.Count; i++) for (var i = 0; i < candidates.Count; i++)
{ {
if (!candidates.IsValidCandidate(i))
{
continue;
}
ref var candidate = ref candidates[i]; ref var candidate = ref candidates[i];
var endpoint = candidate.Endpoint; var endpoint = candidate.Endpoint;

View File

@ -14,14 +14,16 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// <summary> /// <summary>
/// A <see cref="TagHelper"/> that renders a Razor component. /// A <see cref="TagHelper"/> that renders a Razor component.
/// </summary> /// </summary>
[HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement(TagHelperName, Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
public sealed class ComponentTagHelper : TagHelper public sealed class ComponentTagHelper : TagHelper
{ {
private const string TagHelperName = "component";
private const string ComponentParameterName = "params"; private const string ComponentParameterName = "params";
private const string ComponentParameterPrefix = "param-"; private const string ComponentParameterPrefix = "param-";
private const string ComponentTypeName = "type"; private const string ComponentTypeName = "type";
private const string RenderModeName = "render-mode"; private const string RenderModeName = "render-mode";
private IDictionary<string, object> _parameters; private IDictionary<string, object> _parameters;
private RenderMode? _renderMode;
/// <summary> /// <summary>
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request. /// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
@ -54,7 +56,29 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// Gets or sets the <see cref="Rendering.RenderMode"/> /// Gets or sets the <see cref="Rendering.RenderMode"/>
/// </summary> /// </summary>
[HtmlAttributeName(RenderModeName)] [HtmlAttributeName(RenderModeName)]
public RenderMode RenderMode { get; set; } public RenderMode RenderMode
{
get => _renderMode ?? default;
set
{
switch (value)
{
case RenderMode.Server:
case RenderMode.ServerPrerendered:
case RenderMode.Static:
_renderMode = value;
break;
default:
throw new ArgumentException(
message: Resources.FormatInvalidEnumArgument(
nameof(value),
value,
typeof(RenderMode).FullName),
paramName: nameof(value));
}
}
}
/// <inheritdoc /> /// <inheritdoc />
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
@ -69,6 +93,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(output)); throw new ArgumentNullException(nameof(output));
} }
if (_renderMode is null)
{
throw new ArgumentException(Resources.FormatAttributeIsRequired(RenderModeName, TagHelperName), nameof(RenderMode));
}
var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>(); var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters); var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters);

View File

@ -162,4 +162,7 @@
<data name="ViewEngine_FallbackViewNotFound" xml:space="preserve"> <data name="ViewEngine_FallbackViewNotFound" xml:space="preserve">
<value>The fallback partial view '{0}' was not found. The following locations were searched:{1}</value> <value>The fallback partial view '{0}' was not found. The following locations were searched:{1}</value>
</data> </data>
<data name="AttributeIsRequired" xml:space="preserve">
<value>A value for the '{0}' attribute must be supplied to the '{1}' tag helper.</value>
</data>
</root> </root>

View File

@ -3,13 +3,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Pipes;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using Xunit; using Xunit;
@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var tagHelper = new ComponentTagHelper var tagHelper = new ComponentTagHelper
{ {
ViewContext = GetViewContext(), ViewContext = GetViewContext(),
RenderMode = RenderMode.Static,
}; };
var context = GetTagHelperContext(); var context = GetTagHelperContext();
var output = GetTagHelperOutput(); var output = GetTagHelperOutput();
@ -38,6 +39,24 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Null(output.TagName); Assert.Null(output.TagName);
} }
[Fact]
public async Task ProcessAsync_WithoutSpecifyingRenderMode_ThrowsError()
{
// Arrange
var tagHelper = new ComponentTagHelper
{
ViewContext = GetViewContext(),
};
var context = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act & Assert
await ExceptionAssert.ThrowsArgumentAsync(
() => tagHelper.ProcessAsync(context, output),
nameof(RenderMode),
"A value for the 'render-mode' attribute must be supplied to the 'component' tag helper.");
}
private static TagHelperContext GetTagHelperContext() private static TagHelperContext GetTagHelperContext()
{ {
return new TagHelperContext( return new TagHelperContext(

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@ -42,8 +43,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
values, values,
invocationId.Value); invocationId.Value);
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions); var serializedServerComponentBytes = JsonSerializer.SerializeToUtf8Bytes(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
return (serverComponent.Sequence, _dataProtector.Protect(serializedServerComponent, ServerComponentSerializationSettings.DataExpiration)); var protectedBytes = _dataProtector.Protect(serializedServerComponentBytes, ServerComponentSerializationSettings.DataExpiration);
return (serverComponent.Sequence, Convert.ToBase64String(protectedBytes));
} }
internal IEnumerable<string> GetPreamble(ServerComponentMarker record) internal IEnumerable<string> GetPreamble(ServerComponentMarker record)

View File

@ -16,6 +16,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="..\..\Mvc.Formatters.Xml\test\XmlAssert.cs" /> <Compile Include="..\..\Mvc.Formatters.Xml\test\XmlAssert.cs" />
<EmbeddedResource Include="compiler\resources\**\*" /> <EmbeddedResource Include="compiler\resources\**\*" />
<None Remove="compiler\resources\TagHelpersWebSite.Home.GlobbingTagHelpers.html" />
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>

View File

@ -6,6 +6,8 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using RoutingWebSite;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@ -14,12 +16,13 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{ {
public RoutingDynamicTest(MvcTestFixture<RoutingWebSite.StartupForDynamic> fixture) public RoutingDynamicTest(MvcTestFixture<RoutingWebSite.StartupForDynamic> fixture)
{ {
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
Client = factory.CreateDefaultClient(); Client = Factory.CreateDefaultClient();
} }
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup<RoutingWebSite.StartupForDynamic>(); private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup<RoutingWebSite.StartupForDynamic>();
public WebApplicationFactory<StartupForDynamic> Factory { get; }
public HttpClient Client { get; } public HttpClient Client { get; }
[Fact] [Fact]
@ -99,5 +102,39 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello from dynamic page: /DynamicPage", content); Assert.Equal("Hello from dynamic page: /DynamicPage", content);
} }
[Fact]
public async Task AppWithDynamicRouteAndMapRazorPages_CanRouteToRazorPage()
{
// Regression test for https://github.com/aspnet/AspNetCore/issues/13996
// Arrange
var client = Factory.WithWebHostBuilder(b => b.UseStartup<StartupForDynamicAndRazorPages>()).CreateDefaultClient();
var url = "/PageWithLinks";
// Act
var response = await client.GetAsync(url);
// Assert
var document = await response.GetHtmlDocumentAsync();
var editLink = document.RequiredQuerySelector("#editlink");
Assert.Equal("/Edit/10", editLink.GetAttribute("href"));
}
[Fact]
public async Task AppWithDynamicRouteAndMapRazorPages_CanRouteToDynamicController()
{
// Regression test for https://github.com/aspnet/AspNetCore/issues/13996
// Arrange
var client = Factory.WithWebHostBuilder(b => b.UseStartup<StartupForDynamicAndRazorPages>()).CreateDefaultClient();
var url = "/de/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
// Act
var response = await client.GetAsync(url);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
Assert.StartsWith("Hello from dynamic controller", content);
}
} }
} }

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public HttpClient EncodedClient { get; } public HttpClient EncodedClient { get; }
[Theory] [Theory]
[InlineData("GlobbingTagHelpers")]
[InlineData("Index")] [InlineData("Index")]
[InlineData("About")] [InlineData("About")]
[InlineData("Help")] [InlineData("Help")]

View File

@ -0,0 +1,39 @@

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Globbing Page - My ASP.NET Application</title>
</head>
<body>
<h1 style="font-family: cursive;">ASP.NET vNext - Globbing Page</h1>
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
| <a href="/home/about" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
| <a href="/home/help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
<div>
Globbing
<hr />
<footer>
<!-- File wildcard -->
<script src="/js/dist/dashboardHome.js"></script>
<!-- RazorClassLib folder wildcard -->
<script src="/_content/RazorPagesClassLibrary/razorlib/file.js"></script>
<!-- RazorClassLib deep wildcard -->
<script src="/js/dist/dashboardHome.js"></script><script src="/_content/RazorPagesClassLibrary/razorlib/file.js"></script>
<!-- RazorClassLib Exclude local -->
<script src="/_content/RazorPagesClassLibrary/razorlib/file.js"></script>
<!-- local Exclude lib-->
<script src="/js/dist/dashboardHome.js"></script>
</footer>
</div>
</body>
</html>

View File

@ -5,7 +5,7 @@
<IsTestAssetProject>true</IsTestAssetProject> <IsTestAssetProject>true</IsTestAssetProject>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc> <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" /> <Reference Include="Microsoft.AspNetCore.Mvc" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,5 @@
window.exampleJsFunctions = {
showPrompt: function (message) {
return prompt(message, 'Type anything here')
}
};

View File

@ -0,0 +1,62 @@
// 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.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
namespace RoutingWebSite
{
// For by tests for a mix of dynamic routing + Razor Pages
public class StartupForDynamicAndRazorPages
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Latest);
services.AddSingleton<Transformer>();
// Used by some controllers defined in this project.
services.Configure<RouteOptions>(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapDynamicControllerRoute<Transformer>("{language}/{**slug}");
});
}
private class Transformer : DynamicRouteValueTransformer
{
// Turns a format like `controller=Home,action=Index` into an RVD
public override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
if (!(values["slug"] is string slug))
{
return new ValueTask<RouteValueDictionary>(values);
}
var kvps = slug.Split(",");
var results = new RouteValueDictionary();
foreach (var kvp in kvps)
{
var split = kvp.Split("=");
results[split[0]] = split[1];
}
return new ValueTask<RouteValueDictionary>(results);
}
}
}
}

View File

@ -25,6 +25,11 @@ namespace TagHelpersWebSite.Controllers
return View(); return View();
} }
public IActionResult GlobbingTagHelpers()
{
return View();
}
public IActionResult Help() public IActionResult Help()
{ {
return View(); return View();

View File

@ -21,6 +21,7 @@ namespace TagHelpersWebSite
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
{ {
app.UseRouting(); app.UseRouting();
app.UseStaticFiles();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapDefaultControllerRoute(); endpoints.MapDefaultControllerRoute();
@ -38,6 +39,7 @@ namespace TagHelpersWebSite
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
new WebHostBuilder() new WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory()) .UseContentRoot(Directory.GetCurrentDirectory())
.UseStaticWebAssets()
.UseStartup<Startup>() .UseStartup<Startup>()
.UseKestrel() .UseKestrel()
.UseIISIntegration(); .UseIISIntegration();

View File

@ -6,6 +6,10 @@
<IsTestAssetProject>true</IsTestAssetProject> <IsTestAssetProject>true</IsTestAssetProject>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RazorPagesClassLibrary\RazorPagesClassLibrary.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" /> <Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" /> <Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />

View File

@ -0,0 +1,19 @@
@{
ViewData["Title"] = "Globbing Page";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@section footerContent {
<!-- File wildcard -->
<script asp-src-include="/js/dist/dashboard*.js"></script>
<!-- RazorClassLib folder wildcard -->
<script asp-src-include="/_content/RazorPagesClassLibrary/**/file.js"></script>
<!-- RazorClassLib deep wildcard -->
<script asp-src-include="/**/*.js"></script>
<!-- RazorClassLib Exclude local -->
<script asp-src-exclude="/js/dist/*.js" asp-src-include="**/*.js"></script>
<!-- local Exclude lib-->
<script asp-src-exclude="/_content/**/*.js" asp-src-include="**/*.js"></script>
}
Globbing

View File

@ -0,0 +1 @@


View File

@ -1,6 +1,9 @@
@page "/" @page "/"
@namespace BlazorServerWeb_CSharp.Pages @namespace BlazorServerWeb_CSharp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">

View File

@ -12,47 +12,47 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "8.0.0", "@angular/animations": "8.2.12",
"@angular/common": "8.0.0", "@angular/common": "8.2.12",
"@angular/compiler": "8.0.0", "@angular/compiler": "8.2.12",
"@angular/core": "8.0.0", "@angular/core": "8.2.12",
"@angular/forms": "8.0.0", "@angular/forms": "8.2.12",
"@angular/platform-browser": "8.0.0", "@angular/platform-browser": "8.2.12",
"@angular/platform-browser-dynamic": "8.0.0", "@angular/platform-browser-dynamic": "8.2.12",
"@angular/platform-server": "8.0.0", "@angular/platform-server": "8.2.12",
"@angular/router": "8.0.0", "@angular/router": "8.2.12",
"@nguniversal/module-map-ngfactory-loader": "8.0.0-rc.1", "@nguniversal/module-map-ngfactory-loader": "8.1.1",
"aspnet-prerendering": "^3.0.1", "aspnet-prerendering": "^3.0.1",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"core-js": "^2.6.5", "core-js": "^3.3.3",
"jquery": "3.4.1", "jquery": "3.4.1",
"oidc-client": "^1.9.0", "oidc-client": "^1.9.1",
"popper.js": "^1.14.3", "popper.js": "^1.16.0",
"rxjs": "^6.4.0", "rxjs": "^6.5.3",
"zone.js": "~0.9.1" "zone.js": "0.9.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.800.6", "@angular-devkit/build-angular": "^0.803.14",
"@angular/cli": "8.0.6", "@angular/cli": "8.3.14",
"@angular/compiler-cli": "8.0.0", "@angular/compiler-cli": "8.2.12",
"@angular/language-service": "8.0.0", "@angular/language-service": "8.2.12",
"@types/jasmine": "~3.3.9", "@types/jasmine": "~3.4.4",
"@types/jasminewd2": "~2.0.6", "@types/jasminewd2": "~2.0.8",
"@types/node": "~11.10.5", "@types/node": "~12.11.6",
"codelyzer": "^5.0.1", "codelyzer": "^5.2.0",
"jasmine-core": "~3.3.0", "jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1", "jasmine-spec-reporter": "~4.2.1",
"karma": "^4.0.0", "karma": "^4.4.1",
"karma-chrome-launcher": "~2.2.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.0.5", "karma-coverage-istanbul-reporter": "~2.1.0",
"karma-jasmine": "~2.0.1", "karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0", "karma-jasmine-html-reporter": "^1.4.2",
"typescript": "3.4.5" "typescript": "3.5.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"node-sass": "^4.9.3", "node-sass": "^4.12.0",
"protractor": "~5.4.0", "protractor": "~5.4.2",
"ts-node": "~5.0.1", "ts-node": "~8.4.1",
"tslint": "~5.9.1" "tslint": "~5.20.0"
} }
} }

View File

@ -1,3 +1,7 @@
{ {
"parser": "typescript-eslint-parser" "parser": "@typescript-eslint/parser",
} "parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
}

View File

@ -4,39 +4,40 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"connected-react-router": "6.2.1", "connected-react-router": "6.5.2",
"history": "4.7.2", "history": "4.10.1",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"merge": "1.2.1", "merge": "1.2.1",
"popper.js": "^1.14.7", "popper.js": "^1.16.0",
"react": "16.7.0", "react": "16.11.0",
"react-dom": "16.7.0", "react-dom": "16.11.0",
"react-redux": "6.0.0", "react-redux": "7.1.1",
"react-router": "4.3.1", "react-router": "5.1.2",
"react-router-dom": "4.3.1", "react-router-dom": "5.1.2",
"react-scripts": "^3.0.1", "react-scripts": "^3.2.0",
"reactstrap": "7.0.2", "reactstrap": "8.1.1",
"redux": "4.0.1", "redux": "4.0.4",
"redux-thunk": "2.3.0", "redux-thunk": "2.3.0",
"typescript": "3.2.2" "svgo": "1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/history": "4.7.2", "@types/history": "4.7.3",
"@types/jest": "23.3.11", "@types/jest": "24.0.19",
"@types/node": "10.12.18", "@types/node": "12.11.6",
"@types/react": "16.7.18", "@types/react": "16.9.9",
"@types/react-dom": "16.0.11", "@types/react-dom": "16.9.2",
"@types/react-redux": "6.0.11", "@types/react-redux": "7.1.5",
"@types/react-router": "4.4.3", "@types/react-router": "5.1.2",
"@types/react-router-dom": "4.3.1", "@types/react-router-dom": "5.1.0",
"@types/reactstrap": "6.4.3", "@types/reactstrap": "8.0.6",
"cross-env": "5.2.0", "@typescript-eslint/parser": "^2.5.0",
"eslint": "^5.16.0", "cross-env": "6.0.3",
"eslint-plugin-flowtype": "2.50.3", "eslint": "^6.5.1",
"eslint-plugin-import": "2.14.0", "eslint-plugin-flowtype": "^3.13.0",
"eslint-plugin-jsx-a11y": "6.1.2", "eslint-plugin-import": "2.18.2",
"eslint-plugin-react": "7.11.1", "eslint-plugin-jsx-a11y": "6.2.3",
"typescript-eslint-parser": "21.0.2" "eslint-plugin-react": "7.16.0",
"typescript": "3.6.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@ -104,11 +104,13 @@ namespace Templates.Test.Helpers
public async Task ContainsLinks(Page page) public async Task ContainsLinks(Page page)
{ {
var request = new HttpRequestMessage( var response = await RequestWithRetries(client =>
HttpMethod.Get, {
new Uri(ListeningUri, page.Url)); var request = new HttpRequestMessage(
HttpMethod.Get,
var response = await RequestWithRetries(client => client.SendAsync(request), _httpClient); new Uri(ListeningUri, page.Url));
return client.SendAsync(request);
}, _httpClient);
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var parser = new HtmlParser(); var parser = new HtmlParser();
@ -235,16 +237,18 @@ namespace Templates.Test.Helpers
public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null) public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null)
{ {
var request = new HttpRequestMessage( var response = await RequestWithRetries(client => {
HttpMethod.Get, var request = new HttpRequestMessage(
new Uri(ListeningUri, requestUrl)); HttpMethod.Get,
new Uri(ListeningUri, requestUrl));
if (!string.IsNullOrEmpty(acceptContentType)) if (!string.IsNullOrEmpty(acceptContentType))
{ {
request.Headers.Add("Accept", acceptContentType); request.Headers.Add("Accept", acceptContentType);
} }
var response = await RequestWithRetries(client => client.SendAsync(request), _httpClient); return client.SendAsync(request);
}, _httpClient);
Assert.True(statusCode == response.StatusCode, $"Expected {requestUrl} to have status '{statusCode}' but it was '{response.StatusCode}'."); Assert.True(statusCode == response.StatusCode, $"Expected {requestUrl} to have status '{statusCode}' but it was '{response.StatusCode}'.");
} }

View File

@ -397,7 +397,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
Assert.Contains("HTTP Error 500.31 - ANCM Failed to Find Native Dependencies", responseContent); Assert.Contains("HTTP Error 500.31 - ANCM Failed to Find Native Dependencies", responseContent);
Assert.Contains("The specified framework 'Microsoft.NETCore.App', version '2.9.9'", responseContent); Assert.Contains("The framework 'Microsoft.NETCore.App', version '2.9.9'", responseContent);
} }
else else
{ {

View File

@ -308,7 +308,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
} }
else else
{ {
return "The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found."; return "The framework 'Microsoft.NETCore.App', version '2.9.9' was not found.";
} }
} }

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
StopServer(); StopServer();
EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.VerifyEventLogEvent(deploymentResult,
"The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found.", Logger); "The framework 'Microsoft.NETCore.App', version '2.9.9' was not found.", Logger);
} }
[ConditionalFact] [ConditionalFact]
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
StopServer(); StopServer();
var contents = Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath), Logger); var contents = Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath), Logger);
var expectedString = "The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found."; var expectedString = "The framework 'Microsoft.NETCore.App', version '2.9.9' was not found.";
EventLogHelpers.VerifyEventLogEvent(deploymentResult, expectedString, Logger); EventLogHelpers.VerifyEventLogEvent(deploymentResult, expectedString, Logger);
Assert.Contains(expectedString, contents); Assert.Contains(expectedString, contents);
} }

View File

@ -619,5 +619,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
</data> </data>
<data name="HttpParserTlsOverHttpError" xml:space="preserve"> <data name="HttpParserTlsOverHttpError" xml:space="preserve">
<value>Detected a TLS handshake to an endpoint that does not have TLS enabled.</value> <value>Detected a TLS handshake to an endpoint that does not have TLS enabled.</value>
</data> <data name="BadDeveloperCertificateState" xml:space="preserve">
<value>The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.</value>
</data> </data>
</root> </root>

View File

@ -142,8 +142,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>(); var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
try try
{ {
var certificateManager = new CertificateManager(); DefaultCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
DefaultCertificate = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
.FirstOrDefault(); .FirstOrDefault();
if (DefaultCertificate != null) if (DefaultCertificate != null)

View File

@ -11,6 +11,7 @@ using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
@ -204,12 +205,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
await sslStream.DisposeAsync(); await sslStream.DisposeAsync();
return; return;
} }
catch (Exception ex) when (ex is IOException || ex is AuthenticationException) catch (IOException ex)
{ {
_logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed); _logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
await sslStream.DisposeAsync(); await sslStream.DisposeAsync();
return; return;
} }
catch (AuthenticationException ex)
{
if (_serverCertificate == null ||
!CertificateManager.IsHttpsDevelopmentCertificate(_serverCertificate) ||
CertificateManager.CheckDeveloperCertificateKey(_serverCertificate))
{
_logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
}
else
{
_logger?.LogError(2, ex, CoreStrings.BadDeveloperCertificateState);
}
await sslStream.DisposeAsync();
return;
}
} }
feature.ApplicationProtocol = sslStream.NegotiatedApplicationProtocol.Protocol; feature.ApplicationProtocol = sslStream.NegotiatedApplicationProtocol.Protocol;

View File

@ -364,7 +364,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
new TestServiceContext(LoggerFactory), new TestServiceContext(LoggerFactory),
listenOptions => listenOptions =>
{ {
listenOptions.UseHttps(TestResources.GetTestCertificate()); listenOptions.UseHttps(TestResources.GetTestCertificate("no_extensions.pfx"));
})) }))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
@ -383,6 +383,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel); Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
} }
[Fact]
public async Task DevCertWithInvalidPrivateKeyProducesCustomWarning()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
await using (var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.GetTestCertificate());
}))
{
using (var connection = server.CreateConnection())
using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
{
// SslProtocols.Tls is TLS 1.0 which isn't supported by Kestrel by default.
await Assert.ThrowsAsync<IOException>(() =>
sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
enabledSslProtocols: SslProtocols.Tls,
checkCertificateRevocation: false));
}
}
await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
Assert.Equal(2, loggerProvider.FilterLogger.LastEventId);
Assert.Equal(LogLevel.Error, loggerProvider.FilterLogger.LastLogLevel);
}
[Fact] [Fact]
public async Task OnAuthenticate_SeesOtherSettings() public async Task OnAuthenticate_SeesOtherSettings()
{ {

View File

@ -2541,7 +2541,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
} }
[Fact] [Fact]
[Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/2408", FlakyOn.AzP.All)] [Flaky("https://github.com/aspnet/AspNetCore/issues/12401", FlakyOn.All)]
public async Task AppAbortViaIConnectionLifetimeFeatureIsLogged() public async Task AppAbortViaIConnectionLifetimeFeatureIsLogged()
{ {
var testContext = new TestServiceContext(LoggerFactory); var testContext = new TestServiceContext(LoggerFactory);

View File

@ -44,9 +44,13 @@ namespace Microsoft.AspNetCore.Certificates.Generation
// Setting to 0 means we don't append the version byte, // Setting to 0 means we don't append the version byte,
// which is what all machines currently have. // which is what all machines currently have.
public int AspNetHttpsCertificateVersion { get; set; } = 1; public static int AspNetHttpsCertificateVersion { get; set; } = 1;
public IList<X509Certificate2> ListCertificates( public static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate) =>
certificate.Extensions.OfType<X509Extension>()
.Any(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal));
public static IList<X509Certificate2> ListCertificates(
CertificatePurpose purpose, CertificatePurpose purpose,
StoreName storeName, StoreName storeName,
StoreLocation location, StoreLocation location,
@ -228,6 +232,33 @@ namespace Microsoft.AspNetCore.Certificates.Generation
return certificate; return certificate;
} }
internal static bool CheckDeveloperCertificateKey(X509Certificate2 candidate)
{
// Tries to use the certificate key to validate it can't access it
try
{
var rsa = candidate.GetRSAPrivateKey();
if (rsa == null)
{
return false;
}
// Encrypting a random value is the ultimate test for a key validity.
// Windows and Mac OS both return HasPrivateKey = true if there is (or there has been) a private key associated
// with the certificate at some point.
var value = new byte[32];
RandomNumberGenerator.Fill(value);
rsa.Decrypt(rsa.Encrypt(value, RSAEncryptionPadding.Pkcs1), RSAEncryptionPadding.Pkcs1);
// Being able to encrypt and decrypt a payload is the strongest guarantee that the key is valid.
return true;
}
catch (Exception)
{
return false;
}
}
public X509Certificate2 CreateSelfSignedCertificate( public X509Certificate2 CreateSelfSignedCertificate(
X500DistinguishedName subject, X500DistinguishedName subject,
IEnumerable<X509Extension> extensions, IEnumerable<X509Extension> extensions,

View File

@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
Assert.NotNull(exportedCertificate); Assert.NotNull(exportedCertificate);
Assert.False(exportedCertificate.HasPrivateKey); Assert.False(exportedCertificate.HasPrivateKey);
var httpsCertificates = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false); var httpsCertificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false);
var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject); var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject);
Assert.True(httpsCertificate.HasPrivateKey); Assert.True(httpsCertificate.HasPrivateKey);
Assert.Equal(TestCertificateSubject, httpsCertificate.Subject); Assert.Equal(TestCertificateSubject, httpsCertificate.Subject);
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
httpsCertificate.Extensions.OfType<X509Extension>(), httpsCertificate.Extensions.OfType<X509Extension>(),
e => e.Critical == false && e => e.Critical == false &&
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" && e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
e.RawData[0] == _manager.AspNetHttpsCertificateVersion); e.RawData[0] == CertificateManager.AspNetHttpsCertificateVersion);
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
@ -137,7 +137,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
var httpsCertificate = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject); var httpsCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
// Act // Act
var result = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject); var result = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject);
@ -164,9 +164,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
_manager.AspNetHttpsCertificateVersion = 2; CertificateManager.AspNetHttpsCertificateVersion = 2;
var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true); var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.Empty(httpsCertificateList); Assert.Empty(httpsCertificateList);
} }
@ -178,12 +178,12 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.AspNetHttpsCertificateVersion = 0; CertificateManager.AspNetHttpsCertificateVersion = 0;
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
_manager.AspNetHttpsCertificateVersion = 1; CertificateManager.AspNetHttpsCertificateVersion = 1;
var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true); var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.Empty(httpsCertificateList); Assert.Empty(httpsCertificateList);
} }
@ -195,10 +195,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.AspNetHttpsCertificateVersion = 0; CertificateManager.AspNetHttpsCertificateVersion = 0;
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true); var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.NotEmpty(httpsCertificateList); Assert.NotEmpty(httpsCertificateList);
} }
@ -210,11 +210,11 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.AspNetHttpsCertificateVersion = 2; CertificateManager.AspNetHttpsCertificateVersion = 2;
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
_manager.AspNetHttpsCertificateVersion = 1; CertificateManager.AspNetHttpsCertificateVersion = 1;
var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true); var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.NotEmpty(httpsCertificateList); Assert.NotEmpty(httpsCertificateList);
} }
@ -241,10 +241,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
_manager.CleanupHttpsCertificates(TestCertificateSubject); _manager.CleanupHttpsCertificates(TestCertificateSubject);
Assert.Empty(_manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject)); Assert.Empty(CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
Assert.Empty(_manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject)); Assert.Empty(CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
} }
} }
} }

View File

@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
{ {
var now = DateTimeOffset.Now; var now = DateTimeOffset.Now;
var certificateManager = new CertificateManager(); var certificateManager = new CertificateManager();
var certificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true); var certificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
if (certificates.Count == 0) if (certificates.Count == 0)
{ {
reporter.Output("No valid certificate found."); reporter.Output("No valid certificate found.");
@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
if (trust != null && trust.HasValue()) if (trust != null && trust.HasValue())
{ {
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root; var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
var trustedCertificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true); var trustedCertificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true);
if (!certificates.Any(c => certificateManager.IsTrusted(c))) if (!certificates.Any(c => certificateManager.IsTrusted(c)))
{ {
reporter.Output($@"The following certificates were found, but none of them is trusted: reporter.Output($@"The following certificates were found, but none of them is trusted: