Skip to content

Commit d52d7e3

Browse files
authored
Harden StartCircuit (#12825)
* Harden StartCircuit Fixes: #12057 Adds some upfront argument validation as well as error handling for circuit intialization failures.
1 parent cd613fe commit d52d7e3

File tree

8 files changed

+199
-47
lines changed

8 files changed

+199
-47
lines changed

src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ protected LayoutComponentBase() { }
278278
[Microsoft.AspNetCore.Components.ParameterAttribute]
279279
public Microsoft.AspNetCore.Components.RenderFragment Body { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
280280
}
281+
public sealed partial class LocationChangeException : System.Exception
282+
{
283+
public LocationChangeException(string message, System.Exception innerException) { }
284+
}
281285
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
282286
public readonly partial struct MarkupString
283287
{
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Components
7+
{
8+
/// <summary>
9+
/// An exception thrown when <see cref="NavigationManager.LocationChanged"/> throws an exception.
10+
/// </summary>
11+
public sealed class LocationChangeException : Exception
12+
{
13+
/// <summary>
14+
/// Creates a new instance of <see cref="LocationChangeException"/>.
15+
/// </summary>
16+
/// <param name="message">The exception message.</param>
17+
/// <param name="innerException">The inner exception.</param>
18+
public LocationChangeException(string message, Exception innerException)
19+
: base(message, innerException)
20+
{
21+
}
22+
}
23+
}

src/Components/Components/src/NavigationManager.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Runtime.InteropServices.ComTypes;
65
using Microsoft.AspNetCore.Components.Routing;
76

87
namespace Microsoft.AspNetCore.Components
@@ -129,8 +128,9 @@ protected void Initialize(string baseUri, string uri)
129128

130129
_isInitialized = true;
131130

132-
Uri = uri;
131+
// Setting BaseUri before Uri so they get validated.
133132
BaseUri = baseUri;
133+
Uri = uri;
134134
}
135135

136136
/// <summary>
@@ -201,7 +201,14 @@ internal static string NormalizeBaseUri(string baseUri)
201201
/// </summary>
202202
protected void NotifyLocationChanged(bool isInterceptedLink)
203203
{
204-
_locationChanged?.Invoke(this, new LocationChangedEventArgs(_uri, isInterceptedLink));
204+
try
205+
{
206+
_locationChanged?.Invoke(this, new LocationChangedEventArgs(_uri, isInterceptedLink));
207+
}
208+
catch (Exception ex)
209+
{
210+
throw new LocationChangeException("An exception occurred while dispatching a location changed event.", ex);
211+
}
205212
}
206213

207214
private void AssertInitialized()

src/Components/Components/test/NavigationManagerTest.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ public void ComputesCorrectValidBaseRelativePaths(string baseUri, string uri, st
3838
Assert.Equal(expectedResult, actualResult);
3939
}
4040

41+
[Theory]
42+
[InlineData("scheme://host/", "otherscheme://host/")]
43+
[InlineData("scheme://host/", "scheme://otherhost/")]
44+
[InlineData("scheme://host/path/", "scheme://host/")]
45+
public void Initialize_ThrowsForInvalidBaseRelativePaths(string baseUri, string absoluteUri)
46+
{
47+
var navigationManager = new TestNavigationManager();
48+
49+
var ex = Assert.Throws<ArgumentException>(() =>
50+
{
51+
navigationManager.Initialize(baseUri, absoluteUri);
52+
});
53+
54+
Assert.Equal(
55+
$"The URI '{absoluteUri}' is not contained by the base URI '{baseUri}'.",
56+
ex.Message);
57+
}
58+
4159
[Theory]
4260
[InlineData("scheme://host/", "otherscheme://host/")]
4361
[InlineData("scheme://host/", "scheme://otherhost/")]
@@ -76,9 +94,18 @@ public void ToBaseRelativePath_ThrowsForInvalidBaseRelativePaths(string baseUri,
7694

7795
private class TestNavigationManager : NavigationManager
7896
{
97+
public TestNavigationManager()
98+
{
99+
}
100+
79101
public TestNavigationManager(string baseUri = null, string uri = null)
80102
{
81-
Initialize(baseUri ?? "http://example.com/", uri ?? "http://example.com/welcome-page");
103+
Initialize(baseUri ?? "http://example.com/", uri ?? baseUri ?? "http://example.com/welcome-page");
104+
}
105+
106+
public new void Initialize(string baseUri, string uri)
107+
{
108+
base.Initialize(baseUri, uri);
82109
}
83110

84111
protected override void NavigateToCore(string uri, bool forceLoad)

src/Components/Server/src/Circuits/DefaultCircuitFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public override CircuitHost CreateCircuitHost(
4343
string uri,
4444
ClaimsPrincipal user)
4545
{
46+
// We do as much intialization as possible eagerly in this method, which makes the error handling
47+
// story much simpler. If we throw from here, it's handled inside the initial hub method.
4648
var components = ResolveComponentMetadata(httpContext, client);
4749

4850
var scope = _scopeFactory.CreateScope();

src/Components/Server/src/ComponentHub.cs

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ public string StartCircuit(string baseUri, string uri)
110110
return null;
111111
}
112112

113+
if (baseUri == null ||
114+
uri == null ||
115+
!Uri.IsWellFormedUriString(baseUri, UriKind.Absolute) ||
116+
!Uri.IsWellFormedUriString(uri, UriKind.Absolute))
117+
{
118+
// We do some really minimal validation here to prevent obviously wrong data from getting in
119+
// without duplicating too much logic.
120+
Log.InvalidInputData(_logger);
121+
_ = NotifyClientError(Clients.Caller, $"The uris provided are invalid.");
122+
return null;
123+
}
124+
113125
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
114126
if (DefaultCircuitFactory.ResolveComponentMetadata(Context.GetHttpContext(), circuitClient).Count == 0)
115127
{
@@ -122,26 +134,35 @@ public string StartCircuit(string baseUri, string uri)
122134
return null;
123135
}
124136

125-
var circuitHost = _circuitFactory.CreateCircuitHost(
126-
Context.GetHttpContext(),
127-
circuitClient,
128-
baseUri,
129-
uri,
130-
Context.User);
131-
132-
circuitHost.UnhandledException += CircuitHost_UnhandledException;
133-
134-
// Fire-and-forget the initialization process, because we can't block the
135-
// SignalR message loop (we'd get a deadlock if any of the initialization
136-
// logic relied on receiving a subsequent message from SignalR), and it will
137-
// take care of its own errors anyway.
138-
_ = circuitHost.InitializeAsync(Context.ConnectionAborted);
139-
140-
_circuitRegistry.Register(circuitHost);
141-
142-
CircuitHost = circuitHost;
143-
144-
return circuitHost.CircuitId;
137+
try
138+
{
139+
var circuitHost = _circuitFactory.CreateCircuitHost(
140+
Context.GetHttpContext(),
141+
circuitClient,
142+
baseUri,
143+
uri,
144+
Context.User);
145+
146+
circuitHost.UnhandledException += CircuitHost_UnhandledException;
147+
148+
// Fire-and-forget the initialization process, because we can't block the
149+
// SignalR message loop (we'd get a deadlock if any of the initialization
150+
// logic relied on receiving a subsequent message from SignalR), and it will
151+
// take care of its own errors anyway.
152+
_ = circuitHost.InitializeAsync(Context.ConnectionAborted);
153+
154+
// It's safe to *publish* the circuit now because nothing will be able
155+
// to run inside it until after InitializeAsync completes.
156+
_circuitRegistry.Register(circuitHost);
157+
CircuitHost = circuitHost;
158+
return circuitHost.CircuitId;
159+
}
160+
catch (Exception ex)
161+
{
162+
Log.CircuitInitializationFailed(_logger, ex);
163+
NotifyClientError(Clients.Caller, "The circuit failed to initialize.");
164+
return null;
165+
}
145166
}
146167

147168
/// <summary>
@@ -292,6 +313,12 @@ private static class Log
292313
private static readonly Action<ILogger, string, Exception> _circuitTerminatedGracefully =
293314
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(7, "CircuitTerminatedGracefully"), "Circuit '{CircuitId}' terminated gracefully");
294315

316+
private static readonly Action<ILogger, string, Exception> _invalidInputData =
317+
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(8, "InvalidInputData"), "Call to '{CallSite}' received invalid input data");
318+
319+
private static readonly Action<ILogger, Exception> _circuitInitializationFailed =
320+
LoggerMessage.Define(LogLevel.Debug, new EventId(9, "CircuitInitializationFailed"), "Circuit initialization failed");
321+
295322
public static void NoComponentsRegisteredInEndpoint(ILogger logger, string endpointDisplayName)
296323
{
297324
_noComponentsRegisteredInEndpoint(logger, endpointDisplayName, null);
@@ -317,6 +344,10 @@ public static void FailedToTransmitException(ILogger logger, string circuitId, E
317344
public static void CircuitHostNotInitialized(ILogger logger, [CallerMemberName] string callSite = "") => _circuitHostNotInitialized(logger, callSite, null);
318345

319346
public static void CircuitTerminatedGracefully(ILogger logger, string circuitId) => _circuitTerminatedGracefully(logger, circuitId, null);
347+
348+
public static void InvalidInputData(ILogger logger, [CallerMemberName] string callSite = "") => _invalidInputData(logger, callSite, null);
349+
350+
public static void CircuitInitializationFailed(ILogger logger, Exception exception) => _circuitInitializationFailed(logger, exception);
320351
}
321352
}
322353
}

src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,65 @@ public async Task CannotStartMultipleCircuits()
6161
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
6262
"StartCircuit",
6363
baseUri,
64-
baseUri.GetLeftPart(UriPartial.Authority)));
64+
baseUri + "/home"));
65+
66+
// Assert
67+
var actualError = Assert.Single(Errors);
68+
Assert.Matches(expectedError, actualError);
69+
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
70+
}
71+
72+
[Fact]
73+
public async Task CannotStartCircuitWithNullData()
74+
{
75+
// Arrange
76+
var expectedError = "The uris provided are invalid.";
77+
var rootUri = _serverFixture.RootUri;
78+
var uri = new Uri(rootUri, "/subdir");
79+
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
80+
81+
// Act
82+
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", null, null));
83+
84+
// Assert
85+
var actualError = Assert.Single(Errors);
86+
Assert.Matches(expectedError, actualError);
87+
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
88+
}
89+
90+
[Fact]
91+
public async Task CannotStartCircuitWithInvalidUris()
92+
{
93+
// Arrange
94+
var expectedError = "The uris provided are invalid.";
95+
var rootUri = _serverFixture.RootUri;
96+
var uri = new Uri(rootUri, "/subdir");
97+
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
98+
99+
// Act
100+
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", uri.AbsoluteUri, "/foo"));
101+
102+
// Assert
103+
var actualError = Assert.Single(Errors);
104+
Assert.Matches(expectedError, actualError);
105+
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
106+
}
107+
108+
// This is a hand-chosen example of something that will cause an exception in creating the circuit host.
109+
// We want to test this case so that we know what happens when creating the circuit host blows up.
110+
[Fact]
111+
public async Task StartCircuitCausesInitializationError()
112+
{
113+
// Arrange
114+
var expectedError = "The circuit failed to initialize.";
115+
var rootUri = _serverFixture.RootUri;
116+
var uri = new Uri(rootUri, "/subdir");
117+
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
118+
119+
// Act
120+
//
121+
// These are valid URIs by the BaseUri doesn't contain the Uri - so it fails to initialize.
122+
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", uri, "http://example.com"), TimeSpan.FromHours(1));
65123

66124
// Assert
67125
var actualError = Assert.Single(Errors);

0 commit comments

Comments
 (0)