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

Commit 9cb2650

Browse files
committed
Merge branch 'develop' into beta
2 parents 1fa17aa + 2c11272 commit 9cb2650

File tree

9 files changed

+324
-57
lines changed

9 files changed

+324
-57
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Setup HTTP proxy:
7272
var proxyServer = new ProxyServer();
7373

7474
// locally trust root certificate used by this proxy
75-
proxyServer.CertificateManager.TrustRootCertificate = true;
75+
proxyServer.CertificateManager.TrustRootCertificate(true);
7676

7777
// optionally set the Certificate Engine
7878
// Under Mono only BouncyCastle will be supported

src/Titanium.Web.Proxy/CertificateHandler.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,6 @@ internal bool ValidateServerCertificate(object sender, SessionEventArgsBase sess
5656
{
5757
X509Certificate? clientCertificate = null;
5858

59-
//TODO: Can we use client certificate from client socket's Sslstream.RemoteCertificate?
60-
//Because only the client can provide the correct certificate.
61-
//Proxy has no idea about client certificate when its running on a remote machine.
62-
//That would mean we need to delay AuthenticateAsServer call with client until we reach this method
63-
//and decide right here if we should set SslServerAuthenticationOptions.ClientCertificateRequired = true for clientStream.AuthenticateAsServer call.
64-
//Sounds like a very complicated change, but technically possible.
65-
6659
//fallback to the first client certificate from proxy machine certificate store
6760
if (acceptableIssuers != null && acceptableIssuers.Length > 0 && localCertificates != null &&
6861
localCertificates.Count > 0)
@@ -92,7 +85,7 @@ internal bool ValidateServerCertificate(object sender, SessionEventArgsBase sess
9285
ClientCertificate = clientCertificate
9386
};
9487

95-
// why is the sender null?
88+
9689
ClientCertificateSelectionCallback.InvokeAsync(this, args, ExceptionFunc).Wait();
9790
return args.ClientCertificate;
9891
}

src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -242,19 +242,22 @@ internal Task<TcpServerConnection> GetServerConnection(ProxyServer proxyServer,
242242
{
243243
if (cache.TryGetValue(cacheKey, out var existingConnections))
244244
{
245-
// +3 seconds for potential delay after getting connection
246-
var cutOff = DateTime.UtcNow.AddSeconds(-proxyServer.ConnectionTimeOutSeconds + 3);
247-
while (existingConnections.Count > 0)
245+
lock (existingConnections)
248246
{
249-
if (existingConnections.TryDequeue(out var recentConnection))
247+
// +3 seconds for potential delay after getting connection
248+
var cutOff = DateTime.UtcNow.AddSeconds(-proxyServer.ConnectionTimeOutSeconds + 3);
249+
while (existingConnections.Count > 0)
250250
{
251-
if (recentConnection.LastAccess > cutOff
252-
&& recentConnection.TcpSocket.IsGoodConnection())
251+
if (existingConnections.TryDequeue(out var recentConnection))
253252
{
254-
return recentConnection;
255-
}
253+
if (recentConnection.LastAccess > cutOff
254+
&& recentConnection.TcpSocket.IsGoodConnection())
255+
{
256+
return recentConnection;
257+
}
256258

257-
disposalBag.Add(recentConnection);
259+
disposalBag.Add(recentConnection);
260+
}
258261
}
259262
}
260263
}
@@ -290,7 +293,7 @@ internal Task<TcpServerConnection> GetServerConnection(ProxyServer proxyServer,
290293
bool prefetch, CancellationToken cancellationToken)
291294
{
292295
// deny connection to proxy end points to avoid infinite connection loop.
293-
if (Server.ProxyEndPoints.Any(x => x.Port == remotePort)
296+
if (Server.ProxyEndPoints.Any(x => x.Port == remotePort)
294297
&& NetworkHelper.IsLocalIpAddress(remoteHostName))
295298
{
296299
throw new Exception($"A client is making HTTP request to one of the listening ports of this proxy {remoteHostName}:{remotePort}");
@@ -413,7 +416,7 @@ internal Task<TcpServerConnection> GetServerConnection(ProxyServer proxyServer,
413416
}
414417

415418
Task connectTask;
416-
419+
417420
if (socks)
418421
{
419422
if (externalProxy!.ProxyDnsRequests)
@@ -629,6 +632,16 @@ internal Task<TcpServerConnection> GetServerConnection(ProxyServer proxyServer,
629632
/// <param name="close">Should we just close the connection instead of reusing?</param>
630633
internal async Task Release(TcpServerConnection connection, bool close = false)
631634
{
635+
if (connection == null)
636+
{
637+
return;
638+
}
639+
640+
if (disposalBag.Any(x => x == connection))
641+
{
642+
return;
643+
}
644+
632645
if (close || connection.IsWinAuthenticated || !Server.EnableConnectionPool || connection.IsClosed)
633646
{
634647
disposalBag.Add(connection);
@@ -653,6 +666,11 @@ internal async Task Release(TcpServerConnection connection, bool close = false)
653666
}
654667
}
655668

669+
if (existingConnections.Any(x => x == connection))
670+
{
671+
break;
672+
}
673+
656674
existingConnections.Enqueue(connection);
657675
break;
658676
}

src/Titanium.Web.Proxy/ProxyServer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,9 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam
121121
private SystemProxyManager? systemProxySettingsManager { get; }
122122

123123
/// <summary>
124-
/// Number of exception retries when connection pool is enabled.
124+
/// Number of times to retry upon network failures when connection pool is enabled.
125125
/// </summary>
126-
private int retries => EnableConnectionPool ? MaxCachedConnections : 0;
126+
public int NetworkFailureRetryAttempts { get; set; } = 0;
127127

128128
/// <summary>
129129
/// Is the proxy currently running?
@@ -928,7 +928,7 @@ internal async Task InvokeServerConnectionCreateEvent(Socket serverSocket)
928928
/// </summary>
929929
private RetryPolicy<T> retryPolicy<T>() where T : Exception
930930
{
931-
return new RetryPolicy<T>(retries, tcpConnectionFactory);
931+
return new RetryPolicy<T>(NetworkFailureRetryAttempts, tcpConnectionFactory);
932932
}
933933

934934
private bool disposed = false;

tests/Titanium.Web.Proxy.IntegrationTests/HttpsTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
2+
using System.IO;
23
using System.Net;
34
using System.Net.Http;
5+
using System.Security.Cryptography.X509Certificates;
46
using System.Threading.Tasks;
57
using Microsoft.AspNetCore.Http;
68
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -62,5 +64,36 @@ public async Task Can_Handle_Https_Fake_Tunnel_Request()
6264
Assert.AreEqual("I am server. I received your greetings.", body);
6365
}
6466

67+
[TestMethod]
68+
public async Task Can_Handle_Https_Mutual_Tls_Request()
69+
{
70+
var testSuite = new TestSuite(true);
71+
72+
var server = testSuite.GetServer();
73+
server.HandleRequest((context) =>
74+
{
75+
return context.Response.WriteAsync("I am server. I received your greetings.");
76+
});
77+
78+
var proxy = testSuite.GetProxy();
79+
var clientCert = proxy.CertificateManager.CreateCertificate("client.com", false);
80+
81+
proxy.ClientCertificateSelectionCallback += async (sender, e) =>
82+
{
83+
e.ClientCertificate = clientCert;
84+
await Task.CompletedTask;
85+
};
86+
87+
var client = testSuite.GetClient(proxy);
88+
89+
var response = await client.PostAsync(new Uri(server.ListeningHttpsUrl),
90+
new StringContent("hello server. I am a client."));
91+
92+
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
93+
var body = await response.Content.ReadAsStringAsync();
94+
95+
Assert.AreEqual("I am server. I received your greetings.", body);
96+
}
97+
6598
}
6699
}

tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
25
using System.Net;
36
using System.Net.Http;
47
using System.Threading.Tasks;
58
using Microsoft.AspNetCore.Http;
69
using Microsoft.VisualStudio.TestTools.UnitTesting;
10+
using Titanium.Web.Proxy.Models;
711

812
namespace Titanium.Web.Proxy.IntegrationTests
913
{
@@ -73,5 +77,202 @@ public async Task Smoke_Test_Nested_Proxy_UserData()
7377
Assert.AreEqual("I am server. I received your greetings.", body);
7478
}
7579

80+
[TestMethod]
81+
[Timeout(2 * 60 * 1000)]
82+
public async Task Nested_Proxy_Farm_Without_Connection_Cache_Should_Not_Hang()
83+
{
84+
var rnd = new Random();
85+
86+
var testSuite = new TestSuite();
87+
88+
var server = testSuite.GetServer();
89+
server.HandleRequest((context) =>
90+
{
91+
return context.Response.WriteAsync("I am server. I received your greetings.");
92+
});
93+
94+
var proxies2 = new List<ProxyServer>();
95+
96+
//create a level 2 upstream proxy farm that forwards to server
97+
for (int i = 0; i < 10; i++)
98+
{
99+
var proxy2 = testSuite.GetProxy();
100+
proxy2.ProxyBasicAuthenticateFunc += (_, _, _) =>
101+
{
102+
return Task.FromResult(true);
103+
};
104+
105+
proxies2.Add(proxy2);
106+
}
107+
108+
var proxies1 = new List<ProxyServer>();
109+
110+
//create a level 1 upstream proxy farm that forwards to level 2 farm
111+
for (int i = 0; i < 10; i++)
112+
{
113+
var proxy1 = testSuite.GetProxy();
114+
proxy1.EnableConnectionPool = false;
115+
var proxy2 = proxies2[rnd.Next() % proxies2.Count];
116+
117+
var explicitEndpoint = proxy1.ProxyEndPoints.OfType<ExplicitProxyEndPoint>().First();
118+
explicitEndpoint.BeforeTunnelConnectRequest += (_, e) =>
119+
{
120+
e.CustomUpStreamProxy = new ExternalProxy()
121+
{
122+
HostName = "localhost",
123+
Port = proxy2.ProxyEndPoints[0].Port,
124+
ProxyType = ExternalProxyType.Http,
125+
UserName = "test_user",
126+
Password = "test_password"
127+
};
128+
129+
return Task.CompletedTask;
130+
};
131+
132+
proxy1.BeforeRequest += (_, e) =>
133+
{
134+
e.CustomUpStreamProxy = new ExternalProxy()
135+
{
136+
HostName = "localhost",
137+
Port = proxy2.ProxyEndPoints[0].Port,
138+
ProxyType = ExternalProxyType.Http,
139+
UserName = "test_user",
140+
Password = "test_password"
141+
};
142+
143+
return Task.CompletedTask;
144+
};
145+
146+
proxies1.Add(proxy1);
147+
}
148+
149+
var tasks = new List<Task>();
150+
151+
//send multiple concurrent requests from client => proxy farm 1 => proxy farm 2 => server
152+
for (int j = 0; j < 10_000; j++)
153+
{
154+
var task = Task.Run(async () =>
155+
{
156+
try
157+
{
158+
var proxy = proxies1[rnd.Next() % proxies1.Count];
159+
using var client = testSuite.GetClient(proxy);
160+
161+
//tests should not keep hanging for 30 mins.
162+
client.Timeout = TimeSpan.FromMinutes(30);
163+
await client.PostAsync(new Uri(server.ListeningHttpsUrl),
164+
new StringContent("hello server. I am a client."));
165+
166+
}
167+
//if error is thrown because of server overloading its okay.
168+
//But client.PostAsync should'nt hang in all cases.
169+
catch { }
170+
});
171+
172+
tasks.Add(task);
173+
}
174+
175+
await Task.WhenAll(tasks);
176+
}
177+
178+
179+
//Reproduce bug reported so that we can fix it.
180+
//https://github.com/justcoding121/titanium-web-proxy/issues/826
181+
[TestMethod]
182+
[Timeout(2 * 60 * 1000)]
183+
public async Task Nested_Proxy_Farm_With_Connection_Cache_Should_Not_Hang()
184+
{
185+
var rnd = new Random();
186+
187+
var testSuite = new TestSuite();
188+
189+
var server = testSuite.GetServer();
190+
server.HandleRequest((context) =>
191+
{
192+
return context.Response.WriteAsync("I am server. I received your greetings.");
193+
});
194+
195+
var proxies2 = new List<ProxyServer>();
196+
197+
//create a level 2 upstream proxy farm that forwards to server
198+
for (int i = 0; i < 10; i++)
199+
{
200+
var proxy2 = testSuite.GetProxy();
201+
proxy2.ProxyBasicAuthenticateFunc += (_, _, _) =>
202+
{
203+
return Task.FromResult(true);
204+
};
205+
206+
proxies2.Add(proxy2);
207+
}
208+
209+
var proxies1 = new List<ProxyServer>();
210+
211+
//create a level 1 upstream proxy farm that forwards to level 2 farm
212+
for (int i = 0; i < 10; i++)
213+
{
214+
var proxy1 = testSuite.GetProxy();
215+
//proxy1.EnableConnectionPool = false;
216+
var proxy2 = proxies2[rnd.Next() % proxies2.Count];
217+
218+
var explicitEndpoint = proxy1.ProxyEndPoints.OfType<ExplicitProxyEndPoint>().First();
219+
explicitEndpoint.BeforeTunnelConnectRequest += (_, e) =>
220+
{
221+
e.CustomUpStreamProxy = new ExternalProxy()
222+
{
223+
HostName = "localhost",
224+
Port = proxy2.ProxyEndPoints[0].Port,
225+
ProxyType = ExternalProxyType.Http,
226+
UserName = "test_user",
227+
Password = "test_password"
228+
};
229+
230+
return Task.CompletedTask;
231+
};
232+
233+
proxy1.BeforeRequest += (_, e) =>
234+
{
235+
e.CustomUpStreamProxy = new ExternalProxy()
236+
{
237+
HostName = "localhost",
238+
Port = proxy2.ProxyEndPoints[0].Port,
239+
ProxyType = ExternalProxyType.Http,
240+
UserName = "test_user",
241+
Password = "test_password"
242+
};
243+
244+
return Task.CompletedTask;
245+
};
246+
247+
proxies1.Add(proxy1);
248+
}
249+
250+
var tasks = new List<Task>();
251+
252+
//send multiple concurrent requests from client => proxy farm 1 => proxy farm 2 => server
253+
for (int j = 0; j < 10_000; j++)
254+
{
255+
var task = Task.Run(async () =>
256+
{
257+
try
258+
{
259+
var proxy = proxies1[rnd.Next() % proxies1.Count];
260+
using var client = testSuite.GetClient(proxy);
261+
262+
//tests should not keep hanging for 30 mins.
263+
client.Timeout = TimeSpan.FromMinutes(30);
264+
await client.PostAsync(new Uri(server.ListeningHttpsUrl),
265+
new StringContent("hello server. I am a client."));
266+
}
267+
//if error is thrown because of server overloading its okay.
268+
//But client.PostAsync should'nt hang in all cases.
269+
catch { }
270+
});
271+
272+
tasks.Add(task);
273+
}
274+
275+
await Task.WhenAll(tasks);
276+
}
76277
}
77278
}

0 commit comments

Comments
 (0)