private async Task <RetryResult> handleHttpSessionRequest(string requestHttpMethod, string requestHttpUrl, Version requestVersion, SessionEventArgs args,
                                                                  TcpServerConnection?serverConnection, SslApplicationProtocol sslApplicationProtocol,
                                                                  CancellationToken cancellationToken, CancellationTokenSource cancellationTokenSource)
        {
            // a connection generator task with captured parameters via closure.
            Func <Task <TcpServerConnection> > generator = () =>
                                                           tcpConnectionFactory.GetServerConnection(this, args, isConnect: false,
                                                                                                    applicationProtocol: sslApplicationProtocol,
                                                                                                    noCache: false, cancellationToken: cancellationToken);

            // for connection pool, retry fails until cache is exhausted.
            return(await retryPolicy <ServerConnectionException>().ExecuteAsync(async(connection) =>
            {
                args.TimeLine["Connection Ready"] = DateTime.Now;

                if (args.HttpClient.Request.UpgradeToWebSocket)
                {
                    args.HttpClient.ConnectRequest !.TunnelType = TunnelType.Websocket;

                    // if upgrading to websocket then relay the request without reading the contents
                    await handleWebSocketUpgrade(requestHttpMethod, requestHttpUrl, requestVersion, args, args.HttpClient.Request,
                                                 args.HttpClient.Response, args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamWriter,
                                                 connection, cancellationTokenSource, cancellationToken);
                    return false;
                }

                // construct the web request that we are going to issue on behalf of the client.
                await handleHttpSessionRequest(connection, args);
                return true;
            }, generator, serverConnection));
        }
        // before retry clear connection
        private async Task disposeConnection()
        {
            if (currentConnection != null)
            {
                // close connection on error
                await tcpConnectionFactory.Release(currentConnection, true);

                currentConnection = null;
            }
        }
        /// <summary>
        ///     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        internal void FinishSession()
        {
            connection = null;

            ConnectRequest?.FinishSession();
            Request?.FinishSession();
            Response?.FinishSession();

            Data.Clear();
            UserData = null;
        }
Exemple #4
0
        private async Task <RetryResult> handleHttpSessionRequest(SessionEventArgs args,
                                                                  TcpServerConnection?serverConnection, SslApplicationProtocol sslApplicationProtocol,
                                                                  CancellationToken cancellationToken, CancellationTokenSource cancellationTokenSource)
        {
            args.HttpClient.Request.Locked = true;

            // do not cache server connections for WebSockets
            bool noCache = args.HttpClient.Request.UpgradeToWebSocket;

            if (noCache)
            {
                serverConnection = null;
            }

            // a connection generator task with captured parameters via closure.
            Func <Task <TcpServerConnection> > generator = () =>
                                                           tcpConnectionFactory.GetServerConnection(this,
                                                                                                    args,
                                                                                                    false,
                                                                                                    sslApplicationProtocol,
                                                                                                    noCache,
                                                                                                    cancellationToken);

            /// Retry with new connection if the initial stream.WriteAsync call to server fails.
            /// i.e if request line and headers failed to get send.
            /// Do not retry after reading data from client stream,
            /// because subsequent try will not have data to read from client
            /// and will hang at clientStream.ReadAsync call.
            /// So, throw RetryableServerConnectionException only when we are sure we can retry safely.
            return(await retryPolicy <RetryableServerConnectionException>().ExecuteAsync(async connection =>
            {
                // set the connection and send request headers
                args.HttpClient.SetConnection(connection);

                args.TimeLine["Connection Ready"] = DateTime.UtcNow;

                if (args.HttpClient.Request.UpgradeToWebSocket)
                {
                    // connectRequest can be null for SOCKS connection
                    if (args.HttpClient.ConnectRequest != null)
                    {
                        args.HttpClient.ConnectRequest !.TunnelType = TunnelType.Websocket;
                    }

                    // if upgrading to websocket then relay the request without reading the contents
                    await handleWebSocketUpgrade(args, args.ClientStream, connection, cancellationTokenSource, cancellationToken);
                    return false;
                }

                // construct the web request that we are going to issue on behalf of the client.
                await handleHttpSessionRequest(args);
                return true;
            }, generator, serverConnection));
        }
Exemple #5
0
        private async Task <RetryResult> handleHttpSessionRequest(SessionEventArgs args,
                                                                  TcpServerConnection?serverConnection, SslApplicationProtocol sslApplicationProtocol,
                                                                  CancellationToken cancellationToken, CancellationTokenSource cancellationTokenSource)
        {
            args.HttpClient.Request.Locked = true;

            // do not cache server connections for WebSockets
            bool noCache = args.HttpClient.Request.UpgradeToWebSocket;

            if (noCache)
            {
                serverConnection = null;
            }

            // a connection generator task with captured parameters via closure.
            Func <Task <TcpServerConnection> > generator = () =>
                                                           tcpConnectionFactory.GetServerConnection(this,
                                                                                                    args,
                                                                                                    false,
                                                                                                    sslApplicationProtocol,
                                                                                                    noCache,
                                                                                                    cancellationToken);

            // for connection pool, retry fails until cache is exhausted.
            return(await retryPolicy <ServerConnectionException>().ExecuteAsync(async connection =>
            {
                // set the connection and send request headers
                args.HttpClient.SetConnection(connection);

                args.TimeLine["Connection Ready"] = DateTime.UtcNow;

                if (args.HttpClient.Request.UpgradeToWebSocket)
                {
                    // connectRequest can be null for SOCKS connection
                    if (args.HttpClient.ConnectRequest != null)
                    {
                        args.HttpClient.ConnectRequest !.TunnelType = TunnelType.Websocket;
                    }

                    // if upgrading to websocket then relay the request without reading the contents
                    await handleWebSocketUpgrade(args, args.ClientStream, connection, cancellationTokenSource, cancellationToken);
                    return false;
                }

                // construct the web request that we are going to issue on behalf of the client.
                await handleHttpSessionRequest(args);
                return true;
            }, generator, serverConnection));
        }
Exemple #6
0
 internal async Task Release(Task <TcpServerConnection>?connectionCreateTask, bool closeServerConnection)
 {
     if (connectionCreateTask != null)
     {
         TcpServerConnection?connection = null;
         try
         {
             connection = await connectionCreateTask;
         }
         catch { }
         finally
         {
             if (connection != null)
             {
                 await Release(connection, closeServerConnection);
             }
         }
     }
 }
        /// <summary>
        ///     Execute and retry the given action until retry number of times.
        /// </summary>
        /// <param name="action">The action to retry with return value specifying whether caller should continue execution.</param>
        /// <param name="generator">The Tcp connection generator to be invoked to get new connection for retry.</param>
        /// <param name="initialConnection">Initial Tcp connection to use.</param>
        /// <returns>Returns the latest connection used and the latest exception if any.</returns>
        internal async Task <RetryResult> ExecuteAsync(Func <TcpServerConnection, Task <bool> > action,
                                                       Func <Task <TcpServerConnection> > generator, TcpServerConnection?initialConnection)
        {
            currentConnection = initialConnection;
            bool      @continue = true;
            Exception?exception = null;

            var attempts = retries;

            while (true)
            {
                try
                {
                    // setup connection
                    currentConnection = currentConnection as TcpServerConnection ??
                                        await generator();

                    // try
                    @continue = await action(currentConnection);
                }
                catch (Exception ex)
                {
                    exception = ex;
                }

                attempts--;

                if (attempts < 0 ||
                    exception == null ||
                    !(exception is T))
                {
                    break;
                }

                exception = null;
                await disposeConnection();
            }

            return(new RetryResult(currentConnection, exception, @continue));
        }
Exemple #8
0
        /// <summary>
        ///     Execute and retry the given action until retry number of times.
        /// </summary>
        /// <param name="action">The action to retry with return value specifying whether caller should continue execution.</param>
        /// <param name="generator">The Tcp connection generator to be invoked to get new connection for retry.</param>
        /// <param name="initialConnection">Initial Tcp connection to use.</param>
        /// <returns>Returns the latest connection used and the latest exception if any.</returns>
        internal async Task <RetryResult> ExecuteAsync(Func <TcpServerConnection, Task <bool> > action,
                                                       Func <Task <TcpServerConnection> > generator, TcpServerConnection?initialConnection)
        {
            currentConnection = initialConnection;
            bool      @continue = true;
            Exception?exception = null;

            var attempts = retries;

            while (true)
            {
                // setup connection
                currentConnection ??= await generator();

                try
                {
                    @continue = await action(currentConnection);
                }
                catch (Exception ex)
                {
                    exception = ex;
                }

                attempts--;

                if (attempts < 0 || exception == null || !(exception is T))
                {
                    break;
                }

                exception = null;

                // before retry clear connection
                await tcpConnectionFactory.Release(currentConnection, true);

                currentConnection = null;
            }

            return(new RetryResult(currentConnection, exception, @continue));
        }
 internal RetryResult(TcpServerConnection?lastConnection, Exception?exception, bool @continue)
 {
     LatestConnection = lastConnection;
     Exception        = exception;
     Continue         = @continue;
 }
 /// <summary>
 ///     Set the tcp connection to server used by this webclient
 /// </summary>
 /// <param name="serverConnection">Instance of <see cref="TcpServerConnection" /></param>
 internal void SetConnection(TcpServerConnection serverConnection)
 {
     serverConnection.LastAccess = DateTime.UtcNow;
     connection = serverConnection;
 }
Exemple #11
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>
        /// <param name="isHttps">Is HTTPS</param>
        private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, HttpClientStream clientStream,
                                                    CancellationTokenSource cancellationTokenSource, TunnelConnectSessionEventArgs?connectArgs = null,
                                                    Task <TcpServerConnection>?prefetchConnectionTask = null, bool isHttps = false)
        {
            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(this, endPoint, clientStream, connectRequest, cancellationTokenSource)
                    {
                        UserData = connectArgs?.UserData
                    };

                    var request = args.HttpClient.Request;
                    if (isHttps)
                    {
                        request.IsHttps = true;
                    }

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

                            if (connectRequest != null)
                            {
                                request.IsHttps   = connectRequest.IsHttps;
                                request.Authority = connectRequest.Authority;
                            }

                            request.RequestUriString8 = requestLine.RequestUri;

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

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

                            if (!args.IsTransparent && !args.IsSocks)
                            {
                                // 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);
                            }

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

                            if (connection != null)
                            {
                                var  socket = connection.TcpSocket;
                                bool part1  = socket.Poll(1000, SelectMode.SelectRead);
                                bool part2  = socket.Available == 0;
                                if (part1 & part2)
                                {
                                    //connection is closed
                                    await tcpConnectionFactory.Release(connection, true);

                                    connection = 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);
            }
        }
        /// <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="clientConnection">The client connection.</param>
        /// <param name="clientStream">The client stream.</param>
        /// <param name="clientStreamWriter">The client stream writer.</param>
        /// <param name="cancellationTokenSource">The cancellation token source for this async task.</param>
        /// <param name="httpsConnectHostname">
        ///     The https hostname as appeared in CONNECT request if this is a HTTPS request from
        ///     explicit endpoint.
        /// </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, TcpClientConnection clientConnection,
                                                    CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter,
                                                    CancellationTokenSource cancellationTokenSource, string?httpsConnectHostname, TunnelConnectSessionEventArgs?connectArgs,
                                                    Task <TcpServerConnection>?prefetchConnectionTask = null)
        {
            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
                    string?httpCmd = await clientStream.ReadLineAsync(cancellationToken);

                    if (string.IsNullOrEmpty(httpCmd))
                    {
                        return;
                    }

                    var args = new SessionEventArgs(this, endPoint, cancellationTokenSource)
                    {
                        ProxyClient = { Connection = clientConnection },
                        HttpClient  = { ConnectRequest = connectRequest },
                        UserData    = connectArgs?.UserData
                    };

                    try
                    {
                        try
                        {
                            Request.ParseRequestLine(httpCmd !, out string httpMethod, out string httpUrl, out var version);

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

                            Uri httpRemoteUri;
                            if (ProxyConstants.UriSchemeRegex.IsMatch(httpUrl))
                            {
                                try
                                {
                                    httpRemoteUri = new Uri(httpUrl);
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception($"Invalid URI: '{httpUrl}'", ex);
                                }
                            }
                            else
                            {
                                string?host        = args.HttpClient.Request.Host ?? httpsConnectHostname;
                                string?hostAndPath = host;
                                if (httpUrl.StartsWith("/"))
                                {
                                    hostAndPath += httpUrl;
                                }

                                string url = string.Concat(httpsConnectHostname == null ? "http://" : "https://",
                                                           hostAndPath);
                                try
                                {
                                    httpRemoteUri = new Uri(url);
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception($"Invalid URI: '{url}'", ex);
                                }
                            }

                            var request = args.HttpClient.Request;
                            request.RequestUri  = httpRemoteUri;
                            request.OriginalUrl = httpUrl;

                            request.Method                      = httpMethod;
                            request.HttpVersion                 = version;
                            args.ProxyClient.ClientStream       = clientStream;
                            args.ProxyClient.ClientStreamWriter = clientStreamWriter;

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

                                    // send the response
                                    await clientStreamWriter.WriteResponseAsync(args.HttpClient.Response,
                                                                                cancellationToken : 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,
                                                                                  clientConnection.NegotiatedApplicationProtocol)
                                 != connection.CacheKey))
                            {
                                await tcpConnectionFactory.Release(connection);

                                connection = null;
                            }

                            var result = await handleHttpSessionRequest(httpMethod, httpUrl, version, args, connection,
                                                                        clientConnection.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);
            }
        }