Harden StartCircuit (#12825)

* Harden StartCircuit

Fixes: #12057

Adds some upfront argument validation as well as error handling for
circuit intialization failures.
This commit is contained in:
Ryan Nowak 2019-08-02 20:35:29 -07:00 committed by GitHub
parent cd613fe703
commit d52d7e3284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 196 additions and 44 deletions

View File

@ -278,6 +278,10 @@ namespace Microsoft.AspNetCore.Components
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Body { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public sealed partial class LocationChangeException : System.Exception
{
public LocationChangeException(string message, System.Exception innerException) { }
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct MarkupString
{

View File

@ -0,0 +1,23 @@
// 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;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// An exception thrown when <see cref="NavigationManager.LocationChanged"/> throws an exception.
/// </summary>
public sealed class LocationChangeException : Exception
{
/// <summary>
/// Creates a new instance of <see cref="LocationChangeException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The inner exception.</param>
public LocationChangeException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.InteropServices.ComTypes;
using Microsoft.AspNetCore.Components.Routing;
namespace Microsoft.AspNetCore.Components
@ -129,8 +128,9 @@ namespace Microsoft.AspNetCore.Components
_isInitialized = true;
Uri = uri;
// Setting BaseUri before Uri so they get validated.
BaseUri = baseUri;
Uri = uri;
}
/// <summary>
@ -201,7 +201,14 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
protected void NotifyLocationChanged(bool isInterceptedLink)
{
_locationChanged?.Invoke(this, new LocationChangedEventArgs(_uri, isInterceptedLink));
try
{
_locationChanged?.Invoke(this, new LocationChangedEventArgs(_uri, isInterceptedLink));
}
catch (Exception ex)
{
throw new LocationChangeException("An exception occurred while dispatching a location changed event.", ex);
}
}
private void AssertInitialized()

View File

@ -38,6 +38,24 @@ namespace Microsoft.AspNetCore.Components
Assert.Equal(expectedResult, actualResult);
}
[Theory]
[InlineData("scheme://host/", "otherscheme://host/")]
[InlineData("scheme://host/", "scheme://otherhost/")]
[InlineData("scheme://host/path/", "scheme://host/")]
public void Initialize_ThrowsForInvalidBaseRelativePaths(string baseUri, string absoluteUri)
{
var navigationManager = new TestNavigationManager();
var ex = Assert.Throws<ArgumentException>(() =>
{
navigationManager.Initialize(baseUri, absoluteUri);
});
Assert.Equal(
$"The URI '{absoluteUri}' is not contained by the base URI '{baseUri}'.",
ex.Message);
}
[Theory]
[InlineData("scheme://host/", "otherscheme://host/")]
[InlineData("scheme://host/", "scheme://otherhost/")]
@ -76,9 +94,18 @@ namespace Microsoft.AspNetCore.Components
private class TestNavigationManager : NavigationManager
{
public TestNavigationManager()
{
}
public TestNavigationManager(string baseUri = null, string uri = null)
{
Initialize(baseUri ?? "http://example.com/", uri ?? "http://example.com/welcome-page");
Initialize(baseUri ?? "http://example.com/", uri ?? baseUri ?? "http://example.com/welcome-page");
}
public new void Initialize(string baseUri, string uri)
{
base.Initialize(baseUri, uri);
}
protected override void NavigateToCore(string uri, bool forceLoad)

View File

@ -43,6 +43,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
string uri,
ClaimsPrincipal user)
{
// We do as much intialization as possible eagerly in this method, which makes the error handling
// story much simpler. If we throw from here, it's handled inside the initial hub method.
var components = ResolveComponentMetadata(httpContext, client);
var scope = _scopeFactory.CreateScope();

View File

@ -110,6 +110,18 @@ namespace Microsoft.AspNetCore.Components.Server
return null;
}
if (baseUri == null ||
uri == null ||
!Uri.IsWellFormedUriString(baseUri, UriKind.Absolute) ||
!Uri.IsWellFormedUriString(uri, UriKind.Absolute))
{
// We do some really minimal validation here to prevent obviously wrong data from getting in
// without duplicating too much logic.
Log.InvalidInputData(_logger);
_ = NotifyClientError(Clients.Caller, $"The uris provided are invalid.");
return null;
}
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
if (DefaultCircuitFactory.ResolveComponentMetadata(Context.GetHttpContext(), circuitClient).Count == 0)
{
@ -122,26 +134,35 @@ namespace Microsoft.AspNetCore.Components.Server
return null;
}
var circuitHost = _circuitFactory.CreateCircuitHost(
Context.GetHttpContext(),
circuitClient,
baseUri,
uri,
Context.User);
try
{
var circuitHost = _circuitFactory.CreateCircuitHost(
Context.GetHttpContext(),
circuitClient,
baseUri,
uri,
Context.User);
circuitHost.UnhandledException += CircuitHost_UnhandledException;
circuitHost.UnhandledException += CircuitHost_UnhandledException;
// Fire-and-forget the initialization process, because we can't block the
// SignalR message loop (we'd get a deadlock if any of the initialization
// logic relied on receiving a subsequent message from SignalR), and it will
// take care of its own errors anyway.
_ = circuitHost.InitializeAsync(Context.ConnectionAborted);
// Fire-and-forget the initialization process, because we can't block the
// SignalR message loop (we'd get a deadlock if any of the initialization
// logic relied on receiving a subsequent message from SignalR), and it will
// take care of its own errors anyway.
_ = circuitHost.InitializeAsync(Context.ConnectionAborted);
_circuitRegistry.Register(circuitHost);
CircuitHost = circuitHost;
return circuitHost.CircuitId;
// It's safe to *publish* the circuit now because nothing will be able
// to run inside it until after InitializeAsync completes.
_circuitRegistry.Register(circuitHost);
CircuitHost = circuitHost;
return circuitHost.CircuitId;
}
catch (Exception ex)
{
Log.CircuitInitializationFailed(_logger, ex);
NotifyClientError(Clients.Caller, "The circuit failed to initialize.");
return null;
}
}
/// <summary>
@ -292,6 +313,12 @@ namespace Microsoft.AspNetCore.Components.Server
private static readonly Action<ILogger, string, Exception> _circuitTerminatedGracefully =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(7, "CircuitTerminatedGracefully"), "Circuit '{CircuitId}' terminated gracefully");
private static readonly Action<ILogger, string, Exception> _invalidInputData =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(8, "InvalidInputData"), "Call to '{CallSite}' received invalid input data");
private static readonly Action<ILogger, Exception> _circuitInitializationFailed =
LoggerMessage.Define(LogLevel.Debug, new EventId(9, "CircuitInitializationFailed"), "Circuit initialization failed");
public static void NoComponentsRegisteredInEndpoint(ILogger logger, string endpointDisplayName)
{
_noComponentsRegisteredInEndpoint(logger, endpointDisplayName, null);
@ -317,6 +344,10 @@ namespace Microsoft.AspNetCore.Components.Server
public static void CircuitHostNotInitialized(ILogger logger, [CallerMemberName] string callSite = "") => _circuitHostNotInitialized(logger, callSite, null);
public static void CircuitTerminatedGracefully(ILogger logger, string circuitId) => _circuitTerminatedGracefully(logger, circuitId, null);
public static void InvalidInputData(ILogger logger, [CallerMemberName] string callSite = "") => _invalidInputData(logger, callSite, null);
public static void CircuitInitializationFailed(ILogger logger, Exception exception) => _circuitInitializationFailed(logger, exception);
}
}
}

View File

@ -61,7 +61,65 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"StartCircuit",
baseUri,
baseUri.GetLeftPart(UriPartial.Authority)));
baseUri + "/home"));
// Assert
var actualError = Assert.Single(Errors);
Assert.Matches(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
}
[Fact]
public async Task CannotStartCircuitWithNullData()
{
// Arrange
var expectedError = "The uris provided are invalid.";
var rootUri = _serverFixture.RootUri;
var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", null, null));
// Assert
var actualError = Assert.Single(Errors);
Assert.Matches(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
}
[Fact]
public async Task CannotStartCircuitWithInvalidUris()
{
// Arrange
var expectedError = "The uris provided are invalid.";
var rootUri = _serverFixture.RootUri;
var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", uri.AbsoluteUri, "/foo"));
// Assert
var actualError = Assert.Single(Errors);
Assert.Matches(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
}
// This is a hand-chosen example of something that will cause an exception in creating the circuit host.
// We want to test this case so that we know what happens when creating the circuit host blows up.
[Fact]
public async Task StartCircuitCausesInitializationError()
{
// Arrange
var expectedError = "The circuit failed to initialize.";
var rootUri = _serverFixture.RootUri;
var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
// Act
//
// These are valid URIs by the BaseUri doesn't contain the Uri - so it fails to initialize.
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", uri, "http://example.com"), TimeSpan.FromHours(1));
// Assert
var actualError = Assert.Single(Errors);

View File

@ -76,38 +76,38 @@ namespace Ignitor
return NextBatchReceived.Completion.Task;
}
public Task PrepareForNextJSInterop()
public Task PrepareForNextJSInterop(TimeSpan? timeout)
{
if (NextJSInteropReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextJSInteropReceived = new CancellableOperation(DefaultLatencyTimeout);
NextJSInteropReceived = new CancellableOperation(timeout);
return NextJSInteropReceived.Completion.Task;
}
public Task PrepareForNextDotNetInterop()
public Task PrepareForNextDotNetInterop(TimeSpan? timeout)
{
if (NextDotNetInteropCompletionReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextDotNetInteropCompletionReceived = new CancellableOperation(DefaultLatencyTimeout);
NextDotNetInteropCompletionReceived = new CancellableOperation(timeout);
return NextDotNetInteropCompletionReceived.Completion.Task;
}
public Task PrepareForNextCircuitError()
public Task PrepareForNextCircuitError(TimeSpan? timeout)
{
if (NextErrorReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextErrorReceived = new CancellableOperation(DefaultLatencyTimeout);
NextErrorReceived = new CancellableOperation(timeout);
return NextErrorReceived.Completion.Task;
}
@ -139,23 +139,23 @@ namespace Ignitor
await task;
}
public async Task ExpectJSInterop(Func<Task> action)
public async Task ExpectJSInterop(Func<Task> action, TimeSpan? timeout = null)
{
var task = WaitForJSInterop();
var task = WaitForJSInterop(timeout);
await action();
await task;
}
public async Task ExpectDotNetInterop(Func<Task> action)
public async Task ExpectDotNetInterop(Func<Task> action, TimeSpan? timeout = null)
{
var task = WaitForDotNetInterop();
var task = WaitForDotNetInterop(timeout);
await action();
await task;
}
public async Task ExpectCircuitError(Func<Task> action)
public async Task ExpectCircuitError(Func<Task> action, TimeSpan? timeout = null)
{
var task = WaitForCircuitError();
var task = WaitForCircuitError(timeout);
await action();
await task;
}
@ -175,42 +175,42 @@ namespace Ignitor
return Task.CompletedTask;
}
private async Task WaitForJSInterop()
private async Task WaitForJSInterop(TimeSpan? timeout = null)
{
if (ImplicitWait)
{
if (DefaultLatencyTimeout == null)
if (DefaultLatencyTimeout == null && timeout == null)
{
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
}
await PrepareForNextJSInterop();
await PrepareForNextJSInterop(timeout ?? DefaultLatencyTimeout);
}
}
private async Task WaitForDotNetInterop()
private async Task WaitForDotNetInterop(TimeSpan? timeout = null)
{
if (ImplicitWait)
{
if (DefaultLatencyTimeout == null)
if (DefaultLatencyTimeout == null && timeout == null)
{
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
}
await PrepareForNextDotNetInterop();
await PrepareForNextDotNetInterop(timeout ?? DefaultLatencyTimeout);
}
}
private async Task WaitForCircuitError()
private async Task WaitForCircuitError(TimeSpan? timeout = null)
{
if (ImplicitWait)
{
if (DefaultLatencyTimeout == null)
if (DefaultLatencyTimeout == null && timeout == null)
{
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
}
await PrepareForNextCircuitError();
await PrepareForNextCircuitError(timeout ?? DefaultLatencyTimeout);
}
}
@ -246,7 +246,7 @@ namespace Ignitor
else
{
await ExpectRenderBatch(
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, new Uri(uri.GetLeftPart(UriPartial.Authority))),
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri),
TimeSpan.FromSeconds(10));
return CircuitId != null;
}