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