/// <summary>
        ///     Call back to select client certificate used for mutual authentication
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="sessionArgs">The http session.</param>
        /// <param name="targetHost">The remote hostname.</param>
        /// <param name="localCertificates">Selected local certificates by SslStream.</param>
        /// <param name="remoteCertificate">The remote certificate of server.</param>
        /// <param name="acceptableIssuers">The acceptable issues for client certificate as listed by server.</param>
        /// <returns></returns>
        internal X509Certificate?SelectClientCertificate(RequestStateBase state, object sender, string targetHost,
                                                         X509CertificateCollection localCertificates,
                                                         X509Certificate remoteCertificate, string[] acceptableIssuers)
        {
            X509Certificate?clientCertificate = null;

            if (acceptableIssuers != null && acceptableIssuers.Length > 0 && localCertificates != null &&
                localCertificates.Count > 0)
            {
                foreach (var certificate in localCertificates)
                {
                    string issuer = certificate.Issuer;
                    if (Array.IndexOf(acceptableIssuers, issuer) != -1)
                    {
                        clientCertificate = certificate;
                    }
                }
            }

            if (localCertificates != null && localCertificates.Count > 0)
            {
                clientCertificate = localCertificates[0];
            }

            // If user call back is registered
            if (ClientCertificateSelectionCallback != null)
            {
                var args = new CertificateSelectionEventArgs(state)
                {
                    ClientCertificate = clientCertificate
                };

                // why is the sender null?
                ClientCertificateSelectionCallback.InvokeAsync(this, args, ExceptionFunc).Wait();
                return(args.ClientCertificate);
            }

            return(clientCertificate);
        }
        /// <summary>
        ///     Call back to override server certificate validation
        /// </summary>
        /// <param name="sender">The sender object.</param>
        /// <param name="sessionArgs">The http session.</param>
        /// <param name="certificate">The remote certificate.</param>
        /// <param name="chain">The certificate chain.</param>
        /// <param name="sslPolicyErrors">Ssl policy errors</param>
        /// <returns>Return true if valid certificate.</returns>
        internal bool ValidateServerCertificate(RequestStateBase state, object sender, X509Certificate certificate, X509Chain chain,
                                                SslPolicyErrors sslPolicyErrors)
        {
            // if user callback is registered then do it
            if (ServerCertificateValidationCallback != null)
            {
                var args = new CertificateValidationEventArgs(state, certificate, chain, sslPolicyErrors);

                // why is the sender null?
                ServerCertificateValidationCallback.InvokeAsync(this, args, state.OnError).Wait();
                return(args.IsValid);
            }

            if (sslPolicyErrors == SslPolicyErrors.None)
            {
                return(true);
            }

            // By default
            // do not allow this client to communicate with unauthenticated servers.
            return(false);
        }
Exemple #3
0
        /// <summary>
        ///     This is called when this proxy acts as a reverse proxy (like a real http server).
        ///     So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client
        /// </summary>
        /// <param name="endPoint">The transparent endpoint.</param>
        /// <param name="clientConnection">The client connection.</param>
        /// <returns></returns>
        internal virtual async Task handleClient(TransparentProxyEndPoint endPoint, RequestStateBase state)
        {
            var clientConnection        = state.ClientConnection;
            var cancellationTokenSource = new CancellationTokenSource();
            var cancellationToken       = cancellationTokenSource.Token;

            var clientStream = new HttpClientStream(clientConnection, clientConnection.GetStream(), BufferPool);

            SslStream?sslStream = null;

            try
            {
                var clientHelloInfo = await SslTools.PeekClientHello(clientStream, BufferPool, cancellationToken);

                if (clientHelloInfo != null)
                {
                    var httpsHostName = clientHelloInfo.GetServerName() ?? endPoint.GenericCertificateName;

                    var args = new BeforeSslAuthenticateEventArgs(state, cancellationTokenSource, httpsHostName);

                    await endPoint.InvokeBeforeSslAuthenticate(this, args, ExceptionFunc);

                    if (cancellationTokenSource.IsCancellationRequested)
                    {
                        throw new Exception("Session was terminated by user.");
                    }

                    if (endPoint.DecryptSsl && args.DecryptSsl)
                    {
                        clientStream.Connection.SslProtocol = clientHelloInfo.SslProtocol;

                        // do client authentication using certificate
                        X509Certificate2?certificate = null;
                        try
                        {
                            sslStream = new SslStream(clientStream, false);

                            string certName = HttpHelper.GetWildCardDomainName(httpsHostName);
                            certificate = endPoint.GenericCertificate ??
                                          await CertificateManager.CreateServerCertificate(certName);

                            // Successfully managed to authenticate the client using the certificate
                            await sslStream.AuthenticateAsServerAsync(certificate, false, SslProtocols.Tls, false);

                            // HTTPS server created - we can now decrypt the client's traffic
                            clientStream = new HttpClientStream(clientStream.Connection, sslStream, BufferPool);
                            sslStream    = null; // clientStream was created, no need to keep SSL stream reference
                        }
                        catch (Exception e)
                        {
                            var certName = certificate?.GetNameInfo(X509NameType.SimpleName, false);
                            var session  = new SessionEventArgs(state, endPoint, clientStream, null,
                                                                cancellationTokenSource);
                            throw new ProxyConnectException(
                                      $"Couldn't authenticate host '{httpsHostName}' with certificate '{certName}'.", e, session);
                        }
                    }
                    else
                    {
                        var sessionArgs = new SessionEventArgs(state, endPoint, clientStream, null, cancellationTokenSource);
                        var connection  = await tcpConnectionFactory.GetServerConnection(state, httpsHostName, endPoint.Port,
                                                                                         HttpHeader.VersionUnknown, false, null,
                                                                                         true, UpStreamEndPoint,
                                                                                         UpStreamHttpsProxy, true, cancellationToken);

                        try
                        {
                            int available = clientStream.Available;

                            if (available > 0)
                            {
                                // send the buffered data
                                var data = BufferPool.GetBuffer();
                                try
                                {
                                    // clientStream.Available should be at most BufferSize because it is using the same buffer size
                                    await clientStream.ReadAsync(data, 0, available, cancellationToken);

                                    await connection.Stream.WriteAsync(data, 0, available, true, cancellationToken);
                                }
                                finally
                                {
                                    BufferPool.ReturnBuffer(data);
                                }
                            }

                            if (!clientStream.IsClosed && !connection.Stream.IsClosed)
                            {
                                await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool,
                                                        null, null, cancellationTokenSource, ExceptionFunc);
                            }
                        }
                        finally
                        {
                            await tcpConnectionFactory.Release(connection, true);
                        }

                        return;
                    }
                }

                // HTTPS server created - we can now decrypt the client's traffic
                // Now create the request
                await handleHttpSessionRequest(endPoint, state, clientStream, cancellationTokenSource);
            }
            catch (ProxyException e)
            {
                onException(clientStream, e);
            }
            catch (IOException e)
            {
                onException(clientStream, new Exception("Connection was aborted", e));
            }
            catch (SocketException e)
            {
                onException(clientStream, new Exception("Could not connect", e));
            }
            catch (Exception e)
            {
                onException(clientStream, new Exception("Error occured in whilst handling the client", e));
            }
            finally
            {
                sslStream?.Dispose();
                clientStream.Dispose();
            }
        }
Exemple #4
0
        /// <summary>
        ///     This is called when client is aware of proxy
        ///     So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy
        /// </summary>
        /// <param name="endPoint">The explicit endpoint.</param>
        /// <param name="clientConnection">The client connection.</param>
        /// <returns>The task.</returns>
        internal async Task handleClient(ExplicitProxyEndPoint endPoint, RequestStateBase state)
        {
            TcpClientConnection clientConnection = state.ClientConnection;
            var cancellationTokenSource          = new CancellationTokenSource();
            var cancellationToken = cancellationTokenSource.Token;

            var clientStream = new HttpClientStream(clientConnection, clientConnection.GetStream(), BufferPool);

            Task <TcpServerConnection>?prefetchConnectionTask = null;
            bool closeServerConnection = false;
            bool calledRequestHandler  = false;

            SslStream?sslStream = null;

            try
            {
                TunnelConnectSessionEventArgs?connectArgs = null;

                var method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken);

                if (clientStream.IsClosed)
                {
                    return;
                }

                // Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request)
                if (method == KnownMethod.Connect)
                {
                    // read the first line HTTP command
                    var requestLine = await clientStream.ReadRequestLine(cancellationToken);

                    if (requestLine.IsEmpty())
                    {
                        return;
                    }

                    var connectRequest = new ConnectRequest(requestLine.RequestUri)
                    {
                        RequestUriString8 = requestLine.RequestUri,
                        HttpVersion       = requestLine.Version
                    };

                    await HeaderParser.ReadHeaders(clientStream, connectRequest.Headers, cancellationToken);

                    connectArgs = new TunnelConnectSessionEventArgs(state, endPoint, connectRequest,
                                                                    clientConnection, clientStream, cancellationTokenSource);
                    clientStream.DataRead  += (o, args) => connectArgs.OnDataSent(args.Buffer, args.Offset, args.Count);
                    clientStream.DataWrite += (o, args) => connectArgs.OnDataReceived(args.Buffer, args.Offset, args.Count);

                    await endPoint.InvokeBeforeTunnelConnectRequest(this, connectArgs, ExceptionFunc);

                    // filter out excluded host names
                    bool decryptSsl  = endPoint.DecryptSsl && connectArgs.DecryptSsl;
                    bool sendRawData = !decryptSsl;

                    if (connectArgs.DenyConnect)
                    {
                        if (connectArgs.HttpClient.Response.StatusCode == 0)
                        {
                            connectArgs.HttpClient.Response = new Response
                            {
                                HttpVersion       = HttpHeader.Version11,
                                StatusCode        = (int)HttpStatusCode.Forbidden,
                                StatusDescription = "Forbidden"
                            };
                        }

                        // send the response
                        await clientStream.WriteResponseAsync(connectArgs.HttpClient.Response, cancellationToken);

                        return;
                    }

                    if (await checkAuthorization(connectArgs) == false)
                    {
                        await endPoint.InvokeBeforeTunnelConnectResponse(this, connectArgs, ExceptionFunc);

                        // send the response
                        await clientStream.WriteResponseAsync(connectArgs.HttpClient.Response, cancellationToken);

                        return;
                    }

                    // write back successful CONNECT response
                    var response = ConnectResponse.CreateSuccessfulConnectResponse(requestLine.Version);

                    // Set ContentLength explicitly to properly handle HTTP 1.0
                    response.ContentLength = 0;
                    response.Headers.FixProxyHeaders();
                    connectArgs.HttpClient.Response = response;

                    await clientStream.WriteResponseAsync(response, cancellationToken);

                    var clientHelloInfo = await SslTools.PeekClientHello(clientStream, BufferPool, cancellationToken);

                    if (clientStream.IsClosed)
                    {
                        return;
                    }

                    bool isClientHello = clientHelloInfo != null;
                    if (clientHelloInfo != null)
                    {
                        connectRequest.TunnelType      = TunnelType.Https;
                        connectRequest.ClientHelloInfo = clientHelloInfo;
                    }

                    await endPoint.InvokeBeforeTunnelConnectResponse(this, connectArgs, ExceptionFunc, isClientHello);

                    if (decryptSsl && clientHelloInfo != null)
                    {
                        connectRequest.IsHttps = true; // todo: move this line to the previous "if"
                        clientStream.Connection.SslProtocol = clientHelloInfo.SslProtocol;

                        bool http2Supported = false;

                        if (EnableHttp2)
                        {
                            var alpn = clientHelloInfo.GetAlpn();
                            if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2))
                            {
                                // test server HTTP/2 support
                                try
                                {
                                    // todo: this is a hack, because Titanium does not support HTTP protocol changing currently
                                    var connection = await tcpConnectionFactory.GetServerConnection(state,
                                                                                                    true, SslExtensions.Http2ProtocolAsList,
                                                                                                    true, cancellationToken);

                                    http2Supported = connection.NegotiatedApplicationProtocol ==
                                                     SslApplicationProtocol.Http2;

                                    // release connection back to pool instead of closing when connection pool is enabled.
                                    await tcpConnectionFactory.Release(connection, true);
                                }
                                catch (Exception)
                                {
                                    // ignore
                                }
                            }
                        }

                        if (EnableTcpServerConnectionPrefetch)
                        {
                            IPAddress[]? ipAddresses = null;
                            try
                            {
                                // make sure the host can be resolved before creating the prefetch task
                                ipAddresses = await Dns.GetHostAddressesAsync(connectArgs.HttpClient.Request.RequestUri.Host);
                            }
                            catch (SocketException) { }

                            if (ipAddresses != null && ipAddresses.Length > 0)
                            {
                                // don't pass cancellation token here
                                // it could cause floating server connections when client exits
                                prefetchConnectionTask = tcpConnectionFactory.GetServerConnection(state,
                                                                                                  true, null, false,
                                                                                                  CancellationToken.None);
                            }
                        }

                        string connectHostname = requestLine.RequestUri.GetString();
                        int    idx             = connectHostname.IndexOf(":");
                        if (idx >= 0)
                        {
                            connectHostname = connectHostname.Substring(0, idx);
                        }

                        X509Certificate2?certificate = null;
                        try
                        {
                            sslStream = new SslStream(clientStream, false);

                            string certName = HttpHelper.GetWildCardDomainName(connectHostname);
                            certificate = endPoint.GenericCertificate ??
                                          await CertificateManager.CreateServerCertificate(certName);

                            // Successfully managed to authenticate the client using the fake certificate
                            var options = new SslServerAuthenticationOptions();
                            if (EnableHttp2 && http2Supported)
                            {
                                options.ApplicationProtocols = clientHelloInfo.GetAlpn();
                                if (options.ApplicationProtocols == null || options.ApplicationProtocols.Count == 0)
                                {
                                    options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList;
                                }
                            }

                            options.ServerCertificate              = certificate;
                            options.ClientCertificateRequired      = false;
                            options.EnabledSslProtocols            = SupportedSslProtocols;
                            options.CertificateRevocationCheckMode = X509RevocationMode.NoCheck;
                            await sslStream.AuthenticateAsServerAsync(options, cancellationToken);

#if NETSTANDARD2_1
                            clientStream.Connection.NegotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol;
#endif

                            // HTTPS server created - we can now decrypt the client's traffic
                            clientStream = new HttpClientStream(clientStream.Connection, sslStream, BufferPool);
                            sslStream    = null; // clientStream was created, no need to keep SSL stream reference

                            clientStream.DataRead  += (o, args) => connectArgs.OnDecryptedDataSent(args.Buffer, args.Offset, args.Count);
                            clientStream.DataWrite += (o, args) => connectArgs.OnDecryptedDataReceived(args.Buffer, args.Offset, args.Count);
                        }
                        catch (Exception e)
                        {
                            var certName = certificate?.GetNameInfo(X509NameType.SimpleName, false);
                            throw new ProxyConnectException(
                                      $"Couldn't authenticate host '{connectHostname}' with certificate '{certName}'.", e, connectArgs);
                        }

                        method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken);

                        if (clientStream.IsClosed)
                        {
                            return;
                        }

                        if (method == KnownMethod.Invalid)
                        {
                            sendRawData = true;
                            await tcpConnectionFactory.Release(prefetchConnectionTask, true);

                            prefetchConnectionTask = null;
                        }
                    }
                    else if (clientHelloInfo == null)
                    {
                        method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken);

                        if (clientStream.IsClosed)
                        {
                            return;
                        }
                    }

                    if (cancellationTokenSource.IsCancellationRequested)
                    {
                        throw new Exception("Session was terminated by user.");
                    }

                    if (method == KnownMethod.Invalid)
                    {
                        sendRawData = true;
                    }

                    // Hostname is excluded or it is not an HTTPS connect
                    if (sendRawData)
                    {
                        // create new connection to server.
                        // If we detected that client tunnel CONNECTs without SSL by checking for empty client hello then
                        // this connection should not be HTTPS.
                        var connection = await tcpConnectionFactory.GetServerConnection(state,
                                                                                        true, SslExtensions.Http2ProtocolAsList,
                                                                                        true, cancellationToken);

                        try
                        {
                            if (isClientHello)
                            {
                                int available = clientStream.Available;
                                if (available > 0)
                                {
                                    // send the buffered data
                                    var data = BufferPool.GetBuffer();

                                    try
                                    {
                                        // clientStream.Available should be at most BufferSize because it is using the same buffer size
                                        int read = await clientStream.ReadAsync(data, 0, available, cancellationToken);

                                        if (read != available)
                                        {
                                            throw new Exception("Internal error.");
                                        }

                                        await connection.Stream.WriteAsync(data, 0, available, true, cancellationToken);
                                    }
                                    finally
                                    {
                                        BufferPool.ReturnBuffer(data);
                                    }
                                }

                                var serverHelloInfo = await SslTools.PeekServerHello(connection.Stream, BufferPool, cancellationToken);

                                ((ConnectResponse)connectArgs.HttpClient.Response).ServerHelloInfo = serverHelloInfo;
                            }

                            if (!clientStream.IsClosed && !connection.Stream.IsClosed)
                            {
                                await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool,
                                                        null, null, connectArgs.CancellationTokenSource, ExceptionFunc);
                            }
                        }
                        finally
                        {
                            await tcpConnectionFactory.Release(connection, true);
                        }

                        return;
                    }
                }

                if (connectArgs != null && method == KnownMethod.Pri)
                {
                    // todo
                    string?httpCmd = await clientStream.ReadLineAsync(cancellationToken);

                    if (httpCmd == "PRI * HTTP/2.0")
                    {
                        connectArgs.HttpClient.ConnectRequest !.TunnelType = TunnelType.Http2;

                        // HTTP/2 Connection Preface
                        string?line = await clientStream.ReadLineAsync(cancellationToken);

                        if (line != string.Empty)
                        {
                            throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received");
                        }

                        line = await clientStream.ReadLineAsync(cancellationToken);

                        if (line != "SM")
                        {
                            throw new Exception($"HTTP/2 Protocol violation. 'SM' expected, '{line}' received");
                        }

                        line = await clientStream.ReadLineAsync(cancellationToken);

                        if (line != string.Empty)
                        {
                            throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received");
                        }

                        var connection = await tcpConnectionFactory.GetServerConnection(state,
                                                                                        true, SslExtensions.Http2ProtocolAsList,
                                                                                        true, cancellationToken);

                        try
                        {
#if NETSTANDARD2_1
                            var connectionPreface = new ReadOnlyMemory <byte>(Http2Helper.ConnectionPreface);
                            await connection.Stream.WriteAsync(connectionPreface, cancellationToken);

                            await Http2Helper.SendHttp2(clientStream, connection.Stream,
                                                        () => new SessionEventArgs(state, endPoint, clientStream, connectArgs?.HttpClient.ConnectRequest, cancellationTokenSource)
                            {
                                UserData = connectArgs?.UserData
                            },
                                                        async args => { await onBeforeRequest(args); },
                                                        async args => { await onBeforeResponse(args); },
                                                        connectArgs.CancellationTokenSource, clientStream.Connection.Id, ExceptionFunc);
#endif
                        }
                        finally
                        {
                            await tcpConnectionFactory.Release(connection, true);
                        }
                    }
                }

                calledRequestHandler = true;

                // Now create the request
                await handleHttpSessionRequest(endPoint, state, clientStream, cancellationTokenSource, connectArgs, prefetchConnectionTask);
            }
            catch (ProxyException e)
            {
                closeServerConnection = true;
                onException(clientStream, e);
            }
            catch (IOException e)
            {
                closeServerConnection = true;
                onException(clientStream, new Exception("Connection was aborted", e));
            }
            catch (SocketException e)
            {
                closeServerConnection = true;
                onException(clientStream, new Exception("Could not connect", e));
            }
            catch (Exception e)
            {
                closeServerConnection = true;
                onException(clientStream, new Exception("Error occured in whilst handling the client", e));
            }
            finally
            {
                if (!calledRequestHandler)
                {
                    await tcpConnectionFactory.Release(prefetchConnectionTask, closeServerConnection);
                }

                sslStream?.Dispose();
                clientStream.Dispose();
            }
        }
Exemple #5
0
        /// <summary>
        ///     This is the core request handler method for a particular connection from client.
        ///     Will create new session (request/response) sequence until
        ///     client/server abruptly terminates connection or by normal HTTP termination.
        /// </summary>
        /// <param name="endPoint">The proxy endpoint.</param>
        /// <param name="clientStream">The client stream.</param>
        /// <param name="cancellationTokenSource">The cancellation token source for this async task.</param>
        /// <param name="connectArgs">The Connect request if this is a HTTPS request from explicit endpoint.</param>
        /// <param name="prefetchConnectionTask">Prefetched server connection for current client using Connect/SNI headers.</param>
        private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, RequestStateBase state,
                                                    HttpClientStream clientStream, CancellationTokenSource cancellationTokenSource, TunnelConnectSessionEventArgs?connectArgs = null,
                                                    Task <TcpServerConnection>?prefetchConnectionTask = null)
        {
            var clientConnection = state.ClientConnection;
            var connectRequest   = connectArgs?.HttpClient.ConnectRequest;

            var prefetchTask = prefetchConnectionTask;
            TcpServerConnection?connection = null;
            bool closeServerConnection     = false;

            try
            {
                var cancellationToken = cancellationTokenSource.Token;

                // Loop through each subsequent request on this particular client connection
                // (assuming HTTP connection is kept alive by client)
                while (true)
                {
                    if (clientStream.IsClosed)
                    {
                        return;
                    }

                    // read the request line
                    var requestLine = await clientStream.ReadRequestLine(cancellationToken);

                    if (requestLine.IsEmpty())
                    {
                        return;
                    }

                    var args = new SessionEventArgs(state, endPoint, clientStream, connectRequest, cancellationTokenSource)
                    {
                        UserData = connectArgs?.UserData
                    };

                    try
                    {
                        try
                        {
                            // Read the request headers in to unique and non-unique header collections
                            await HeaderParser.ReadHeaders(clientStream, args.HttpClient.Request.Headers,
                                                           cancellationToken);

                            var request = args.HttpClient.Request;
                            if (connectRequest != null)
                            {
                                request.IsHttps   = connectRequest.IsHttps;
                                request.Authority = connectRequest.Authority;
                            }

                            request.RequestUriString8 = requestLine.RequestUri;

                            request.Method      = requestLine.Method;
                            request.HttpVersion = requestLine.Version;

                            if (!args.IsTransparent)
                            {
                                // proxy authorization check
                                if (connectRequest == null && await checkAuthorization(args) == false)
                                {
                                    await onBeforeResponse(args);

                                    // send the response
                                    await clientStream.WriteResponseAsync(args.HttpClient.Response, cancellationToken);

                                    return;
                                }

                                prepareRequestHeaders(request.Headers);
                                request.Host = request.RequestUri.Authority;
                            }

                            // if win auth is enabled
                            // we need a cache of request body
                            // so that we can send it after authentication in WinAuthHandler.cs
                            if (args.EnableWinAuth && request.HasBody)
                            {
                                await args.GetRequestBody(cancellationToken);
                            }

                            // we need this to syphon out data from connection if API user changes them.
                            request.SetOriginalHeaders();

                            // If user requested interception do it
                            await onBeforeRequest(args);

                            var response = args.HttpClient.Response;

                            if (request.CancelRequest)
                            {
                                if (!(Enable100ContinueBehaviour && request.ExpectContinue))
                                {
                                    // syphon out the request body from client before setting the new body
                                    await args.SyphonOutBodyAsync(true, cancellationToken);
                                }

                                await handleHttpSessionResponse(args);

                                if (!response.KeepAlive)
                                {
                                    return;
                                }

                                continue;
                            }

                            // If prefetch task is available.
                            if (connection == null && prefetchTask != null)
                            {
                                try
                                {
                                    connection = await prefetchTask;
                                }
                                catch (SocketException e)
                                {
                                    if (e.SocketErrorCode != SocketError.HostNotFound)
                                    {
                                        throw;
                                    }
                                }

                                prefetchTask = null;
                            }

                            // create a new connection if cache key changes.
                            // only gets hit when connection pool is disabled.
                            // or when prefetch task has a unexpectedly different connection.
                            if (connection != null &&
                                (await tcpConnectionFactory.GetConnectionCacheKey(this, args,
                                                                                  clientStream.Connection.NegotiatedApplicationProtocol)
                                 != connection.CacheKey))
                            {
                                await tcpConnectionFactory.Release(connection);

                                connection = null;
                            }

                            var result = await handleHttpSessionRequest(args, connection,
                                                                        clientStream.Connection.NegotiatedApplicationProtocol,
                                                                        cancellationToken, cancellationTokenSource);

                            // update connection to latest used
                            connection            = result.LatestConnection;
                            closeServerConnection = !result.Continue;

                            // throw if exception happened
                            if (result.Exception != null)
                            {
                                throw result.Exception;
                            }

                            if (!result.Continue)
                            {
                                return;
                            }

                            // user requested
                            if (args.HttpClient.CloseServerConnection)
                            {
                                closeServerConnection = true;
                                return;
                            }

                            // if connection is closing exit
                            if (!response.KeepAlive)
                            {
                                closeServerConnection = true;
                                return;
                            }

                            if (cancellationTokenSource.IsCancellationRequested)
                            {
                                throw new Exception("Session was terminated by user.");
                            }

                            // Release server connection for each HTTP session instead of per client connection.
                            // This will be more efficient especially when client is idly holding server connection
                            // between sessions without using it.
                            // Do not release authenticated connections for performance reasons.
                            // Otherwise it will keep authenticating per session.
                            if (EnableConnectionPool && connection != null &&
                                !connection.IsWinAuthenticated)
                            {
                                await tcpConnectionFactory.Release(connection);

                                connection = null;
                            }
                        }
                        catch (Exception e) when(!(e is ProxyHttpException))
                        {
                            throw new ProxyHttpException("Error occured whilst handling session request", e, args);
                        }
                    }
                    catch (Exception e)
                    {
                        args.Exception        = e;
                        closeServerConnection = true;
                        throw;
                    }
                    finally
                    {
                        await onAfterResponse(args);

                        args.Dispose();
                    }
                }
            }
            finally
            {
                if (connection != null)
                {
                    await tcpConnectionFactory.Release(connection, closeServerConnection);
                }

                await tcpConnectionFactory.Release(prefetchTask, closeServerConnection);
            }
        }