Esempio n. 1
0
        internal ListenerOperation CreateProxy(Listener listener)
        {
            var proxy = new ListenerOperation(listener, Operation, Handler, Uri);

            proxy.TargetOperation = this;
            parentOperation       = proxy;
            return(proxy);
        }
Esempio n. 2
0
 internal void ReleaseInstrumentation(ListenerOperation operation)
 {
     lock (this) {
         Debug($"{ME} RELEASE CONTEXT: {operation.ME}");
         if (currentOperation != operation)
         {
             throw new InvalidOperationException();
         }
         currentOperation = null;
         mainLoopEvent.Set();
     }
 }
Esempio n. 3
0
        public ListenerContext ReuseConnection()
        {
            disposed = true;
            var oldConnection = Interlocked.Exchange(ref connection, null);

            if (oldConnection == null)
            {
                throw new InvalidOperationException();
            }

            var newContext = new ListenerContext(Listener, oldConnection, true)
            {
                State = Listener.UsingInstrumentation ? ConnectionState.WaitingForContext : ConnectionState.WaitingForRequest
            };

            assignedOperation = null;
            currentOperation  = null;
            State             = ConnectionState.Closed;
            return(newContext);
        }
Esempio n. 4
0
 public ListenerOperation RegisterOperation(TestContext ctx, HttpOperation operation, Handler handler, string path)
 {
     lock (this) {
         if (TargetListener != null)
         {
             var targetOperation = TargetListener.RegisterOperation(ctx, operation, handler, path);
             registry.Add(targetOperation.Uri.LocalPath, targetOperation);
             return(targetOperation.CreateProxy(this));
         }
         if (path == null)
         {
             var id = Interlocked.Increment(ref nextRequestID);
             path = $"/id/{operation.ID}/{handler.GetType ().Name}/";
         }
         var me = $"{nameof (RegisterOperation)}({handler.Value})";
         Debug($"{me} {path}");
         var uri = new Uri(Server.TargetUri, path);
         var listenerOperation = new ListenerOperation(this, operation, handler, uri);
         registry.Add(path, listenerOperation);
         return(listenerOperation);
     }
 }
Esempio n. 5
0
        internal async Task <ListenerContext> FindContext(TestContext ctx, ListenerOperation operation, bool reusing)
        {
            var me = $"{ME}({operation.Operation.ME}:{reusing}) FIND CONTEXT";

            var instrumentation = operation;

            while (true)
            {
                ListenerOperation oldInstrumentation;
                lock (this) {
                    oldInstrumentation = Interlocked.CompareExchange(ref currentOperation, instrumentation, null);
                    if (oldInstrumentation == null)
                    {
                        break;
                    }
                    ctx.LogDebug(2, $"{me} - WAITING FOR OPERATION {oldInstrumentation.Operation.ME}");
                }

                await oldInstrumentation.Wait().ConfigureAwait(false);
            }

            mainLoopEvent.Set();
            return(await instrumentation.WaitForContext().ConfigureAwait(false));
        }
Esempio n. 6
0
        internal void AssignContext(ListenerOperation operation)
        {
            if (!Listener.UsingInstrumentation)
            {
                throw new InvalidOperationException();
            }
            var oldOperation = Interlocked.CompareExchange(ref assignedOperation, operation, null);

            if (oldOperation != null)
            {
                if (!operation.Operation.HasAnyFlags(HttpOperationFlags.DelayedListenerContext) || oldOperation != operation)
                {
                    throw new InvalidOperationException();
                }
            }
            if (State != ConnectionState.Idle && State != ConnectionState.WaitingForContext)
            {
                if (!operation.Operation.HasAnyFlags(HttpOperationFlags.DelayedListenerContext) || State != ConnectionState.Listening)
                {
                    throw new InvalidOperationException();
                }
            }
            State = ConnectionState.Listening;
        }
Esempio n. 7
0
 internal void UnregisterOperation(ListenerOperation redirect)
 {
     lock (this) {
         registry.Remove(redirect.Uri.LocalPath);
     }
 }
Esempio n. 8
0
        public async Task <Response> RunWithContext(TestContext ctx, ListenerOperation operation, Request request,
                                                    ClientFunc clientFunc, CancellationToken cancellationToken)
        {
            var me = $"{ME}({operation.Operation.ME}) RUN WITH CONTEXT";

            ListenerContext context        = null;
            ListenerContext targetContext  = null;
            var             reusing        = !operation.Operation.HasAnyFlags(HttpOperationFlags.DontReuseConnection);
            var             delayedContext = operation.Operation.HasAnyFlags(HttpOperationFlags.DelayedListenerContext);

            /*
             * When using HttpOperationFlags.DelayedListenerContext, then we call the `clientFunc` before we
             * actually start listening.
             */

            if (UsingInstrumentation && !delayedContext)
            {
                context = await FindContext(ctx, operation, reusing);

                reusing = context.ReusingConnection;

                ctx.LogDebug(2, $"{me} - CREATE CONTEXT: {reusing} {context.ME}");

                await context.ServerStartTask.ConfigureAwait(false);

                ctx.LogDebug(2, $"{me} - CREATE CONTEXT #1: {reusing} {context.ME}");
            }

            if (TargetListener?.UsingInstrumentation ?? false)
            {
                targetContext = await TargetListener.FindContext(ctx, operation.TargetOperation, false);

                ctx.LogDebug(2, $"{me} - CREATE TARGET CONTEXT: {reusing} {targetContext.ME}");
                try {
                    await targetContext.ServerStartTask.ConfigureAwait(false);
                } catch {
                    context?.Dispose();
                    throw;
                }
            }

            var clientTask         = clientFunc(ctx, request, cancellationToken);
            var serverInitTask     = operation.ServerInitTask;
            var serverFinishedTask = operation.ServerFinishedTask;

            ExceptionDispatchInfo throwMe = null;
            bool initDone = false, serverDone = false, clientDone = false;

            while (!initDone || !serverDone || !clientDone)
            {
                ctx.LogDebug(2, $"{me} LOOP: init={initDone} server={serverDone} client={clientDone}");

                if (clientDone)
                {
                    if (operation.Operation.HasAnyFlags(
                            HttpOperationFlags.AbortAfterClientExits, HttpOperationFlags.ServerAbortsHandshake,
                            HttpOperationFlags.ClientAbortsHandshake))
                    {
                        ctx.LogDebug(2, $"{me} - ABORTING");
                        break;
                    }
                    if (!initDone)
                    {
                        ctx.LogDebug(2, $"{me} - ERROR: {clientTask.Result}");
                        throwMe = ExceptionDispatchInfo.Capture(new ConnectionException(
                                                                    $"{me} client exited before server accepted connection."));
                        break;
                    }
                }

                var tasks = new List <Task> ();
                if (!initDone)
                {
                    tasks.Add(serverInitTask);
                }
                if (!serverDone)
                {
                    tasks.Add(serverFinishedTask);
                }
                if (!clientDone)
                {
                    tasks.Add(clientTask);
                }
                var finished = await Task.WhenAny(tasks).ConfigureAwait(false);

                string which;
                if (finished == serverInitTask)
                {
                    which    = "init";
                    initDone = true;
                }
                else if (finished == serverFinishedTask)
                {
                    which      = "server";
                    serverDone = true;
                }
                else if (finished == clientTask)
                {
                    which      = "client";
                    clientDone = true;
                }
                else
                {
                    throwMe = ExceptionDispatchInfo.Capture(new InvalidOperationException());
                    break;
                }

                ctx.LogDebug(2, $"{me} #4: {which} exited - {finished.Status}");
                if (finished.Status == TaskStatus.Faulted || finished.Status == TaskStatus.Canceled)
                {
                    if (operation.Operation.HasAnyFlags(HttpOperationFlags.ExpectServerException) &&
                        (finished == serverFinishedTask || finished == serverInitTask))
                    {
                        ctx.LogDebug(2, $"{me} EXPECTED EXCEPTION {finished.Exception.GetType ()}");
                    }
                    else if (finished.Status == TaskStatus.Canceled)
                    {
                        ctx.LogDebug(2, $"{me} CANCELED");
                        throwMe = ExceptionDispatchInfo.Capture(new OperationCanceledException());
                        break;
                    }
                    else
                    {
                        ctx.LogDebug(2, $"{me} FAILED: {finished.Exception.Message}");
                        throwMe = ExceptionDispatchInfo.Capture(finished.Exception);
                        break;
                    }
                }
            }

            if (throwMe != null)
            {
                ctx.LogDebug(2, $"{me} THROWING {throwMe.SourceException.Message}");
                lock (this) {
                    operation.OnError(throwMe.SourceException);
                    if (context != null)
                    {
                        context.Dispose();
                    }
                    if (targetContext != null)
                    {
                        targetContext.Dispose();
                    }
                    mainLoopEvent.Set();
                }
                throwMe.Throw();
            }

            return(clientTask.Result);
        }
Esempio n. 9
0
        void RunScheduler()
        {
            do
            {
                Cleanup();

                if (!UsingInstrumentation)
                {
                    CreateConnections();
                }
            } while (!StartTasks());

            void Cleanup()
            {
                var iter = connections.First;

                while (iter != null)
                {
                    var node    = iter;
                    var context = node.Value;
                    iter = iter.Next;

                    if (context.State == ConnectionState.Closed)
                    {
                        connections.Remove(node);
                        context.Dispose();
                    }
                    else if (context.State == ConnectionState.ReuseConnection)
                    {
                        connections.Remove(node);
                        var newContext = context.ReuseConnection();
                        connections.AddLast(newContext);
                    }
                }
            }

            void CreateConnections()
            {
                while (connections.Count < requestParallelConnections)
                {
                    Debug($"RUN SCHEDULER: {connections.Count}");
                    var connection = Backend.CreateConnection();
                    connections.AddLast(new ListenerContext(this, connection, false));
                    Debug($"RUN SCHEDULER #1: {connection.ME}");
                }
            }

            bool StartTasks()
            {
                var             listening        = false;
                ListenerContext listeningContext = null;
                ListenerContext redirectContext  = null;
                ListenerContext idleContext      = null;

                var me = $"RUN SCHEDULER - TASKS";

                Debug($"{me}");

                var idx  = 0;
                var iter = connections.First;

                while (iter != null)
                {
                    var node    = iter;
                    var context = node.Value;
                    iter = iter.Next;

                    Debug($"{me} ({++idx}/{connections.Count}): {context.State} {context.CurrentTask != null} {context.ME}");

                    if (!listening && context.State == ConnectionState.Listening)
                    {
                        listening = true;
                    }

                    if (false && UsingInstrumentation && listening && context.State == ConnectionState.Listening)
                    {
                        continue;
                    }

                    if (context.CurrentTask != null)
                    {
                        continue;
                    }

                    switch (context.State)
                    {
                    case ConnectionState.Idle:
                    case ConnectionState.WaitingForContext:
                        if (!UsingInstrumentation)
                        {
                            throw new InvalidOperationException();
                        }
                        if (idleContext == null && listeningContext == null)
                        {
                            idleContext = context;
                        }
                        continue;

                    case ConnectionState.NeedContextForRedirect:
                    case ConnectionState.CannotReuseConnection:
                        if (redirectContext == null)
                        {
                            redirectContext = context;
                        }
                        continue;

                    case ConnectionState.Listening:
                        if (idleContext == null && listeningContext == null)
                        {
                            listeningContext = context;
                        }
                        break;
                    }

                    var task = context.MainLoopListenerTask(TestContext, cts.Token);
                    listenerTasks.AddLast(task);

                    if (false && context.State == ConnectionState.Listening && !listening)
                    {
                        listeningContext = context;
                        listening        = true;
                    }
                }

                Debug($"{me} DONE: instrumentation={currentOperation?.ID} listening={listening} " +
                      $"redirect={redirectContext != null} idleContext={idleContext != null} " +
                      $"listeningContext={listeningContext != null} " +
                      $"assigned={currentOperation?.AssignedContext != null}");

                var availableContext = idleContext ?? listeningContext;

                if (currentOperation != null)
                {
                    if (currentOperation.AssignedContext != null)
                    {
                        return(true);
                    }

                    var operation   = currentOperation.Operation;
                    var forceNewCnc = operation.HasAnyFlags(HttpOperationFlags.ForceNewConnection);
                    if (listening && !forceNewCnc)
                    {
                        return(true);
                    }

                    if (availableContext == null || forceNewCnc)
                    {
                        var connection = Backend.CreateConnection();
                        availableContext = new ListenerContext(this, connection, false);
                        connections.AddLast(availableContext);
                    }
                    Debug($"{me} ASSIGN CONTEXT: {availableContext.ME} {currentOperation.ME}");
                    currentOperation.AssignContext(availableContext);
                    return(false);
                }

                if (redirectContext == null)
                {
                    return(true);
                }

                if (availableContext == null)
                {
                    var connection = Backend.CreateConnection();
                    availableContext = new ListenerContext(this, connection, false);
                    connections.AddLast(availableContext);
                }

                currentOperation = redirectContext.Redirect(availableContext);
                currentOperation.AssignContext(availableContext);
                Debug($"{me} DONE #1: {availableContext.ME} {currentOperation?.ME}");
                return(false);
            }
        }
Esempio n. 10
0
        async void MainLoop()
        {
            while (!closed)
            {
                Debug($"MAIN LOOP");

                var taskList    = new List <Task> ();
                var contextList = new List <ListenerTask> ();
                lock (this) {
                    Debug($"MAIN LOOP - SCHEDULER: {connections.Count}");
                    RunScheduler();

                    taskList.Add(mainLoopEvent.WaitAsync());
                    PopulateTaskList(contextList, taskList);

                    Debug($"MAIN LOOP - SCHEDULER DONE: {connections.Count} {taskList.Count}");
                }

                var finished = await Task.WhenAny(taskList).ConfigureAwait(false);

                Debug($"MAIN LOOP TASK: {finished.Status} {finished == taskList[0]} {taskList[0].Status}");

                ListenerOperation operation = null;
                ListenerContext   context;
                bool success;

                lock (this) {
                    if (closed)
                    {
                        break;
                    }
                    if (finished == taskList[0])
                    {
                        mainLoopEvent.Reset();
                        continue;
                    }

                    int idx = -1;
                    for (int i = 0; i < contextList.Count; i++)
                    {
                        if (finished == taskList[i + 1])
                        {
                            idx = i;
                            break;
                        }
                    }

                    var task = contextList[idx];
                    context = task.Context;
                    listenerTasks.Remove(task);

                    var me = $"MAIN LOOP TASK #1 ({idx}, {context.State})";
                    Debug($"{me}");

                    if (context.State == ConnectionState.Listening && currentOperation?.AssignedContext == context)
                    {
                        operation = Interlocked.Exchange(ref currentOperation, null);
                        Debug($"{me} GOT OPERATION {operation.ID}");
                    }

                    try {
                        success = context.MainLoopListenerTaskDone(TestContext, cts.Token);
                    } catch (Exception ex) {
                        Debug($"{me} EX: {ex.Message}");
                        connections.Remove(context);
                        context.Dispose();
                        success = false;
                    }

                    Debug($"{me} OP={operation?.ID}");
                }

                if (operation != null)
                {
                    operation.Finish();
                }
            }

            Debug($"MAIN LOOP COMPLETE");

            lock (this) {
                var iter = connections.First;
                while (iter != null)
                {
                    var node = iter.Value;
                    iter = iter.Next;

                    node.Dispose();
                    connections.Remove(node);
                }

                cts.Dispose();
                exited = true;
            }

            Debug($"MAIN LOOP COMPLETE #1");

            finishedEvent.SetResult(null);

            void PopulateTaskList(List <ListenerTask> contextList, List <Task> taskList)
            {
                var iter = listenerTasks.First;

                while (iter != null)
                {
                    var node = iter;
                    iter = iter.Next;

                    contextList.Add(node.Value);
                    taskList.Add(node.Value.Task);
                }
            }
        }
Esempio n. 11
0
 internal void RegisterProxyAuth(ListenerOperation redirect)
 {
     redirect.parentOperation = this;
 }
Esempio n. 12
0
        public ListenerTask MainLoopListenerTask(TestContext ctx, CancellationToken cancellationToken)
        {
            var me = $"{Listener.ME}({Connection.ME}) TASK";

            HttpOperation clientOperation = null;

            lock (Listener) {
                if (currentListenerTask != null)
                {
                    throw new InvalidOperationException();
                }

                if (Listener.UsingInstrumentation)
                {
                    if (assignedOperation == null)
                    {
                        throw new InvalidOperationException();
                    }
                    clientOperation = assignedOperation.Operation;
                }
            }

            ctx.LogDebug(5, $"{me} {State}");

            currentListenerTask = StartListenerTask();
            currentListenerTask.Start();

            return(currentListenerTask);

            ListenerTask StartListenerTask()
            {
                switch (State)
                {
                case ConnectionState.Listening:
                    return(ListenerTask.Create(this, State, Start, Accepted));

                case ConnectionState.InitializeConnection:
                    return(ListenerTask.Create(this, State, InitializeConnection, Initialized));

                case ConnectionState.WaitingForRequest:
                    return(ListenerTask.Create(this, State, ReadRequestHeader, GotRequest));

                case ConnectionState.HasRequest:
                    return(ListenerTask.Create(this, State, HandleRequest, RequestComplete));

                case ConnectionState.RequestComplete:
                    return(ListenerTask.Create(this, State, WriteResponse, ResponseWritten));

                case ConnectionState.InitializeProxyConnection:
                    return(ListenerTask.Create(this, State, InitProxyConnection, InitProxyConnectionDone));

                case ConnectionState.ConnectToTarget:
                    return(ListenerTask.Create(this, State, ConnectToTarget, ProxyConnectionEstablished));

                case ConnectionState.HandleProxyConnection:
                    return(ListenerTask.Create(this, State, HandleProxyConnection, ProxyConnectionFinished));

                case ConnectionState.RunTunnel:
                    return(ListenerTask.Create(this, State, ProxyConnect, ProxyConnectDone));

                case ConnectionState.WaitForClose:
                    return(ListenerTask.Create(this, State, WaitForClose, WaitForCloseDone));

                default:
                    throw ctx.AssertFail(State);
                }
            }

            Task Start()
            {
                return(Accept(ctx, cancellationToken));
            }

            ConnectionState Accepted()
            {
                return(ConnectionState.InitializeConnection);
            }

            Task <(bool complete, bool success)> InitializeConnection()
            {
                return(Initialize(ctx, clientOperation, cancellationToken));
            }

            ConnectionState Initialized(bool completed, bool success)
            {
                ctx.LogDebug(5, $"{me} INITIALIZED: {completed} {success}");

                if (!completed)
                {
                    return(ConnectionState.CannotReuseConnection);
                }

                if (!success)
                {
                    return(ConnectionState.Closed);
                }

                return(ConnectionState.WaitingForRequest);
            }

            Task <HttpRequest> ReadRequestHeader()
            {
                ctx.LogDebug(5, $"{me} READ REQUEST HEADER");

                return(Connection.ReadRequestHeader(ctx, cancellationToken));
            }

            ConnectionState GotRequest(HttpRequest request)
            {
                currentRequest = request;

                if (request.Method == "CONNECT")
                {
                    Server.BumpRequestCount();
                    return(ConnectionState.RunTunnel);
                }

                var operation = Listener.GetOperation(this, request);

                if (operation == null)
                {
                    ctx.LogDebug(5, $"{me} INVALID REQUEST: {request.Path}");
                    return(ConnectionState.Closed);
                }

                currentOperation = operation;
                ctx.LogDebug(5, $"{me} GOT REQUEST");

                if (Listener.TargetListener == null)
                {
                    return(ConnectionState.HasRequest);
                }

                return(ConnectionState.InitializeProxyConnection);
            }

            Task <HttpResponse> HandleRequest()
            {
                return(currentOperation.HandleRequest(ctx, this, Connection, currentRequest, cancellationToken));
            }

            async Task InitProxyConnection()
            {
                await currentRequest.ReadHeaders(ctx, cancellationToken).ConfigureAwait(false);

                await currentRequest.Read(ctx, cancellationToken);
            }

            ConnectionState InitProxyConnectionDone()
            {
                var request       = currentRequest;
                var operation     = currentOperation;
                var remoteAddress = connection.RemoteEndPoint.Address;

                request.AddHeader("X-Forwarded-For", remoteAddress);

                var authManager = ((BuiltinProxyServer)Listener.Server).AuthenticationManager;

                if (authManager != null)
                {
                    AuthenticationState state;
                    var response = authManager.HandleAuthentication(ctx, connection, request, out state);
                    if (response != null)
                    {
                        Listener.TargetListener.UnregisterOperation(operation);
                        response.Redirect = Listener.RegisterOperation(ctx, operation.Operation, operation.Handler, request.Path);
                        operation.RegisterProxyAuth(response.Redirect);
                        return(RequestComplete(response));
                    }

                    // HACK: Mono rewrites chunked requests into non-chunked.
                    request.AddHeader("X-Mono-Redirected", "true");
                }

                return(ConnectionState.ConnectToTarget);
            }

            async Task ConnectToTarget()
            {
                ctx.LogDebug(5, $"{me} CONNECT TO TARGET");

                targetConnection = new SocketConnection(Listener.TargetListener.Server);
                var targetEndPoint = new DnsEndPoint(currentOperation.Uri.Host, currentOperation.Uri.Port);

                ctx.LogDebug(5, $"{me} CONNECT TO TARGET #1: {targetEndPoint}");

                cancellationToken.ThrowIfCancellationRequested();
                await targetConnection.ConnectAsync(ctx, targetEndPoint, cancellationToken);

                ctx.LogDebug(5, $"{me} CONNECT TO TARGET #2");

                cancellationToken.ThrowIfCancellationRequested();
                await targetConnection.Initialize(ctx, currentOperation.Operation, cancellationToken);

                ctx.LogDebug(5, $"{me} CONNECT TO TARGET #3");
            }

            ConnectionState ProxyConnectionEstablished()
            {
                return(ConnectionState.HandleProxyConnection);
            }

            async Task <HttpResponse> HandleProxyConnection()
            {
                var copyResponseTask = CopyResponse();

                cancellationToken.ThrowIfCancellationRequested();
                await targetConnection.WriteRequest(ctx, currentRequest, cancellationToken);

                ctx.LogDebug(5, $"{me} HANDLE PROXY CONNECTION");

                cancellationToken.ThrowIfCancellationRequested();
                var response = await copyResponseTask.ConfigureAwait(false);

                ctx.LogDebug(5, $"{me} HANDLE PROXY CONNECTION #1");
                return(response);
            }

            ConnectionState ProxyConnectionFinished(HttpResponse response)
            {
                currentResponse = response;
                targetConnection.Dispose();
                targetConnection = null;
                return(ConnectionState.RequestComplete);
            }

            async Task <HttpResponse> CopyResponse()
            {
                cancellationToken.ThrowIfCancellationRequested();
                var response = await targetConnection.ReadResponse(ctx, cancellationToken).ConfigureAwait(false);

                response.SetHeader("Connection", "close");
                response.SetHeader("Proxy-Connection", "close");

                return(response);
            }

            async Task ProxyConnect()
            {
                await CreateTunnel(ctx, ((ProxyConnection)connection).Stream, currentRequest, cancellationToken);
            }

            ConnectionState ProxyConnectDone()
            {
                return(ConnectionState.Closed);
            }

            ConnectionState RequestComplete(HttpResponse response)
            {
                ctx.LogDebug(5, $"{me}: {response} {response.Redirect?.ME}");

                currentResponse = response;

                var keepAlive = (response.KeepAlive ?? false) && !response.CloseConnection;

                if (response.Redirect != null && Listener.UsingInstrumentation &&
                    (!keepAlive || Operation.Operation.HasAnyFlags(HttpOperationFlags.RedirectOnNewConnection)))
                {
                    return(ConnectionState.NeedContextForRedirect);
                }

                return(ConnectionState.RequestComplete);
            }

            async Task <bool> WriteResponse()
            {
                var response = Interlocked.Exchange(ref currentResponse, null);
                var redirect = Interlocked.Exchange(ref redirectContext, null);

                redirectRequested = response.Redirect;

                var keepAlive = (response.KeepAlive ?? false) && !response.CloseConnection;

                if (redirect != null)
                {
                    ctx.LogDebug(5, $"{me} REDIRECT ON NEW CONTEXT: {redirect.ME}!");
                    await redirect.ServerStartTask.ConfigureAwait(false);

                    ctx.LogDebug(5, $"{me} REDIRECT ON NEW CONTEXT #1: {redirect.ME}!");
                    keepAlive = false;
                }

                if (!Operation.Operation.HasAnyFlags(HttpOperationFlags.DontWriteResponse))
                {
                    await connection.WriteResponse(ctx, response, cancellationToken).ConfigureAwait(false);
                }

                return(keepAlive);
            }

            ConnectionState ResponseWritten(bool keepAlive)
            {
                var request   = Interlocked.Exchange(ref currentRequest, null);
                var operation = Interlocked.Exchange(ref currentOperation, null);
                var redirect  = Interlocked.Exchange(ref redirectRequested, null);

                if (redirect != null && (operation?.Operation.HasAnyFlags(HttpOperationFlags.RedirectOnNewConnection) ?? false))
                {
                    return(ConnectionState.WaitForClose);
                }

                if (!keepAlive)
                {
                    return(ConnectionState.Closed);
                }

                if (redirect == null)
                {
                    return(ConnectionState.ReuseConnection);
                }

                if (operation?.Operation.HasAnyFlags(HttpOperationFlags.ServerAbortsRedirection) ?? false)
                {
                    connection.Dispose();
                }

                return(ConnectionState.WaitingForRequest);
            }

            async Task <bool> WaitForClose()
            {
                var result = await Connection.WaitForRequest(2500, cancellationToken).ConfigureAwait(false);

                ctx.LogDebug(5, $"{me} WAIT FOR CLOSE #1: {result}");
                return(ctx.Expect(result, Is.False, "expected client to close the connection"));
            }

            ConnectionState WaitForCloseDone(bool result)
            {
                return(result ? ConnectionState.Closed : ConnectionState.CannotReuseConnection);
            }
        }