Commit 8b46f917 authored by justcoding121's avatar justcoding121 Committed by GitHub

Merge pull request #99 from justcoding121/release

Performance: Fix abrupt termination of SSL requests
parents 4456a224 e1cf60d6
......@@ -9,26 +9,37 @@ using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Shared;
using Titanium.Web.Proxy.Network;
namespace Titanium.Web.Proxy.Helpers
{
internal class TcpHelper
{
/// <summary>
/// relays the input clientStream to the server at the specified host name & port with the given httpCmd & headers as prefix
/// Usefull for websocket requests
/// </summary>
/// <param name="clientStream"></param>
/// <param name="bufferSize"></param>
/// <param name="connectionTimeOutSeconds"></param>
/// <param name="remoteHostName"></param>
/// <param name="httpCmd"></param>
/// <param name="httpVersion"></param>
/// <param name="requestHeaders"></param>
/// <param name="hostName"></param>
/// <param name="tunnelPort"></param>
/// <param name="isHttps"></param>
/// <param name="remotePort"></param>
/// <param name="supportedProtocols"></param>
/// <param name="remoteCertificateValidationCallback"></param>
/// <param name="localCertificateSelectionCallback"></param>
/// <param name="clientStream"></param>
/// <param name="tcpConnectionFactory"></param>
/// <returns></returns>
internal static async Task SendRaw(Stream clientStream, string httpCmd, Dictionary<string, HttpHeader> requestHeaders, string hostName,
int tunnelPort, bool isHttps, SslProtocols supportedProtocols, int connectionTimeOutSeconds)
internal static async Task SendRaw(int bufferSize, int connectionTimeOutSeconds,
string remoteHostName, int remotePort, string httpCmd, Version httpVersion, Dictionary<string, HttpHeader> requestHeaders,
bool isHttps, SslProtocols supportedProtocols,
RemoteCertificateValidationCallback remoteCertificateValidationCallback, LocalCertificateSelectionCallback localCertificateSelectionCallback,
Stream clientStream, TcpConnectionFactory tcpConnectionFactory)
{
//prepare the prefix content
StringBuilder sb = null;
......@@ -54,40 +65,17 @@ namespace Titanium.Web.Proxy.Helpers
sb.Append(Environment.NewLine);
}
TcpClient tunnelClient = null;
Stream tunnelStream = null;
//create the TcpClient to the server
var tcpConnection = await tcpConnectionFactory.CreateClient(bufferSize, connectionTimeOutSeconds,
remoteHostName, remotePort,
httpVersion, isHttps,
supportedProtocols, remoteCertificateValidationCallback, localCertificateSelectionCallback,
null, null, clientStream);
try
{
tunnelClient = new TcpClient(hostName, tunnelPort);
tunnelStream = tunnelClient.GetStream();
if (isHttps)
{
SslStream sslStream = null;
try
{
sslStream = new SslStream(tunnelStream);
await sslStream.AuthenticateAsClientAsync(hostName, null, supportedProtocols, false);
tunnelStream = sslStream;
}
catch
{
if (sslStream != null)
{
sslStream.Dispose();
}
throw;
}
}
tunnelClient.SendTimeout = connectionTimeOutSeconds * 1000;
tunnelClient.ReceiveTimeout = connectionTimeOutSeconds * 1000;
TcpClient tunnelClient = tcpConnection.TcpClient;
tunnelStream.ReadTimeout = connectionTimeOutSeconds * 1000;
tunnelStream.WriteTimeout = connectionTimeOutSeconds * 1000;
Stream tunnelStream = tcpConnection.Stream;
Task sendRelay;
......@@ -108,19 +96,13 @@ namespace Titanium.Web.Proxy.Helpers
}
catch
{
if (tunnelStream != null)
{
tunnelStream.Close();
tunnelStream.Dispose();
}
if (tunnelClient != null)
{
tunnelClient.Close();
}
throw;
}
finally
{
tcpConnection.Dispose();
}
}
}
}
\ No newline at end of file
......@@ -8,7 +8,7 @@ namespace Titanium.Web.Proxy.Network
/// <summary>
/// An object that holds TcpConnection to a particular server & port
/// </summary>
public class TcpConnection: IDisposable
public class TcpConnection : IDisposable
{
internal string HostName { get; set; }
internal int port { get; set; }
......@@ -44,11 +44,15 @@ namespace Titanium.Web.Proxy.Network
public void Dispose()
{
Stream.Close();
Stream.Dispose();
TcpClient.LingerState = new LingerOption(true, 0);
TcpClient.Client.Shutdown(SocketShutdown.Both);
TcpClient.Client.Close();
TcpClient.Close();
TcpClient.Client.Dispose();
TcpClient.Close();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Net.Security;
using Titanium.Web.Proxy.Helpers;
using System.Threading;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Models;
using System.Security.Authentication;
......@@ -19,38 +15,30 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
internal class TcpConnectionFactory
{
/// <summary>
/// Get a TcpConnection to the specified host, port optionally HTTPS and a particular HTTP version
/// </summary>
/// <param name="hostname"></param>
/// <param name="port"></param>
/// <param name="isHttps"></param>
/// <param name="version"></param>
/// <returns></returns>
internal async Task<TcpConnection> GetClient(string hostname, int port, bool isHttps, Version version,
ExternalProxy upStreamHttpProxy, ExternalProxy upStreamHttpsProxy, int bufferSize, SslProtocols supportedSslProtocols,
int connectionTimeOutSeconds,
RemoteCertificateValidationCallback remoteCertificateValidationCallBack,
LocalCertificateSelectionCallback localCertificateSelectionCallback)
{
//not in cache so create and return
return await CreateClient(hostname, port, isHttps, version, connectionTimeOutSeconds, upStreamHttpProxy, upStreamHttpsProxy, bufferSize, supportedSslProtocols,
remoteCertificateValidationCallBack, localCertificateSelectionCallback);
}
/// <summary>
/// Create connection to a particular host/port optionally with SSL and a particular HTTP version
/// Creates a TCP connection to server
/// </summary>
/// <param name="hostname"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
/// <param name="connectionTimeOutSeconds"></param>
/// <param name="remoteHostName"></param>
/// <param name="httpCmd"></param>
/// <param name="httpVersion"></param>
/// <param name="isHttps"></param>
/// <param name="version"></param>
/// <param name="remotePort"></param>
/// <param name="supportedSslProtocols"></param>
/// <param name="remoteCertificateValidationCallback"></param>
/// <param name="localCertificateSelectionCallback"></param>
/// <param name="externalHttpProxy"></param>
/// <param name="externalHttpsProxy"></param>
/// <param name="clientStream"></param>
/// <returns></returns>
private async Task<TcpConnection> CreateClient(string hostname, int port, bool isHttps, Version version, int connectionTimeOutSeconds,
ExternalProxy upStreamHttpProxy, ExternalProxy upStreamHttpsProxy, int bufferSize, SslProtocols supportedSslProtocols,
RemoteCertificateValidationCallback remoteCertificateValidationCallBack, LocalCertificateSelectionCallback localCertificateSelectionCallback)
internal async Task<TcpConnection> CreateClient(int bufferSize, int connectionTimeOutSeconds,
string remoteHostName, int remotePort, Version httpVersion,
bool isHttps, SslProtocols supportedSslProtocols,
RemoteCertificateValidationCallback remoteCertificateValidationCallback, LocalCertificateSelectionCallback localCertificateSelectionCallback,
ExternalProxy externalHttpProxy, ExternalProxy externalHttpsProxy,
Stream clientStream)
{
TcpClient client;
Stream stream;
......@@ -60,15 +48,15 @@ namespace Titanium.Web.Proxy.Network
SslStream sslStream = null;
//If this proxy uses another external proxy then create a tunnel request for HTTPS connections
if (upStreamHttpsProxy != null)
if (externalHttpsProxy != null)
{
client = new TcpClient(upStreamHttpsProxy.HostName, upStreamHttpsProxy.Port);
client = new TcpClient(externalHttpsProxy.HostName, externalHttpsProxy.Port);
stream = client.GetStream();
using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize, true))
{
await writer.WriteLineAsync(string.Format("CONNECT {0}:{1} {2}", hostname, port, version));
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", hostname, port));
await writer.WriteLineAsync(string.Format("CONNECT {0}:{1} {2}", remoteHostName, remotePort, httpVersion));
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", remoteHostName, remotePort));
await writer.WriteLineAsync("Connection: Keep-Alive");
await writer.WriteLineAsync();
await writer.FlushAsync();
......@@ -89,16 +77,16 @@ namespace Titanium.Web.Proxy.Network
}
else
{
client = new TcpClient(hostname, port);
client = new TcpClient(remoteHostName, remotePort);
stream = client.GetStream();
}
try
{
sslStream = new SslStream(stream, true, remoteCertificateValidationCallBack,
sslStream = new SslStream(stream, true, remoteCertificateValidationCallback,
localCertificateSelectionCallback);
await sslStream.AuthenticateAsClientAsync(hostname, null, supportedSslProtocols, false);
await sslStream.AuthenticateAsClientAsync(remoteHostName, null, supportedSslProtocols, false);
stream = sslStream;
}
......@@ -114,14 +102,14 @@ namespace Titanium.Web.Proxy.Network
}
else
{
if (upStreamHttpProxy != null)
if (externalHttpProxy != null)
{
client = new TcpClient(upStreamHttpProxy.HostName, upStreamHttpProxy.Port);
client = new TcpClient(externalHttpProxy.HostName, externalHttpProxy.Port);
stream = client.GetStream();
}
else
{
client = new TcpClient(hostname, port);
client = new TcpClient(remoteHostName, remotePort);
stream = client.GetStream();
}
}
......@@ -132,15 +120,17 @@ namespace Titanium.Web.Proxy.Network
stream.ReadTimeout = connectionTimeOutSeconds * 1000;
stream.WriteTimeout = connectionTimeOutSeconds * 1000;
client.NoDelay = true;
return new TcpConnection()
{
HostName = hostname,
port = port,
HostName = remoteHostName,
port = remotePort,
IsHttps = isHttps,
TcpClient = client,
StreamReader = new CustomBinaryReader(stream),
Stream = stream,
Version = version
Version = httpVersion
};
}
......
......@@ -15,9 +15,9 @@ namespace Titanium.Web.Proxy
/// <summary>
/// Proxy Server Main class
/// </summary>
public partial class ProxyServer: IDisposable
public partial class ProxyServer : IDisposable
{
/// <summary>
/// Manages certificates used by this proxy
/// </summary>
......@@ -197,7 +197,7 @@ namespace Titanium.Web.Proxy
Console.WriteLine("Set endpoint at Ip {1} and port: {2} as System HTTP Proxy", endPoint.GetType().Name, endPoint.IpAddress, endPoint.Port);
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine
......@@ -360,17 +360,12 @@ namespace Titanium.Web.Proxy
{
var endPoint = (ProxyEndPoint)asyn.AsyncState;
TcpClient tcpClient = null;
try
{
//based on end point type call appropriate request handlers
var client = endPoint.listener.EndAcceptTcpClient(asyn);
if (endPoint.GetType() == typeof(TransparentProxyEndPoint))
HandleClient(endPoint as TransparentProxyEndPoint, client);
else
HandleClient(endPoint as ExplicitProxyEndPoint, client);
// Get the listener that handles the client request.
endPoint.listener.BeginAcceptTcpClient(OnAcceptConnection, endPoint);
tcpClient = endPoint.listener.EndAcceptTcpClient(asyn);
}
catch (ObjectDisposedException)
{
......@@ -384,6 +379,43 @@ namespace Titanium.Web.Proxy
//Other errors are discarded to keep proxy running
}
if (tcpClient != null)
{
Task.Run(async () =>
{
try
{
tcpClient.NoDelay = true;
if (endPoint.GetType() == typeof(TransparentProxyEndPoint))
{
await HandleClient(endPoint as TransparentProxyEndPoint, tcpClient);
}
else
{
await HandleClient(endPoint as ExplicitProxyEndPoint, tcpClient);
}
}
finally
{
if (tcpClient != null)
{
tcpClient.LingerState = new LingerOption(true, 0);
tcpClient.Client.Shutdown(SocketShutdown.Both);
tcpClient.Client.Close();
tcpClient.Client.Dispose();
tcpClient.Close();
}
}
});
}
// Get the listener that handles the client request.
endPoint.listener.BeginAcceptTcpClient(OnAcceptConnection, endPoint);
}
public void Dispose()
......
......@@ -25,7 +25,7 @@ namespace Titanium.Web.Proxy
{
//This is called when client is aware of proxy
//So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy
private async void HandleClient(ExplicitProxyEndPoint endPoint, TcpClient client)
private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient client)
{
Stream clientStream = client.GetStream();
......@@ -43,7 +43,7 @@ namespace Titanium.Web.Proxy
if (string.IsNullOrEmpty(httpCmd))
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null);
Dispose(clientStream, clientStreamReader, clientStreamWriter, null);
return;
}
......@@ -109,7 +109,7 @@ namespace Titanium.Web.Proxy
sslStream.Dispose();
}
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null);
Dispose(clientStream, clientStreamReader, clientStreamWriter, null);
return;
}
......@@ -125,11 +125,15 @@ namespace Titanium.Web.Proxy
//write back successfull CONNECT response
await WriteConnectResponse(clientStreamWriter, version);
//Just relay the request/response without decrypting it
await TcpHelper.SendRaw(clientStream, null, null, httpRemoteUri.Host, httpRemoteUri.Port,
false, SupportedSslProtocols, ConnectionTimeOutSeconds);
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null);
await TcpHelper.SendRaw(BUFFER_SIZE, ConnectionTimeOutSeconds, httpRemoteUri.Host, httpRemoteUri.Port,
httpCmd, version, null,
false, SupportedSslProtocols,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
new LocalCertificateSelectionCallback(SelectClientCertificate),
clientStream, tcpConnectionFactory);
Dispose(clientStream, clientStreamReader, clientStreamWriter, null);
return;
}
......@@ -139,13 +143,13 @@ namespace Titanium.Web.Proxy
}
catch
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null);
Dispose(clientStream, clientStreamReader, clientStreamWriter, null);
}
}
//This is called when this proxy acts as a reverse proxy (like a real http server)
//So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client
private async void HandleClient(TransparentProxyEndPoint endPoint, TcpClient tcpClient)
private async Task HandleClient(TransparentProxyEndPoint endPoint, TcpClient tcpClient)
{
Stream clientStream = tcpClient.GetStream();
......@@ -181,7 +185,7 @@ namespace Titanium.Web.Proxy
sslStream.Dispose();
}
Dispose(tcpClient, sslStream, clientStreamReader, clientStreamWriter, null);
Dispose(sslStream, clientStreamReader, clientStreamWriter, null);
return;
}
clientStream = sslStream;
......@@ -219,7 +223,7 @@ namespace Titanium.Web.Proxy
{
if (string.IsNullOrEmpty(httpCmd))
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null);
Dispose(clientStream, clientStreamReader, clientStreamWriter, null);
break;
}
......@@ -234,14 +238,14 @@ namespace Titanium.Web.Proxy
var httpMethod = httpCmdSplit[0];
//find the request HTTP version
Version version = new Version(1, 1);
Version httpVersion = new Version(1, 1);
if (httpCmdSplit.Length == 3)
{
var httpVersion = httpCmdSplit[2].ToLower().Trim();
var httpVersionString = httpCmdSplit[2].ToLower().Trim();
if (httpVersion == "http/1.0")
if (httpVersionString == "http/1.0")
{
version = new Version(1, 0);
httpVersion = new Version(1, 0);
}
}
......@@ -285,7 +289,7 @@ namespace Titanium.Web.Proxy
args.WebSession.Request.RequestUri = httpRemoteUri;
args.WebSession.Request.Method = httpMethod;
args.WebSession.Request.HttpVersion = version;
args.WebSession.Request.HttpVersion = httpVersion;
args.ProxyClient.ClientStream = clientStream;
args.ProxyClient.ClientStreamReader = clientStreamReader;
args.ProxyClient.ClientStreamWriter = clientStreamWriter;
......@@ -310,24 +314,33 @@ namespace Titanium.Web.Proxy
//if upgrading to websocket then relay the requet without reading the contents
if (args.WebSession.Request.UpgradeToWebSocket)
{
await TcpHelper.SendRaw(clientStream, httpCmd, args.WebSession.Request.RequestHeaders,
httpRemoteUri.Host, httpRemoteUri.Port, args.IsHttps, SupportedSslProtocols, ConnectionTimeOutSeconds);
await TcpHelper.SendRaw(BUFFER_SIZE, ConnectionTimeOutSeconds, httpRemoteUri.Host, httpRemoteUri.Port,
httpCmd, httpVersion, args.WebSession.Request.RequestHeaders, args.IsHttps,
SupportedSslProtocols, new RemoteCertificateValidationCallback(ValidateServerCertificate),
new LocalCertificateSelectionCallback(SelectClientCertificate),
clientStream, tcpConnectionFactory);
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args);
Dispose(clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
//construct the web request that we are going to issue on behalf of the client.
connection = connection!=null? connection : await tcpConnectionFactory.GetClient(args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version,
UpStreamHttpProxy, UpStreamHttpsProxy, BUFFER_SIZE, SupportedSslProtocols, ConnectionTimeOutSeconds, new RemoteCertificateValidationCallback(ValidateServerCertificate),
new LocalCertificateSelectionCallback(SelectClientCertificate));
if (connection == null)
{
connection = await tcpConnectionFactory.CreateClient(BUFFER_SIZE, ConnectionTimeOutSeconds,
args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, httpVersion,
args.IsHttps, SupportedSslProtocols,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
new LocalCertificateSelectionCallback(SelectClientCertificate),
UpStreamHttpProxy, UpStreamHttpsProxy, clientStream);
}
args.WebSession.Request.RequestLocked = true;
//If request was cancelled by user then dispose the client
if (args.WebSession.Request.CancelRequest)
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args);
Dispose(clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
......@@ -397,7 +410,7 @@ namespace Titanium.Web.Proxy
//if connection is closing exit
if (args.WebSession.Response.ResponseKeepAlive == false)
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args);
Dispose(clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
......@@ -407,7 +420,7 @@ namespace Titanium.Web.Proxy
}
catch
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args);
Dispose(clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
......
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Compression;
......@@ -114,7 +112,7 @@ namespace Titanium.Web.Proxy
}
catch
{
Dispose(args.ProxyClient.TcpClient, args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader,
Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader,
args.ProxyClient.ClientStreamWriter, args);
}
finally
......@@ -216,32 +214,30 @@ namespace Titanium.Web.Proxy
/// <param name="clientStreamReader"></param>
/// <param name="clientStreamWriter"></param>
/// <param name="args"></param>
private void Dispose(TcpClient tcpClient, IDisposable clientStream, IDisposable clientStreamReader,
private void Dispose(Stream clientStream, IDisposable clientStreamReader,
IDisposable clientStreamWriter, IDisposable args)
{
if (clientStream != null)
{
(clientStream as Stream).Close();
(clientStream as Stream).Dispose();
}
if (tcpClient != null)
{
tcpClient.Client.Close();
tcpClient.Close();
tcpClient.Client.Dispose();
clientStream.Close();
clientStream.Dispose();
}
if (args != null)
{
args.Dispose();
}
if (clientStreamReader != null)
{
clientStreamReader.Dispose();
}
if (clientStreamWriter != null)
{
clientStreamWriter.Dispose();
}
}
}
}
\ No newline at end of file
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