public async Task TransportFailsOnTimeoutWithErrorWhenApplicationFailsAndClientDoesNotSendCloseFrame() { using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)), pair.Transport, pair.Application, new()); using (var feature = new TestWebSocketConnectionFeature()) { var options = new WebSocketOptions { CloseTimeout = TimeSpan.FromSeconds(1) }; var ws = new WebSocketsServerTransport(options, connection.Application, connection, LoggerFactory); var serverSocket = await feature.AcceptAsync(); // Give the server socket to the transport and run it var transport = ws.ProcessSocketAsync(serverSocket); // Run the client socket var client = feature.Client.ExecuteAndCaptureFramesAsync(); // fail the client to server channel connection.Transport.Output.Complete(new Exception()); await transport.DefaultTimeout(); Assert.Equal(WebSocketState.Aborted, serverSocket.State); } } }
public async Task ClientReceivesInternalServerErrorWhenTheApplicationFails() { using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)), pair.Transport, pair.Application, new()); using (var feature = new TestWebSocketConnectionFeature()) { var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connection, LoggerFactory); // Give the server socket to the transport and run it var transport = ws.ProcessSocketAsync(await feature.AcceptAsync()); // Run the client socket var client = feature.Client.ExecuteAndCaptureFramesAsync(); // Fail in the app connection.Transport.Output.Complete(new InvalidOperationException("Catastrophic failure.")); var clientSummary = await client.DefaultTimeout(); Assert.Equal(WebSocketCloseStatus.InternalServerError, clientSummary.CloseResult.CloseStatus); // Close from the client await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); await transport.DefaultTimeout(); } } }
public async Task TransportClosesOnCloseTimeoutIfClientDoesNotSendCloseFrame() { using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)), pair.Transport, pair.Application, new()); using (var feature = new TestWebSocketConnectionFeature()) { var options = new WebSocketOptions() { CloseTimeout = TimeSpan.FromSeconds(1) }; var ws = new WebSocketsServerTransport(options, connection.Application, connection, LoggerFactory); var serverSocket = await feature.AcceptAsync(); // Give the server socket to the transport and run it var transport = ws.ProcessSocketAsync(serverSocket); // End the app connection.Transport.Output.Complete(); await transport.DefaultTimeout(TimeSpan.FromSeconds(10)); // Now we're closed Assert.Equal(WebSocketState.Aborted, serverSocket.State); serverSocket.Dispose(); } } }
public async Task TransportCommunicatesErrorToApplicationWhenClientDisconnectsAbnormally() { using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1")) { Transport = pair.Transport, Application = pair.Application, }; using (var feature = new TestWebSocketConnectionFeature()) { async Task CompleteApplicationAfterTransportCompletes() { try { // Wait until the transport completes so that we can end the application var result = await connection.Transport.Input.ReadAsync(); connection.Transport.Input.AdvanceTo(result.Buffer.End); } catch (Exception ex) { Assert.IsType <WebSocketError>(ex); } finally { // Complete the application so that the connection unwinds without aborting connection.Transport.Output.Complete(); } } var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2")); var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); // Give the server socket to the transport and run it var transport = ws.ProcessSocketAsync(await feature.AcceptAsync()); // Run the client socket var client = feature.Client.ExecuteAndCaptureFramesAsync(); // When the close frame is received, we complete the application so the send // loop unwinds _ = CompleteApplicationAfterTransportCompletes(); // Terminate the client to server channel with an exception feature.Client.SendAbort(); // Wait for the transport await transport.OrTimeout(); await client.OrTimeout(); } } }
public async Task SubProtocolSelectorIsUsedToSelectSubProtocol() { const string ExpectedSubProtocol = "expected"; var providedSubProtocols = new[] { "provided1", "provided2" }; using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext))) { Transport = pair.Transport, Application = pair.Application, }; using (var feature = new TestWebSocketConnectionFeature()) { var options = new WebSocketOptions { // We want to verify behavior without timeout affecting it CloseTimeout = TimeSpan.FromSeconds(20), SubProtocolSelector = protocols => { Assert.Equal(providedSubProtocols, protocols.ToArray()); return(ExpectedSubProtocol); }, }; var connectionContext = new HttpConnectionContext(string.Empty, null, null); var ws = new WebSocketsServerTransport(options, connection.Application, connectionContext, LoggerFactory); // Create an HttpContext var context = new DefaultHttpContext(); context.Request.Headers.Add(HeaderNames.WebSocketSubProtocols, providedSubProtocols.ToArray()); context.Features.Set <IHttpWebSocketFeature>(feature); var transport = ws.ProcessRequestAsync(context, CancellationToken.None); await feature.Accepted.OrThrowIfOtherFails(transport); // Assert the feature got the right subprotocol Assert.Equal(ExpectedSubProtocol, feature.SubProtocol); // Run the client socket var client = feature.Client.ExecuteAndCaptureFramesAsync(); await feature.Client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None).OrTimeout(); // close the client to server channel connection.Transport.Output.Complete(); _ = await client.OrTimeout(); await transport.OrTimeout(); } } }
public async Task ReceivedFramesAreWrittenToChannel(string webSocketMessageType) { using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1")) { Transport = pair.Transport, Application = pair.Application, }; using (var feature = new TestWebSocketConnectionFeature()) { var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2")); var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); // Give the server socket to the transport and run it var transport = ws.ProcessSocketAsync(await feature.AcceptAsync()); // Run the client socket var client = feature.Client.ExecuteAndCaptureFramesAsync(); // Send a frame, then close await feature.Client.SendAsync( buffer : new ArraySegment <byte>(Encoding.UTF8.GetBytes("Hello")), messageType : (WebSocketMessageType)Enum.Parse(typeof(WebSocketMessageType), webSocketMessageType), endOfMessage : true, cancellationToken : CancellationToken.None); await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); var result = await connection.Transport.Input.ReadAsync(); var buffer = result.Buffer; Assert.Equal("Hello", Encoding.UTF8.GetString(buffer.ToArray())); connection.Transport.Input.AdvanceTo(buffer.End); connection.Transport.Output.Complete(); // The transport should finish now await transport; // The connection should close after this, which means the client will get a close frame. var clientSummary = await client; Assert.Equal(WebSocketCloseStatus.NormalClosure, clientSummary.CloseResult.CloseStatus); } } }
public async Task ServerGracefullyClosesWhenClientSendsCloseFrameThenApplicationEnds() { using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext))) { Transport = pair.Transport, Application = pair.Application, }; using (var feature = new TestWebSocketConnectionFeature()) { var options = new WebSocketOptions { // We want to verify behavior without timeout affecting it CloseTimeout = TimeSpan.FromSeconds(20) }; var connectionContext = new HttpConnectionContext(string.Empty, null, null); var ws = new WebSocketsServerTransport(options, connection.Application, connectionContext, LoggerFactory); var serverSocket = await feature.AcceptAsync(); // Give the server socket to the transport and run it var transport = ws.ProcessSocketAsync(serverSocket); // Run the client socket var client = feature.Client.ExecuteAndCaptureFramesAsync(); await feature.Client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None).OrTimeout(); // close the client to server channel connection.Transport.Output.Complete(); _ = await client.OrTimeout(); await transport.OrTimeout(); Assert.Equal(WebSocketCloseStatus.NormalClosure, serverSocket.CloseStatus); } } }
public async Task WebSocketTransportSetsMessageTypeBasedOnTransferFormatFeature(TransferFormat transferFormat, string expectedMessageType) { using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1")) { Transport = pair.Transport, Application = pair.Application, }; using (var feature = new TestWebSocketConnectionFeature()) { var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2")); connectionContext.ActiveFormat = transferFormat; var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); // Give the server socket to the transport and run it var transport = ws.ProcessSocketAsync(await feature.AcceptAsync()); // Run the client socket var client = feature.Client.ExecuteAndCaptureFramesAsync(); // Write to the output channel, and then complete it await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello")); connection.Transport.Output.Complete(); // The client should finish now, as should the server var clientSummary = await client; await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); await transport; Assert.Equal(1, clientSummary.Received.Count); Assert.True(clientSummary.Received[0].EndOfMessage); Assert.Equal((WebSocketMessageType)Enum.Parse(typeof(WebSocketMessageType), expectedMessageType), clientSummary.Received[0].MessageType); Assert.Equal("Hello", Encoding.UTF8.GetString(clientSummary.Received[0].Buffer)); } } }
private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connectionDelegate, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope) { // set a tag to allow Application Performance Management tools to differentiate long running requests for reporting purposes context.Features.Get <IHttpActivityFeature>()?.Activity.AddTag("http.long_running", "true"); var supportedTransports = options.Transports; // Server sent events transport // GET /{path} // Accept: text/event-stream var headers = context.Request.GetTypedHeaders(); if (headers.Accept?.Contains(new Net.Http.Headers.MediaTypeHeaderValue("text/event-stream")) == true) { // Connection must already exist var connection = await GetConnectionAsync(context); if (connection == null) { // No such connection, GetConnection already set the response status code return; } if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.ServerSentEvents, supportedTransports, logScope, options)) { // Bad connection state. It's already set the response status code. return; } Log.EstablishedConnection(_logger); // ServerSentEvents is a text protocol only connection.SupportedFormats = TransferFormat.Text; // We only need to provide the Input channel since writing to the application is handled through /send. var sse = new ServerSentEventsServerTransport(connection.Application.Input, connection.ConnectionId, connection, _loggerFactory); await DoPersistentConnection(connectionDelegate, sse, context, connection); } else if (context.WebSockets.IsWebSocketRequest) { // Connection can be established lazily var connection = await GetOrCreateConnectionAsync(context, options); if (connection == null) { // No such connection, GetOrCreateConnection already set the response status code return; } if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.WebSockets, supportedTransports, logScope, options)) { // Bad connection state. It's already set the response status code. return; } Log.EstablishedConnection(_logger); // Allow the reads to be canceled connection.Cancellation = new CancellationTokenSource(); var ws = new WebSocketsServerTransport(options.WebSockets, connection.Application, connection, _loggerFactory); await DoPersistentConnection(connectionDelegate, ws, context, connection); } else { // GET /{path} maps to long polling // Connection must already exist var connection = await GetConnectionAsync(context); if (connection == null) { // No such connection, GetConnection already set the response status code return; } if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.LongPolling, supportedTransports, logScope, options)) { // Bad connection state. It's already set the response status code. return; } if (!await connection.CancelPreviousPoll(context)) { // Connection closed. It's already set the response status code. return; } // Create a new Tcs every poll to keep track of the poll finishing, so we can properly wait on previous polls var currentRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); if (!connection.TryActivateLongPollingConnection( connectionDelegate, context, options.LongPolling.PollTimeout, currentRequestTcs.Task, _loggerFactory, _logger)) { return; } var resultTask = await Task.WhenAny(connection.ApplicationTask !, connection.TransportTask !); try { // If the application ended before the transport task then we potentially need to end the connection if (resultTask == connection.ApplicationTask) { // Complete the transport (notifying it of the application error if there is one) connection.Transport.Output.Complete(connection.ApplicationTask.Exception); // Wait for the transport to run // Ignore exceptions, it has been logged if there is one and the application has finished // So there is no one to give the exception to await connection.TransportTask !.NoThrow(); // If the status code is a 204 it means the connection is done if (context.Response.StatusCode == StatusCodes.Status204NoContent) { // Cancel current request to release any waiting poll and let dispose acquire the lock currentRequestTcs.TrySetCanceled(); // We should be able to safely dispose because there's no more data being written // We don't need to wait for close here since we've already waited for both sides await _manager.DisposeAndRemoveAsync(connection, closeGracefully : false); } else { // Only allow repoll if we aren't removing the connection. connection.MarkInactive(); } } else if (resultTask.IsFaulted || resultTask.IsCanceled) { // Cancel current request to release any waiting poll and let dispose acquire the lock currentRequestTcs.TrySetCanceled(); // We should be able to safely dispose because there's no more data being written // We don't need to wait for close here since we've already waited for both sides await _manager.DisposeAndRemoveAsync(connection, closeGracefully : false); } else { // Only allow repoll if we aren't removing the connection. connection.MarkInactive(); } } finally { // Artificial task queue // This will cause incoming polls to wait until the previous poll has finished updating internal state info currentRequestTcs.TrySetResult(); } } }