public async Task ExecuteAsync(HttpContext context, HttpConnectionOptions 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
                {
                    context.Response.ContentType = "text/plain";
                    context.Response.StatusCode  = StatusCodes.Status405MethodNotAllowed;
                }
            }
        }
        private HttpConnectionContext CreateConnection(HttpConnectionOptions 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, HttpConnectionOptions 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();
            }
        }
示例#4
0
        private async Task ProcessSend(HttpContext context, HttpConnectionOptions options)
        {
            var connection = await GetConnectionAsync(context, options);

            if (connection == null)
            {
                // No such connection, GetConnection already set the response status code
                return;
            }

            context.Response.ContentType = "text/plain";

            var transport = (TransportType?)connection.Items[ConnectionMetadataNames.Transport];

            if (transport == TransportType.WebSockets)
            {
                Log.PostNotAllowedForWebSockets(_logger);
                context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
                await context.Response.WriteAsync("POST requests are not allowed for WebSocket connections.");

                return;
            }

            var pipeWriterStream = new PipeWriterStream(connection.Application.Output);
            await context.Request.Body.CopyToAsync(pipeWriterStream);

            Log.ReceivedBytes(_logger, pipeWriterStream.Length);
        }
示例#5
0
        public void MapConnections(PathString path, HttpConnectionOptions options, Action <IConnectionBuilder> configure)
        {
            var connectionBuilder = new ConnectionBuilder(_routes.ServiceProvider);

            configure(connectionBuilder);
            var socket = connectionBuilder.Build();

            _routes.MapRoute(path, c => _dispatcher.ExecuteAsync(c, options, socket));
            _routes.MapRoute(path + "/negotiate", c => _dispatcher.ExecuteNegotiateAsync(c, options));
        }
示例#6
0
 private void EnsureConnectionStateInternal(HttpConnectionContext connection, HttpConnectionOptions options)
 {
     // If the connection doesn't have a pipe yet then create one, we lazily create the pipe to save on allocations until the client actually connects
     if (connection.Transport == null)
     {
         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);
         var pair = DuplexPipe.CreateConnectionPair(transportPipeOptions, appPipeOptions);
         connection.Transport   = pair.Application;
         connection.Application = pair.Transport;
     }
 }
        private async Task ProcessSend(HttpContext context, HttpConnectionOptions options)
        {
            var connection = await GetConnectionAsync(context, options);

            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 == HttpConnectionContext.ConnectionStatus.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
        public void MapConnectionHandler <TConnectionHandler>(PathString path, Action <HttpConnectionOptions> configureOptions) where TConnectionHandler : ConnectionHandler
        {
            var authorizeAttributes = typeof(TConnectionHandler).GetCustomAttributes <AuthorizeAttribute>(inherit: true);
            var options             = new HttpConnectionOptions();

            foreach (var attribute in authorizeAttributes)
            {
                options.AuthorizationData.Add(attribute);
            }
            configureOptions?.Invoke(options);

            MapConnections(path, options, builder =>
            {
                builder.UseConnectionHandler <TConnectionHandler>();
            });
        }
示例#9
0
        private Task ProcessNegotiate(HttpContext context, HttpConnectionOptions options, ConnectionLogScope logScope)
        {
            context.Response.ContentType = "application/json";

            // Establish the connection
            var connection = _manager.CreateConnection();

            // 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;

            // Get the bytes for the connection id
            var negotiateResponseBuffer = GetNegotiatePayload(connection.ConnectionId, context, options);

            Log.NegotiationRequest(_logger);

            // Write it out to the response with the right content length
            context.Response.ContentLength = negotiateResponseBuffer.Length;
            return(context.Response.Body.WriteAsync(negotiateResponseBuffer, 0, negotiateResponseBuffer.Length));
        }
示例#10
0
        public async Task ExecuteNegotiateAsync(HttpContext context, HttpConnectionOptions 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 (!await AuthorizeHelper.AuthorizeAsync(context, options.AuthorizationData))
                {
                    return;
                }

                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;
                }
            }
        }
示例#11
0
        // This is only used for WebSockets connections, which can connect directly without negotiating
        private async Task <HttpConnectionContext> GetOrCreateConnectionAsync(HttpContext context, HttpConnectionOptions options)
        {
            var connectionId = GetConnectionId(context);
            HttpConnectionContext connection;

            // There's no connection id so this is a brand new connection
            if (StringValues.IsNullOrEmpty(connectionId))
            {
                connection = CreateConnection(options);
            }
            else if (!_manager.TryGetConnection(connectionId, 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);
        }
示例#12
0
        private async Task <HttpConnectionContext> GetConnectionAsync(HttpContext context, HttpConnectionOptions options)
        {
            var connectionId = GetConnectionId(context);

            if (StringValues.IsNullOrEmpty(connectionId))
            {
                // There's no connection ID: bad request
                context.Response.StatusCode  = StatusCodes.Status400BadRequest;
                context.Response.ContentType = "text/plain";
                await context.Response.WriteAsync("Connection ID required");

                return(null);
            }

            if (!_manager.TryGetConnection(connectionId, out var connection))
            {
                // No connection with that ID: Not Found
                context.Response.StatusCode  = StatusCodes.Status404NotFound;
                context.Response.ContentType = "text/plain";
                await context.Response.WriteAsync("No Connection with that ID");

                return(null);
            }

            return(connection);
        }
示例#13
0
        private async Task <bool> EnsureConnectionStateAsync(HttpConnectionContext connection, HttpContext context, HttpTransportType transportType, HttpTransportType supportedTransports, ConnectionLogScope logScope, HttpConnectionOptions 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;
                connection.Items[ConnectionMetadataNames.Transport] = 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);
            }

            // Setup the connection state from the http context
            connection.User = context.User;

            // Configure transport-specific features.
            if (transportType == HttpTransportType.LongPolling)
            {
                connection.Features.Set <IConnectionInherentKeepAliveFeature>(new ConnectionInherentKeepAliveFeature(options.LongPolling.PollTimeout));

                // 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)
                {
                    var httpContext = CloneHttpContext(context);
                    connection.HttpContext = httpContext;
                }
                else
                {
                    // Set the request trace identifier to the current http request handling the poll
                    existing.TraceIdentifier = context.TraceIdentifier;
                    existing.User            = context.User;
                }
            }
            else
            {
                connection.HttpContext = context;
            }

            // 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);
        }
示例#14
0
        private static void WriteNegotiatePayload(IBufferWriter <byte> writer, string connectionId, HttpContext context, HttpConnectionOptions 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);
        }
示例#15
0
        private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connectionDelegate, HttpConnectionOptions 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, options);

                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);

                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, options);

                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;
                }

                try
                {
                    await connection.Lock.WaitAsync();

                    if (connection.Status == HttpConnectionContext.ConnectionStatus.Disposed)
                    {
                        Log.ConnectionDisposed(_logger, connection.ConnectionId);

                        // The connection was disposed
                        context.Response.StatusCode  = StatusCodes.Status404NotFound;
                        context.Response.ContentType = "text/plain";
                        return;
                    }

                    if (connection.Status == HttpConnectionContext.ConnectionStatus.Active)
                    {
                        var existing = connection.GetHttpContext();
                        Log.ConnectionAlreadyActive(_logger, connection.ConnectionId, existing.TraceIdentifier);

                        using (connection.Cancellation)
                        {
                            // Cancel the previous request
                            connection.Cancellation.Cancel();

                            // Wait for the previous request to drain
                            await connection.TransportTask;

                            Log.PollCanceled(_logger, connection.ConnectionId, existing.TraceIdentifier);
                        }
                    }

                    // Mark the connection as active
                    connection.Status = HttpConnectionContext.ConnectionStatus.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);
                    }
                    else
                    {
                        Log.ResumingConnection(_logger);
                    }

                    // REVIEW: Performance of this isn't great as this does a bunch of per request allocations
                    connection.Cancellation = new CancellationTokenSource();

                    var timeoutSource = new CancellationTokenSource();
                    var tokenSource   = CancellationTokenSource.CreateLinkedTokenSource(connection.Cancellation.Token, context.RequestAborted, timeoutSource.Token);

                    // Dispose these tokens when the request is over
                    context.Response.RegisterForDispose(timeoutSource);
                    context.Response.RegisterForDispose(tokenSource);

                    var longPolling = new LongPollingTransport(timeoutSource.Token, connection.Application.Input, connection.ConnectionId, _loggerFactory);

                    // Start the transport
                    connection.TransportTask = longPolling.ProcessRequestAsync(context, tokenSource.Token);

                    // Start the timeout after we return from creating the transport task
                    timeoutSource.CancelAfter(options.LongPolling.PollTimeout);
                }
                finally
                {
                    connection.Lock.Release();
                }

                var resultTask = await Task.WhenAny(connection.ApplicationTask, connection.TransportTask);

                var pollAgain = true;

                // If the application ended before the transport task then we potentially need to end the connection
                if (resultTask == connection.ApplicationTask)
                {
                    // Complete the transport (notifying it of the application error if there is one)
                    connection.Transport.Output.Complete(connection.ApplicationTask.Exception);

                    // Wait for the transport to run
                    await connection.TransportTask;

                    // If the status code is a 204 it means the connection is done
                    if (context.Response.StatusCode == StatusCodes.Status204NoContent)
                    {
                        // We should be able to safely dispose because there's no more data being written
                        // 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 (context.Response.StatusCode == StatusCodes.Status204NoContent)
                {
                    // Don't poll if the transport task was cancelled
                    pollAgain = false;
                }

                if (pollAgain)
                {
                    // Otherwise, we update the state to inactive again and wait for the next poll
                    try
                    {
                        await connection.Lock.WaitAsync();

                        if (connection.Status == HttpConnectionContext.ConnectionStatus.Active)
                        {
                            // Mark the connection as inactive
                            connection.LastSeenUtc = DateTime.UtcNow;

                            connection.Status = HttpConnectionContext.ConnectionStatus.Inactive;

                            // Dispose the cancellation token
                            connection.Cancellation.Dispose();

                            connection.Cancellation = null;
                        }
                    }
                    finally
                    {
                        connection.Lock.Release();
                    }
                }
            }
        }
示例#16
0
        private static byte[] GetNegotiatePayload(string connectionId, HttpContext context, HttpConnectionOptions options)
        {
            NegotiationResponse response = new NegotiationResponse();

            response.ConnectionId        = connectionId;
            response.AvailableTransports = new List <AvailableTransport>();

            if ((options.Transports & TransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features))
            {
                response.AvailableTransports.Add(_webSocketAvailableTransport);
            }

            if ((options.Transports & TransportType.ServerSentEvents) != 0)
            {
                response.AvailableTransports.Add(_serverSentEventsAvailableTransport);
            }

            if ((options.Transports & TransportType.LongPolling) != 0)
            {
                response.AvailableTransports.Add(_longPollingAvailableTransport);
            }

            MemoryStream ms = new MemoryStream();

            NegotiateProtocol.WriteResponse(response, ms);

            return(ms.ToArray());
        }