Skip to content
This repository was archived by the owner on Jul 9, 2023. It is now read-only.

Commit 65b923f

Browse files
committed
Add mutual authentication capability
1 parent c525dda commit 65b923f

11 files changed

+221
-63
lines changed

Examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs

+16-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public void StartProxy()
1515
ProxyServer.BeforeRequest += OnRequest;
1616
ProxyServer.BeforeResponse += OnResponse;
1717
ProxyServer.ServerCertificateValidationCallback += OnCertificateValidation;
18+
ProxyServer.ClientCertificateSelectionCallback += OnCertificateSelection;
1819

1920
//Exclude Https addresses you don't want to proxy
2021
//Usefull for clients that use certificate pinning
@@ -129,13 +130,25 @@ public async Task OnResponse(object sender, SessionEventArgs e)
129130
/// </summary>
130131
/// <param name="sender"></param>
131132
/// <param name="e"></param>
132-
public async Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
133+
public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
133134
{
134135
//set IsValid to true/false based on Certificate Errors
135136
if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
136137
e.IsValid = true;
137-
else
138-
await e.Session.Ok("Cannot validate server certificate! Not safe to proceed.");
138+
139+
return Task.FromResult(0);
140+
}
141+
142+
/// <summary>
143+
/// Allows overriding default client certificate selection logic during mutual authentication
144+
/// </summary>
145+
/// <param name="sender"></param>
146+
/// <param name="e"></param>
147+
public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs e)
148+
{
149+
//set e.clientCertificate to override
150+
151+
return Task.FromResult(0);
139152
}
140153
}
141154
}

README.md

+21-11
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ Features
1212
========
1313

1414
* Supports Http(s) and most features of HTTP 1.1
15-
* Supports relaying of WebSockets
16-
* Supports script injection
15+
* Support redirect/block/update requests
16+
* Supports updating response
17+
* Safely relays WebSocket requests over Http
18+
* Support mutual SSL authentication
19+
* Fully asynchronous proxy
1720

1821
Usage
1922
=====
@@ -35,6 +38,8 @@ Setup HTTP proxy:
3538
ProxyServer.BeforeRequest += OnRequest;
3639
ProxyServer.BeforeResponse += OnResponse;
3740
ProxyServer.ServerCertificateValidationCallback += OnCertificateValidation;
41+
ProxyServer.ClientCertificateSelectionCallback += OnCertificateSelection;
42+
3843

3944
//Exclude Https addresses you don't want to proxy
4045
//Usefull for clients that use certificate pinning
@@ -84,9 +89,7 @@ Setup HTTP proxy:
8489
```
8590
Sample request and response event handlers
8691

87-
```csharp
88-
89-
//intecept & cancel, redirect or update requests
92+
```csharp
9093
public async Task OnRequest(object sender, SessionEventArgs e)
9194
{
9295
Console.WriteLine(e.WebSession.Request.Url);
@@ -148,20 +151,27 @@ Sample request and response event handlers
148151
}
149152
}
150153

151-
152-
/// Allows overriding default certificate validation logic
153-
public async Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
154+
/// Allows overriding default certificate validation logic
155+
public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
154156
{
155157
//set IsValid to true/false based on Certificate Errors
156158
if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
157159
e.IsValid = true;
158-
else
159-
await e.Session.Ok("Cannot validate server certificate! Not safe to proceed.");
160+
161+
return Task.FromResult(0);
162+
}
163+
164+
/// Allows overriding default client certificate selection logic during mutual authentication
165+
public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs e)
166+
{
167+
//set e.clientCertificate to override
168+
169+
return Task.FromResult(0);
160170
}
161171
```
162172
Future roadmap
163173
============
164-
* Support mutual authentication
174+
* Implement Kerberos/NTLM authentication over HTTP protocols for windows domain
165175
* Support Server Name Indication (SNI) for transparent endpoints
166176
* Support HTTP 2.0
167177

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Security.Cryptography.X509Certificates;
3+
4+
namespace Titanium.Web.Proxy.EventArguments
5+
{
6+
public class CertificateSelectionEventArgs : EventArgs, IDisposable
7+
{
8+
public object sender { get; internal set; }
9+
public string targetHost { get; internal set; }
10+
public X509CertificateCollection localCertificates { get; internal set; }
11+
public X509Certificate remoteCertificate { get; internal set; }
12+
public string[] acceptableIssuers { get; internal set; }
13+
14+
public X509Certificate clientCertificate { get; set; }
15+
16+
public void Dispose()
17+
{
18+
throw new NotImplementedException();
19+
}
20+
}
21+
}

Titanium.Web.Proxy/EventArguments/CertificateValidationEventArgs.cs

-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
42
using System.Net.Security;
53
using System.Security.Cryptography.X509Certificates;
6-
using System.Text;
7-
using System.Threading.Tasks;
8-
using Titanium.Web.Proxy.Http;
94

105
namespace Titanium.Web.Proxy.EventArguments
116
{
127
public class CertificateValidationEventArgs : EventArgs, IDisposable
138
{
14-
public string HostName => Session.WebSession.Request.Host;
15-
16-
public SessionEventArgs Session { get; internal set; }
17-
189
public X509Certificate Certificate { get; internal set; }
1910
public X509Chain Chain { get; internal set; }
2011
public SslPolicyErrors SslPolicyErrors { get; internal set; }

Titanium.Web.Proxy/Helpers/CertificateManager.cs

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ protected async virtual Task<X509Certificate2> CreateCertificate(X509Store store
9797
cached.LastAccess = DateTime.Now;
9898
return cached.Certificate;
9999
}
100+
100101
X509Certificate2 certificate = null;
101102
store.Open(OpenFlags.ReadWrite);
102103
string certificateSubject = string.Format("CN={0}, O={1}", certificateName, Issuer);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
using System.IO;
3+
4+
namespace Titanium.Web.Proxy.Models
5+
{
6+
internal class ConnectRequest
7+
{
8+
internal Stream Stream { get; set; }
9+
internal Uri Uri { get; set; }
10+
}
11+
}

Titanium.Web.Proxy/Network/CustomSslStream.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ internal class CustomSslStream : SslStream
1717
/// <summary>
1818
/// Holds the current session
1919
/// </summary>
20-
internal SessionEventArgs Session { get; set; }
20+
internal object Param { get; set; }
2121

22-
internal CustomSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback)
23-
:base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback)
22+
internal CustomSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback, LocalCertificateSelectionCallback clientCertificateSelectionCallback)
23+
:base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback, clientCertificateSelectionCallback)
2424
{
2525

2626
}

Titanium.Web.Proxy/Network/TcpConnectionManager.cs

+16-9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Titanium.Web.Proxy.Shared;
1313
using System.Security.Cryptography.X509Certificates;
1414
using Titanium.Web.Proxy.EventArguments;
15+
using Titanium.Web.Proxy.Models;
1516

1617
namespace Titanium.Web.Proxy.Network
1718
{
@@ -38,7 +39,12 @@ internal class TcpConnectionManager
3839
{
3940
static Dictionary<string, List<TcpConnection>> connectionCache = new Dictionary<string, List<TcpConnection>>();
4041
static SemaphoreSlim connectionAccessLock = new SemaphoreSlim(1);
41-
internal static async Task<TcpConnection> GetClient(SessionEventArgs sessionArgs, string hostname, int port, bool isHttps, Version version)
42+
43+
internal static async Task<TcpConnection> GetClient(string hostname, int port, bool isHttps, Version version)
44+
{
45+
return await GetClient(null, hostname, port, isHttps, version);
46+
}
47+
internal static async Task<TcpConnection> GetClient(ConnectRequest connectRequest, string hostname, int port, bool isHttps, Version version)
4248
{
4349
List<TcpConnection> cachedConnections = null;
4450
TcpConnection cached = null;
@@ -72,14 +78,14 @@ internal static async Task<TcpConnection> GetClient(SessionEventArgs sessionArgs
7278
}
7379

7480
if (cached == null)
75-
cached = await CreateClient(sessionArgs, hostname, port, isHttps, version).ConfigureAwait(false);
81+
cached = await CreateClient(connectRequest, hostname, port, isHttps, version).ConfigureAwait(false);
7682

7783

7884
//just create one more preemptively
7985
if (cachedConnections == null || cachedConnections.Count() < 2)
8086
{
81-
var task = CreateClient(sessionArgs, hostname, port, isHttps, version)
82-
.ContinueWith(async (x) => { if (x.Status == TaskStatus.RanToCompletion) await ReleaseClient(x.Result); });
87+
var task = CreateClient(connectRequest, hostname, port, isHttps, version)
88+
.ContinueWith(async (x) => { if (x.Status == TaskStatus.RanToCompletion) await ReleaseClient(x.Result); });
8389
}
8490

8591
return cached;
@@ -90,7 +96,7 @@ internal static string GetConnectionKey(string hostname, int port, bool isHttps,
9096
return string.Format("{0}:{1}:{2}:{3}:{4}", hostname.ToLower(), port, isHttps, version.Major, version.Minor);
9197
}
9298

93-
private static async Task<TcpConnection> CreateClient(SessionEventArgs sessionArgs, string hostname, int port, bool isHttps, Version version)
99+
private static async Task<TcpConnection> CreateClient(ConnectRequest connectRequest, string hostname, int port, bool isHttps, Version version)
94100
{
95101
TcpClient client;
96102
Stream stream;
@@ -106,8 +112,8 @@ private static async Task<TcpConnection> CreateClient(SessionEventArgs sessionAr
106112

107113
using (var writer = new StreamWriter(stream, Encoding.ASCII, Constants.BUFFER_SIZE, true))
108114
{
109-
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);
110-
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", sessionArgs.WebSession.Request.RequestUri.Host, sessionArgs.WebSession.Request.RequestUri.Port)).ConfigureAwait(false);
115+
await writer.WriteLineAsync(string.Format("CONNECT {0}:{1} {2}", hostname, port, version)).ConfigureAwait(false);
116+
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", hostname, port)).ConfigureAwait(false);
111117
await writer.WriteLineAsync("Connection: Keep-Alive").ConfigureAwait(false);
112118
await writer.WriteLineAsync().ConfigureAwait(false);
113119
await writer.FlushAsync().ConfigureAwait(false);
@@ -132,8 +138,9 @@ private static async Task<TcpConnection> CreateClient(SessionEventArgs sessionAr
132138

133139
try
134140
{
135-
sslStream = new CustomSslStream(stream, true, new RemoteCertificateValidationCallback(ProxyServer.ValidateServerCertificate));
136-
sslStream.Session = sessionArgs;
141+
sslStream = new CustomSslStream(stream, true, new RemoteCertificateValidationCallback(ProxyServer.ValidateServerCertificate),
142+
new LocalCertificateSelectionCallback(ProxyServer.SelectClientCertificate));
143+
sslStream.Param = connectRequest;
137144
await sslStream.AuthenticateAsClientAsync(hostname, null, Constants.SupportedProtocols, false).ConfigureAwait(false);
138145
stream = (Stream)sslStream;
139146
}

Titanium.Web.Proxy/ProxyServer.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,16 @@ static ProxyServer()
4545
/// </summary>
4646
public static int CertificateCacheTimeOutMinutes { get; set; }
4747

48+
49+
/// <summary>
50+
/// Intercept request to server
51+
/// </summary>
4852
public static event Func<object, SessionEventArgs, Task> BeforeRequest;
53+
54+
55+
/// <summary>
56+
/// Intercept response from server
57+
/// </summary>
4958
public static event Func<object, SessionEventArgs, Task> BeforeResponse;
5059

5160
/// <summary>
@@ -62,7 +71,11 @@ static ProxyServer()
6271
/// Verifies the remote Secure Sockets Layer (SSL) certificate used for authentication
6372
/// </summary>
6473
public static event Func<object, CertificateValidationEventArgs, Task> ServerCertificateValidationCallback;
65-
74+
75+
/// <summary>
76+
/// Callback tooverride client certificate during SSL mutual authentication
77+
/// </summary>
78+
public static event Func<object, CertificateSelectionEventArgs, Task> ClientCertificateSelectionCallback;
6679

6780
public static List<ProxyEndPoint> ProxyEndPoints { get; set; }
6881

0 commit comments

Comments
 (0)