diff --git a/NuGet.config b/NuGet.config index 80454d301e83..8e21b29e5c5a 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,10 @@ - + - + @@ -26,10 +26,10 @@ - + - + diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index 543b6cca8d1f..8c7fa19269fc 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,28 +2,28 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - - - + + + @@ -35,105 +35,105 @@ - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 @@ -141,121 +141,121 @@ - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 - - + + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - - + + - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 - - - + + + - 7.0.7 + 7.0.10 - - + + - 7.0.7 + 7.0.10 - - + + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - - + + @@ -263,7 +263,7 @@ - 7.0.7 + 7.0.10 @@ -272,50 +272,50 @@ - 7.0.7 + 7.0.10 - + - + - + - + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - - + + @@ -325,8 +325,8 @@ - - + + @@ -334,8 +334,8 @@ - - + + @@ -346,58 +346,58 @@ - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - - + + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 @@ -414,7 +414,7 @@ - 7.0.7 + 7.0.10 @@ -422,71 +422,71 @@ - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - + - + - + - + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - - + + - 7.0.7 + 7.0.10 - - + + - 7.0.7 + 7.0.10 @@ -502,27 +502,27 @@ - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 @@ -531,151 +531,151 @@ - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - - + + - - + + - - + + - 7.0.7 + 7.0.10 - - + + - - + + - - + + - - + + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - - - - + + + + - 7.0.7 + 7.0.10 @@ -684,60 +684,60 @@ - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 @@ -756,7 +756,7 @@ - 7.0.7 + 7.0.10 @@ -778,7 +778,7 @@ - 7.0.7 + 7.0.10 @@ -794,46 +794,46 @@ - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - - - + + + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 @@ -843,7 +843,7 @@ - 7.0.7 + 7.0.10 @@ -852,73 +852,73 @@ - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - + - + - + - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 @@ -947,11 +947,11 @@ - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 @@ -969,13 +969,13 @@ - 7.0.7 + 7.0.10 - 7.0.7 + 7.0.10 - + \ No newline at end of file diff --git a/eng/Baseline.xml b/eng/Baseline.xml index b428ac509c8f..7f521c84aeeb 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -4,109 +4,109 @@ This file contains a list of all the packages and their versions which were rele Update this list when preparing for a new patch. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 08c476cea384..e338eb9b5222 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,37 +9,37 @@ --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - e00be013488a9a56a0e29230be0fe10026dbb209 + 8133d0c827462d8d84e47b70f294fc060a5d7a84 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - e00be013488a9a56a0e29230be0fe10026dbb209 + 8133d0c827462d8d84e47b70f294fc060a5d7a84 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - e00be013488a9a56a0e29230be0fe10026dbb209 + 8133d0c827462d8d84e47b70f294fc060a5d7a84 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - e00be013488a9a56a0e29230be0fe10026dbb209 + 8133d0c827462d8d84e47b70f294fc060a5d7a84 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - e00be013488a9a56a0e29230be0fe10026dbb209 + 8133d0c827462d8d84e47b70f294fc060a5d7a84 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - e00be013488a9a56a0e29230be0fe10026dbb209 + 8133d0c827462d8d84e47b70f294fc060a5d7a84 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - e00be013488a9a56a0e29230be0fe10026dbb209 + 8133d0c827462d8d84e47b70f294fc060a5d7a84 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - e00be013488a9a56a0e29230be0fe10026dbb209 + 8133d0c827462d8d84e47b70f294fc060a5d7a84 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -177,9 +177,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime d099f075e45d2aa6007a22b71b45a08758559f80 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 + ecb34f85ec92e1b3c814edf7da83337e199e7f66 https://github.com/dotnet/source-build-externals @@ -254,41 +254,41 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime d099f075e45d2aa6007a22b71b45a08758559f80 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - d099f075e45d2aa6007a22b71b45a08758559f80 + ecb34f85ec92e1b3c814edf7da83337e199e7f66 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime d099f075e45d2aa6007a22b71b45a08758559f80 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 + ecb34f85ec92e1b3c814edf7da83337e199e7f66 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 + ecb34f85ec92e1b3c814edf7da83337e199e7f66 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 + ecb34f85ec92e1b3c814edf7da83337e199e7f66 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 + ecb34f85ec92e1b3c814edf7da83337e199e7f66 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 + ecb34f85ec92e1b3c814edf7da83337e199e7f66 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 + ecb34f85ec92e1b3c814edf7da83337e199e7f66 https://github.com/dotnet/xdt @@ -300,24 +300,24 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 + 8587d13a2764c025277d628471984bae8e16427c - + https://github.com/dotnet/arcade - 59ac824080b9807fd91dbc8a6d2b4447e74874ab + 3b8f3de4606c338f99e8ce85cfb6f960f6a428c8 - + https://github.com/dotnet/arcade - 59ac824080b9807fd91dbc8a6d2b4447e74874ab + 3b8f3de4606c338f99e8ce85cfb6f960f6a428c8 - + https://github.com/dotnet/arcade - 59ac824080b9807fd91dbc8a6d2b4447e74874ab + 3b8f3de4606c338f99e8ce85cfb6f960f6a428c8 - + https://github.com/dotnet/arcade - 59ac824080b9807fd91dbc8a6d2b4447e74874ab + 3b8f3de4606c338f99e8ce85cfb6f960f6a428c8 diff --git a/eng/Versions.props b/eng/Versions.props index 2f5daccfadee..570db361b37c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,8 +8,8 @@ 7 0 - 9 - false + 11 + true @@ -63,12 +63,12 @@ 7.0.0 - 7.0.9 - 7.0.9 - 7.0.9 - 7.0.9 - 7.0.9 - 7.0.9-servicing.23320.18 + 7.0.11 + 7.0.11 + 7.0.11 + 7.0.11 + 7.0.11 + 7.0.11-servicing.23424.27 7.0.0 7.0.0 7.0.0 @@ -103,7 +103,7 @@ 7.0.0 7.0.1 7.0.0 - 7.0.9-servicing.23320.18 + 7.0.11-servicing.23424.27 7.0.0 7.0.2 7.0.0 @@ -121,21 +121,21 @@ 7.0.3 7.0.1 7.0.0 - 7.0.0 + 7.0.1 7.0.4 - 7.0.9 - 7.0.9 - 7.0.9 - 7.0.9 - 7.0.9 - 7.0.9 - 7.0.9 - 7.0.9 + 7.0.11 + 7.0.11 + 7.0.11 + 7.0.11 + 7.0.11 + 7.0.11 + 7.0.11 + 7.0.11 - 7.0.0-beta.23313.4 - 7.0.0-beta.23313.4 + 7.0.0-beta.23408.3 + 7.0.0-beta.23408.3 7.0.0-alpha.1.22505.1 @@ -238,7 +238,7 @@ 5.0.17-servicing-22215-7 $(MicrosoftAspNetCoreAzureAppServicesSiteExtension50Version) $(MicrosoftAspNetCoreAzureAppServicesSiteExtension50Version) - 6.0.18-servicing-23269-9 + 6.0.20-servicing-23321-6 $(MicrosoftAspNetCoreAzureAppServicesSiteExtension60Version) $(MicrosoftAspNetCoreAzureAppServicesSiteExtension60Version) diff --git a/global.json b/global.json index afc8fc984bd9..73543a53a4dd 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "7.0.107" + "version": "7.0.110" }, "tools": { - "dotnet": "7.0.107", + "dotnet": "7.0.110", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" @@ -27,7 +27,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.22.10", - "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.23313.4", - "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.23313.4" + "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.23408.3", + "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.23408.3" } } diff --git a/src/Middleware/WebSockets/src/ServerWebSocket.cs b/src/Middleware/WebSockets/src/ServerWebSocket.cs new file mode 100644 index 000000000000..70be31cb0459 --- /dev/null +++ b/src/Middleware/WebSockets/src/ServerWebSocket.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.WebSockets; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.WebSockets; + +/// +/// Used in ASP.NET Core to wrap a WebSocket with its associated HttpContext so that when the WebSocket is aborted +/// the underlying HttpContext is aborted. All other methods are delegated to the underlying WebSocket. +/// +internal sealed class ServerWebSocket : WebSocket +{ + private readonly WebSocket _wrappedSocket; + private readonly HttpContext _context; + + internal ServerWebSocket(WebSocket wrappedSocket, HttpContext context) + { + ArgumentNullException.ThrowIfNull(wrappedSocket); + ArgumentNullException.ThrowIfNull(context); + + _wrappedSocket = wrappedSocket; + _context = context; + } + + public override WebSocketCloseStatus? CloseStatus => _wrappedSocket.CloseStatus; + + public override string? CloseStatusDescription => _wrappedSocket.CloseStatusDescription; + + public override WebSocketState State => _wrappedSocket.State; + + public override string? SubProtocol => _wrappedSocket.SubProtocol; + + public override void Abort() + { + _wrappedSocket.Abort(); + _context.Abort(); + } + + public override Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken) + { + return _wrappedSocket.CloseAsync(closeStatus, statusDescription, cancellationToken); + } + + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken) + { + return _wrappedSocket.CloseOutputAsync(closeStatus, statusDescription, cancellationToken); + } + + public override void Dispose() + { + _wrappedSocket.Dispose(); + } + + public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + return _wrappedSocket.ReceiveAsync(buffer, cancellationToken); + } + + public override ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken) + { + return _wrappedSocket.ReceiveAsync(buffer, cancellationToken); + } + + public override ValueTask SendAsync(ReadOnlyMemory buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return _wrappedSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken); + } + + public override ValueTask SendAsync(ReadOnlyMemory buffer, WebSocketMessageType messageType, WebSocketMessageFlags messageFlags, CancellationToken cancellationToken) + { + return _wrappedSocket.SendAsync(buffer, messageType, messageFlags, cancellationToken); + } + + public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return _wrappedSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken); + } +} diff --git a/src/Middleware/WebSockets/src/WebSocketMiddleware.cs b/src/Middleware/WebSockets/src/WebSocketMiddleware.cs index 8f2bcca80ef6..0d9f61947c20 100644 --- a/src/Middleware/WebSockets/src/WebSocketMiddleware.cs +++ b/src/Middleware/WebSockets/src/WebSocketMiddleware.cs @@ -207,13 +207,15 @@ public async Task AcceptAsync(WebSocketAcceptContext acceptContext) opaqueTransport = await _upgradeFeature!.UpgradeAsync(); // Sets status code to 101 } - return WebSocket.CreateFromStream(opaqueTransport, new WebSocketCreationOptions() + var wrappedSocket = WebSocket.CreateFromStream(opaqueTransport, new WebSocketCreationOptions() { IsServer = true, KeepAliveInterval = keepAliveInterval, SubProtocol = subProtocol, DangerousDeflateOptions = deflateOptions }); + + return new ServerWebSocket(wrappedSocket, _context); } public static bool CheckSupportedWebSocketRequest(string method, IHeaderDictionary requestHeaders) diff --git a/src/Middleware/WebSockets/test/UnitTests/WebSocketMiddlewareTests.cs b/src/Middleware/WebSockets/test/UnitTests/WebSocketMiddlewareTests.cs index 90fa6f08c9c0..a89b161212c6 100644 --- a/src/Middleware/WebSockets/test/UnitTests/WebSocketMiddlewareTests.cs +++ b/src/Middleware/WebSockets/test/UnitTests/WebSocketMiddlewareTests.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Net.WebSockets; using System.Text; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Testing; using Microsoft.Net.Http.Headers; @@ -495,6 +496,146 @@ public async Task CloseFromCloseReceived_Success() } } + [Fact] + public async Task WebSocket_Abort_Interrupts_Pending_ReceiveAsync() + { + WebSocket serverSocket = null; + + // Events that we want to sequence execution across client and server. + var socketWasAccepted = new ManualResetEventSlim(); + var socketWasAborted = new ManualResetEventSlim(); + var firstReceiveOccured = new ManualResetEventSlim(); + var secondReceiveInitiated = new ManualResetEventSlim(); + + Exception receiveException = null; + + await using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + serverSocket = await context.WebSockets.AcceptWebSocketAsync(); + socketWasAccepted.Set(); + + var serverBuffer = new byte[1024]; + + try + { + while (serverSocket.State is WebSocketState.Open or WebSocketState.CloseSent) + { + if (firstReceiveOccured.IsSet) + { + var pendingResponse = serverSocket.ReceiveAsync(serverBuffer, default); + secondReceiveInitiated.Set(); + var response = await pendingResponse; + } + else + { + var response = await serverSocket.ReceiveAsync(serverBuffer, default); + firstReceiveOccured.Set(); + } + } + } + catch (ConnectionAbortedException ex) + { + socketWasAborted.Set(); + receiveException = ex; + } + catch (Exception ex) + { + // Capture this exception so a test failure can give us more information. + receiveException = ex; + } + finally + { + Assert.IsType(receiveException); + } + })) + { + var clientBuffer = new byte[1024]; + + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://127.0.0.1:{port}/"), CancellationToken.None); + + var socketWasAcceptedDidNotTimeout = socketWasAccepted.Wait(10000); + Assert.True(socketWasAcceptedDidNotTimeout, "Socket was not accepted within the allotted time."); + + await client.SendAsync(clientBuffer, WebSocketMessageType.Binary, false, default); + + var firstReceiveOccuredDidNotTimeout = firstReceiveOccured.Wait(10000); + Assert.True(firstReceiveOccuredDidNotTimeout, "First receive did not occur within the allotted time."); + + var secondReceiveInitiatedDidNotTimeout = secondReceiveInitiated.Wait(10000); + Assert.True(secondReceiveInitiatedDidNotTimeout, "Second receive was not initiated within the allotted time."); + + serverSocket.Abort(); + + var socketWasAbortedDidNotTimeout = socketWasAborted.Wait(1000); // Give it a second to process the abort. + Assert.True(socketWasAbortedDidNotTimeout, "Abort did not occur within the allotted time."); + } + } + } + + [Fact] + public async Task WebSocket_AllowsCancelling_Pending_ReceiveAsync_When_CancellationTokenProvided() + { + WebSocket serverSocket = null; + CancellationTokenSource cts = new CancellationTokenSource(); + + var socketWasAccepted = new ManualResetEventSlim(); + var operationWasCancelled = new ManualResetEventSlim(); + var firstReceiveOccured = new ManualResetEventSlim(); + + await using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + serverSocket = await context.WebSockets.AcceptWebSocketAsync(); + socketWasAccepted.Set(); + + var serverBuffer = new byte[1024]; + + var finishedWithOperationCancelled = false; + + try + { + while (serverSocket.State is WebSocketState.Open or WebSocketState.CloseSent) + { + var response = await serverSocket.ReceiveAsync(serverBuffer, cts.Token); + firstReceiveOccured.Set(); + } + } + catch (OperationCanceledException) + { + operationWasCancelled.Set(); + finishedWithOperationCancelled = true; + } + finally + { + Assert.True(finishedWithOperationCancelled); + } + })) + { + var clientBuffer = new byte[1024]; + + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://127.0.0.1:{port}/"), CancellationToken.None); + + var socketWasAcceptedDidNotTimeout = socketWasAccepted.Wait(10000); + Assert.True(socketWasAcceptedDidNotTimeout, "Socket was not accepted within the allotted time."); + + await client.SendAsync(clientBuffer, WebSocketMessageType.Binary, false, default); + + var firstReceiveOccuredDidNotTimeout = firstReceiveOccured.Wait(10000); + Assert.True(firstReceiveOccuredDidNotTimeout, "First receive did not occur within the allotted time."); + + cts.Cancel(); + + var operationWasCancelledDidNotTimeout = operationWasCancelled.Wait(1000); // Give it a second to process the abort. + Assert.True(operationWasCancelledDidNotTimeout, "Cancel did not occur within the allotted time."); + } + } + } + [Theory] [InlineData(HttpStatusCode.OK, null)] [InlineData(HttpStatusCode.Forbidden, "")] diff --git a/src/Servers/IIS/IIS/test/Common.LongTests/ShutdownTests.cs b/src/Servers/IIS/IIS/test/Common.LongTests/ShutdownTests.cs index fe4f3735c2b3..bebd1743f725 100644 --- a/src/Servers/IIS/IIS/test/Common.LongTests/ShutdownTests.cs +++ b/src/Servers/IIS/IIS/test/Common.LongTests/ShutdownTests.cs @@ -535,8 +535,15 @@ public async Task ClosesConnectionOnServerAbortOutOfProcess() var response = await deploymentResult.HttpClient.GetAsync("/Abort").TimeoutAfter(TimeoutExtensions.DefaultTimeoutValue); Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + +#if NEWSHIM_FUNCTIONALS + // In-proc SocketConnection isn't used and there's no abort // 0x80072f78 ERROR_HTTP_INVALID_SERVER_RESPONSE The server returned an invalid or unrecognized response Assert.Contains("0x80072f78", await response.Content.ReadAsStringAsync()); +#else + // 0x80072efe ERROR_INTERNET_CONNECTION_ABORTED The connection with the server was terminated abnormally + Assert.Contains("0x80072efe", await response.Content.ReadAsStringAsync()); +#endif } catch (HttpRequestException) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 87998e408288..6608c8a2f6e8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -98,7 +98,14 @@ protected override void OnRequestProcessingEnded() _http1Output.Dispose(); } - public void OnInputOrOutputCompleted() + void IRequestProcessor.OnInputOrOutputCompleted() + { + // Closed gracefully. + _http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!); + CancelRequestAbortedToken(); + } + + void IHttpOutputAborter.OnInputOrOutputCompleted() { _http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); CancelRequestAbortedToken(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index 5b346164b93b..178326795cb6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -204,7 +204,7 @@ protected void ThrowUnexpectedEndOfRequestContent() // so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400 // response is written after observing the unexpected end of request content instead of just // closing the connection without a response as expected. - _context.OnInputOrOutputCompleted(); + ((IHttpOutputAborter)_context).OnInputOrOutputCompleted(); KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 45aae35e77c4..10c2766507e0 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -159,7 +159,8 @@ public Http2Connection(HttpConnectionContext context) public void OnInputOrOutputCompleted() { TryClose(); - _frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); + var useException = _context.ServiceContext.ServerOptions.FinOnError || _clientActiveStreamCount != 0; + _frameWriter.Abort(useException ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!); } public void Abort(ConnectionAbortedException ex) diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index ba5c6151b4eb..2d59e83b0975 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -26,10 +26,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core; public class KestrelServerOptions { internal const string DisableHttp1LineFeedTerminatorsSwitchKey = "Microsoft.AspNetCore.Server.Kestrel.DisableHttp1LineFeedTerminators"; + private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError"; + private static readonly bool _finOnError; + + static KestrelServerOptions() + { + AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError); + } // internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged. internal static readonly Func DefaultHeaderEncodingSelector = _ => null; + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } = _finOnError; + private Func _requestHeaderEncodingSelector = DefaultHeaderEncodingSelector; private Func _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs index fc1be0b47e13..d3a1b8f6927b 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs @@ -30,6 +30,7 @@ internal sealed partial class SocketConnection : TransportConnection private readonly TaskCompletionSource _waitForConnectionClosedTcs = new TaskCompletionSource(); private bool _connectionClosed; private readonly bool _waitForData; + private readonly bool _finOnError; internal SocketConnection(Socket socket, MemoryPool memoryPool, @@ -38,7 +39,8 @@ internal SocketConnection(Socket socket, SocketSenderPool socketSenderPool, PipeOptions inputOptions, PipeOptions outputOptions, - bool waitForData = true) + bool waitForData = true, + bool finOnError = false) { Debug.Assert(socket != null); Debug.Assert(memoryPool != null); @@ -49,6 +51,7 @@ internal SocketConnection(Socket socket, _logger = logger; _waitForData = waitForData; _socketSenderPool = socketSenderPool; + _finOnError = finOnError; LocalEndPoint = _socket.LocalEndPoint; RemoteEndPoint = _socket.RemoteEndPoint; @@ -380,11 +383,21 @@ private void Shutdown(Exception? shutdownReason) // ever observe the nondescript ConnectionAbortedException except for connection middleware attempting // to half close the connection which is currently unsupported. _shutdownReason = shutdownReason ?? new ConnectionAbortedException("The Socket transport's send loop completed gracefully."); + + // NB: not _shutdownReason since we don't want to do this on graceful completion + if (!_finOnError && shutdownReason is not null) + { + SocketsLog.ConnectionWriteRst(_logger, this, shutdownReason.Message); + + // This forces an abortive close with linger time 0 (and implies Dispose) + _socket.Close(timeout: 0); + return; + } + SocketsLog.ConnectionWriteFin(_logger, this, _shutdownReason.Message); try { - // Try to gracefully close the socket even for aborts to match libuv behavior. _socket.Shutdown(SocketShutdown.Both); } catch diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs index 7d5c791fc98f..e0cdbf77e5c1 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs @@ -31,6 +31,17 @@ public static void ConnectionWriteFin(ILogger logger, SocketConnection connectio } } + [LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = "ConnectionWriteRst", SkipEnabledCheck = true)] + private static partial void ConnectionWriteRstCore(ILogger logger, string connectionId, string reason); + + public static void ConnectionWriteRst(ILogger logger, SocketConnection connection, string reason) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ConnectionWriteRstCore(logger, connection.ConnectionId, reason); + } + } + // Reserved: Event ID 11, EventName = ConnectionWrite // Reserved: Event ID 12, EventName = ConnectionWriteCallback diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs index 4029ddc022de..2bcc98ee11b9 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs @@ -113,7 +113,8 @@ public ConnectionContext Create(Socket socket) setting.SocketSenderPool, setting.InputOptions, setting.OutputOptions, - waitForData: _options.WaitForDataBeforeAllocatingBuffer); + waitForData: _options.WaitForDataBeforeAllocatingBuffer, + finOnError: _options.FinOnError); connection.Start(); return connection; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs index 69d524adf41e..35ce2bd8fd75 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -23,8 +23,12 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) MaxWriteBufferSize = transportOptions.MaxWriteBufferSize; UnsafePreferInlineScheduling = transportOptions.UnsafePreferInlineScheduling; MemoryPoolFactory = transportOptions.MemoryPoolFactory; + FinOnError = transportOptions.FinOnError; } + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } + /// /// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool. /// diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index d6d93b08dd19..06ac641e0b51 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -13,6 +13,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; /// public class SocketTransportOptions { + private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError"; + private static readonly bool _finOnError; + + static SocketTransportOptions() + { + AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError); + } + + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } = _finOnError; + /// /// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool. /// diff --git a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs index 9edcd7e5c253..787e7ff4bdc7 100644 --- a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs +++ b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs @@ -138,7 +138,7 @@ public async Task ReceiveEnd(params string[] lines) public async Task WaitForConnectionClose() { var buffer = new byte[128]; - var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).TimeoutAfter(Timeout); + var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).ContinueWith(t => t.IsFaulted ? 0 : t.Result).TimeoutAfter(Timeout); if (bytesTransferred > 0) { diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index 5b249eb97d8a..5c322f4eadb8 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -48,7 +48,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions { } - public TestServer(RequestDelegate app, TestServiceContext context, Action configureListenOptions) + public TestServer(RequestDelegate app, TestServiceContext context, Action configureListenOptions, Action configureServices = null) : this(app, context, options => { var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) @@ -57,7 +57,10 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action { }) + }, s => + { + configureServices?.Invoke(s); + }) { } diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 26fe5d07d0bc..037d6a1cd8cd 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -44,6 +44,60 @@ public ShutdownTests() }; } + [ConditionalFact] + public async Task ConnectionClosedWithoutActiveRequestsOrGoAwayFIN() + { + var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var readFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var writeFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + TestSink.MessageLogged += context => + { + + if (context.EventId.Name == "Http2ConnectionClosed") + { + connectionClosed.SetResult(); + } + else if (context.EventId.Name == "ConnectionReadFin") + { + readFin.SetResult(); + } + else if (context.EventId.Name == "ConnectionWriteFin") + { + writeFin.SetResult(); + } + }; + + var testContext = new TestServiceContext(LoggerFactory); + + testContext.InitializeHeartbeat(); + + await using (var server = new TestServer(context => + { + return context.Response.WriteAsync("hello world " + context.Request.Protocol); + }, + testContext, + kestrelOptions => + { + kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + listenOptions.UseHttps(_x509Certificate2); + }); + })) + { + var response = await Client.GetStringAsync($"https://localhost:{server.Port}/"); + Assert.Equal("hello world HTTP/2", response); + Client.Dispose(); // Close the socket, no GoAway is sent. + + await readFin.Task.DefaultTimeout(); + await writeFin.Task.DefaultTimeout(); + await connectionClosed.Task.DefaultTimeout(); + + await server.StopAsync(); + } + } + [CollectDump] [ConditionalFact] public async Task GracefulShutdownWaitsForRequestsToFinish() diff --git a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs index fff5268a4bbb..e1d4cdd6886a 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs @@ -21,7 +21,9 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Moq; @@ -38,6 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; public class RequestTests : LoggedTest { private const int _connectionStartedEventId = 1; + private const int _connectionReadFinEventId = 6; private const int _connectionResetEventId = 19; private static readonly int _semaphoreWaitTimeout = Debugger.IsAttached ? 10000 : 2500; @@ -232,6 +235,58 @@ await connection2.Receive($"HTTP/1.1 200 OK", } } + [Fact] + public async Task ConnectionClosedPriorToRequestIsLoggedAsDebug() + { + var connectionStarted = new SemaphoreSlim(0); + var connectionReadFin = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + TestSink.MessageLogged += context => + { + if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Connections" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets") + { + return; + } + + if (context.EventId.Id == _connectionStartedEventId) + { + connectionStarted.Release(); + } + else if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + + if (context.LogLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + }; + + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + // Wait until connection is established + Assert.True(await connectionStarted.WaitAsync(TestConstants.DefaultTimeout)); + + connection.ShutdownSend(); + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout)); + await connection.ReceiveEnd(); + } + } + + Assert.False(loggedHigherThanDebug); + } + [Fact] public async Task ConnectionResetPriorToRequestIsLoggedAsDebug() { @@ -283,6 +338,65 @@ public async Task ConnectionResetPriorToRequestIsLoggedAsDebug() Assert.False(loggedHigherThanDebug); } + [Fact] + public async Task ConnectionClosedBetweenRequestsIsLoggedAsDebug() + { + var connectionReadFin = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + TestSink.MessageLogged += context => + { + if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets") + { + return; + } + + if (context.LogLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + + if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + }; + + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + // Make sure the response is fully received, so a write failure (e.g. EPIPE) doesn't cause + // a more critical log message. + await connection.Receive( + "HTTP/1.1 200 OK", + "Content-Length: 0", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + + connection.ShutdownSend(); + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout)); + + await connection.ReceiveEnd(); + } + } + + Assert.False(loggedHigherThanDebug); + } + [Fact] public async Task ConnectionResetBetweenRequestsIsLoggedAsDebug() { @@ -341,10 +455,13 @@ await connection.Receive( Assert.False(loggedHigherThanDebug); } - [Fact] - public async Task ConnectionResetMidRequestIsLoggedAsDebug() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedOrResetMidRequestIsLoggedAsDebug(bool close) { var requestStarted = new SemaphoreSlim(0); + var connectionReadFin = new SemaphoreSlim(0); var connectionReset = new SemaphoreSlim(0); var connectionClosing = new SemaphoreSlim(0); var loggedHigherThanDebug = false; @@ -362,6 +479,11 @@ public async Task ConnectionResetMidRequestIsLoggedAsDebug() loggedHigherThanDebug = true; } + if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + if (context.EventId.Id == _connectionResetEventId) { connectionReset.Release(); @@ -382,15 +504,23 @@ public async Task ConnectionResetMidRequestIsLoggedAsDebug() // Wait until connection is established Assert.True(await requestStarted.WaitAsync(TestConstants.DefaultTimeout), "request should have started"); - connection.Reset(); - } + if (close) + { + connection.ShutdownSend(); + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout), "Connection close event should have been logged"); + } + else + { + connection.Reset(); - // If the reset is correctly logged as Debug, the wait below should complete shortly. - // This check MUST come before disposing the server, otherwise there's a race where the RST - // is still in flight when the connection is aborted, leading to the reset never being received - // and therefore not logged. - Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged"); - connectionClosing.Release(); + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged"); + } + connectionClosing.Release(); + } } Assert.False(loggedHigherThanDebug, "Logged event should not have been higher than debug."); @@ -534,14 +664,23 @@ await connection1.Send( Assert.Equal(beforeAbort.Value, afterAbort.Value); } - [Fact] - public async Task AbortingTheConnectionSendsFIN() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task AbortingTheConnection(bool fin) { var builder = TransportSelector.GetHostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder - .UseKestrel() + .UseSockets(options => + { + options.FinOnError = fin; + }) + .UseKestrel(o => + { + o.FinOnError = fin; + }) .UseUrls("http://127.0.0.1:0") .Configure(app => app.Run(context => { @@ -559,8 +698,15 @@ public async Task AbortingTheConnectionSendsFIN() { socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n")); - int result = socket.Receive(new byte[32]); - Assert.Equal(0, result); + if (fin) + { + int result = socket.Receive(new byte[32]); + Assert.Equal(0, result); + } + else + { + Assert.Throws(() => socket.Receive(new byte[32])); + } } await host.StopAsync(); @@ -770,16 +916,21 @@ await connection.Send("POST / HTTP/1.1", } [Theory] - [MemberData(nameof(ConnectionMiddlewareDataName))] - public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOptionsName) + [InlineData("Loopback", true)] + [InlineData("Loopback", false)] + [InlineData("PassThrough", true)] + [InlineData("PassThrough", false)] + public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOptionsName, bool fin) { const int connectionPausedEventId = 4; const int connectionFinSentEventId = 7; + const int connectionRstSentEventId = 8; const int maxRequestBufferSize = 4096; var readCallbackUnwired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var serverClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverFinConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverRstConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => @@ -795,7 +946,11 @@ public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOpti } else if (context.EventId == connectionFinSentEventId) { - serverClosedConnection.SetResult(); + serverFinConnection.SetResult(); + } + else if (context.EventId == connectionRstSentEventId) + { + serverRstConnection.SetResult(); } }; @@ -803,6 +958,7 @@ public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOpti { ServerOptions = { + FinOnError = fin, Limits = { MaxRequestBufferSize = maxRequestBufferSize, @@ -820,10 +976,30 @@ public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOpti context.Abort(); - await serverClosedConnection.Task; + if (fin) + { + await serverFinConnection.Task.DefaultTimeout(); + } + else + { + await serverRstConnection.Task.DefaultTimeout(); + } appFuncCompleted.SetResult(); - }, testContext, ConnectionMiddlewareData[listenOptionsName]())) + }, testContext, listen => + { + if (listenOptionsName == "PassThrough") + { + listen.UsePassThrough(); + } + }, + services => + { + services.Configure(options => + { + options.FinOnError = fin; + }); + })) { using (var connection = server.CreateConnection()) { @@ -949,21 +1125,21 @@ await context.Response.WriteAsync(JsonConvert.SerializeObject(new private static async Task AssertStreamContains(Stream stream, string expectedSubstring) { var expectedBytes = Encoding.ASCII.GetBytes(expectedSubstring); - var exptectedLength = expectedBytes.Length; - var responseBuffer = new byte[exptectedLength]; + var expectedLength = expectedBytes.Length; + var responseBuffer = new byte[expectedLength]; var matchedChars = 0; - while (matchedChars < exptectedLength) + while (matchedChars < expectedLength) { - var count = await stream.ReadAsync(responseBuffer, 0, exptectedLength - matchedChars).DefaultTimeout(); + var count = await stream.ReadAsync(responseBuffer, 0, expectedLength - matchedChars).DefaultTimeout(); if (count == 0) { Assert.True(false, "Stream completed without expected substring."); } - for (var i = 0; i < count && matchedChars < exptectedLength; i++) + for (var i = 0; i < count && matchedChars < expectedLength; i++) { if (responseBuffer[i] == expectedBytes[matchedChars]) { diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs index fb0b7446a3ab..d631a1c51ab5 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs @@ -22,7 +22,9 @@ using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -453,8 +455,10 @@ await connection.Send( Assert.Empty(coreLogs.Where(w => w.LogLevel > LogLevel.Information)); } - [Fact] - public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool fin) { var logger = LoggerFactory.CreateLogger($"{ typeof(ResponseTests).FullName}.{ nameof(ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate)}"); const int chunkSize = 1024; @@ -464,18 +468,27 @@ public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => { - if (context.EventId.Name == "ResponseMinimumDataRateNotSatisfied") - { - responseRateTimeoutMessageLogged.SetResult(); - } - if (context.EventId.Name == "ConnectionStop") + switch (context.EventId.Name) { - connectionStopMessageLogged.SetResult(); + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; } }; @@ -483,6 +496,7 @@ public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) @@ -528,7 +542,14 @@ async Task App(HttpContext context) } } - await using (var server = new TestServer(App, testContext)) + await using (var server = new TestServer(App, testContext, configureListenOptions: _ => { }, + services => + { + services.Configure(o => + { + o.FinOnError = fin; + }); + })) { using (var connection = server.CreateConnection()) { @@ -548,6 +569,14 @@ await connection.Send( await requestAborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } await appFuncCompleted.Task.DefaultTimeout(); await AssertStreamAborted(connection.Stream, chunkSize * chunks); @@ -557,8 +586,10 @@ await connection.Send( } } - [Fact] - public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool fin) { const int chunkSize = 1024; const int chunks = 256 * 1024; @@ -568,18 +599,27 @@ public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => { - if (context.EventId.Name == "ResponseMinimumDataRateNotSatisfied") - { - responseRateTimeoutMessageLogged.SetResult(); - } - if (context.EventId.Name == "ConnectionStop") + switch (context.EventId.Name) { - connectionStopMessageLogged.SetResult(); + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; } }; @@ -587,6 +627,7 @@ public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) @@ -626,7 +667,14 @@ void ConfigureListenOptions(ListenOptions listenOptions) { await aborted.Task.DefaultTimeout(); } - }, testContext, ConfigureListenOptions)) + }, testContext, ConfigureListenOptions, + services => + { + services.Configure(o => + { + o.FinOnError = fin; + }); + })) { using (var connection = server.CreateConnection()) { @@ -641,6 +689,14 @@ void ConfigureListenOptions(ListenOptions listenOptions) await aborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } await appFuncCompleted.Task.DefaultTimeout(); await AssertStreamAborted(connection.Stream, chunkSize * chunks); @@ -649,8 +705,10 @@ void ConfigureListenOptions(ListenOptions listenOptions) } } - [Fact] - public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure(bool fin) { const int bufferSize = 65536; const int bufferCount = 100; @@ -659,18 +717,27 @@ public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressu var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var copyToAsyncCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => { - if (context.EventId.Name == "ResponseMinimumDataRateNotSatisfied") - { - responseRateTimeoutMessageLogged.SetResult(); - } - if (context.EventId.Name == "ConnectionStop") + switch (context.EventId.Name) { - connectionStopMessageLogged.SetResult(); + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; } }; @@ -678,6 +745,7 @@ public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressu { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)), @@ -688,8 +756,6 @@ public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressu testContext.InitializeHeartbeat(); - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); - async Task App(HttpContext context) { context.RequestAborted.Register(() => @@ -714,7 +780,14 @@ async Task App(HttpContext context) copyToAsyncCts.SetException(new Exception("This shouldn't be reached.")); } - await using (var server = new TestServer(App, testContext, listenOptions)) + await using (var server = new TestServer(App, testContext, configureListenOptions: _ => { }, + services => + { + services.Configure(o => + { + o.FinOnError = fin; + }); + })) { using (var connection = server.CreateConnection()) { @@ -739,6 +812,14 @@ await connection.Send( await requestAborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } // Expect OperationCanceledException instead of IOException because the server initiated the abort due to a response rate timeout. await Assert.ThrowsAnyAsync(() => copyToAsyncCts.Task).DefaultTimeout(); diff --git a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs index 676e07bc7f1a..b9b819ccce4b 100644 --- a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs +++ b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; +using StackExchange.Redis; namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Internal; @@ -77,4 +78,6 @@ public string Ack(string serverName) { return _prefix + ":internal:ack:" + serverName; } + + public static RedisChannel GetChannel(string channelName) => new RedisChannel(channelName, RedisChannel.PatternMode.Literal); } diff --git a/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs b/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs index 84c9e5d41af0..db6ec58f35eb 100644 --- a/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs +++ b/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs @@ -123,7 +123,7 @@ public override Task OnDisconnectedAsync(HubConnectionContext connection) var tasks = new List(); RedisLog.Unsubscribe(_logger, connectionChannel); - tasks.Add(_bus.UnsubscribeAsync(connectionChannel)); + tasks.Add(_bus.UnsubscribeAsync(RedisChannels.GetChannel(connectionChannel))); var feature = connection.Features.GetRequiredFeature(); var groupNames = feature.Groups; @@ -324,7 +324,7 @@ private async Task PublishAsync(string channel, byte[] payload) { await EnsureRedisServerConnection(); RedisLog.PublishToChannel(_logger, channel); - return await _bus!.PublishAsync(channel, payload); + return await _bus!.PublishAsync(RedisChannels.GetChannel(channel), payload); } private Task AddGroupAsyncCore(HubConnectionContext connection, string groupName) @@ -357,7 +357,7 @@ await _groups.RemoveSubscriptionAsync(groupChannel, connection, this, static (st { var lifetimeManager = (RedisHubLifetimeManager)state; RedisLog.Unsubscribe(lifetimeManager._logger, channelName); - return lifetimeManager._bus!.UnsubscribeAsync(channelName); + return lifetimeManager._bus!.UnsubscribeAsync(RedisChannels.GetChannel(channelName)); }); var feature = connection.Features.GetRequiredFeature(); @@ -390,7 +390,7 @@ private Task RemoveUserAsync(HubConnectionContext connection) { var lifetimeManager = (RedisHubLifetimeManager)state; RedisLog.Unsubscribe(lifetimeManager._logger, channelName); - return lifetimeManager._bus!.UnsubscribeAsync(channelName); + return lifetimeManager._bus!.UnsubscribeAsync(RedisChannels.GetChannel(channelName)); }); } @@ -480,7 +480,7 @@ public override bool TryGetReturnType(string invocationId, [NotNullWhen(true)] o private async Task SubscribeToAll() { RedisLog.Subscribing(_logger, _channels.All); - var channel = await _bus!.SubscribeAsync(_channels.All); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(_channels.All)); channel.OnMessage(async channelMessage => { try @@ -510,7 +510,7 @@ private async Task SubscribeToAll() private async Task SubscribeToGroupManagementChannel() { - var channel = await _bus!.SubscribeAsync(_channels.GroupManagement); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(_channels.GroupManagement)); channel.OnMessage(async channelMessage => { try @@ -547,7 +547,7 @@ private async Task SubscribeToGroupManagementChannel() private async Task SubscribeToAckChannel() { // Create server specific channel in order to send an ack to a single server - var channel = await _bus!.SubscribeAsync(_channels.Ack(_serverName)); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(_channels.Ack(_serverName))); channel.OnMessage(channelMessage => { var ackId = RedisProtocol.ReadAck((byte[])channelMessage.Message); @@ -561,7 +561,7 @@ private async Task SubscribeToConnection(HubConnectionContext connection) var connectionChannel = _channels.Connection(connection.ConnectionId); RedisLog.Subscribing(_logger, connectionChannel); - var channel = await _bus!.SubscribeAsync(connectionChannel); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(connectionChannel)); channel.OnMessage(channelMessage => { var invocation = RedisProtocol.ReadInvocation((byte[])channelMessage.Message); @@ -618,7 +618,7 @@ private Task SubscribeToUser(HubConnectionContext connection) return _users.AddSubscriptionAsync(userChannel, connection, async (channelName, subscriptions) => { RedisLog.Subscribing(_logger, channelName); - var channel = await _bus!.SubscribeAsync(channelName); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(channelName)); channel.OnMessage(async channelMessage => { try @@ -644,7 +644,7 @@ private Task SubscribeToUser(HubConnectionContext connection) private async Task SubscribeToGroupAsync(string groupChannel, HubConnectionStore groupConnections) { RedisLog.Subscribing(_logger, groupChannel); - var channel = await _bus!.SubscribeAsync(groupChannel); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(groupChannel)); channel.OnMessage(async (channelMessage) => { try @@ -673,7 +673,7 @@ private async Task SubscribeToGroupAsync(string groupChannel, HubConnectionStore private async Task SubscribeToReturnResultsAsync() { - var channel = await _bus!.SubscribeAsync(_channels.ReturnResults); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(_channels.ReturnResults)); channel.OnMessage((channelMessage) => { var completion = RedisProtocol.ReadCompletion(channelMessage.Message); diff --git a/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs b/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs index db4312874c74..6047bbab39ef 100644 --- a/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs +++ b/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs @@ -147,6 +147,73 @@ public async Task CanSendAndReceiveUserMessagesWhenOneConnectionWithUserDisconne } } + [ConditionalTheory] + [SkipIfDockerNotPresent] + [MemberData(nameof(TransportTypesAndProtocolTypes))] + public async Task HubConnectionCanSendAndReceiveGroupMessagesGroupNameWithPatternIsTreatedAsLiteral(HttpTransportType transportType, string protocolName) + { + using (StartVerifiableLog()) + { + var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); + + var connection = CreateConnection(_serverFixture.FirstServer.Url + "/echo", transportType, protocol, LoggerFactory); + var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/echo", transportType, protocol, LoggerFactory); + + var tcs = new TaskCompletionSource(); + connection.On("Echo", message => tcs.TrySetResult(message)); + var tcs2 = new TaskCompletionSource(); + secondConnection.On("Echo", message => tcs2.TrySetResult(message)); + + var groupName = $"TestGroup_{transportType}_{protocolName}_{Guid.NewGuid()}"; + + await secondConnection.StartAsync().DefaultTimeout(); + await connection.StartAsync().DefaultTimeout(); + await connection.InvokeAsync("AddSelfToGroup", "*").DefaultTimeout(); + await secondConnection.InvokeAsync("AddSelfToGroup", groupName).DefaultTimeout(); + await connection.InvokeAsync("EchoGroup", groupName, "Hello, World!").DefaultTimeout(); + + Assert.Equal("Hello, World!", await tcs2.Task.DefaultTimeout()); + Assert.False(tcs.Task.IsCompleted); + + await connection.InvokeAsync("EchoGroup", "*", "Hello, World!").DefaultTimeout(); + Assert.Equal("Hello, World!", await tcs.Task.DefaultTimeout()); + + await connection.DisposeAsync().DefaultTimeout(); + } + } + + [ConditionalTheory] + [SkipIfDockerNotPresent] + [MemberData(nameof(TransportTypesAndProtocolTypes))] + public async Task CanSendAndReceiveUserMessagesUserNameWithPatternIsTreatedAsLiteral(HttpTransportType transportType, string protocolName) + { + using (StartVerifiableLog()) + { + var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); + + var connection = CreateConnection(_serverFixture.FirstServer.Url + "/echo", transportType, protocol, LoggerFactory, userName: "*"); + var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/echo", transportType, protocol, LoggerFactory, userName: "userA"); + + var tcs = new TaskCompletionSource(); + connection.On("Echo", message => tcs.TrySetResult(message)); + var tcs2 = new TaskCompletionSource(); + secondConnection.On("Echo", message => tcs2.TrySetResult(message)); + + await secondConnection.StartAsync().DefaultTimeout(); + await connection.StartAsync().DefaultTimeout(); + await connection.InvokeAsync("EchoUser", "userA", "Hello, World!").DefaultTimeout(); + + Assert.Equal("Hello, World!", await tcs2.Task.DefaultTimeout()); + Assert.False(tcs.Task.IsCompleted); + + await connection.InvokeAsync("EchoUser", "*", "Hello, World!").DefaultTimeout(); + Assert.Equal("Hello, World!", await tcs.Task.DefaultTimeout()); + + await connection.DisposeAsync().DefaultTimeout(); + await secondConnection.DisposeAsync().DefaultTimeout(); + } + } + private static HubConnection CreateConnection(string url, HttpTransportType transportType, IHubProtocol protocol, ILoggerFactory loggerFactory, string userName = null) { var hubConnectionBuilder = new HubConnectionBuilder() diff --git a/src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs b/src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs index 57827e499c67..e6cd2d53e912 100644 --- a/src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs +++ b/src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs @@ -125,6 +125,26 @@ public async Task ErrorFromConnectionFactoryLogsAndAllowsDisconnect() Assert.Equal("throw from connect", logs[0].Exception.Message); } + // Smoke test that Debug.Asserts in TestSubscriber aren't hit + [Fact] + public async Task PatternGroupAndUser() + { + var server = new TestRedisServer(); + using (var client = new TestClient()) + { + var manager = CreateLifetimeManager(server); + + var connection = HubConnectionContextUtils.Create(client.Connection); + connection.UserIdentifier = "*"; + + await manager.OnConnectedAsync(connection).DefaultTimeout(); + + var groupName = "*"; + + await manager.AddToGroupAsync(connection.ConnectionId, groupName).DefaultTimeout(); + } + } + public override TestRedisServer CreateBackplane() { return new TestRedisServer(); diff --git a/src/SignalR/server/StackExchangeRedis/test/TestConnectionMultiplexer.cs b/src/SignalR/server/StackExchangeRedis/test/TestConnectionMultiplexer.cs index 99190e6488ac..56ecea3816b2 100644 --- a/src/SignalR/server/StackExchangeRedis/test/TestConnectionMultiplexer.cs +++ b/src/SignalR/server/StackExchangeRedis/test/TestConnectionMultiplexer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net; using System.Reflection; @@ -11,6 +12,7 @@ using System.Threading.Tasks; using StackExchange.Redis; using StackExchange.Redis.Profiling; +using Xunit; namespace Microsoft.AspNetCore.SignalR.Tests; @@ -230,6 +232,8 @@ public class TestRedisServer public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { + AssertRedisChannel(channel); + if (_subscriptions.TryGetValue(channel, out var handlers)) { lock (handlers) @@ -246,6 +250,8 @@ public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags public void Subscribe(ChannelMessageQueue messageQueue, int subscriberId, CommandFlags flags = CommandFlags.None) { + AssertRedisChannel(messageQueue.Channel); + Action handler = (channel, value) => { // Workaround for https://github.com/StackExchange/StackExchange.Redis/issues/969 @@ -266,6 +272,8 @@ public void Subscribe(ChannelMessageQueue messageQueue, int subscriberId, Comman public void Unsubscribe(RedisChannel channel, int subscriberId, CommandFlags flags = CommandFlags.None) { + AssertRedisChannel(channel); + if (_subscriptions.TryGetValue(channel, out var list)) { lock (list) @@ -274,6 +282,13 @@ public void Unsubscribe(RedisChannel channel, int subscriberId, CommandFlags fla } } } + + internal static void AssertRedisChannel(RedisChannel channel) + { + var patternField = typeof(RedisChannel).GetField("IsPatternBased", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(patternField); + Assert.False((bool)patternField.GetValue(channel)); + } } public class TestSubscriber : ISubscriber @@ -319,11 +334,15 @@ public Task PingAsync(CommandFlags flags = CommandFlags.None) public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + return _server.Publish(channel, message, flags); } public async Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + await Task.Yield(); return Publish(channel, message, flags); } @@ -335,6 +354,8 @@ public void Subscribe(RedisChannel channel, Action han public Task SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + Subscribe(channel, handler, flags); return Task.CompletedTask; } @@ -351,6 +372,8 @@ public bool TryWait(Task task) public void Unsubscribe(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + _server.Unsubscribe(channel, _id, flags); } @@ -366,6 +389,8 @@ public Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None) public Task UnsubscribeAsync(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + Unsubscribe(channel, handler, flags); return Task.CompletedTask; } @@ -387,6 +412,8 @@ public void WaitAll(params Task[] tasks) public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + // Workaround for https://github.com/StackExchange/StackExchange.Redis/issues/969 var redisSubscriberType = typeof(RedisChannel).Assembly.GetType("StackExchange.Redis.RedisSubscriber"); var ctor = typeof(ChannelMessageQueue).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, @@ -400,6 +427,8 @@ public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = public Task SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + var t = Subscribe(channel, flags); return Task.FromResult(t); } diff --git a/src/submodules/googletest b/src/submodules/googletest index 04cf2989168a..6f6ab4212aa0 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 04cf2989168a3f9218d463bea6f15f8ade2032fd +Subproject commit 6f6ab4212aa02cfe02e480711246da4fc17b0761 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy