public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat) { using (StartVerifableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echoAndClose"), transferFormat); await webSocketsTransport.Output.WriteAsync(new byte[] { 0x42 }); // The echoAndClose endpoint closes the connection immediately after sending response which should stop the transport await webSocketsTransport.Running.OrTimeout(); Assert.True(webSocketsTransport.Input.TryRead(out var result)); Assert.Equal(new byte[] { 0x42 }, result.Buffer.ToArray()); webSocketsTransport.Input.AdvanceTo(result.Buffer.End); } }
public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat) { await using (var server = await StartServer <Startup>()) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: LoggerFactory, accessTokenProvider: null); await webSocketsTransport.StartAsync(new Uri(server.WebSocketsUrl + "/echoAndClose"), transferFormat); await webSocketsTransport.Output.WriteAsync(new byte[] { 0x42 }); // The echoAndClose endpoint closes the connection immediately after sending response which should stop the transport await webSocketsTransport.Running.DefaultTimeout(); Assert.True(webSocketsTransport.Input.TryRead(out var result)); Assert.Equal(new byte[] { 0x42 }, result.Buffer.ToArray()); webSocketsTransport.Input.AdvanceTo(result.Buffer.End); } }
public async Task ReceivedFramesAreWrittenToChannel(WebSocketMessageType webSocketMessageType) { using (StartLog(out var loggerFactory)) { var transportToApplication = Channel.CreateUnbounded <byte[]>(); var applicationToTransport = Channel.CreateUnbounded <byte[]>(); using (var transportSide = ChannelConnection.Create <byte[]>(applicationToTransport, transportToApplication)) using (var applicationSide = ChannelConnection.Create <byte[]>(transportToApplication, applicationToTransport)) using (var feature = new TestWebSocketConnectionFeature()) { var connectionContext = new DefaultConnectionContext(string.Empty, null, null); var ws = new WebSocketsTransport(new WebSocketOptions(), transportSide, 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, endOfMessage : true, cancellationToken : CancellationToken.None); await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); var buffer = await applicationSide.Reader.ReadAsync(); Assert.Equal("Hello", Encoding.UTF8.GetString(buffer)); Assert.True(applicationSide.Writer.TryComplete()); // 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 ReceivedFramesAreWrittenToChannel(WebSocketMessageType webSocketMessageType) { using (StartLog(out var loggerFactory, LogLevel.Debug)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new DefaultConnectionContext("foo", pair.Transport, pair.Application); using (var feature = new TestWebSocketConnectionFeature()) { var connectionContext = new DefaultConnectionContext(string.Empty, null, null); var ws = new WebSocketsTransport(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, 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 TransportFailsWhenClientDisconnectsAbnormally() { using (StartLog(out var loggerFactory)) { var transportToApplication = Channel.CreateUnbounded <byte[]>(); var applicationToTransport = Channel.CreateUnbounded <byte[]>(); using (var transportSide = ChannelConnection.Create <byte[]>(applicationToTransport, transportToApplication)) using (var applicationSide = ChannelConnection.Create <byte[]>(transportToApplication, applicationToTransport)) using (var feature = new TestWebSocketConnectionFeature()) { async Task CompleteApplicationAfterTransportCompletes() { // Wait until the transport completes so that we can end the application await applicationSide.Reader.WaitToReadAsync(); // Complete the application so that the connection unwinds without aborting applicationSide.Writer.TryComplete(); } var connectionContext = new DefaultConnectionContext(string.Empty, null, null); var ws = new WebSocketsTransport(new WebSocketOptions(), transportSide, 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 Assert.ThrowsAsync <WebSocketException>(() => transport).OrTimeout(); var summary = await client.OrTimeout(); Assert.Equal(WebSocketCloseStatus.InternalServerError, summary.CloseResult.CloseStatus); } } }
public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat) { using (StartLog(out var loggerFactory)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echoAndClose"), pair.Application, transferFormat, connection : Mock.Of <IConnection>()); await pair.Transport.Output.WriteAsync(new byte[] { 0x42 }); // The echoAndClose endpoint closes the connection immediately after sending response which should stop the transport await webSocketsTransport.Running.OrTimeout(); Assert.True(pair.Transport.Input.TryRead(out var result)); Assert.Equal(new byte[] { 0x42 }, result.Buffer.ToArray()); pair.Transport.Input.AdvanceTo(result.Buffer.End); } }
public async Task WebSocketsTransportSetsTransferMode(TransferMode transferMode) { using (StartLog(out var loggerFactory)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); Assert.Null(webSocketsTransport.Mode); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, transferMode, connection : Mock.Of <IConnection>()).OrTimeout(); Assert.Equal(transferMode, webSocketsTransport.Mode); await webSocketsTransport.StopAsync().OrTimeout(); await webSocketsTransport.Running.OrTimeout(); } }
public async Task TransportFailsWhenClientDisconnectsAbnormally() { using (StartLog(out var loggerFactory, LogLevel.Debug)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new DefaultConnectionContext("foo", pair.Transport, pair.Application); using (var feature = new TestWebSocketConnectionFeature()) { async Task CompleteApplicationAfterTransportCompletes() { // 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); // Complete the application so that the connection unwinds without aborting connection.Transport.Output.Complete(); } var connectionContext = new DefaultConnectionContext(string.Empty, null, null); var ws = new WebSocketsTransport(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 Assert.ThrowsAsync <WebSocketException>(() => transport).OrTimeout(); await client.OrTimeout(); } } }
public async Task WebSocketsTransportSendsXRequestedWithHeader() { using (StartVerifiableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory, accessTokenProvider: null); await webSocketsTransport.StartAsync(new Uri(ServerFixture.WebSocketsUrl + "/httpheader"), TransferFormat.Binary).OrTimeout(); await webSocketsTransport.Output.WriteAsync(Encoding.UTF8.GetBytes("X-Requested-With")); // The HTTP header endpoint closes the connection immediately after sending response which should stop the transport await webSocketsTransport.Running.OrTimeout(); Assert.True(webSocketsTransport.Input.TryRead(out var result)); var headerValue = Encoding.UTF8.GetString(result.Buffer.ToArray()); Assert.Equal("XMLHttpRequest", headerValue); } }
public async Task WebSocketTransportSetsMessageTypeBasedOnTransferModeFeature(TransferMode transferMode, WebSocketMessageType expectedMessageType) { using (StartLog(out var loggerFactory)) { var transportToApplication = Channel.CreateUnbounded <byte[]>(); var applicationToTransport = Channel.CreateUnbounded <byte[]>(); using (var transportSide = ChannelConnection.Create <byte[]>(applicationToTransport, transportToApplication)) using (var applicationSide = ChannelConnection.Create <byte[]>(transportToApplication, applicationToTransport)) using (var feature = new TestWebSocketConnectionFeature()) { var connectionContext = new DefaultConnectionContext(string.Empty, null, null) { TransferMode = transferMode }; var ws = new WebSocketsTransport(new WebSocketOptions(), transportSide, 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 applicationSide.Writer.WriteAsync(Encoding.UTF8.GetBytes("Hello")); Assert.True(applicationSide.Writer.TryComplete()); // 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(expectedMessageType, clientSummary.Received[0].MessageType); Assert.Equal("Hello", Encoding.UTF8.GetString(clientSummary.Received[0].Buffer)); } } }
public async Task ServerGracefullyClosesWhenApplicationEndsThenClientSendsCloseFrame() { using (StartLog(out var loggerFactory)) { var transportToApplication = Channel.CreateUnbounded <byte[]>(); var applicationToTransport = Channel.CreateUnbounded <byte[]>(); using (var transportSide = ChannelConnection.Create <byte[]>(applicationToTransport, transportToApplication)) using (var applicationSide = ChannelConnection.Create <byte[]>(transportToApplication, applicationToTransport)) 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 DefaultConnectionContext(string.Empty, null, null); var ws = new WebSocketsTransport(options, transportSide, 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(); // close the client to server channel applicationToTransport.Writer.TryComplete(); _ = await client.OrTimeout(); await feature.Client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None).OrTimeout(); await transport.OrTimeout(); Assert.Equal(WebSocketCloseStatus.NormalClosure, serverSocket.CloseStatus); } } }
public async Task WebSocketsTransportSetsTransferMode(TransferMode transferMode) { using (StartLog(out var loggerFactory)) { var connectionToTransport = Channel.CreateUnbounded <SendMessage>(); var transportToConnection = Channel.CreateUnbounded <byte[]>(); var channelConnection = new ChannelConnection <SendMessage, byte[]>(connectionToTransport, transportToConnection); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); Assert.Null(webSocketsTransport.Mode); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), channelConnection, transferMode, connectionId : string.Empty, connection : Mock.Of <IConnection>()).OrTimeout(); Assert.Equal(transferMode, webSocketsTransport.Mode); await webSocketsTransport.StopAsync().OrTimeout(); await webSocketsTransport.Running.OrTimeout(); } }
public async Task WebSocketsTransportSendsXRequestedWithHeader() { using (StartLog(out var loggerFactory)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/httpheader"), pair.Application, TransferFormat.Binary, connection : Mock.Of <IConnection>()).OrTimeout(); await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("X-Requested-With")); // The HTTP header endpoint closes the connection immediately after sending response which should stop the transport await webSocketsTransport.Running.OrTimeout(); Assert.True(pair.Transport.Input.TryRead(out var result)); var headerValue = Encoding.UTF8.GetString(result.Buffer.ToArray()); Assert.Equal("XMLHttpRequest", headerValue); } }
private static async Task UpgradeToWebSockets(RavenDBOptions options, IOwinContext context, Func <Task> next) { var accept = context.Get <Action <IDictionary <string, object>, Func <IDictionary <string, object>, Task> > >("websocket.Accept"); if (accept == null) { // Not a websocket request await next(); return; } WebSocketsTransport webSocketsTrasport = WebSocketTransportFactory.CreateWebSocketTransport(options, context); if (webSocketsTrasport != null) { if (await webSocketsTrasport.TrySetupRequest()) { accept(null, webSocketsTrasport.Run); } } }
public async Task ServerGracefullyClosesWhenApplicationEndsThenClientSendsCloseFrame() { using (StartLog(out var loggerFactory, LogLevel.Debug)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new DefaultConnectionContext("foo", pair.Transport, 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 DefaultConnectionContext(string.Empty, null, null); var ws = new WebSocketsTransport(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(); // close the client to server channel connection.Transport.Output.Complete(); _ = await client.OrTimeout(); await feature.Client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None).OrTimeout(); await transport.OrTimeout(); Assert.Equal(WebSocketCloseStatus.NormalClosure, serverSocket.CloseStatus); } } }
public async Task WebSocketTransportSetsMessageTypeBasedOnTransferFormatFeature(TransferFormat transferFormat, WebSocketMessageType expectedMessageType) { using (StartLog(out var loggerFactory, LogLevel.Debug)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new DefaultConnectionContext("foo", pair.Transport, pair.Application); using (var feature = new TestWebSocketConnectionFeature()) { var connectionContext = new DefaultConnectionContext(string.Empty, null, null); connectionContext.ActiveFormat = transferFormat; var ws = new WebSocketsTransport(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(expectedMessageType, clientSummary.Received[0].MessageType); Assert.Equal("Hello", Encoding.UTF8.GetString(clientSummary.Received[0].Buffer)); } } }
public async Task TransportClosesOnCloseTimeoutIfClientDoesNotSendCloseFrame() { using (StartLog(out var loggerFactory)) { var transportToApplication = Channel.CreateUnbounded <byte[]>(); var applicationToTransport = Channel.CreateUnbounded <byte[]>(); using (var transportSide = ChannelConnection.Create <byte[]>(applicationToTransport, transportToApplication)) using (var applicationSide = ChannelConnection.Create <byte[]>(transportToApplication, applicationToTransport)) using (var feature = new TestWebSocketConnectionFeature()) { var options = new WebSocketOptions() { CloseTimeout = TimeSpan.FromSeconds(1) }; var connectionContext = new DefaultConnectionContext(string.Empty, null, null); var ws = new WebSocketsTransport(options, transportSide, connectionContext, loggerFactory); var serverSocket = await feature.AcceptAsync(); // Give the server socket to the transport and run it var transport = ws.ProcessSocketAsync(serverSocket); // End the app applicationSide.Dispose(); await transport.OrTimeout(TimeSpan.FromSeconds(10)); // Now we're closed Assert.Equal(WebSocketState.Aborted, serverSocket.State); serverSocket.Dispose(); } } }
public async Task TransportFailsOnTimeoutWithErrorWhenApplicationFailsAndClientDoesNotSendCloseFrame() { using (StartLog(out var loggerFactory)) { var transportToApplication = Channel.CreateUnbounded <byte[]>(); var applicationToTransport = Channel.CreateUnbounded <byte[]>(); using (var transportSide = ChannelConnection.Create <byte[]>(applicationToTransport, transportToApplication)) using (var applicationSide = ChannelConnection.Create <byte[]>(transportToApplication, applicationToTransport)) using (var feature = new TestWebSocketConnectionFeature()) { var options = new WebSocketOptions { CloseTimeout = TimeSpan.FromSeconds(1) }; var connectionContext = new DefaultConnectionContext(string.Empty, null, null); var ws = new WebSocketsTransport(options, transportSide, 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(); // fail the client to server channel applicationToTransport.Writer.TryComplete(new Exception()); await Assert.ThrowsAsync <Exception>(() => transport).OrTimeout(); Assert.Equal(WebSocketState.Aborted, serverSocket.State); } } }
private async Task ExecuteEndpointAsync(HttpContext context, ConnectionDelegate ConnectionDelegate, HttpSocketOptions options, ConnectionLogScope logScope) { 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, TransportType.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 ServerSentEventsTransport(connection.Application.Input, connection.ConnectionId, _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, TransportType.WebSockets, supportedTransports, logScope, options)) { // Bad connection state. It's already set the response status code. return; } Log.EstablishedConnection(_logger); var ws = new WebSocketsTransport(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, TransportType.LongPolling, supportedTransports, logScope, options)) { // Bad connection state. It's already set the response status code. return; } try { await connection.Lock.WaitAsync(); if (connection.Status == DefaultConnectionContext.ConnectionStatus.Disposed) { Log.ConnectionDisposed(_logger, connection.ConnectionId); // The connection was disposed context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.ContentType = "text/plain"; return; } if (connection.Status == DefaultConnectionContext.ConnectionStatus.Active) { var existing = connection.GetHttpContext(); Log.ConnectionAlreadyActive(_logger, connection.ConnectionId, existing.TraceIdentifier); using (connection.Cancellation) { // Cancel the previous request connection.Cancellation.Cancel(); // Wait for the previous request to drain await connection.TransportTask; Log.PollCanceled(_logger, connection.ConnectionId, existing.TraceIdentifier); } } // Mark the connection as active connection.Status = DefaultConnectionContext.ConnectionStatus.Active; // Raise OnConnected for new connections only since polls happen all the time if (connection.ApplicationTask == null) { Log.EstablishedConnection(_logger); connection.Items[ConnectionMetadataNames.Transport] = TransportType.LongPolling; connection.ApplicationTask = ExecuteApplication(ConnectionDelegate, connection); } else { Log.ResumingConnection(_logger); } // REVIEW: Performance of this isn't great as this does a bunch of per request allocations connection.Cancellation = new CancellationTokenSource(); var timeoutSource = new CancellationTokenSource(); var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(connection.Cancellation.Token, context.RequestAborted, timeoutSource.Token); // Dispose these tokens when the request is over context.Response.RegisterForDispose(timeoutSource); context.Response.RegisterForDispose(tokenSource); var longPolling = new LongPollingTransport(timeoutSource.Token, connection.Application.Input, connection.ConnectionId, _loggerFactory); // Start the transport connection.TransportTask = longPolling.ProcessRequestAsync(context, tokenSource.Token); // Start the timeout after we return from creating the transport task timeoutSource.CancelAfter(options.LongPolling.PollTimeout); } finally { connection.Lock.Release(); } var resultTask = await Task.WhenAny(connection.ApplicationTask, connection.TransportTask); var pollAgain = true; // 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 await connection.TransportTask; // If the status code is a 204 it means the connection is done if (context.Response.StatusCode == StatusCodes.Status204NoContent) { // We should be able to safely dispose because there's no more data being written await _manager.DisposeAndRemoveAsync(connection); // Don't poll again if we've removed the connection completely pollAgain = false; } } else if (context.Response.StatusCode == StatusCodes.Status204NoContent) { // Don't poll if the transport task was cancelled pollAgain = false; } if (pollAgain) { // Otherwise, we update the state to inactive again and wait for the next poll try { await connection.Lock.WaitAsync(); if (connection.Status == DefaultConnectionContext.ConnectionStatus.Active) { // Mark the connection as inactive connection.LastSeenUtc = DateTime.UtcNow; connection.Status = DefaultConnectionContext.ConnectionStatus.Inactive; connection.SetHttpContext(null); // Dispose the cancellation token connection.Cancellation.Dispose(); connection.Cancellation = null; } } finally { connection.Lock.Release(); } } } }
private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connectionDelegate, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope) { 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 ServerSentEventsTransport(connection.Application.Input, connection.ConnectionId, _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 cancelled connection.Cancellation = new CancellationTokenSource(); var ws = new WebSocketsTransport(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; } // 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 <object>(TaskCreationOptions.RunContinuationsAsynchronously); using (connection.Cancellation) { // Cancel the previous request connection.Cancellation?.Cancel(); try { // Wait for the previous request to drain await connection.PreviousPollTask; } catch (OperationCanceledException) { // Previous poll canceled due to connection closing, close this poll too context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status204NoContent; return; } } 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 await connection.TransportTask; // 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) { // Cancel current request to release any waiting poll and let dispose acquire the lock currentRequestTcs.TrySetCanceled(); // transport task was faulted, we should remove the connection 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(null); } } }
public WebSocketConnectionContext(WebSocketConnectionOptions httpConnectionOptions, ILoggerFactory loggerFactory, Func <Task <string> > accessTokenProvider) { Transport = _websocketTransport = new WebSocketsTransport(httpConnectionOptions, loggerFactory, accessTokenProvider); ConnectionId = "sc_" + Guid.NewGuid(); }
private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connectionDelegate, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope) { 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 ServerSentEventsTransport(connection.Application.Input, connection.ConnectionId, _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 cancelled connection.Cancellation = new CancellationTokenSource(); var ws = new WebSocketsTransport(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; } // 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 <object>(TaskCreationOptions.RunContinuationsAsynchronously); await connection.StateLock.WaitAsync(); try { if (connection.Status == HttpConnectionStatus.Disposed) { Log.ConnectionDisposed(_logger, connection.ConnectionId); // The connection was disposed context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.ContentType = "text/plain"; return; } if (connection.Status == HttpConnectionStatus.Active) { var existing = connection.GetHttpContext(); Log.ConnectionAlreadyActive(_logger, connection.ConnectionId, existing.TraceIdentifier); } using (connection.Cancellation) { // Cancel the previous request connection.Cancellation?.Cancel(); try { // Wait for the previous request to drain await connection.PreviousPollTask; } catch (OperationCanceledException) { // Previous poll canceled due to connection closing, close this poll too context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status204NoContent; return; } connection.PreviousPollTask = currentRequestTcs.Task; } // Mark the connection as active connection.Status = HttpConnectionStatus.Active; // Raise OnConnected for new connections only since polls happen all the time if (connection.ApplicationTask == null) { Log.EstablishedConnection(_logger); connection.ApplicationTask = ExecuteApplication(connectionDelegate, connection); context.Response.ContentType = "application/octet-stream"; // This request has no content context.Response.ContentLength = 0; // On the first poll, we flush the response immediately to mark the poll as "initialized" so future // requests can be made safely connection.TransportTask = context.Response.Body.FlushAsync(); } else { Log.ResumingConnection(_logger); // REVIEW: Performance of this isn't great as this does a bunch of per request allocations connection.Cancellation = new CancellationTokenSource(); var timeoutSource = new CancellationTokenSource(); var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(connection.Cancellation.Token, context.RequestAborted, timeoutSource.Token); // Dispose these tokens when the request is over context.Response.RegisterForDispose(timeoutSource); context.Response.RegisterForDispose(tokenSource); var longPolling = new LongPollingTransport(timeoutSource.Token, connection.Application.Input, _loggerFactory); // Start the transport connection.TransportTask = longPolling.ProcessRequestAsync(context, tokenSource.Token); // Start the timeout after we return from creating the transport task timeoutSource.CancelAfter(options.LongPolling.PollTimeout); } } finally { connection.StateLock.Release(); } var resultTask = await Task.WhenAny(connection.ApplicationTask, connection.TransportTask); try { var pollAgain = true; // 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 await connection.TransportTask; // 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); // Don't poll again if we've removed the connection completely pollAgain = false; } } else if (resultTask.IsFaulted) { // Cancel current request to release any waiting poll and let dispose acquire the lock currentRequestTcs.TrySetCanceled(); // transport task was faulted, we should remove the connection await _manager.DisposeAndRemoveAsync(connection, closeGracefully : false); pollAgain = false; } else if (context.Response.StatusCode == StatusCodes.Status204NoContent) { // Don't poll if the transport task was canceled pollAgain = false; } if (pollAgain) { // Mark the connection as inactive connection.LastSeenUtc = DateTime.UtcNow; connection.Status = HttpConnectionStatus.Inactive; } } finally { // Artificial task queue // This will cause incoming polls to wait until the previous poll has finished updating internal state info currentRequestTcs.TrySetResult(null); } } }