public void MapHubEndPointRoutingFindsAttributesOnHubAndFromOptions() { var authCount = 0; HttpConnectionDispatcherOptions configuredOptions = null; using (var host = BuildWebHost(routes => routes.MapHub <AuthHub>("/path", options => { authCount += options.AuthorizationData.Count; options.AuthorizationData.Add(new AuthorizeAttribute()); configuredOptions = options; }))) { host.Start(); var dataSource = host.Services.GetRequiredService <EndpointDataSource>(); // We register 2 endpoints (/negotiate and /) Assert.Collection(dataSource.Endpoints, endpoint => { Assert.Equal("/path/negotiate", endpoint.DisplayName); Assert.Equal(2, endpoint.Metadata.GetOrderedMetadata <IAuthorizeData>().Count); }, endpoint => { Assert.Equal("/path", endpoint.DisplayName); Assert.Equal(2, endpoint.Metadata.GetOrderedMetadata <IAuthorizeData>().Count); }); } Assert.Equal(0, authCount); }
/// <summary> /// Maps incoming requests with the specified path to the provided connection pipeline. /// </summary> /// <typeparam name="TConnectionHandler">The <see cref="ConnectionHandler"/> type.</typeparam> /// <param name="endpoints">The <see cref="IEndpointBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="configureOptions">A callback to configure dispatcher options.</param> /// <returns>An <see cref="ConnectionEndpointRouteBuilder"/> for endpoints associated with the connections.</returns> public static ConnectionEndpointRouteBuilder MapConnectionHandler <TConnectionHandler>(this IEndpointBuilder endpoints, string pattern, Action <HttpConnectionDispatcherOptions>?configureOptions) where TConnectionHandler : ConnectionHandler { var options = new HttpConnectionDispatcherOptions(); configureOptions?.Invoke(options); var conventionBuilder = endpoints.MapConnections(pattern, options, b => { b.UseConnectionHandler <TConnectionHandler>(); }); var attributes = typeof(TConnectionHandler).GetCustomAttributes(inherit: true); conventionBuilder.Add(e => { // Add all attributes on the ConnectionHandler has metadata (this will allow for things like) // auth attributes and cors attributes to work seamlessly foreach (var item in attributes) { e.Metadata.Add(item); } }); return(conventionBuilder); }
/// <summary> /// Maps incoming requests with the specified path to the provided connection pipeline. /// </summary> /// <typeparam name="TConnectionHandler">The <see cref="ConnectionHandler"/> type.</typeparam> /// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="configureOptions">A callback to configure dispatcher options.</param> /// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with the connections.</returns> public static IEndpointConventionBuilder MapConnectionHandler <TConnectionHandler>(this IEndpointRouteBuilder routes, string pattern, Action <HttpConnectionDispatcherOptions> configureOptions) where TConnectionHandler : ConnectionHandler { var options = new HttpConnectionDispatcherOptions(); // REVIEW: WE should consider removing this and instead just relying on the // AuthorizationMiddleware var attributes = typeof(TConnectionHandler).GetCustomAttributes(inherit: true); foreach (var attribute in attributes.OfType <AuthorizeAttribute>()) { options.AuthorizationData.Add(attribute); } configureOptions?.Invoke(options); var conventionBuilder = routes.MapConnections(pattern, options, b => { b.UseConnectionHandler <TConnectionHandler>(); }); conventionBuilder.Add(e => { // Add all attributes on the ConnectionHandler has metadata (this will allow for things like) // auth attributes and cors attributes to work seamlessly foreach (var item in attributes) { e.Metadata.Add(item); } }); return(conventionBuilder); }
private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options) { var transportPipeOptions = new PipeOptions(pauseWriterThreshold: options.TransportMaxBufferSize, resumeWriterThreshold: options.TransportMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false); var appPipeOptions = new PipeOptions(pauseWriterThreshold: options.ApplicationMaxBufferSize, resumeWriterThreshold: options.ApplicationMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false); return(_manager.CreateConnection(transportPipeOptions, appPipeOptions)); }
private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope) { context.Response.ContentType = "application/json"; // Establish the connection var connection = CreateConnection(options); // Set the Connection ID on the logging scope so that logs from now on will have the // Connection ID metadata set. logScope.ConnectionId = connection.ConnectionId; // Don't use thread static instance here because writer is used with async var writer = new MemoryBufferWriter(); try { // Get the bytes for the connection id WriteNegotiatePayload(writer, connection.ConnectionId, context, options); Log.NegotiationRequest(_logger); // Write it out to the response with the right content length context.Response.ContentLength = writer.Length; await writer.CopyToAsync(context.Response.Body); } finally { writer.Reset(); } }
public void HttpConnectionDispatcherOptionsDefaults() { var options = new HttpConnectionDispatcherOptions(); Assert.Equal(TimeSpan.FromSeconds(10), options.TransportSendTimeout); Assert.Equal(65536, options.TransportMaxBufferSize); Assert.Equal(65536, options.ApplicationMaxBufferSize); Assert.Equal(HttpTransports.All, options.Transports); Assert.False(options.CloseOnAuthenticationExpiration); }
private async Task ProcessSend(HttpContext context, HttpConnectionDispatcherOptions options) { var connection = await GetConnectionAsync(context); if (connection == null) { // No such connection, GetConnection already set the response status code return; } context.Response.ContentType = "text/plain"; if (connection.TransportType == HttpTransportType.WebSockets) { Log.PostNotAllowedForWebSockets(_logger); context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; await context.Response.WriteAsync("POST requests are not allowed for WebSocket connections."); return; } const int bufferSize = 4096; // REVIEW: Consider spliting the connection lock into a read lock and a write lock // Need to think about HttpConnectionContext.DisposeAsync and whether one or both locks would be needed await connection.Lock.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; } await context.Request.Body.CopyToAsync(connection.ApplicationStream, bufferSize); Log.ReceivedBytes(_logger, connection.ApplicationStream.Length); // Clear the amount of read bytes so logging is accurate connection.ApplicationStream.Reset(); } finally { connection.Lock.Release(); } }
private static RSocketEndpointConventionBuilder MapRSocketInternal( IEndpointRouteBuilder routeBuilder, RoutePattern pattern, Action <HttpConnectionDispatcherOptions> configureOptions) { var dispatcherOptions = new HttpConnectionDispatcherOptions(); var conventionBuilder = new RSocketEndpointConventionBuilder(); configureOptions.Invoke(dispatcherOptions); routeBuilder.Map(pattern, ProcessRSocketAsync); return(conventionBuilder); }
private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope) { context.Response.ContentType = "application/json"; string error = null; int clientProtocolVersion = 0; if (context.Request.Query.TryGetValue("NegotiateVersion", out var queryStringVersion)) { // Set the negotiate response to the protocol we use. var queryStringVersionValue = queryStringVersion.ToString(); if (!int.TryParse(queryStringVersionValue, out clientProtocolVersion)) { error = $"The client requested an invalid protocol version '{queryStringVersionValue}'"; Log.InvalidNegotiateProtocolVersion(_logger, queryStringVersionValue); } } // Establish the connection HttpConnectionContext connection = null; if (error == null) { connection = CreateConnection(options, clientProtocolVersion); } // Set the Connection ID on the logging scope so that logs from now on will have the // Connection ID metadata set. logScope.ConnectionId = connection?.ConnectionId; // Don't use thread static instance here because writer is used with async var writer = new MemoryBufferWriter(); try { // Get the bytes for the connection id WriteNegotiatePayload(writer, connection?.ConnectionId, connection?.ConnectionToken, context, options, clientProtocolVersion, error); Log.NegotiationRequest(_logger); // Write it out to the response with the right content length context.Response.ContentLength = writer.Length; await writer.CopyToAsync(context.Response.Body); } finally { writer.Reset(); } }
/// <summary> /// Maps incoming requests with the specified path to the specified <see cref="Hub"/> type. /// </summary> /// <typeparam name="THub">The <see cref="Hub"/> type to map requests to.</typeparam> /// <param name="path">The request path.</param> /// <param name="configureOptions">A callback to configure dispatcher options.</param> public void MapHub <THub>(PathString path, Action <HttpConnectionDispatcherOptions> configureOptions) where THub : Hub { // find auth attributes var authorizeAttributes = typeof(THub).GetCustomAttributes <AuthorizeAttribute>(inherit: true); var options = new HttpConnectionDispatcherOptions(); foreach (var attribute in authorizeAttributes) { options.AuthorizationData.Add(attribute); } configureOptions?.Invoke(options); _routes.MapConnections(path, options, builder => { builder.UseHub <THub>(); }); }
internal ServerSignalRTransport( IHubContext <EnvelopeHub> hubContext, string connectionId, Channel <string> envelopeChannel, IEnvelopeSerializer envelopeSerializer, HubOptions hubOptions, HttpConnectionDispatcherOptions httpConnectionDispatcherOptions, ITraceWriter traceWriter = null) : base( envelopeChannel, envelopeSerializer, traceWriter) { _hubContext = hubContext; _connectionId = connectionId; _hubOptions = hubOptions; _httpConnectionDispatcherOptions = httpConnectionDispatcherOptions; }
public async Task ExecuteNegotiateAsync(HttpContext context, HttpConnectionDispatcherOptions options) { // Create the log scope and the scope connectionId param will be set when the connection is created. var logScope = new ConnectionLogScope(connectionId: string.Empty); using (_logger.BeginScope(logScope)) { if (HttpMethods.IsPost(context.Request.Method)) { // POST /{path}/negotiate await ProcessNegotiate(context, options, logScope); } else { context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; } } }
public EnvelopeHub( Channel <ITransport> transportChannel, ConcurrentDictionary <string, Channel <string> > clientChannels, IEnvelopeSerializer envelopeSerializer, IHubContext <EnvelopeHub> hubContext, HubOptions hubOptions, HttpConnectionDispatcherOptions httpConnectionDispatcherOptions, EnvelopeHubOptions envelopeHubOptions, ITraceWriter traceWriter = null) { _transportChannel = transportChannel; _clientChannels = clientChannels; _traceWriter = traceWriter; _envelopeSerializer = envelopeSerializer; _hubContext = hubContext; _hubOptions = hubOptions; _httpConnectionDispatcherOptions = httpConnectionDispatcherOptions; _envelopeHubOptions = envelopeHubOptions; }
/// <summary> /// Maps incoming requests with the specified path to the specified <see cref="Hub"/> type. /// </summary> /// <typeparam name="THub">The <see cref="Hub"/> type to map requests to.</typeparam> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="configureOptions">A callback to configure dispatcher options.</param> /// <returns>An <see cref="HubEndpointConventionBuilder"/> for endpoints associated with the connections.</returns> public static HubEndpointConventionBuilder MapHub <THub>(this IEndpointRouteBuilder endpoints, string pattern, Action <HttpConnectionDispatcherOptions> configureOptions) where THub : Hub { var marker = endpoints.ServiceProvider.GetService <SignalRMarkerService>(); if (marker == null) { throw new InvalidOperationException("Unable to find the required services. Please add all the required services by calling " + "'IServiceCollection.AddSignalR' inside the call to 'ConfigureServices(...)' in the application startup code."); } var options = new HttpConnectionDispatcherOptions(); // REVIEW: WE should consider removing this and instead just relying on the // AuthorizationMiddleware var attributes = typeof(THub).GetCustomAttributes(inherit: true); foreach (var attribute in attributes.OfType <AuthorizeAttribute>()) { options.AuthorizationData.Add(attribute); } configureOptions?.Invoke(options); var conventionBuilder = endpoints.MapConnections(pattern, options, b => { b.UseHub <THub>(); }); conventionBuilder.Add(e => { // Add all attributes on the Hub has metadata (this will allow for things like) // auth attributes and cors attributes to work seamlessly foreach (var item in attributes) { e.Metadata.Add(item); } // Add metadata that captures the hub type this endpoint is associated with e.Metadata.Add(new HubMetadata(typeof(THub))); }); return(new HubEndpointConventionBuilder(conventionBuilder)); }
/// <summary> /// Maps incoming requests with the specified path to the specified <see cref="Hub"/> type. /// </summary> /// <typeparam name="THub">The <see cref="Hub"/> type to map requests to.</typeparam> /// <param name="endpoints">The <see cref="IEndpointBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="configureOptions">A callback to configure dispatcher options.</param> /// <returns>An <see cref="HubEndpointConventionBuilder"/> for endpoints associated with the connections.</returns> public static HubEndpointConventionBuilder MapHub <THub>(this IEndpointBuilder endpoints, string pattern, Action <HttpConnectionDispatcherOptions>?configureOptions) where THub : Hub { var marker = endpoints.ServiceProvider.GetService(_signalRMarkerServiceType); if (marker == null) { throw new InvalidOperationException( "Unable to find the required services. Please add all the required services by calling " + "'IServiceCollection.AddSignalR' inside the call to 'ConfigureServices(...)' in the application startup code."); } var options = new HttpConnectionDispatcherOptions(); configureOptions?.Invoke(options); var conventionBuilder = endpoints.MapConnections(pattern, options, b => { b.UseHub <THub>(); }); var attributes = typeof(THub).GetCustomAttributes(inherit: true); conventionBuilder.Add(e => { // Add all attributes on the Hub as metadata (this will allow for things like) // auth attributes and cors attributes to work seamlessly foreach (var item in attributes) { e.Metadata.Add(item); } // Add metadata that captures the hub type this endpoint is associated with e.Metadata.Add(new HubMetadata(typeof(THub))); }); return((HubEndpointConventionBuilder) typeof(HubEndpointConventionBuilder) .GetTypeInfo().DeclaredConstructors .Single() .Invoke(new object[] { conventionBuilder })); }
public async Task ExecuteAsync(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionDelegate connectionDelegate) { // Create the log scope and attempt to pass the Connection ID to it so as many logs as possible contain // the Connection ID metadata. If this is the negotiate request then the Connection ID for the scope will // be set a little later. HttpConnectionContext?connectionContext = null; var connectionToken = GetConnectionToken(context); if (!StringValues.IsNullOrEmpty(connectionToken)) { // Use ToString; IsNullOrEmpty doesn't tell the compiler anything about implicit conversion to string. _manager.TryGetConnection(connectionToken.ToString(), out connectionContext); } var logScope = new ConnectionLogScope(connectionContext?.ConnectionId); using (_logger.BeginScope(logScope)) { if (HttpMethods.IsPost(context.Request.Method)) { // POST /{path} await ProcessSend(context, options); } else if (HttpMethods.IsGet(context.Request.Method)) { // GET /{path} await ExecuteAsync(context, connectionDelegate, options, logScope); } else if (HttpMethods.IsDelete(context.Request.Method)) { // DELETE /{path} await ProcessDeleteAsync(context); } else { context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; } } }
public WebSocketConnectionListener(KestrelServer server, Action <Microsoft.AspNetCore.Http.Connections.WebSocketOptions> configure, IServiceProvider serviceProvider, string path) { _server = server; var builder = new ApplicationBuilder(serviceProvider); builder.UseRouting(); builder.UseEndpoints(routes => { var options = new HttpConnectionDispatcherOptions(); configure(options.WebSockets); routes.MapConnections(path, options, cb => cb.Run(inner => { var connection = new WebSocketConnectionContext(inner); _acceptQueue.Writer.TryWrite(connection); return(connection.ExecutionTask); })); }); _application = builder.Build(); }
public async Task ExecuteAsync(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionDelegate connectionDelegate) { // Create the log scope and attempt to pass the Connection ID to it so as many logs as possible contain // the Connection ID metadata. If this is the negotiate request then the Connection ID for the scope will // be set a little later. var logScope = new ConnectionLogScope(GetConnectionId(context)); using (_logger.BeginScope(logScope)) { if (!await AuthorizeHelper.AuthorizeAsync(context, options.AuthorizationData)) { return; } if (HttpMethods.IsPost(context.Request.Method)) { // POST /{path} await ProcessSend(context, options); } else if (HttpMethods.IsGet(context.Request.Method)) { // GET /{path} await ExecuteAsync(context, connectionDelegate, options, logScope); } else if (HttpMethods.IsDelete(context.Request.Method)) { // DELETE /{path} await ProcessDeleteAsync(context); } else { context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; } } }
// This is only used for WebSockets connections, which can connect directly without negotiating private async Task <HttpConnectionContext?> GetOrCreateConnectionAsync(HttpContext context, HttpConnectionDispatcherOptions options) { var connectionToken = GetConnectionToken(context); HttpConnectionContext?connection; // There's no connection id so this is a brand new connection if (StringValues.IsNullOrEmpty(connectionToken)) { connection = CreateConnection(options); } else if (!_manager.TryGetConnection(connectionToken, out connection)) { // No connection with that ID: Not Found context.Response.StatusCode = StatusCodes.Status404NotFound; await context.Response.WriteAsync("No Connection with that ID"); return(null); } return(connection); }
private async Task <bool> EnsureConnectionStateAsync(HttpConnectionContext connection, HttpContext context, HttpTransportType transportType, HttpTransportType supportedTransports, ConnectionLogScope logScope, HttpConnectionDispatcherOptions options) { if ((supportedTransports & transportType) == 0) { context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status404NotFound; Log.TransportNotSupported(_logger, transportType); await context.Response.WriteAsync($"{transportType} transport not supported by this end point type"); return(false); } // Set the IHttpConnectionFeature now that we can access it. connection.Features.Set(context.Features.Get <IHttpConnectionFeature>()); if (connection.TransportType == HttpTransportType.None) { connection.TransportType = transportType; } else if (connection.TransportType != transportType) { context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status400BadRequest; Log.CannotChangeTransport(_logger, connection.TransportType, transportType); await context.Response.WriteAsync("Cannot change transports mid-connection"); return(false); } // Configure transport-specific features. if (transportType == HttpTransportType.LongPolling) { connection.HasInherentKeepAlive = true; // For long polling, the requests come and go but the connection is still alive. // To make the IHttpContextFeature work well, we make a copy of the relevant properties // to a new HttpContext. This means that it's impossible to affect the context // with subsequent requests. var existing = connection.HttpContext; if (existing == null) { CloneHttpContext(context, connection); } else { // Set the request trace identifier to the current http request handling the poll existing.TraceIdentifier = context.TraceIdentifier; // Don't copy the identity if it's a windows identity // We specifically clone the identity on first poll if it's a windows identity // If we swapped the new User here we'd have to dispose the old identities which could race with the application // trying to access the identity. if (!(context.User.Identity is WindowsIdentity)) { existing.User = context.User; } } } else { connection.HttpContext = context; } // Setup the connection state from the http context connection.User = connection.HttpContext?.User; // Set the Connection ID on the logging scope so that logs from now on will have the // Connection ID metadata set. logScope.ConnectionId = connection.ConnectionId; return(true); }
private async Task ProcessSend(HttpContext context, HttpConnectionDispatcherOptions options) { var connection = await GetConnectionAsync(context); if (connection == null) { // No such connection, GetConnection already set the response status code return; } context.Response.ContentType = "text/plain"; if (connection.TransportType == HttpTransportType.WebSockets) { Log.PostNotAllowedForWebSockets(_logger); context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; await context.Response.WriteAsync("POST requests are not allowed for WebSocket connections."); return; } const int bufferSize = 4096; await connection.WriteLock.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; } try { try { await context.Request.Body.CopyToAsync(connection.ApplicationStream, bufferSize); } catch (InvalidOperationException ex) { // PipeWriter will throw an error if it is written to while dispose is in progress and the writer has been completed // Dispose isn't taking WriteLock because it could be held because of backpressure, and calling CancelPendingFlush // then taking the lock introduces a race condition that could lead to a deadlock Log.ConnectionDisposedWhileWriteInProgress(_logger, connection.ConnectionId, ex); context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.ContentType = "text/plain"; return; } catch (OperationCanceledException) { // CancelPendingFlush has canceled pending writes caused by backpresure Log.ConnectionDisposed(_logger, connection.ConnectionId); context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.ContentType = "text/plain"; // There are no writes anymore (since this is the write "loop") // So it is safe to complete the writer // We complete the writer here because we already have the WriteLock acquired // and it's unsafe to complete outside of the lock // Other code isn't guaranteed to be able to acquire the lock before another write // even if CancelPendingFlush is called, and the other write could hang if there is backpressure connection.Application.Output.Complete(); return; } catch (IOException ex) { // Can occur when the HTTP request is canceled by the client Log.FailedToReadHttpRequestBody(_logger, connection.ConnectionId, ex); context.Response.StatusCode = StatusCodes.Status400BadRequest; context.Response.ContentType = "text/plain"; return; } Log.ReceivedBytes(_logger, connection.ApplicationStream.Length); } finally { // Clear the amount of read bytes so logging is accurate connection.ApplicationStream.Reset(); } } finally { connection.WriteLock.Release(); } }
private void WriteNegotiatePayload(IBufferWriter <byte> writer, string?connectionId, string?connectionToken, HttpContext context, HttpConnectionDispatcherOptions options, int clientProtocolVersion, string?error) { var response = new NegotiationResponse(); if (!string.IsNullOrEmpty(error)) { response.Error = error; NegotiateProtocol.WriteResponse(response, writer); return; } response.Version = clientProtocolVersion; response.ConnectionId = connectionId; response.ConnectionToken = connectionToken; response.AvailableTransports = new List <AvailableTransport>(); if ((options.Transports & HttpTransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features)) { response.AvailableTransports.Add(_webSocketAvailableTransport); } if ((options.Transports & HttpTransportType.ServerSentEvents) != 0) { response.AvailableTransports.Add(_serverSentEventsAvailableTransport); } if ((options.Transports & HttpTransportType.LongPolling) != 0) { response.AvailableTransports.Add(_longPollingAvailableTransport); } NegotiateProtocol.WriteResponse(response, writer); }
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(); } } }
private async Task ProcessSend(HttpContext context, HttpConnectionDispatcherOptions options) { var connection = await GetConnectionAsync(context); if (connection == null) { // No such connection, GetConnection already set the response status code return; } context.Response.ContentType = "text/plain"; if (connection.TransportType == HttpTransportType.WebSockets) { Log.PostNotAllowedForWebSockets(_logger); context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; await context.Response.WriteAsync("POST requests are not allowed for WebSocket connections."); return; } const int bufferSize = 4096; await connection.WriteLock.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; } try { try { await context.Request.Body.CopyToAsync(connection.ApplicationStream, bufferSize); } catch (InvalidOperationException ex) { // PipeWriter will throw an error if it is written to while dispose is in progress and the writer has been completed // Dispose isn't taking WriteLock because it could be held because of backpressure, and calling CancelPendingFlush // then taking the lock introduces a race condition that could lead to a deadlock Log.ConnectionDisposedWhileWriteInProgress(_logger, connection.ConnectionId, ex); context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.ContentType = "text/plain"; return; } catch (OperationCanceledException) { // CancelPendingFlush has canceled pending writes caused by backpresure Log.ConnectionDisposed(_logger, connection.ConnectionId); context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.ContentType = "text/plain"; return; } Log.ReceivedBytes(_logger, connection.ApplicationStream.Length); } finally { // Clear the amount of read bytes so logging is accurate connection.ApplicationStream.Reset(); } } finally { connection.WriteLock.Release(); } }
private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options, int clientProtocolVersion = 0) { return(_manager.CreateConnection(options, clientProtocolVersion)); }
public void CheckLongPollingTimeoutValue() { var options = new HttpConnectionDispatcherOptions(); Assert.Equal(options.LongPolling.PollTimeout, TimeSpan.FromSeconds(90)); }
// This is only used for WebSockets connections, which can connect directly without negotiating private async Task <HttpConnectionContext?> GetOrCreateConnectionAsync(HttpContext context, HttpConnectionDispatcherOptions options) { var connectionToken = GetConnectionToken(context); HttpConnectionContext?connection; // There's no connection id so this is a brand new connection if (StringValues.IsNullOrEmpty(connectionToken)) { connection = CreateConnection(options); } // Use ToString; IsNullOrEmpty doesn't tell the compiler anything about implicit conversion to string. else if (!_manager.TryGetConnection(connectionToken.ToString(), out connection)) { // No connection with that ID: Not Found context.Response.StatusCode = StatusCodes.Status404NotFound; await context.Response.WriteAsync("No Connection with that ID"); return(null); } return(connection); }
private static void WriteNegotiatePayload(IBufferWriter <byte> writer, string connectionId, HttpContext context, HttpConnectionDispatcherOptions options) { var response = new NegotiationResponse(); response.ConnectionId = connectionId; response.AvailableTransports = new List <AvailableTransport>(); if ((options.Transports & HttpTransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features)) { response.AvailableTransports.Add(_webSocketAvailableTransport); } if ((options.Transports & HttpTransportType.ServerSentEvents) != 0) { response.AvailableTransports.Add(_serverSentEventsAvailableTransport); } if ((options.Transports & HttpTransportType.LongPolling) != 0) { response.AvailableTransports.Add(_longPollingAvailableTransport); } NegotiateProtocol.WriteResponse(response, writer); }
/// <summary> /// Maps incoming requests with the specified path to the provided connection pipeline. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="options">Options used to configure the connection.</param> /// <param name="configure">A callback to configure the connection.</param> /// <returns>An <see cref="ConnectionEndpointRouteBuilder"/> for endpoints associated with the connections.</returns> public static ConnectionEndpointRouteBuilder MapConnections(this IEndpointRouteBuilder endpoints, string pattern, HttpConnectionDispatcherOptions options, Action <IConnectionBuilder> configure) { var dispatcher = endpoints.ServiceProvider.GetRequiredService <HttpConnectionDispatcher>(); var connectionBuilder = new ConnectionBuilder(endpoints.ServiceProvider); configure(connectionBuilder); var connectionDelegate = connectionBuilder.Build(); // REVIEW: Consider expanding the internals of the dispatcher as endpoint routes instead of // using if statements we can let the matcher handle var conventionBuilders = new List <IEndpointConventionBuilder>(); // Build the negotiate application var app = endpoints.CreateApplicationBuilder(); app.UseWebSockets(); app.Run(c => dispatcher.ExecuteNegotiateAsync(c, options)); var negotiateHandler = app.Build(); var negotiateBuilder = endpoints.Map(pattern + "/negotiate", negotiateHandler); conventionBuilders.Add(negotiateBuilder); // Add the negotiate metadata so this endpoint can be identified negotiateBuilder.WithMetadata(_negotiateMetadata); negotiateBuilder.WithMetadata(options); // build the execute handler part of the protocol app = endpoints.CreateApplicationBuilder(); app.UseWebSockets(); app.Run(c => dispatcher.ExecuteAsync(c, options, connectionDelegate)); var executehandler = app.Build(); var executeBuilder = endpoints.Map(pattern, executehandler); conventionBuilders.Add(executeBuilder); var compositeConventionBuilder = new CompositeEndpointConventionBuilder(conventionBuilders); // Add metadata to all of Endpoints compositeConventionBuilder.Add(e => { // Add the authorization data as metadata foreach (var data in options.AuthorizationData) { e.Metadata.Add(data); } }); return(new ConnectionEndpointRouteBuilder(compositeConventionBuilder)); }
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); } } }