Commit b7a9c729 authored by justcoding121's avatar justcoding121

implement tcp server connection pool

parent ad1ab97f
......@@ -23,7 +23,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
public ProxyTestController()
{
proxyServer = new ProxyServer();
proxyServer.EnableConnectionPool = true;
// generate root certificate without storing it in file system
//proxyServer.CertificateManager.CreateRootCertificate(false);
......
......@@ -140,7 +140,7 @@ namespace Titanium.Web.Proxy
SslExtensions.Http2ProtocolAsList, cancellationToken);
http2Supproted = connection.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2;
tcpConnectionFactory.Release(connection);
tcpConnectionFactory.Release(connection, true);
}
SslStream sslStream = null;
......@@ -235,7 +235,7 @@ namespace Titanium.Web.Proxy
(buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); },
connectArgs.CancellationTokenSource, ExceptionFunc);
tcpConnectionFactory.Release(connection);
tcpConnectionFactory.Release(connection, true);
return;
}
}
......@@ -280,7 +280,7 @@ namespace Titanium.Web.Proxy
(buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); },
connectArgs.CancellationTokenSource, clientConnection.Id, ExceptionFunc);
#endif
tcpConnectionFactory.Release(connection);
tcpConnectionFactory.Release(connection, true);
}
}
......
......@@ -216,6 +216,9 @@ namespace Titanium.Web.Proxy.Http
ConnectRequest?.FinishSession();
Request?.FinishSession();
Response?.FinishSession();
Data.Clear();
UserData = null;
}
}
}
......@@ -69,6 +69,15 @@ namespace Titanium.Web.Proxy.Models
/// </summary>
public int Port { get; set; }
/// <summary>
/// Get cache key for Tcp connection cahe.
/// </summary>
/// <returns></returns>
internal string GetCacheKey()
{
return $"{HostName}-{Port}" + (UseDefaultCredentials ? $"-{UserName}-{Password}" : string.Empty);
}
/// <summary>
/// returns data in Hostname:port format.
/// </summary>
......
......@@ -43,7 +43,6 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
private readonly ConcurrentDictionary<string, CachedCertificate> certificateCache;
private ExceptionHandler exceptionFunc;
private readonly ConcurrentDictionary<string, Task<X509Certificate2>> pendingCertificateCreationTasks;
private ICertificateMaker certEngine;
......@@ -77,7 +76,7 @@ namespace Titanium.Web.Proxy.Network
bool userTrustRootCertificate, bool machineTrustRootCertificate, bool trustRootCertificateAsAdmin,
ExceptionHandler exceptionFunc)
{
this.exceptionFunc = exceptionFunc;
ExceptionFunc = exceptionFunc;
UserTrustRoot = userTrustRootCertificate || machineTrustRootCertificate;
......@@ -94,14 +93,7 @@ namespace Titanium.Web.Proxy.Network
RootCertificateIssuerName = rootCertificateIssuerName;
}
if (RunTime.IsWindows)
{
CertificateEngine = CertificateEngine.DefaultWindows;
}
else
{
CertificateEngine = CertificateEngine.BouncyCastle;
}
CertificateEngine = RunTime.IsWindows ? CertificateEngine.DefaultWindows : CertificateEngine.BouncyCastle;
certificateCache = new ConcurrentDictionary<string, CachedCertificate>();
pendingCertificateCreationTasks = new ConcurrentDictionary<string, Task<X509Certificate2>>();
......@@ -131,6 +123,11 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
internal bool TrustRootAsAdministrator { get; set; }
/// <summary>
/// Exception handler
/// </summary>
internal ExceptionHandler ExceptionFunc { get; set; }
/// <summary>
/// Select Certificate Engine.
/// Optionally set to BouncyCastle.
......@@ -156,8 +153,8 @@ namespace Titanium.Web.Proxy.Network
if (certEngine == null)
{
certEngine = engine == CertificateEngine.BouncyCastle
? (ICertificateMaker)new BCCertificateMaker(exceptionFunc)
: new WinCertificateMaker(exceptionFunc);
? (ICertificateMaker)new BCCertificateMaker(ExceptionFunc)
: new WinCertificateMaker(ExceptionFunc);
}
}
}
......@@ -235,10 +232,6 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
public X509KeyStorageFlags StorageFlag { get; set; } = X509KeyStorageFlags.Exportable;
public ExceptionHandler ExceptionFunc {
set => exceptionFunc = value;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
......@@ -329,7 +322,7 @@ namespace Titanium.Web.Proxy.Network
{
if (RootCertificate == null)
{
exceptionFunc(new Exception("Could not install certificate as it is null or empty."));
ExceptionFunc(new Exception("Could not install certificate as it is null or empty."));
return;
}
......@@ -344,7 +337,7 @@ namespace Titanium.Web.Proxy.Network
}
catch (Exception e)
{
exceptionFunc(
ExceptionFunc(
new Exception("Failed to make system trust root certificate "
+ $" for {storeName}\\{storeLocation} store location. You may need admin rights.",
e));
......@@ -366,7 +359,7 @@ namespace Titanium.Web.Proxy.Network
{
if (certificate == null)
{
exceptionFunc(new Exception("Could not remove certificate as it is null or empty."));
ExceptionFunc(new Exception("Could not remove certificate as it is null or empty."));
return;
}
......@@ -380,7 +373,7 @@ namespace Titanium.Web.Proxy.Network
}
catch (Exception e)
{
exceptionFunc(
ExceptionFunc(
new Exception("Failed to remove root certificate trust "
+ $" for {storeLocation} store location. You may need admin rights.", e));
}
......@@ -438,7 +431,7 @@ namespace Titanium.Web.Proxy.Network
}
catch (Exception e)
{
exceptionFunc(new Exception("Failed to save fake certificate.", e));
ExceptionFunc(new Exception("Failed to save fake certificate.", e));
}
});
}
......@@ -462,7 +455,7 @@ namespace Titanium.Web.Proxy.Network
}
catch (Exception e)
{
exceptionFunc(e);
ExceptionFunc(e);
}
return certificate;
......@@ -572,7 +565,7 @@ namespace Titanium.Web.Proxy.Network
}
catch (Exception e)
{
exceptionFunc(e);
ExceptionFunc(e);
}
if (persistToFile && RootCertificate != null)
......@@ -593,7 +586,7 @@ namespace Titanium.Web.Proxy.Network
}
catch (Exception e)
{
exceptionFunc(e);
ExceptionFunc(e);
}
}
......@@ -619,7 +612,7 @@ namespace Titanium.Web.Proxy.Network
}
catch (Exception e)
{
exceptionFunc(e);
ExceptionFunc(e);
return null;
}
}
......@@ -728,7 +721,7 @@ namespace Titanium.Web.Proxy.Network
}
catch (Exception e)
{
exceptionFunc(e);
ExceptionFunc(e);
return false;
}
......
......@@ -5,6 +5,7 @@ using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
......@@ -21,9 +22,21 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// </summary>
internal class TcpConnectionFactory
{
//private readonly ConcurrentDictionary<string, List<TcpServerConnection>> cache
// = new ConcurrentDictionary<string, List<TcpServerConnection>>();
//Tcp server connection pool cache
private readonly ConcurrentDictionary<string, ConcurrentQueue<TcpServerConnection>> cache
= new ConcurrentDictionary<string, ConcurrentQueue<TcpServerConnection>>();
//Tcp connections waiting to be disposed by cleanup task
private readonly ConcurrentBag<TcpServerConnection> disposalBag =
new ConcurrentBag<TcpServerConnection>();
//cache object race operations lock
private readonly SemaphoreSlim @lock = new SemaphoreSlim(1);
internal TcpConnectionFactory(int connectionTimeOutSeconds)
{
Task.Run(async () => await ClearOutdatedConnections(connectionTimeOutSeconds));
}
/// <summary>
/// Gets a TCP connection to server from connection pool.
......@@ -35,7 +48,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// <param name="applicationProtocols">The list of HTTPS application level protocol to negotiate if needed.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="proxyServer">The current ProxyServer instance.</param>
/// <param name="upStreamEndPoint">The upstream endpoint to make request via.</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="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
......@@ -44,23 +57,51 @@ namespace Titanium.Web.Proxy.Network.Tcp
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy,
CancellationToken cancellationToken)
{
//TODO fix cacheKey with all possible properties that uniquely identify a connection
//var cacheKey = $"{remoteHostName}{remotePort}{httpVersion}" +
// $"{isHttps}{isConnect}";
string cacheKey = null;
if (proxyServer.EnableConnectionPool)
{
var cacheKeyBuilder = new StringBuilder($"{remoteHostName}-{remotePort}" +
$"-{(httpVersion == null ? string.Empty : httpVersion.ToString())}" +
$"-{isHttps}-{isConnect}-");
if (applicationProtocols != null)
{
foreach (var protocol in applicationProtocols)
{
cacheKeyBuilder.Append($"{protocol}-");
}
}
cacheKeyBuilder.Append(upStreamEndPoint != null
? $"{upStreamEndPoint.Address}-{upStreamEndPoint.Port}-"
: string.Empty);
cacheKeyBuilder.Append(externalProxy != null ? $"{externalProxy.GetCacheKey()}-" : string.Empty);
cacheKey = cacheKeyBuilder.ToString();
if (cache.TryGetValue(cacheKey, out var existingConnections))
{
while (existingConnections.TryDequeue(out var recentConnection))
{
//+1 seconds for potential delay after getting connection
var cutOff = DateTime.Now.AddSeconds((-1 * proxyServer.ConnectionTimeOutSeconds) + 1);
if (recentConnection.LastAccess > cutOff
&& IsGoodConnection(recentConnection.TcpClient))
{
return recentConnection;
}
disposalBag.Add(recentConnection);
}
//if (cache.TryGetValue(cacheKey, out var existingConnections))
//{
// var recentConnection = existingConnections.Last();
// existingConnections.RemoveAt(existingConnections.Count - 1);
// //TODO check if connection is still active before returning
// return recentConnection;
//}
}
}
var connection = await CreateClient(remoteHostName, remotePort, httpVersion, isHttps,
applicationProtocols, isConnect, proxyServer, upStreamEndPoint, externalProxy, cancellationToken);
//connection.CacheKey = cacheKey;
connection.CacheKey = cacheKey;
return connection;
}
......@@ -74,7 +115,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// <param name="applicationProtocols">The list of HTTPS application level protocol to negotiate if needed.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="proxyServer">The current ProxyServer instance.</param>
/// <param name="upStreamEndPoint">The upstream endpoint to make request via.</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="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
......@@ -114,7 +155,6 @@ namespace Titanium.Web.Proxy.Network.Tcp
ReceiveBufferSize = proxyServer.BufferSize
};
await proxyServer.InvokeConnectionCreateEvent(tcpClient, false);
// If this proxy uses another external proxy then create a tunnel request for HTTP/HTTPS connections
......@@ -192,7 +232,6 @@ namespace Titanium.Web.Proxy.Network.Tcp
return new TcpServerConnection(proxyServer, tcpClient)
{
//CacheKey = cacheKey,
UpStreamProxy = externalProxy,
UpStreamEndPoint = upStreamEndPoint,
HostName = remoteHostName,
......@@ -210,24 +249,106 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// Release connection back to cache.
/// </summary>
/// <param name="connection">The Tcp server connection to return.</param>
internal void Release(TcpServerConnection connection)
{
//while (true)
//{
// if (cache.TryGetValue(connection.Key, out var existingConnections))
// {
// existingConnections.Add(connection);
// break;
// }
// if (cache.TryAdd(connection.Key, new List<TcpServerConnection> { connection }))
// {
// break;
// };
//}
/// <param name="close">Should we just close the connection instead of reusing?</param>
internal void Release(TcpServerConnection connection, bool close = false)
{
if (close || connection.IsWinAuthenticated)
{
disposalBag.Add(connection);
return;
}
connection.LastAccess = DateTime.Now;
@lock.Wait();
while (true)
{
if (cache.TryGetValue(connection.CacheKey, out var existingConnections))
{
existingConnections.Enqueue(connection);
break;
}
if (cache.TryAdd(connection.CacheKey, new ConcurrentQueue<TcpServerConnection>(new[] { connection })))
{
break;
};
}
@lock.Release();
}
private async Task ClearOutdatedConnections(int connectionTimeOutSeconds)
{
while (true)
{
foreach (var item in cache)
{
var queue = item.Value;
while (queue.TryDequeue(out var connection))
{
var cutOff = DateTime.Now.AddSeconds(-1 * connectionTimeOutSeconds);
if (connection.LastAccess < cutOff)
{
disposalBag.Add(connection);
continue;
}
queue.Enqueue(connection);
break;
}
}
//clear empty queues
await @lock.WaitAsync();
var emptyKeys = cache.Where(x => x.Value.Count == 0).Select(x => x.Key).ToList();
foreach (var key in emptyKeys)
{
cache.TryRemove(key, out var _);
}
@lock.Release();
while (disposalBag.TryTake(out TcpServerConnection connection))
{
connection?.Dispose();
}
//cleanup every ten seconds by default
await Task.Delay(1000 * 10);
}
}
/// <summary>
/// Check if a TcpClient is good to be used.
/// https://msdn.microsoft.com/en-us/library/system.net.sockets.socket.connected(v=vs.110).aspx
/// </summary>
/// <param name="client"></param>
/// <returns></returns>
private static bool IsGoodConnection(TcpClient client)
{
var socket = client.Client;
// This is how you can determine whether a socket is still connected.
bool blockingState = socket.Blocking;
try
{
var tmp = new byte[1];
socket.Blocking = false;
socket.Send(tmp, 0, 0);
//Connected!
}
catch
{
//Should we let 10035 == WSAEWOULDBLOCK as valid connection?
return false;
}
finally
{
socket.Blocking = blockingState;
}
return true;
}
}
}
......@@ -48,6 +48,11 @@ namespace Titanium.Web.Proxy.Network.Tcp
private readonly TcpClient tcpClient;
/// <summary>
/// The TcpClient.
/// </summary>
internal TcpClient TcpClient => tcpClient;
/// <summary>
/// Used to write lines to server
/// </summary>
......@@ -68,6 +73,11 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// </summary>
internal string CacheKey { get; set; }
/// <summary>
/// Is this connection authenticated via WinAuth
/// </summary>
internal bool IsWinAuthenticated { get; set; }
/// <summary>
/// Dispose.
/// </summary>
......
......@@ -98,7 +98,7 @@ namespace Titanium.Web.Proxy
ConnectionTimeOutSeconds = 60;
ProxyEndPoints = new List<ProxyEndPoint>();
tcpConnectionFactory = new TcpConnectionFactory();
tcpConnectionFactory = new TcpConnectionFactory(ConnectionTimeOutSeconds);
if (!RunTime.IsRunningOnMono && RunTime.IsWindows)
{
systemProxySettingsManager = new SystemProxyManager();
......@@ -149,6 +149,12 @@ namespace Titanium.Web.Proxy
/// </summary>
public bool Enable100ContinueBehaviour { get; set; }
/// <summary>
/// Should we enable experimental Tcp server connection pool?
/// Defaults to false.
/// </summary>
public bool EnableConnectionPool { get; set; }
/// <summary>
/// Buffer size used throughout this proxy.
/// </summary>
......
......@@ -55,6 +55,7 @@ namespace Titanium.Web.Proxy
{
var cancellationToken = cancellationTokenSource.Token;
TcpServerConnection serverConnection = null;
bool serverConnectionClose = false;
try
{
......@@ -182,7 +183,7 @@ namespace Titanium.Web.Proxy
|| args.WebSession.UpStreamEndPoint?.Equals(serverConnection.UpStreamEndPoint) ==
false))
{
serverConnection.Dispose();
tcpConnectionFactory.Release(serverConnection, true);
serverConnection = null;
}
......@@ -241,6 +242,7 @@ namespace Titanium.Web.Proxy
// if connection is closing exit
if (!response.KeepAlive)
{
serverConnectionClose = true;
return;
}
......@@ -257,6 +259,7 @@ namespace Titanium.Web.Proxy
catch (Exception e)
{
args.Exception = e;
serverConnectionClose = true;
throw;
}
finally
......@@ -268,7 +271,7 @@ namespace Titanium.Web.Proxy
}
finally
{
tcpConnectionFactory.Release(serverConnection);
tcpConnectionFactory.Release(serverConnection, serverConnectionClose || !EnableConnectionPool);
}
}
......
......@@ -89,7 +89,7 @@ namespace Titanium.Web.Proxy
{
// create new connection
var connection = await tcpConnectionFactory.GetClient(httpsHostName, endPoint.Port,
null, false, new List<SslApplicationProtocol> { clientConnection.NegotiatedApplicationProtocol },
null, false, null,
true, this, UpStreamEndPoint, UpStreamHttpsProxy, cancellationToken);
......@@ -117,7 +117,7 @@ namespace Titanium.Web.Proxy
await TcpHelper.SendRaw(clientStream, serverStream, BufferSize,
null, null, cancellationTokenSource, ExceptionFunc);
tcpConnectionFactory.Release(connection);
tcpConnectionFactory.Release(connection, true);
return;
}
......
......@@ -145,6 +145,8 @@ namespace Titanium.Web.Proxy
{
request.ContentLength = request.Body.Length;
}
args.WebSession.ServerConnection.IsWinAuthenticated = true;
}
// Need to revisit this.
......
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