Commit 65b923fb authored by justcoding121's avatar justcoding121

Add mutual authentication capability

parent c525dda7
......@@ -15,6 +15,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
ProxyServer.BeforeRequest += OnRequest;
ProxyServer.BeforeResponse += OnResponse;
ProxyServer.ServerCertificateValidationCallback += OnCertificateValidation;
ProxyServer.ClientCertificateSelectionCallback += OnCertificateSelection;
//Exclude Https addresses you don't want to proxy
//Usefull for clients that use certificate pinning
......@@ -129,13 +130,25 @@ namespace Titanium.Web.Proxy.Examples.Basic
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public async Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
{
//set IsValid to true/false based on Certificate Errors
if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
e.IsValid = true;
else
await e.Session.Ok("Cannot validate server certificate! Not safe to proceed.");
return Task.FromResult(0);
}
/// <summary>
/// Allows overriding default client certificate selection logic during mutual authentication
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs e)
{
//set e.clientCertificate to override
return Task.FromResult(0);
}
}
}
\ No newline at end of file
......@@ -12,8 +12,11 @@ Features
========
* Supports Http(s) and most features of HTTP 1.1
* Supports relaying of WebSockets
* Supports script injection
* Support redirect/block/update requests
* Supports updating response
* Safely relays WebSocket requests over Http
* Support mutual SSL authentication
* Fully asynchronous proxy
Usage
=====
......@@ -35,6 +38,8 @@ Setup HTTP proxy:
ProxyServer.BeforeRequest += OnRequest;
ProxyServer.BeforeResponse += OnResponse;
ProxyServer.ServerCertificateValidationCallback += OnCertificateValidation;
ProxyServer.ClientCertificateSelectionCallback += OnCertificateSelection;
//Exclude Https addresses you don't want to proxy
//Usefull for clients that use certificate pinning
......@@ -84,9 +89,7 @@ Setup HTTP proxy:
```
Sample request and response event handlers
```csharp
//intecept & cancel, redirect or update requests
```csharp
public async Task OnRequest(object sender, SessionEventArgs e)
{
Console.WriteLine(e.WebSession.Request.Url);
......@@ -148,20 +151,27 @@ Sample request and response event handlers
}
}
/// Allows overriding default certificate validation logic
public async Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
/// Allows overriding default certificate validation logic
public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
{
//set IsValid to true/false based on Certificate Errors
if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
e.IsValid = true;
else
await e.Session.Ok("Cannot validate server certificate! Not safe to proceed.");
return Task.FromResult(0);
}
/// Allows overriding default client certificate selection logic during mutual authentication
public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs e)
{
//set e.clientCertificate to override
return Task.FromResult(0);
}
```
Future roadmap
============
* Support mutual authentication
* Implement Kerberos/NTLM authentication over HTTP protocols for windows domain
* Support Server Name Indication (SNI) for transparent endpoints
* Support HTTP 2.0
using System;
using System.Security.Cryptography.X509Certificates;
namespace Titanium.Web.Proxy.EventArguments
{
public class CertificateSelectionEventArgs : EventArgs, IDisposable
{
public object sender { get; internal set; }
public string targetHost { get; internal set; }
public X509CertificateCollection localCertificates { get; internal set; }
public X509Certificate remoteCertificate { get; internal set; }
public string[] acceptableIssuers { get; internal set; }
public X509Certificate clientCertificate { get; set; }
public void Dispose()
{
throw new NotImplementedException();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Http;
namespace Titanium.Web.Proxy.EventArguments
{
public class CertificateValidationEventArgs : EventArgs, IDisposable
{
public string HostName => Session.WebSession.Request.Host;
public SessionEventArgs Session { get; internal set; }
public X509Certificate Certificate { get; internal set; }
public X509Chain Chain { get; internal set; }
public SslPolicyErrors SslPolicyErrors { get; internal set; }
......
......@@ -97,6 +97,7 @@ namespace Titanium.Web.Proxy.Helpers
cached.LastAccess = DateTime.Now;
return cached.Certificate;
}
X509Certificate2 certificate = null;
store.Open(OpenFlags.ReadWrite);
string certificateSubject = string.Format("CN={0}, O={1}", certificateName, Issuer);
......
using System;
using System.IO;
namespace Titanium.Web.Proxy.Models
{
internal class ConnectRequest
{
internal Stream Stream { get; set; }
internal Uri Uri { get; set; }
}
}
......@@ -17,10 +17,10 @@ namespace Titanium.Web.Proxy.Network
/// <summary>
/// Holds the current session
/// </summary>
internal SessionEventArgs Session { get; set; }
internal object Param { get; set; }
internal CustomSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback)
:base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback)
internal CustomSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback, LocalCertificateSelectionCallback clientCertificateSelectionCallback)
:base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback, clientCertificateSelectionCallback)
{
}
......
......@@ -12,6 +12,7 @@ using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Shared;
using System.Security.Cryptography.X509Certificates;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Network
{
......@@ -38,7 +39,12 @@ namespace Titanium.Web.Proxy.Network
{
static Dictionary<string, List<TcpConnection>> connectionCache = new Dictionary<string, List<TcpConnection>>();
static SemaphoreSlim connectionAccessLock = new SemaphoreSlim(1);
internal static async Task<TcpConnection> GetClient(SessionEventArgs sessionArgs, string hostname, int port, bool isHttps, Version version)
internal static async Task<TcpConnection> GetClient(string hostname, int port, bool isHttps, Version version)
{
return await GetClient(null, hostname, port, isHttps, version);
}
internal static async Task<TcpConnection> GetClient(ConnectRequest connectRequest, string hostname, int port, bool isHttps, Version version)
{
List<TcpConnection> cachedConnections = null;
TcpConnection cached = null;
......@@ -72,14 +78,14 @@ namespace Titanium.Web.Proxy.Network
}
if (cached == null)
cached = await CreateClient(sessionArgs, hostname, port, isHttps, version).ConfigureAwait(false);
cached = await CreateClient(connectRequest, hostname, port, isHttps, version).ConfigureAwait(false);
//just create one more preemptively
if (cachedConnections == null || cachedConnections.Count() < 2)
{
var task = CreateClient(sessionArgs, hostname, port, isHttps, version)
.ContinueWith(async (x) => { if (x.Status == TaskStatus.RanToCompletion) await ReleaseClient(x.Result); });
var task = CreateClient(connectRequest, hostname, port, isHttps, version)
.ContinueWith(async (x) => { if (x.Status == TaskStatus.RanToCompletion) await ReleaseClient(x.Result); });
}
return cached;
......@@ -90,7 +96,7 @@ namespace Titanium.Web.Proxy.Network
return string.Format("{0}:{1}:{2}:{3}:{4}", hostname.ToLower(), port, isHttps, version.Major, version.Minor);
}
private static async Task<TcpConnection> CreateClient(SessionEventArgs sessionArgs, string hostname, int port, bool isHttps, Version version)
private static async Task<TcpConnection> CreateClient(ConnectRequest connectRequest, string hostname, int port, bool isHttps, Version version)
{
TcpClient client;
Stream stream;
......@@ -106,8 +112,8 @@ namespace Titanium.Web.Proxy.Network
using (var writer = new StreamWriter(stream, Encoding.ASCII, Constants.BUFFER_SIZE, true))
{
await writer.WriteLineAsync(string.Format("CONNECT {0}:{1} {2}", sessionArgs.WebSession.Request.RequestUri.Host, sessionArgs.WebSession.Request.RequestUri.Port, sessionArgs.WebSession.Request.HttpVersion)).ConfigureAwait(false);
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", sessionArgs.WebSession.Request.RequestUri.Host, sessionArgs.WebSession.Request.RequestUri.Port)).ConfigureAwait(false);
await writer.WriteLineAsync(string.Format("CONNECT {0}:{1} {2}", hostname, port, version)).ConfigureAwait(false);
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", hostname, port)).ConfigureAwait(false);
await writer.WriteLineAsync("Connection: Keep-Alive").ConfigureAwait(false);
await writer.WriteLineAsync().ConfigureAwait(false);
await writer.FlushAsync().ConfigureAwait(false);
......@@ -132,8 +138,9 @@ namespace Titanium.Web.Proxy.Network
try
{
sslStream = new CustomSslStream(stream, true, new RemoteCertificateValidationCallback(ProxyServer.ValidateServerCertificate));
sslStream.Session = sessionArgs;
sslStream = new CustomSslStream(stream, true, new RemoteCertificateValidationCallback(ProxyServer.ValidateServerCertificate),
new LocalCertificateSelectionCallback(ProxyServer.SelectClientCertificate));
sslStream.Param = connectRequest;
await sslStream.AuthenticateAsClientAsync(hostname, null, Constants.SupportedProtocols, false).ConfigureAwait(false);
stream = (Stream)sslStream;
}
......
......@@ -45,7 +45,16 @@ namespace Titanium.Web.Proxy
/// </summary>
public static int CertificateCacheTimeOutMinutes { get; set; }
/// <summary>
/// Intercept request to server
/// </summary>
public static event Func<object, SessionEventArgs, Task> BeforeRequest;
/// <summary>
/// Intercept response from server
/// </summary>
public static event Func<object, SessionEventArgs, Task> BeforeResponse;
/// <summary>
......@@ -62,7 +71,11 @@ namespace Titanium.Web.Proxy
/// Verifies the remote Secure Sockets Layer (SSL) certificate used for authentication
/// </summary>
public static event Func<object, CertificateValidationEventArgs, Task> ServerCertificateValidationCallback;
/// <summary>
/// Callback tooverride client certificate during SSL mutual authentication
/// </summary>
public static event Func<object, CertificateSelectionEventArgs, Task> ClientCertificateSelectionCallback;
public static List<ProxyEndPoint> ProxyEndPoints { get; set; }
......
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using System.Text.RegularExpressions;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Helpers;
......@@ -20,6 +18,7 @@ using System.Threading.Tasks;
namespace Titanium.Web.Proxy
{
partial class ProxyServer
{
//This is called when client is aware of proxy
......@@ -33,7 +32,6 @@ namespace Titanium.Web.Proxy
try
{
//read the first line HTTP command
var httpCmd = await clientStreamReader.ReadLineAsync().ConfigureAwait(false);
if (string.IsNullOrEmpty(httpCmd))
......@@ -56,7 +54,7 @@ namespace Titanium.Web.Proxy
if (httpCmdSplit.Length == 3)
{
string httpVersion = httpCmdSplit[2].Trim();
if (httpVersion == "http/1.0")
{
version = new Version(1, 0);
......@@ -80,18 +78,28 @@ namespace Titanium.Web.Proxy
try
{
sslStream = new SslStream(clientStream, true);
var connectRequest = new ConnectRequest() { Stream = clientStream, Uri = httpRemoteUri };
await TcpConnectionManager.GetClient(connectRequest, httpRemoteUri.Host, httpRemoteUri.Port, true, version).ConfigureAwait(false);
//Successfully managed to authenticate the client using the fake certificate
await sslStream.AuthenticateAsServerAsync(certificate, false,
Constants.SupportedProtocols, false).ConfigureAwait(false);
if (clientStream is SslStream)
{
sslStream = clientStream as SslStream;
}
else
{
sslStream = new SslStream(clientStream, true);
//Successfully managed to authenticate the client using the fake certificate
await sslStream.AuthenticateAsServerAsync(certificate, false,
Constants.SupportedProtocols, false).ConfigureAwait(false);
//HTTPS server created - we can now decrypt the client's traffic
clientStream = sslStream;
}
clientStreamReader = new CustomBinaryReader(sslStream);
clientStreamWriter = new StreamWriter(sslStream);
//HTTPS server created - we can now decrypt the client's traffic
clientStream = sslStream;
}
}
catch
{
if (sslStream != null)
......@@ -226,7 +234,13 @@ namespace Titanium.Web.Proxy
}
var httpRemoteUri = new Uri(!isHttps ? httpCmdSplit[1] : (string.Concat("https://", args.WebSession.Request.Host, httpCmdSplit[1])));
#if DEBUG
if (httpRemoteUri.Host.Contains("localhost"))
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null);
break;
}
#endif
args.WebSession.Request.RequestUri = httpRemoteUri;
args.WebSession.Request.Method = httpMethod;
......@@ -235,7 +249,7 @@ namespace Titanium.Web.Proxy
args.Client.ClientStreamReader = clientStreamReader;
args.Client.ClientStreamWriter = clientStreamWriter;
PrepareRequestHeaders(args.WebSession.Request.RequestHeaders, args.WebSession);
args.WebSession.Request.Host = args.WebSession.Request.RequestUri.Authority;
......@@ -250,7 +264,7 @@ namespace Titanium.Web.Proxy
handlerTasks[i] = ((Func<object, SessionEventArgs, Task>)invocationList[i])(null, args);
}
await Task.WhenAll(handlerTasks).ConfigureAwait(false);
await Task.WhenAll(handlerTasks).ConfigureAwait(false);
}
if (args.WebSession.Request.UpgradeToWebSocket)
......@@ -263,8 +277,8 @@ namespace Titanium.Web.Proxy
//construct the web request that we are going to issue on behalf of the client.
connection = connection == null ?
await TcpConnectionManager.GetClient(args, args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version).ConfigureAwait(false)
: lastRequest != args.WebSession.Request.RequestUri.Host ? await TcpConnectionManager.GetClient(args, args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version).ConfigureAwait(false)
await TcpConnectionManager.GetClient(args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version).ConfigureAwait(false)
: lastRequest != args.WebSession.Request.RequestUri.Host ? await TcpConnectionManager.GetClient(args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version).ConfigureAwait(false)
: connection;
lastRequest = TcpConnectionManager.GetConnectionKey(args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version);
......@@ -294,7 +308,7 @@ namespace Titanium.Web.Proxy
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.Client.ClientStreamWriter);
await args.Client.ClientStreamWriter.WriteLineAsync();
await args.Client.ClientStreamWriter.WriteLineAsync();
}
if (!args.WebSession.Request.ExpectContinue)
......@@ -306,7 +320,7 @@ namespace Titanium.Web.Proxy
//If request was modified by user
if (args.WebSession.Request.RequestBodyRead)
{
if(args.WebSession.Request.ContentEncoding!=null)
if (args.WebSession.Request.ContentEncoding != null)
{
args.WebSession.Request.RequestBody = await GetCompressedResponseBody(args.WebSession.Request.ContentEncoding, args.WebSession.Request.RequestBody);
}
......@@ -354,12 +368,12 @@ namespace Titanium.Web.Proxy
}
if (connection != null)
await TcpConnectionManager.ReleaseClient(connection);
await TcpConnectionManager.ReleaseClient(connection);
}
private static async Task WriteConnectResponse(StreamWriter clientStreamWriter, Version httpVersion)
{
await clientStreamWriter.WriteLineAsync(string.Format("HTTP/{0}.{1} {2}", httpVersion.Major, httpVersion.Minor,"200 Connection established")).ConfigureAwait(false);
await clientStreamWriter.WriteLineAsync(string.Format("HTTP/{0}.{1} {2}", httpVersion.Major, httpVersion.Minor, "200 Connection established")).ConfigureAwait(false);
await clientStreamWriter.WriteLineAsync(string.Format("Timestamp: {0}", DateTime.Now)).ConfigureAwait(false);
await clientStreamWriter.WriteLineAsync().ConfigureAwait(false);
await clientStreamWriter.FlushAsync().ConfigureAwait(false);
......@@ -446,13 +460,12 @@ namespace Titanium.Web.Proxy
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
var param = sender as CustomSslStream;
var customSslStream = sender as CustomSslStream;
if (ServerCertificateValidationCallback != null)
{
var args = new CertificateValidationEventArgs();
args.Session = param.Session;
args.Certificate = certificate;
args.Chain = chain;
args.SslPolicyErrors = sslPolicyErrors;
......@@ -468,11 +481,6 @@ namespace Titanium.Web.Proxy
Task.WhenAll(handlerTasks).Wait();
if (!args.IsValid)
{
param.Session.WebSession.Request.CancelRequest = true;
}
return args.IsValid;
}
......@@ -484,6 +492,87 @@ namespace Titanium.Web.Proxy
return false;
}
/// <summary>
/// Call back to select client certificate used for mutual authentication
/// </summary>
/// <param name="sender"></param>
/// <param name="certificate"></param>
/// <param name="chain"></param>
/// <param name="sslPolicyErrors"></param>
/// <returns></returns>
internal static X509Certificate SelectClientCertificate(
object sender,
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers)
{
X509Certificate clientCertificate = null;
var customSslStream = sender as CustomSslStream;
if (customSslStream.Param is ConnectRequest && remoteCertificate != null)
{
var connectRequest = customSslStream.Param as ConnectRequest;
var sslStream = new SslStream(connectRequest.Stream, true);
var certificate = CertManager.CreateCertificate(connectRequest.Uri.Host, false).Result;
//Successfully managed to authenticate the client using the fake certificate
sslStream.AuthenticateAsServerAsync(certificate, true,
Constants.SupportedProtocols, false).Wait();
connectRequest.Stream = sslStream;
clientCertificate = sslStream.RemoteCertificate;
}
else if (customSslStream.Param is ConnectRequest)
{
if (acceptableIssuers != null &&
acceptableIssuers.Length > 0 &&
localCertificates != null &&
localCertificates.Count > 0)
{
// Use the first certificate that is from an acceptable issuer.
foreach (X509Certificate certificate in localCertificates)
{
string issuer = certificate.Issuer;
if (Array.IndexOf(acceptableIssuers, issuer) != -1)
clientCertificate = certificate;
}
}
if (localCertificates != null &&
localCertificates.Count > 0)
clientCertificate = localCertificates[0];
}
if (ClientCertificateSelectionCallback != null)
{
var args = new CertificateSelectionEventArgs();
args.targetHost = targetHost;
args.localCertificates = localCertificates;
args.remoteCertificate = remoteCertificate;
args.acceptableIssuers = acceptableIssuers;
args.clientCertificate = clientCertificate;
Delegate[] invocationList = ClientCertificateSelectionCallback.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, CertificateSelectionEventArgs, Task>)invocationList[i])(null, args);
}
Task.WhenAll(handlerTasks).Wait();
return args.clientCertificate;
}
return clientCertificate;
}
}
}
\ No newline at end of file
......@@ -60,7 +60,9 @@
<Compile Include="Decompression\GZipDecompression.cs" />
<Compile Include="Decompression\IDecompression.cs" />
<Compile Include="Decompression\ZlibDecompression.cs" />
<Compile Include="EventArguments\CertificateSelectionEventArgs.cs" />
<Compile Include="EventArguments\CertificateValidationEventArgs.cs" />
<Compile Include="Models\ConnectRequest.cs" />
<Compile Include="Network\ProxyClient.cs" />
<Compile Include="Exceptions\BodyNotFoundException.cs" />
<Compile Include="Extensions\HttpWebResponseExtensions.cs" />
......
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