示例#1
0
        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);
        }
示例#2
0
        /// <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);
        }
示例#4
0
        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));
        }
示例#5
0
        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);
        }
示例#7
0
        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();
            }
        }
示例#8
0
        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();
            }
        }
示例#10
0
        /// <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>();
            });
        }
示例#11
0
 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;
 }
示例#12
0
        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;
                }
            }
        }
示例#13
0
 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;
 }
示例#14
0
        /// <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));
        }
示例#15
0
        /// <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;
                }
            }
        }
示例#17
0
        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();
        }
示例#18
0
        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;
                }
            }
        }
示例#19
0
        // 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);
        }
示例#20
0
        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);
        }
示例#21
0
        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();
            }
        }
示例#22
0
        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);
        }
示例#23
0
        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();
                }
            }
        }
示例#24
0
        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();
            }
        }
示例#25
0
 private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options, int clientProtocolVersion = 0)
 {
     return(_manager.CreateConnection(options, clientProtocolVersion));
 }
示例#26
0
        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);
        }
示例#28
0
        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);
        }
示例#29
0
    /// <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));
    }
示例#30
0
        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);
                }
            }
        }