Merge branch 'merge/release/3.1-to-master' of https://github.com/dotnet-maestro-bot/AspNetCore
This commit is contained in:
commit
35403fe669
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 "%(Identity)"')" />
|
<_BlazorAssembliesToLink Include="@(_BlazorDependencyInput->'-a "%(Identity)"')" />
|
||||||
<_BlazorAssembliesToLink Include="@(IntermediateAssembly->'-a "%(FullPath)"')" />
|
<_BlazorAssembliesToLink Include="@(IntermediateAssembly->'-a "%(FullPath)"')" />
|
||||||
<_BlazorFolderLookupPaths Include="@(_MonoBaseClassLibraryFolder->'-d "%(Identity)"')" />
|
<_BlazorFolderLookupPaths Include="@(_MonoBaseClassLibraryFolder->'-d "%(Identity)"')" />
|
||||||
|
|
@ -528,7 +528,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_ReferencesArg Condition="'@(_BlazorDependencyInput)' != ''">--references "$(BlazorResolveDependenciesFilePath)"</_ReferencesArg>
|
<_ReferencesArg Condition="'@(_BlazorDependencyInput)' != ''">--references "$(BlazorResolveDependenciesFilePath)"</_ReferencesArg>
|
||||||
<_BclParameter>--base-class-library "$(MonoBaseClassLibraryPath)" --base-class-library "$(MonoBaseClassLibraryFacadesPath)"</_BclParameter>
|
<_BclParameter>--base-class-library "$(MonoBaseClassLibraryPath)" --base-class-library "$(MonoBaseClassLibraryFacadesPath)" --base-class-library "$(MonoWasmFrameworkPath)"</_BclParameter>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<WriteLinesToFile
|
<WriteLinesToFile
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
window.exampleJsFunctions = {
|
||||||
|
showPrompt: function (message) {
|
||||||
|
return prompt(message, 'Type anything here')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
{
|
{
|
||||||
"parser": "typescript-eslint-parser"
|
"parser": "@typescript-eslint/parser",
|
||||||
}
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 6,
|
||||||
|
"sourceType": "module"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue