Unverified Commit 02ea4aec authored by justcoding121's avatar justcoding121 Committed by GitHub

Merge pull request #445 from justcoding121/master

Use polly for retries
parents 17a1aabe fc3a9308
......@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Security;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Exceptions;
......@@ -14,7 +15,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
{
public class ProxyTestController
{
private readonly object lockObj = new object();
private readonly SemaphoreSlim @lock = new SemaphoreSlim(1);
private readonly ProxyServer proxyServer;
......@@ -23,34 +24,41 @@ namespace Titanium.Web.Proxy.Examples.Basic
public ProxyTestController()
{
proxyServer = new ProxyServer();
//proxyServer.EnableConnectionPool = false;
proxyServer.EnableConnectionPool = true;
// generate root certificate without storing it in file system
//proxyServer.CertificateManager.CreateRootCertificate(false);
//proxyServer.CertificateManager.TrustRootCertificate();
//proxyServer.CertificateManager.TrustRootCertificateAsAdmin();
proxyServer.ExceptionFunc = exception =>
proxyServer.ExceptionFunc = async exception =>
{
lock (lockObj)
await @lock.WaitAsync();
try
{
var color = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
if (exception is ProxyHttpException phex)
{
Console.WriteLine(exception.Message + ": " + phex.InnerException?.Message);
}
else
{
Console.WriteLine(exception.Message);
}
Console.ForegroundColor = color;
}
finally
{
var color = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
if (exception is ProxyHttpException phex)
{
Console.WriteLine(exception.Message + ": " + phex.InnerException?.Message);
}
else
{
Console.WriteLine(exception.Message);
}
Console.ForegroundColor = color;
@lock.Release();
}
};
proxyServer.ForwardToUpstreamGateway = true;
proxyServer.CertificateManager.SaveFakeCertificates = true;
// optionally set the Certificate Engine
// Under Mono or Non-Windows runtimes only BouncyCastle will be supported
//proxyServer.CertificateManager.CertificateEngine = Network.CertificateEngine.BouncyCastle;
......@@ -122,7 +130,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
proxyServer.ClientCertificateSelectionCallback -= OnCertificateSelection;
proxyServer.Stop();
// remove the generated certificates
//proxyServer.CertificateManager.RemoveTrustedRootCertificates();
}
......@@ -130,7 +138,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
private async Task OnBeforeTunnelConnectRequest(object sender, TunnelConnectSessionEventArgs e)
{
string hostname = e.WebSession.Request.RequestUri.Host;
WriteToConsole("Tunnel to: " + hostname);
await WriteToConsole("Tunnel to: " + hostname);
if (hostname.Contains("dropbox.com"))
{
......@@ -148,8 +156,8 @@ namespace Titanium.Web.Proxy.Examples.Basic
// intecept & cancel redirect or update requests
private async Task OnRequest(object sender, SessionEventArgs e)
{
WriteToConsole("Active Client Connections:" + ((ProxyServer)sender).ClientConnectionCount);
WriteToConsole(e.WebSession.Request.Url);
await WriteToConsole("Active Client Connections:" + ((ProxyServer)sender).ClientConnectionCount);
await WriteToConsole(e.WebSession.Request.Url);
// store it in the UserData property
// It can be a simple integer, Guid, or any type
......@@ -187,19 +195,19 @@ namespace Titanium.Web.Proxy.Examples.Basic
}
// Modify response
private void MultipartRequestPartSent(object sender, MultipartRequestPartSentEventArgs e)
private async Task MultipartRequestPartSent(object sender, MultipartRequestPartSentEventArgs e)
{
var session = (SessionEventArgs)sender;
WriteToConsole("Multipart form data headers:");
await WriteToConsole("Multipart form data headers:");
foreach (var header in e.Headers)
{
WriteToConsole(header.ToString());
await WriteToConsole(header.ToString());
}
}
private async Task OnResponse(object sender, SessionEventArgs e)
{
WriteToConsole("Active Server Connections:" + ((ProxyServer)sender).ServerConnectionCount);
await WriteToConsole("Active Server Connections:" + ((ProxyServer)sender).ServerConnectionCount);
string ext = System.IO.Path.GetExtension(e.WebSession.Request.RequestUri.AbsolutePath);
......@@ -271,12 +279,18 @@ namespace Titanium.Web.Proxy.Examples.Basic
return Task.FromResult(0);
}
private void WriteToConsole(string message)
private async Task WriteToConsole(string message)
{
lock (lockObj)
await @lock.WaitAsync();
try
{
Console.WriteLine(message);
}
finally
{
@lock.Release();
}
}
///// <summary>
......
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
......@@ -140,17 +141,17 @@ namespace Titanium.Web.Proxy
// test server HTTP/2 support
// todo: this is a hack, because Titanium does not support HTTP protocol changing currently
var connection = await getServerConnection(connectArgs, true,
SslExtensions.Http2ProtocolAsList, cancellationToken);
SslExtensions.Http2ProtocolAsList, true, cancellationToken);
http2Supported = connection.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2;
//release connection back to pool intead of closing when connection pool is enabled.
await tcpConnectionFactory.Release(connection);
await tcpConnectionFactory.Release(connection, true);
}
SslStream sslStream = null;
prefetchConnectionTask = getServerConnection(connectArgs, true,
null, cancellationToken);
null, false, cancellationToken);
try
{
sslStream = new SslStream(clientStream);
......@@ -209,8 +210,8 @@ namespace Titanium.Web.Proxy
// 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 getServerConnection(connectArgs, true,
null, cancellationToken);
var connection = await getServerConnection(connectArgs,
true, SslExtensions.Http2ProtocolAsList, true, cancellationToken);
try
{
......@@ -224,10 +225,9 @@ namespace Titanium.Web.Proxy
try
{
// clientStream.Available sbould be at most BufferSize because it is using the same buffer size
await clientStream.ReadAsync(data, 0, available, cancellationToken);
await connection.StreamWriter.WriteAsync(data, 0, available, true,
cancellationToken);
// clientStream.Available sbould be at most BufferSize because it is using the same buffer size
await connection.StreamWriter.WriteAsync(data, 0, available, true, cancellationToken);
}
finally
{
......@@ -235,8 +235,7 @@ namespace Titanium.Web.Proxy
}
}
var serverHelloInfo =
await SslTools.PeekServerHello(connection.Stream, BufferPool, cancellationToken);
var serverHelloInfo = await SslTools.PeekServerHello(connection.Stream, BufferPool, cancellationToken);
((ConnectResponse)connectArgs.WebSession.Response).ServerHelloInfo = serverHelloInfo;
}
......@@ -244,11 +243,14 @@ namespace Titanium.Web.Proxy
(buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); },
(buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); },
connectArgs.CancellationTokenSource, ExceptionFunc);
}
finally
{
await tcpConnectionFactory.Release(connection, true);
}
calledRequestHandler = true;
return;
}
}
......@@ -278,22 +280,22 @@ namespace Titanium.Web.Proxy
throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received");
}
// create new connection
var connection = await getServerConnection(connectArgs, true, SslExtensions.Http2ProtocolAsList,
cancellationToken);
var connection = await getServerConnection(connectArgs, true,
SslExtensions.Http2ProtocolAsList, true,
cancellationToken);
try
{
await connection.StreamWriter.WriteLineAsync("PRI * HTTP/2.0", cancellationToken);
await connection.StreamWriter.WriteLineAsync(cancellationToken);
await connection.StreamWriter.WriteLineAsync("SM", cancellationToken);
await connection.StreamWriter.WriteLineAsync(cancellationToken);
#if NETCOREAPP2_1
await Http2Helper.SendHttp2(clientStream, connection.Stream, BufferSize,
(buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); },
(buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); },
connectArgs.CancellationTokenSource, clientConnection.Id, ExceptionFunc);
#endif
}
finally
{
......@@ -301,6 +303,7 @@ namespace Titanium.Web.Proxy
}
}
}
calledRequestHandler = true;
// Now create the request
await handleHttpSessionRequest(endPoint, clientConnection, clientStream, clientStreamWriter,
......@@ -328,19 +331,19 @@ namespace Titanium.Web.Proxy
}
finally
{
if (!calledRequestHandler
&& prefetchConnectionTask != null)
{
var connection = await prefetchConnectionTask;
await tcpConnectionFactory.Release(connection, closeServerConnection);
}
clientStream.Dispose();
if (!cancellationTokenSource.IsCancellationRequested)
{
cancellationTokenSource.Cancel();
}
if (!calledRequestHandler
&& prefetchConnectionTask != null)
{
var connection = await prefetchConnectionTask;
await tcpConnectionFactory.Release(connection, closeServerConnection);
}
}
}
}
......
......@@ -81,18 +81,19 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// <param name="proxyServer">The current ProxyServer instance.</param>
/// <param name="upStreamEndPoint">The local upstream endpoint to make request via.</param>
/// <param name="externalProxy">The external proxy to make request via.</param>
/// <param name="noCache">Not from cache/create new connection.</param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
internal async Task<TcpServerConnection> GetClient(string remoteHostName, int remotePort,
Version httpVersion, bool isHttps, List<SslApplicationProtocol> applicationProtocols, bool isConnect,
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy,
CancellationToken cancellationToken)
bool noCache, CancellationToken cancellationToken)
{
var cacheKey = GetConnectionCacheKey(remoteHostName, remotePort,
isHttps, applicationProtocols,
proxyServer, upStreamEndPoint, externalProxy);
if (proxyServer.EnableConnectionPool)
if (proxyServer.EnableConnectionPool && !noCache)
{
if (cache.TryGetValue(cacheKey, out var existingConnections))
{
......
......@@ -7,6 +7,7 @@ using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Polly;
using StreamExtended;
using StreamExtended.Network;
using Titanium.Web.Proxy.EventArguments;
......@@ -123,6 +124,9 @@ namespace Titanium.Web.Proxy
/// </summary>
private SystemProxyManager systemProxySettingsManager { get; }
//Number of exception retries when connection pool is enabled.
private int retries => EnableConnectionPool ? MaxCachedConnections + 1 : 0;
/// <summary>
/// Is the proxy currently running?
/// </summary>
......@@ -197,7 +201,7 @@ namespace Titanium.Web.Proxy
/// Realm used during Proxy Basic Authentication.
/// </summary>
public string ProxyRealm { get; set; } = "TitaniumProxy";
/// <summary>
/// List of supported Ssl versions.
/// </summary>
......@@ -252,7 +256,8 @@ namespace Titanium.Web.Proxy
public ExceptionHandler ExceptionFunc
{
get => exceptionFunc ?? defaultExceptionFunc;
set {
set
{
exceptionFunc = value;
CertificateManager.ExceptionFunc = value;
}
......@@ -274,7 +279,7 @@ namespace Titanium.Web.Proxy
{
Stop();
}
CertificateManager?.Dispose();
BufferPool?.Dispose();
}
......@@ -800,5 +805,26 @@ namespace Titanium.Web.Proxy
await OnServerConnectionCreate.InvokeAsync(this, client, ExceptionFunc);
}
}
/// <summary>
/// Connection retry policy when using connection pool.
/// </summary>
private Policy retryPolicy<T>() where T : Exception
{
return Policy.Handle<T>()
.RetryAsync(retries,
onRetryAsync: async (ex, i, context) =>
{
if (context.ContainsKey("connection"))
{
//close connection on error
var connection = (TcpServerConnection)context["connection"];
await tcpConnectionFactory.Release(connection, true);
context.Remove("connection");
}
});
}
}
}
......@@ -61,7 +61,7 @@ namespace Titanium.Web.Proxy
var cancellationToken = cancellationTokenSource.Token;
var prefetchTask = prefetchConnectionTask;
TcpServerConnection serverConnection = null;
TcpServerConnection connection = null;
bool closeServerConnection = false;
try
......@@ -186,70 +186,57 @@ namespace Titanium.Web.Proxy
}
//If prefetch task is available.
if (serverConnection == null && prefetchTask != null)
if (connection == null && prefetchTask != null)
{
serverConnection = await prefetchTask;
connection = await prefetchTask;
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 (serverConnection != null
if (connection != null
&& (await getConnectionCacheKey(args,
clientConnection.NegotiatedApplicationProtocol)
!= serverConnection.CacheKey))
!= connection.CacheKey))
{
await tcpConnectionFactory.Release(serverConnection);
serverConnection = null;
await tcpConnectionFactory.Release(connection);
connection = null;
}
//create
if (serverConnection == null)
var contextData = new Dictionary<string, object>();
if (connection != null)
{
serverConnection = await getServerConnection(args, false,
clientConnection.NegotiatedApplicationProtocol, cancellationToken);
contextData.Add("connection", connection);
}
//for connection pool retry fails until cache is exhausted
int attempt = 0;
while (attempt < MaxCachedConnections + 1)
//for connection pool retry fails until cache is exhausted
await retryPolicy<ServerConnectionException>().ExecuteAsync(async (context) =>
{
try
{
// if upgrading to websocket then relay the request without reading the contents
if (request.UpgradeToWebSocket)
{
await handleWebSocketUpgrade(httpCmd, args, request,
response, clientStream, clientStreamWriter,
serverConnection, cancellationTokenSource, cancellationToken);
closeServerConnection = true;
return;
}
// construct the web request that we are going to issue on behalf of the client.
await handleHttpSessionRequestInternal(serverConnection, args);
}
//connection pool retry
catch (ServerConnectionException)
connection = context.ContainsKey("connection")?
(TcpServerConnection)context["connection"]
: await getServerConnection(args, false,
clientConnection.NegotiatedApplicationProtocol,
false, cancellationToken);
context["connection"] = connection;
// if upgrading to websocket then relay the request without reading the contents
if (request.UpgradeToWebSocket)
{
attempt++;
if (!EnableConnectionPool || attempt == MaxCachedConnections + 1)
{
throw;
}
//get new connection from pool
await tcpConnectionFactory.Release(serverConnection, true);
serverConnection = null;
serverConnection = await getServerConnection(args, false,
clientConnection.NegotiatedApplicationProtocol, cancellationToken);
continue;
await handleWebSocketUpgrade(httpCmd, args, request,
response, clientStream, clientStreamWriter,
connection, cancellationTokenSource, cancellationToken);
closeServerConnection = true;
return;
}
break;
}
// construct the web request that we are going to issue on behalf of the client.
await handleHttpSessionRequestInternal(connection, args);
}, contextData);
//user requested
if (args.WebSession.CloseServerConnection)
......@@ -275,8 +262,8 @@ namespace Titanium.Web.Proxy
//between sessions without using it.
if (EnableConnectionPool)
{
await tcpConnectionFactory.Release(serverConnection);
serverConnection = null;
await tcpConnectionFactory.Release(connection);
connection = null;
}
}
......@@ -300,10 +287,10 @@ namespace Titanium.Web.Proxy
}
finally
{
await tcpConnectionFactory.Release(serverConnection,
await tcpConnectionFactory.Release(connection,
closeServerConnection);
if (prefetchTask!=null)
if (prefetchTask != null)
{
await tcpConnectionFactory.Release(await prefetchTask,
closeServerConnection);
......@@ -515,7 +502,7 @@ namespace Titanium.Web.Proxy
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
private Task<TcpServerConnection> getServerConnection(SessionEventArgsBase args, bool isConnect,
SslApplicationProtocol applicationProtocol, CancellationToken cancellationToken)
SslApplicationProtocol applicationProtocol, bool noCache, CancellationToken cancellationToken)
{
List<SslApplicationProtocol> applicationProtocols = null;
if (applicationProtocol != default)
......@@ -523,7 +510,7 @@ namespace Titanium.Web.Proxy
applicationProtocols = new List<SslApplicationProtocol> { applicationProtocol };
}
return getServerConnection(args, isConnect, applicationProtocols, cancellationToken);
return getServerConnection(args, isConnect, applicationProtocols, noCache, cancellationToken);
}
/// <summary>
......@@ -535,7 +522,7 @@ namespace Titanium.Web.Proxy
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
private async Task<TcpServerConnection> getServerConnection(SessionEventArgsBase args, bool isConnect,
List<SslApplicationProtocol> applicationProtocols, CancellationToken cancellationToken)
List<SslApplicationProtocol> applicationProtocols, bool noCache, CancellationToken cancellationToken)
{
ExternalProxy customUpStreamProxy = null;
......@@ -554,7 +541,7 @@ namespace Titanium.Web.Proxy
isHttps, applicationProtocols, isConnect,
this, args.WebSession.UpStreamEndPoint ?? UpStreamEndPoint,
customUpStreamProxy ?? (isHttps ? UpStreamHttpsProxy : UpStreamHttpProxy),
cancellationToken);
noCache, cancellationToken);
}
/// <summary>
......
......@@ -12,6 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Polly" Version="6.0.1" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.2" />
<PackageReference Include="StreamExtended" Version="1.0.175-beta" />
</ItemGroup>
......
......@@ -16,6 +16,7 @@
<dependencies>
<dependency id="StreamExtended" version="1.0.175-beta" />
<dependency id="Portable.BouncyCastle" version="1.8.2" />
<dependency id="Polly" version="6.0.1"/>
</dependencies>
</metadata>
<files>
......
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
......@@ -63,8 +64,8 @@ namespace Titanium.Web.Proxy
if (endPoint.DecryptSsl && args.DecryptSsl)
{
prefetchConnectionTask = tcpConnectionFactory.GetClient(httpsHostName, endPoint.Port,
null, true, null,
false, this, UpStreamEndPoint, UpStreamHttpsProxy, cancellationToken);
null, true, null, false, this,
UpStreamEndPoint, UpStreamHttpsProxy, false, cancellationToken);
SslStream sslStream = null;
......@@ -93,25 +94,24 @@ namespace Titanium.Web.Proxy
}
else
{
// create new connection
var connection = await tcpConnectionFactory.GetClient(httpsHostName, endPoint.Port,
null, false, null,
true, this, UpStreamEndPoint, UpStreamHttpsProxy, cancellationToken);
null, false, null,
true, this, UpStreamEndPoint, UpStreamHttpsProxy, true, cancellationToken);
try
{
var serverStream = connection.Stream;
CustomBufferedStream serverStream = null;
int available = clientStream.Available;
if (available > 0)
{
// send the buffered data
var data = BufferPool.GetBuffer(BufferSize);
try
{
// clientStream.Available sbould be at most BufferSize because it is using the same buffer size
await clientStream.ReadAsync(data, 0, available, cancellationToken);
serverStream = connection.Stream;
await serverStream.WriteAsync(data, 0, available, cancellationToken);
await serverStream.FlushAsync(cancellationToken);
}
......
......@@ -3,4 +3,5 @@
<packages>
<package id="Portable.BouncyCastle" version="1.8.2" targetFramework="net45" />
<package id="StreamExtended" version="1.0.175-beta" targetFramework="net45" />
<package id="Polly" version="6.0.1" targetFramework="net45" />
</packages>
\ No newline at end of file
......@@ -450,8 +450,8 @@ Defaults to false.</p>
<a id="Titanium_Web_Proxy_ProxyServer_EnableConnectionPool_" data-uid="Titanium.Web.Proxy.ProxyServer.EnableConnectionPool*"></a>
<h4 id="Titanium_Web_Proxy_ProxyServer_EnableConnectionPool" data-uid="Titanium.Web.Proxy.ProxyServer.EnableConnectionPool">EnableConnectionPool</h4>
<div class="markdown level1 summary"><p>Should we enable server connection pool?
Defaults to true.</p>
<div class="markdown level1 summary"><p>Should we enable experimental server connection pool?
Defaults to disable.</p>
</div>
<div class="markdown level1 conceptual"></div>
<h5 class="decalaration">Declaration</h5>
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment