From c007cb686f7af2159c32dbb60ebf0994e7b701d5 Mon Sep 17 00:00:00 2001 From: Allen Byron Penner <7310089+ByronAP@users.noreply.github.com> Date: Mon, 14 Oct 2019 11:23:17 -0400 Subject: [PATCH 1/9] Update RunTime.cs add IsSocketReuseAvailable --- src/Titanium.Web.Proxy/Helpers/RunTime.cs | 60 ++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Titanium.Web.Proxy/Helpers/RunTime.cs b/src/Titanium.Web.Proxy/Helpers/RunTime.cs index b2d1e9847..e646fc5ba 100644 --- a/src/Titanium.Web.Proxy/Helpers/RunTime.cs +++ b/src/Titanium.Web.Proxy/Helpers/RunTime.cs @@ -1,6 +1,8 @@ -using System; +using System; +using System.Reflection; using System.Text; using System.Runtime.InteropServices; +using System.Runtime.Versioning; namespace Titanium.Web.Proxy.Helpers { @@ -41,7 +43,63 @@ public static class RunTime public static bool IsUwpOnWindows => IsWindows && UwpHelper.IsRunningAsUwp(); public static bool IsMac => isRunningOnMac; + + /// + /// Is socket reuse available to use? + /// + public static bool IsSocketReuseAvailable => isSocketReuseAvailable(); + + private static bool? _isSocketReuseAvailable; + + private static bool isSocketReuseAvailable() + { + // use the cached value if we have one + if (_isSocketReuseAvailable != null) + return _isSocketReuseAvailable.Value; + + try + { + if (IsWindows) + { + // since we are on windows just return true + // store the result in our static object so we don't have to be bothered going through all this more than once + _isSocketReuseAvailable = true; + return true; + } + + // get the currently running framework name and version (EX: .NETFramework,Version=v4.5.1) (Ex: .NETCoreApp,Version=v2.0) + string ver = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName; + if (ver == null) + return false; // play it safe if we can not figure out what the framework is + + // make sure we are on .NETCoreApp + if (ver.Contains(".NETCoreApp", StringComparison.InvariantCultureIgnoreCase)) + { + var versionString = ver.Replace(".NETCoreApp,Version=v", "", + StringComparison.InvariantCultureIgnoreCase); + var versionArr = versionString.Split('.'); + var majorVersion = Convert.ToInt32(versionArr[0]); + + var result = majorVersion >= 3; // version 3 and up supports socket reuse + + // store the result in our static object so we don't have to be bothered going through all this more than once + _isSocketReuseAvailable = result; + return result; + } + + // store the result in our static object so we don't have to be bothered going through all this more than once + _isSocketReuseAvailable = false; + return false; + } + catch + { + // store the result in our static object so we don't have to be bothered going through all this more than once + _isSocketReuseAvailable = false; + return false; + } + } + // https://github.com/qmatteoq/DesktopBridgeHelpers/blob/master/DesktopBridge.Helpers/Helpers.cs private class UwpHelper { From 20c22ddc8e39e18ea946cb59bb04fa9a4c72df92 Mon Sep 17 00:00:00 2001 From: Allen Byron Penner <7310089+ByronAP@users.noreply.github.com> Date: Mon, 14 Oct 2019 11:25:45 -0400 Subject: [PATCH 2/9] Update TcpConnectionFactory.cs enable socket based on framework version via RunTime.IsSocketReuseAvailable --- src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs index b65b554f4..f0091c725 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs @@ -315,8 +315,7 @@ private async Task createServerConnection(string remoteHost tcpClient.SendTimeout = proxyServer.ConnectionTimeOutSeconds * 1000; tcpClient.LingerState = new LingerOption(true, proxyServer.TcpTimeWaitSeconds); - // linux has a bug with socket reuse in .net core. - if (proxyServer.ReuseSocket && RunTime.IsWindows) + if (proxyServer.ReuseSocket && RunTime.IsSocketReuseAvailable) { tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } From 7f3ef04176a6b032b32ade893e6986780d8398d4 Mon Sep 17 00:00:00 2001 From: Allen Byron Penner <7310089+ByronAP@users.noreply.github.com> Date: Mon, 14 Oct 2019 11:26:48 -0400 Subject: [PATCH 3/9] Update ProxyServer.cs enable socket reuse based on framework via RunTime.IsSocketReuseAvailable --- src/Titanium.Web.Proxy/ProxyServer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index d7d89eb7e..9b681d430 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -654,8 +654,7 @@ private void listen(ProxyEndPoint endPoint) { endPoint.Listener = new TcpListener(endPoint.IpAddress, endPoint.Port); - // linux/macOS has a bug with socket reuse in .net core. - if (ReuseSocket && RunTime.IsWindows) + if (ReuseSocket && RunTime.IsSocketReuseAvailable) { endPoint.Listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } From 6e747d2e9bcfccceb1ce4c4fb4dbafa9bdba1a8c Mon Sep 17 00:00:00 2001 From: Allen Byron Penner <7310089+ByronAP@users.noreply.github.com> Date: Mon, 14 Oct 2019 11:37:28 -0400 Subject: [PATCH 4/9] Update RunTime.cs fix compatibility bug --- src/Titanium.Web.Proxy/Helpers/RunTime.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Titanium.Web.Proxy/Helpers/RunTime.cs b/src/Titanium.Web.Proxy/Helpers/RunTime.cs index e646fc5ba..7b59b757d 100644 --- a/src/Titanium.Web.Proxy/Helpers/RunTime.cs +++ b/src/Titanium.Web.Proxy/Helpers/RunTime.cs @@ -74,10 +74,10 @@ private static bool isSocketReuseAvailable() return false; // play it safe if we can not figure out what the framework is // make sure we are on .NETCoreApp - if (ver.Contains(".NETCoreApp", StringComparison.InvariantCultureIgnoreCase)) + ver = ver.ToLower(); // make everything lowercase to simplify comparison + if (ver.Contains(".netcoreapp", StringComparison.InvariantCultureIgnoreCase)) { - var versionString = ver.Replace(".NETCoreApp,Version=v", "", - StringComparison.InvariantCultureIgnoreCase); + var versionString = ver.Replace(".netcoreapp,version=v", ""); var versionArr = versionString.Split('.'); var majorVersion = Convert.ToInt32(versionArr[0]); From dfb69fdc3776373c37bb9922087bf85434772810 Mon Sep 17 00:00:00 2001 From: Allen Byron Penner <7310089+ByronAP@users.noreply.github.com> Date: Mon, 14 Oct 2019 11:40:48 -0400 Subject: [PATCH 5/9] Update RunTime.cs missed one --- src/Titanium.Web.Proxy/Helpers/RunTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Titanium.Web.Proxy/Helpers/RunTime.cs b/src/Titanium.Web.Proxy/Helpers/RunTime.cs index 7b59b757d..b876b85e3 100644 --- a/src/Titanium.Web.Proxy/Helpers/RunTime.cs +++ b/src/Titanium.Web.Proxy/Helpers/RunTime.cs @@ -75,7 +75,7 @@ private static bool isSocketReuseAvailable() // make sure we are on .NETCoreApp ver = ver.ToLower(); // make everything lowercase to simplify comparison - if (ver.Contains(".netcoreapp", StringComparison.InvariantCultureIgnoreCase)) + if (ver.Contains(".netcoreapp")) { var versionString = ver.Replace(".netcoreapp,version=v", ""); var versionArr = versionString.Split('.'); From c06b3e01880f864e0c230be8dd387675da4da450 Mon Sep 17 00:00:00 2001 From: honfika Date: Tue, 15 Oct 2019 22:56:08 +0200 Subject: [PATCH 6/9] Revert "Enable socket reuse based on framework" --- src/Titanium.Web.Proxy/Helpers/RunTime.cs | 60 +------------------ .../Network/Tcp/TcpConnectionFactory.cs | 3 +- src/Titanium.Web.Proxy/ProxyServer.cs | 3 +- 3 files changed, 5 insertions(+), 61 deletions(-) diff --git a/src/Titanium.Web.Proxy/Helpers/RunTime.cs b/src/Titanium.Web.Proxy/Helpers/RunTime.cs index b876b85e3..b2d1e9847 100644 --- a/src/Titanium.Web.Proxy/Helpers/RunTime.cs +++ b/src/Titanium.Web.Proxy/Helpers/RunTime.cs @@ -1,8 +1,6 @@ -using System; -using System.Reflection; +using System; using System.Text; using System.Runtime.InteropServices; -using System.Runtime.Versioning; namespace Titanium.Web.Proxy.Helpers { @@ -43,63 +41,7 @@ public static class RunTime public static bool IsUwpOnWindows => IsWindows && UwpHelper.IsRunningAsUwp(); public static bool IsMac => isRunningOnMac; - - /// - /// Is socket reuse available to use? - /// - public static bool IsSocketReuseAvailable => isSocketReuseAvailable(); - - private static bool? _isSocketReuseAvailable; - - private static bool isSocketReuseAvailable() - { - // use the cached value if we have one - if (_isSocketReuseAvailable != null) - return _isSocketReuseAvailable.Value; - - try - { - if (IsWindows) - { - // since we are on windows just return true - // store the result in our static object so we don't have to be bothered going through all this more than once - _isSocketReuseAvailable = true; - return true; - } - - // get the currently running framework name and version (EX: .NETFramework,Version=v4.5.1) (Ex: .NETCoreApp,Version=v2.0) - string ver = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName; - if (ver == null) - return false; // play it safe if we can not figure out what the framework is - - // make sure we are on .NETCoreApp - ver = ver.ToLower(); // make everything lowercase to simplify comparison - if (ver.Contains(".netcoreapp")) - { - var versionString = ver.Replace(".netcoreapp,version=v", ""); - var versionArr = versionString.Split('.'); - var majorVersion = Convert.ToInt32(versionArr[0]); - - var result = majorVersion >= 3; // version 3 and up supports socket reuse - - // store the result in our static object so we don't have to be bothered going through all this more than once - _isSocketReuseAvailable = result; - return result; - } - - // store the result in our static object so we don't have to be bothered going through all this more than once - _isSocketReuseAvailable = false; - return false; - } - catch - { - // store the result in our static object so we don't have to be bothered going through all this more than once - _isSocketReuseAvailable = false; - return false; - } - } - // https://github.com/qmatteoq/DesktopBridgeHelpers/blob/master/DesktopBridge.Helpers/Helpers.cs private class UwpHelper { diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs index f0091c725..b65b554f4 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs @@ -315,7 +315,8 @@ private async Task createServerConnection(string remoteHost tcpClient.SendTimeout = proxyServer.ConnectionTimeOutSeconds * 1000; tcpClient.LingerState = new LingerOption(true, proxyServer.TcpTimeWaitSeconds); - if (proxyServer.ReuseSocket && RunTime.IsSocketReuseAvailable) + // linux has a bug with socket reuse in .net core. + if (proxyServer.ReuseSocket && RunTime.IsWindows) { tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index 9b681d430..d7d89eb7e 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -654,7 +654,8 @@ private void listen(ProxyEndPoint endPoint) { endPoint.Listener = new TcpListener(endPoint.IpAddress, endPoint.Port); - if (ReuseSocket && RunTime.IsSocketReuseAvailable) + // linux/macOS has a bug with socket reuse in .net core. + if (ReuseSocket && RunTime.IsWindows) { endPoint.Listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } From 8a5cc3f842a46b02ac201285f132f69fdeadaeaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D1=8C=D0=BA=D0=BE=20=D0=90?= =?UTF-8?q?=20=D0=92?= Date: Sat, 30 Nov 2019 23:34:25 +0300 Subject: [PATCH 7/9] Introduce asynchronous RequestState and simplify error logging --- .../ProxyTestController.cs | 644 +++++---- ....Proxy.Examples.Wpf_lmb5j4or_wpftmp.csproj | 174 +++ src/Titanium.Web.Proxy/CertificateHandler.cs | 27 +- .../BeforeSslAuthenticateEventArgs.cs | 5 +- .../CertificateSelectionEventArgs.cs | 5 +- .../CertificateValidationEventArgs.cs | 5 +- .../MultipartRequestPartSentEventArgs.cs | 7 +- .../EventArguments/SessionEventArgs.cs | 6 +- .../EventArguments/SessionEventArgsBase.cs | 15 +- .../EventArguments/TunnelConnectEventArgs.cs | 4 +- .../ExplicitClientHandler.cs | 21 +- src/Titanium.Web.Proxy/Helpers/ProxyInfo.cs | 4 +- src/Titanium.Web.Proxy/Helpers/SystemProxy.cs | 4 +- src/Titanium.Web.Proxy/Http/Request.cs | 2 +- src/Titanium.Web.Proxy/Http2/Http2Helper.cs | 10 +- .../Models/ExplicitProxyEndPoint.cs | 4 +- .../Models/TransparentProxyEndPoint.cs | 2 +- .../Network/Tcp/TcpClientConnection.cs | 11 +- .../Network/Tcp/TcpConnectionFactory.cs | 34 +- .../Network/Tcp/TcpServerConnection.cs | 4 +- .../ProxyAuthorizationHandler.cs | 2 +- src/Titanium.Web.Proxy/ProxyServer.cs | 1232 +++++++++-------- src/Titanium.Web.Proxy/RequestHandler.cs | 9 +- src/Titanium.Web.Proxy/ResponseHandler.cs | 8 +- .../TransparentClientHandler.cs | 19 +- src/Titanium.Web.Proxy/WebSocketHandler.cs | 2 +- src/Titanium.Web.Proxy/WinAuthHandler.cs | 2 +- 27 files changed, 1307 insertions(+), 955 deletions(-) create mode 100644 examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf_lmb5j4or_wpftmp.csproj diff --git a/examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs b/examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs index 9ae48e1f2..ac9cf5dd3 100644 --- a/examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs +++ b/examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Security; +using System.Text; using System.Threading; using System.Threading.Tasks; using Titanium.Web.Proxy.EventArguments; @@ -12,299 +13,352 @@ using Titanium.Web.Proxy.Models; namespace Titanium.Web.Proxy.Examples.Basic -{ - public class ProxyTestController - { - private readonly SemaphoreSlim @lock = new SemaphoreSlim(1); - private readonly ProxyServer proxyServer; - private ExplicitProxyEndPoint explicitEndPoint; - - public ProxyTestController() - { - proxyServer = new ProxyServer(); - // generate root certificate without storing it in file system - //proxyServer.CertificateManager.CreateRootCertificate(false); - - //proxyServer.CertificateManager.TrustRootCertificate(); - //proxyServer.CertificateManager.TrustRootCertificateAsAdmin(); - - proxyServer.ExceptionFunc = async exception => - { - if (exception is ProxyHttpException phex) - { - await writeToConsole(exception.Message + ": " + phex.InnerException?.Message, true); - } - else - { - await writeToConsole(exception.Message, true); - } - }; - proxyServer.ForwardToUpstreamGateway = true; - proxyServer.CertificateManager.SaveFakeCertificates = true; - - // this is just to show the functionality, provided implementations use junk value - //proxyServer.GetCustomUpStreamProxyFunc = onGetCustomUpStreamProxyFunc; - //proxyServer.CustomUpStreamProxyFailureFunc = onCustomUpStreamProxyFailureFunc; - - // optionally set the Certificate Engine - // Under Mono or Non-Windows runtimes only BouncyCastle will be supported - //proxyServer.CertificateManager.CertificateEngine = Network.CertificateEngine.BouncyCastle; - - // optionally set the Root Certificate - //proxyServer.CertificateManager.RootCertificate = new X509Certificate2("myCert.pfx", string.Empty, X509KeyStorageFlags.Exportable); - } - - public void StartProxy() - { - proxyServer.BeforeRequest += onRequest; - proxyServer.BeforeResponse += onResponse; - - proxyServer.ServerCertificateValidationCallback += OnCertificateValidation; - proxyServer.ClientCertificateSelectionCallback += OnCertificateSelection; - - //proxyServer.EnableWinAuth = true; - - explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, 8000); - - // Fired when a CONNECT request is received - explicitEndPoint.BeforeTunnelConnectRequest += onBeforeTunnelConnectRequest; - explicitEndPoint.BeforeTunnelConnectResponse += onBeforeTunnelConnectResponse; - - // An explicit endpoint is where the client knows about the existence of a proxy - // So client sends request in a proxy friendly manner - proxyServer.AddEndPoint(explicitEndPoint); - proxyServer.Start(); - - // Transparent endpoint is useful for reverse proxy (client is not aware of the existence of proxy) - // A transparent endpoint usually requires a network router port forwarding HTTP(S) packets or DNS - // to send data to this endPoint - //var transparentEndPoint = new TransparentProxyEndPoint(IPAddress.Any, 443, true) - //{ - // // Generic Certificate hostname to use - // // When SNI is disabled by client - // GenericCertificateName = "google.com" - //}; - - //proxyServer.AddEndPoint(transparentEndPoint); - //proxyServer.UpStreamHttpProxy = new ExternalProxy() { HostName = "localhost", Port = 8888 }; - //proxyServer.UpStreamHttpsProxy = new ExternalProxy() { HostName = "localhost", Port = 8888 }; - - foreach (var endPoint in proxyServer.ProxyEndPoints) - { - Console.WriteLine("Listening on '{0}' endpoint at Ip {1} and port: {2} ", endPoint.GetType().Name, - endPoint.IpAddress, endPoint.Port); - } - - // Only explicit proxies can be set as system proxy! - //proxyServer.SetAsSystemHttpProxy(explicitEndPoint); - //proxyServer.SetAsSystemHttpsProxy(explicitEndPoint); - if (RunTime.IsWindows) - { - proxyServer.SetAsSystemProxy(explicitEndPoint, ProxyProtocolType.AllHttp); - } - } - - public void Stop() - { - explicitEndPoint.BeforeTunnelConnectRequest -= onBeforeTunnelConnectRequest; - explicitEndPoint.BeforeTunnelConnectResponse -= onBeforeTunnelConnectResponse; - - proxyServer.BeforeRequest -= onRequest; - proxyServer.BeforeResponse -= onResponse; - proxyServer.ServerCertificateValidationCallback -= OnCertificateValidation; - proxyServer.ClientCertificateSelectionCallback -= OnCertificateSelection; - - proxyServer.Stop(); - - // remove the generated certificates - //proxyServer.CertificateManager.RemoveTrustedRootCertificates(); - } - - private async Task onGetCustomUpStreamProxyFunc(SessionEventArgsBase arg) - { - // this is just to show the functionality, provided values are junk - return new ExternalProxy() { BypassLocalhost = false, HostName = "127.0.0.9", Port = 9090, Password = "fake", UserName = "fake", UseDefaultCredentials = false }; - } - - private async Task onCustomUpStreamProxyFailureFunc(SessionEventArgsBase arg) - { - // this is just to show the functionality, provided values are junk - return new ExternalProxy() { BypassLocalhost = false, HostName = "127.0.0.10", Port = 9191, Password = "fake2", UserName = "fake2", UseDefaultCredentials = false }; - } - - private async Task onBeforeTunnelConnectRequest(object sender, TunnelConnectSessionEventArgs e) - { - string hostname = e.HttpClient.Request.RequestUri.Host; - await writeToConsole("Tunnel to: " + hostname); - - if (hostname.Contains("dropbox.com")) - { - // Exclude Https addresses you don't want to proxy - // Useful for clients that use certificate pinning - // for example dropbox.com - e.DecryptSsl = false; - } - } - - private Task onBeforeTunnelConnectResponse(object sender, TunnelConnectSessionEventArgs e) - { - return Task.FromResult(false); - } - - // intercept & cancel redirect or update requests - private async Task onRequest(object sender, SessionEventArgs e) - { - await writeToConsole("Active Client Connections:" + ((ProxyServer)sender).ClientConnectionCount); - await writeToConsole(e.HttpClient.Request.Url); - - // store it in the UserData property - // It can be a simple integer, Guid, or any type - //e.UserData = new CustomUserData() - //{ - // RequestHeaders = e.HttpClient.Request.Headers, - // RequestBody = e.HttpClient.Request.HasBody ? e.HttpClient.Request.Body:null, - // RequestBodyString = e.HttpClient.Request.HasBody? e.HttpClient.Request.BodyString:null - //}; - - ////This sample shows how to get the multipart form data headers - //if (e.HttpClient.Request.Host == "mail.yahoo.com" && e.HttpClient.Request.IsMultipartFormData) - //{ - // e.MultipartRequestPartSent += MultipartRequestPartSent; - //} - - // To cancel a request with a custom HTML content - // Filter URL - //if (e.HttpClient.Request.RequestUri.AbsoluteUri.Contains("yahoo.com")) - //{ - // e.Ok("" + - // "

" + - // "Website Blocked" + - // "

" + - // "

Blocked by titanium web proxy.

" + - // "" + - // ""); - //} - - ////Redirect example - //if (e.HttpClient.Request.RequestUri.AbsoluteUri.Contains("wikipedia.org")) - //{ - // e.Redirect("https://www.paypal.com"); - //} - } - - // Modify response - private async Task multipartRequestPartSent(object sender, MultipartRequestPartSentEventArgs e) - { - var session = (SessionEventArgs)sender; - await writeToConsole("Multipart form data headers:"); - foreach (var header in e.Headers) - { - await writeToConsole(header.ToString()); - } - } - - private async Task onResponse(object sender, SessionEventArgs e) - { - await writeToConsole("Active Server Connections:" + ((ProxyServer)sender).ServerConnectionCount); - - string ext = System.IO.Path.GetExtension(e.HttpClient.Request.RequestUri.AbsolutePath); - - // access user data set in request to do something with it - //var userData = e.HttpClient.UserData as CustomUserData; - - //if (ext == ".gif" || ext == ".png" || ext == ".jpg") - //{ - // byte[] btBody = Encoding.UTF8.GetBytes("" + - // "

" + - // "Image is blocked" + - // "

" + - // "

Blocked by Titanium

" + - // "" + - // ""); - - // var response = new OkResponse(btBody); - // response.HttpVersion = e.HttpClient.Request.HttpVersion; - - // e.Respond(response); - // e.TerminateServerConnection(); - //} - - //// print out process id of current session - ////WriteToConsole($"PID: {e.HttpClient.ProcessId.Value}"); - - ////if (!e.ProxySession.Request.Host.Equals("medeczane.sgk.gov.tr")) return; - //if (e.HttpClient.Request.Method == "GET" || e.HttpClient.Request.Method == "POST") - //{ - // if (e.HttpClient.Response.StatusCode == (int)HttpStatusCode.OK) - // { - // if (e.HttpClient.Response.ContentType != null && e.HttpClient.Response.ContentType.Trim().ToLower().Contains("text/html")) - // { - // var bodyBytes = await e.GetResponseBody(); - // await e.SetResponseBody(bodyBytes); - - // string body = await e.GetResponseBodyAsString(); - // await e.SetResponseBodyString(body); - // } - // } - //} - } - - /// - /// 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 == SslPolicyErrors.None) - { - e.IsValid = true; - } - - 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); - } - - private async Task writeToConsole(string message, bool useRedColor = false) - { - await @lock.WaitAsync(); - - if (useRedColor) - { - ConsoleColor existing = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(message); - Console.ForegroundColor = existing; - } - else - { - Console.WriteLine(message); - } - - @lock.Release(); - } - - ///// - ///// User data object as defined by user. - ///// User data can be set to each SessionEventArgs.HttpClient.UserData property - ///// - //public class CustomUserData - //{ - // public HeaderCollection RequestHeaders { get; set; } - // public byte[] RequestBody { get; set; } - // public string RequestBodyString { get; set; } - //} - } +{ + static class ExtendedProxyStateHelher + { + internal static ExtendedProxyState Extended(this RequestStateBase state) + { + return (ExtendedProxyState)state; + } + } + public class ExtendedProxyState : RequestState + { + + internal StringBuilder PipelineInfo=new StringBuilder(); + + public override async Task OnErrorAsync(Exception e) + { + await Console.Out.WriteLineAsync($"OnErrorAsync: {PipelineInfo} : {e}"); + } + public override void OnError(Exception e) + { + Console.Out.WriteLine($"OnErrorAsync: {PipelineInfo}: {e}"); + } + + } + + public class ProxyServer : ExtendedProxyState.ProxyServer + { + + } + + public class ProxyTestController + { + private readonly SemaphoreSlim @lock = new SemaphoreSlim(1); + private readonly ProxyServer proxyServer; + private ExplicitProxyEndPoint explicitEndPoint; + + public ProxyTestController() + { + proxyServer = new ProxyServer(); + // generate root certificate without storing it in file system + //proxyServer.CertificateManager.CreateRootCertificate(false); + + //proxyServer.CertificateManager.TrustRootCertificate(); + //proxyServer.CertificateManager.TrustRootCertificateAsAdmin(); + + proxyServer.ExceptionFunc = async exception => + { + if (exception is ProxyHttpException phex) + { + await writeToConsole(exception.Message + ": " + phex.InnerException?.Message, true); + } + else + { + await writeToConsole(exception.Message, true); + } + }; + proxyServer.ForwardToUpstreamGateway = true; + proxyServer.CertificateManager.SaveFakeCertificates = true; + + // this is just to show the functionality, provided implementations use junk value + //proxyServer.GetCustomUpStreamProxyFunc = onGetCustomUpStreamProxyFunc; + //proxyServer.CustomUpStreamProxyFailureFunc = onCustomUpStreamProxyFailureFunc; + + // optionally set the Certificate Engine + // Under Mono or Non-Windows runtimes only BouncyCastle will be supported + //proxyServer.CertificateManager.CertificateEngine = Network.CertificateEngine.BouncyCastle; + + // optionally set the Root Certificate + //proxyServer.CertificateManager.RootCertificate = new X509Certificate2("myCert.pfx", string.Empty, X509KeyStorageFlags.Exportable); + } + + public void StartProxy() + { + proxyServer.BeforeRequest += onRequest; + proxyServer.BeforeResponse += onResponse; + proxyServer.AfterResponse += ProxyServer_AfterResponse; + + proxyServer.ServerCertificateValidationCallback += OnCertificateValidation; + proxyServer.ClientCertificateSelectionCallback += OnCertificateSelection; + + //proxyServer.EnableWinAuth = true; + + explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, 8000); + + // Fired when a CONNECT request is received + explicitEndPoint.BeforeTunnelConnectRequest += onBeforeTunnelConnectRequest; + explicitEndPoint.BeforeTunnelConnectResponse += onBeforeTunnelConnectResponse; + + // An explicit endpoint is where the client knows about the existence of a proxy + // So client sends request in a proxy friendly manner + proxyServer.AddEndPoint(explicitEndPoint); + proxyServer.Start(); + + // Transparent endpoint is useful for reverse proxy (client is not aware of the existence of proxy) + // A transparent endpoint usually requires a network router port forwarding HTTP(S) packets or DNS + // to send data to this endPoint + //var transparentEndPoint = new TransparentProxyEndPoint(IPAddress.Any, 443, true) + //{ + // // Generic Certificate hostname to use + // // When SNI is disabled by client + // GenericCertificateName = "google.com" + //}; + + //proxyServer.AddEndPoint(transparentEndPoint); + //proxyServer.UpStreamHttpProxy = new ExternalProxy() { HostName = "localhost", Port = 8888 }; + //proxyServer.UpStreamHttpsProxy = new ExternalProxy() { HostName = "localhost", Port = 8888 }; + + foreach (var endPoint in proxyServer.ProxyEndPoints) + { + Console.WriteLine("Listening on '{0}' endpoint at Ip {1} and port: {2} ", endPoint.GetType().Name, + endPoint.IpAddress, endPoint.Port); + } + + // Only explicit proxies can be set as system proxy! + //proxyServer.SetAsSystemHttpProxy(explicitEndPoint); + //proxyServer.SetAsSystemHttpsProxy(explicitEndPoint); + if (RunTime.IsWindows) + { + proxyServer.SetAsSystemProxy(explicitEndPoint, ProxyProtocolType.AllHttp); + } + } + + private async Task ProxyServer_AfterResponse(object sender, SessionEventArgs e) + { + await Console.Out.WriteLineAsync($"Piplineinfo: {e.State.Extended().PipelineInfo}"); + } + + public void Stop() + { + explicitEndPoint.BeforeTunnelConnectRequest -= onBeforeTunnelConnectRequest; + explicitEndPoint.BeforeTunnelConnectResponse -= onBeforeTunnelConnectResponse; + + proxyServer.BeforeRequest -= onRequest; + proxyServer.BeforeResponse -= onResponse; + proxyServer.AfterResponse -= ProxyServer_AfterResponse; + + proxyServer.ServerCertificateValidationCallback -= OnCertificateValidation; + proxyServer.ClientCertificateSelectionCallback -= OnCertificateSelection; + + proxyServer.Stop(); + + // remove the generated certificates + //proxyServer.CertificateManager.RemoveTrustedRootCertificates(); + } + + private async Task onGetCustomUpStreamProxyFunc(SessionEventArgsBase arg) + { + + arg.State.Extended().PipelineInfo.AppendLine(nameof(onGetCustomUpStreamProxyFunc)); + + // this is just to show the functionality, provided values are junk + return new ExternalProxy() { BypassLocalhost = false, HostName = "127.0.0.9", Port = 9090, Password = "fake", UserName = "fake", UseDefaultCredentials = false }; + } + + private async Task onCustomUpStreamProxyFailureFunc(SessionEventArgsBase arg) + { + arg.State.Extended().PipelineInfo.AppendLine(nameof(onCustomUpStreamProxyFailureFunc)); + + // this is just to show the functionality, provided values are junk + return new ExternalProxy() { BypassLocalhost = false, HostName = "127.0.0.10", Port = 9191, Password = "fake2", UserName = "fake2", UseDefaultCredentials = false }; + } + + private async Task onBeforeTunnelConnectRequest(object sender, TunnelConnectSessionEventArgs e) + { + + string hostname = e.HttpClient.Request.RequestUri.Host; + e.State.Extended().PipelineInfo.AppendLine(nameof(onBeforeTunnelConnectRequest) + ":" +hostname); + //await writeToConsole("Tunnel to: " + hostname); + + if (hostname.Contains("dropbox.com")) + { + // Exclude Https addresses you don't want to proxy + // Useful for clients that use certificate pinning + // for example dropbox.com + e.DecryptSsl = false; + } + } + + private Task onBeforeTunnelConnectResponse(object sender, TunnelConnectSessionEventArgs e) + { + e.State.Extended().PipelineInfo.AppendLine(nameof(onBeforeTunnelConnectResponse) + ":" + e.HttpClient.Request.RequestUri); + + return Task.FromResult(false); + } + + // intercept & cancel redirect or update requests + private async Task onRequest(object sender, SessionEventArgs e) + { + e.State.Extended().PipelineInfo.AppendLine(nameof(onRequest) + ":" + e.HttpClient.Request.RequestUri); + + await writeToConsole("Active Client Connections:" + ((ProxyServer)sender).ClientConnectionCount); + await writeToConsole(e.HttpClient.Request.Url); + + // store it in the UserData property + // It can be a simple integer, Guid, or any type + //e.UserData = new CustomUserData() + //{ + // RequestHeaders = e.HttpClient.Request.Headers, + // RequestBody = e.HttpClient.Request.HasBody ? e.HttpClient.Request.Body:null, + // RequestBodyString = e.HttpClient.Request.HasBody? e.HttpClient.Request.BodyString:null + //}; + + ////This sample shows how to get the multipart form data headers + //if (e.HttpClient.Request.Host == "mail.yahoo.com" && e.HttpClient.Request.IsMultipartFormData) + //{ + // e.MultipartRequestPartSent += MultipartRequestPartSent; + //} + + // To cancel a request with a custom HTML content + // Filter URL + //if (e.HttpClient.Request.RequestUri.AbsoluteUri.Contains("yahoo.com")) + //{ + // e.Ok("" + + // "

" + + // "Website Blocked" + + // "

" + + // "

Blocked by titanium web proxy.

" + + // "" + + // ""); + //} + + ////Redirect example + //if (e.HttpClient.Request.RequestUri.AbsoluteUri.Contains("wikipedia.org")) + //{ + // e.Redirect("https://www.paypal.com"); + //} + } + + // Modify response + private async Task multipartRequestPartSent(object sender, MultipartRequestPartSentEventArgs e) + { + e.State.Extended().PipelineInfo.AppendLine(nameof(multipartRequestPartSent)); + + var session = (SessionEventArgs)sender; + await writeToConsole("Multipart form data headers:"); + foreach (var header in e.Headers) + { + await writeToConsole(header.ToString()); + } + } + + private async Task onResponse(object sender, SessionEventArgs e) + { + e.State.Extended().PipelineInfo.AppendLine(nameof(onResponse)); + + await writeToConsole("Active Server Connections:" + ((ProxyServer)sender).ServerConnectionCount); + + string ext = System.IO.Path.GetExtension(e.HttpClient.Request.RequestUri.AbsolutePath); + + // access user data set in request to do something with it + //var userData = e.HttpClient.UserData as CustomUserData; + + //if (ext == ".gif" || ext == ".png" || ext == ".jpg") + //{ + // byte[] btBody = Encoding.UTF8.GetBytes("" + + // "

" + + // "Image is blocked" + + // "

" + + // "

Blocked by Titanium

" + + // "" + + // ""); + + // var response = new OkResponse(btBody); + // response.HttpVersion = e.HttpClient.Request.HttpVersion; + + // e.Respond(response); + // e.TerminateServerConnection(); + //} + + //// print out process id of current session + ////WriteToConsole($"PID: {e.HttpClient.ProcessId.Value}"); + + ////if (!e.ProxySession.Request.Host.Equals("medeczane.sgk.gov.tr")) return; + //if (e.HttpClient.Request.Method == "GET" || e.HttpClient.Request.Method == "POST") + //{ + // if (e.HttpClient.Response.StatusCode == (int)HttpStatusCode.OK) + // { + // if (e.HttpClient.Response.ContentType != null && e.HttpClient.Response.ContentType.Trim().ToLower().Contains("text/html")) + // { + // var bodyBytes = await e.GetResponseBody(); + // await e.SetResponseBody(bodyBytes); + + // string body = await e.GetResponseBodyAsString(); + // await e.SetResponseBodyString(body); + // } + // } + //} + } + + /// + /// Allows overriding default certificate validation logic + /// + /// + /// + public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e) + { + e.State.Extended().PipelineInfo.AppendLine(nameof(OnCertificateValidation)); + // set IsValid to true/false based on Certificate Errors + if (e.SslPolicyErrors == SslPolicyErrors.None) + { + e.IsValid = true; + } + + return Task.FromResult(0); + } + + /// + /// Allows overriding default client certificate selection logic during mutual authentication + /// + /// + /// + public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs e) + { + e.State.Extended().PipelineInfo.AppendLine(nameof(OnCertificateSelection)); + // set e.clientCertificate to override + + return Task.FromResult(0); + } + + private async Task writeToConsole(string message, bool useRedColor = false) + { + //await @lock.WaitAsync(); + + if (useRedColor) + { + ConsoleColor existing = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ForegroundColor = existing; + } + else + { + Console.WriteLine(message); + } + + @lock.Release(); + } + + ///// + ///// User data object as defined by user. + ///// User data can be set to each SessionEventArgs.HttpClient.UserData property + ///// + //public class CustomUserData + //{ + // public HeaderCollection RequestHeaders { get; set; } + // public byte[] RequestBody { get; set; } + // public string RequestBodyString { get; set; } + //} + } } diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf_lmb5j4or_wpftmp.csproj b/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf_lmb5j4or_wpftmp.csproj new file mode 100644 index 000000000..952043108 --- /dev/null +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf_lmb5j4or_wpftmp.csproj @@ -0,0 +1,174 @@ + + + WinExe + net461;netcoreapp3.0 + true + + + + + + + + + + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Titanium.Web.Proxy/CertificateHandler.cs b/src/Titanium.Web.Proxy/CertificateHandler.cs index b7efcfbe5..c4222d528 100644 --- a/src/Titanium.Web.Proxy/CertificateHandler.cs +++ b/src/Titanium.Web.Proxy/CertificateHandler.cs @@ -6,7 +6,22 @@ namespace Titanium.Web.Proxy { - public partial class ProxyServer + public partial class RequestStateBase + { + internal bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + return Server.ValidateServerCertificate(this, sender, certificate, chain, sslPolicyErrors); + } + internal X509Certificate? SelectClientCertificate(object sender, string targetHost, + X509CertificateCollection localCertificates, + X509Certificate remoteCertificate, string[] acceptableIssuers) + { + return Server.SelectClientCertificate(this, sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers); + } + + } + public partial class ProxyServerBase { /// /// Call back to override server certificate validation @@ -16,16 +31,16 @@ public partial class ProxyServer /// The certificate chain. /// Ssl policy errors /// Return true if valid certificate. - internal bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, + internal bool ValidateServerCertificate(RequestStateBase state, object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { // if user callback is registered then do it if (ServerCertificateValidationCallback != null) { - var args = new CertificateValidationEventArgs(certificate, chain, sslPolicyErrors); + var args = new CertificateValidationEventArgs(state, certificate, chain, sslPolicyErrors); // why is the sender null? - ServerCertificateValidationCallback.InvokeAsync(this, args, ExceptionFunc).Wait(); + ServerCertificateValidationCallback.InvokeAsync(this, args, state.OnError).Wait(); return args.IsValid; } @@ -48,7 +63,7 @@ internal bool ValidateServerCertificate(object sender, X509Certificate certifica /// The remote certificate of server. /// The acceptable issues for client certificate as listed by server. /// - internal X509Certificate? SelectClientCertificate(object sender, string targetHost, + internal X509Certificate? SelectClientCertificate(RequestStateBase state, object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers) { @@ -75,7 +90,7 @@ internal bool ValidateServerCertificate(object sender, X509Certificate certifica // If user call back is registered if (ClientCertificateSelectionCallback != null) { - var args = new CertificateSelectionEventArgs + var args = new CertificateSelectionEventArgs(state) { TargetHost = targetHost, LocalCertificates = localCertificates, diff --git a/src/Titanium.Web.Proxy/EventArguments/BeforeSslAuthenticateEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/BeforeSslAuthenticateEventArgs.cs index 191a9bf1e..145783316 100644 --- a/src/Titanium.Web.Proxy/EventArguments/BeforeSslAuthenticateEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/BeforeSslAuthenticateEventArgs.cs @@ -6,11 +6,12 @@ namespace Titanium.Web.Proxy.EventArguments /// /// This is used in transparent endpoint before authenticating client. /// - public class BeforeSslAuthenticateEventArgs : EventArgs + public class BeforeSslAuthenticateEventArgs : ProxyEventArgsBase { internal readonly CancellationTokenSource TaskCancellationSource; - internal BeforeSslAuthenticateEventArgs(CancellationTokenSource taskCancellationSource, string sniHostName) + internal BeforeSslAuthenticateEventArgs(RequestStateBase state ,CancellationTokenSource taskCancellationSource, string sniHostName) + :base(state) { TaskCancellationSource = taskCancellationSource; SniHostName = sniHostName; diff --git a/src/Titanium.Web.Proxy/EventArguments/CertificateSelectionEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/CertificateSelectionEventArgs.cs index 257d80ca0..0ee5232d9 100644 --- a/src/Titanium.Web.Proxy/EventArguments/CertificateSelectionEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/CertificateSelectionEventArgs.cs @@ -6,8 +6,11 @@ namespace Titanium.Web.Proxy.EventArguments /// /// An argument passed on to user for client certificate selection during mutual SSL authentication. /// - public class CertificateSelectionEventArgs : EventArgs + public class CertificateSelectionEventArgs : ProxyEventArgsBase { + public CertificateSelectionEventArgs(RequestStateBase state) + :base(state) + { } /// /// The proxy server instance. /// diff --git a/src/Titanium.Web.Proxy/EventArguments/CertificateValidationEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/CertificateValidationEventArgs.cs index 22e9bf381..49f8aef2e 100644 --- a/src/Titanium.Web.Proxy/EventArguments/CertificateValidationEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/CertificateValidationEventArgs.cs @@ -8,9 +8,10 @@ namespace Titanium.Web.Proxy.EventArguments /// An argument passed on to the user for validating the server certificate /// during SSL authentication. /// - public class CertificateValidationEventArgs : EventArgs + public class CertificateValidationEventArgs : ProxyEventArgsBase { - public CertificateValidationEventArgs(X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + public CertificateValidationEventArgs(RequestStateBase state, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + :base(state) { Certificate = certificate; Chain = chain; diff --git a/src/Titanium.Web.Proxy/EventArguments/MultipartRequestPartSentEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/MultipartRequestPartSentEventArgs.cs index 988b19fa7..b9ed84d1f 100644 --- a/src/Titanium.Web.Proxy/EventArguments/MultipartRequestPartSentEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/MultipartRequestPartSentEventArgs.cs @@ -1,4 +1,4 @@ -using System; +using System; using Titanium.Web.Proxy.Http; namespace Titanium.Web.Proxy.EventArguments @@ -6,9 +6,10 @@ namespace Titanium.Web.Proxy.EventArguments /// /// Class that wraps the multipart sent request arguments. /// - public class MultipartRequestPartSentEventArgs : EventArgs + public class MultipartRequestPartSentEventArgs : ProxyEventArgsBase { - internal MultipartRequestPartSentEventArgs(string boundary, HeaderCollection headers) + internal MultipartRequestPartSentEventArgs(RequestStateBase state, string boundary, HeaderCollection headers) + :base(state) { Boundary = boundary; Headers = headers; diff --git a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs index f9201877b..e674fdcc1 100644 --- a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs @@ -34,8 +34,8 @@ public class SessionEventArgs : SessionEventArgsBase /// /// Constructor to initialize the proxy /// - internal SessionEventArgs(ProxyServer server, ProxyEndPoint endPoint, TcpClientConnection clientConnection, HttpClientStream clientStream, ConnectRequest? connectRequest, CancellationTokenSource cancellationTokenSource) - : base(server, endPoint, clientConnection, clientStream, connectRequest, new Request(), cancellationTokenSource) + internal SessionEventArgs(RequestStateBase state, ProxyEndPoint endPoint, TcpClientConnection clientConnection, HttpClientStream clientStream, ConnectRequest? connectRequest, CancellationTokenSource cancellationTokenSource) + : base(state, endPoint, clientConnection, clientStream, connectRequest, new Request(), cancellationTokenSource) { } @@ -123,7 +123,7 @@ internal void OnMultipartRequestPartSent(ReadOnlySpan boundary, HeaderColl { try { - MultipartRequestPartSent?.Invoke(this, new MultipartRequestPartSentEventArgs(boundary.ToString(), headers)); + MultipartRequestPartSent?.Invoke(this, new MultipartRequestPartSentEventArgs(this.State, boundary.ToString(), headers)); } catch (Exception ex) { diff --git a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs index b6bdb5aef..e74cf68c2 100644 --- a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs +++ b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs @@ -12,13 +12,22 @@ namespace Titanium.Web.Proxy.EventArguments { + public abstract class ProxyEventArgsBase : EventArgs + { + public readonly RequestStateBase State; + public ProxyEventArgsBase(RequestStateBase state) + { + this.State = state; + } + + } /// /// Holds info related to a single proxy session (single request/response sequence). /// A proxy session is bounded to a single connection from client. /// A proxy session ends when client terminates connection to proxy /// or when server terminates connection from proxy. /// - public abstract class SessionEventArgsBase : EventArgs, IDisposable + public abstract class SessionEventArgsBase : ProxyEventArgsBase, IDisposable { private static bool isWindowsAuthenticationSupported => RunTime.IsWindows; @@ -49,9 +58,11 @@ public abstract class SessionEventArgsBase : EventArgs, IDisposable /// /// Initializes a new instance of the class. /// - private protected SessionEventArgsBase(ProxyServer server, ProxyEndPoint endPoint, + private protected SessionEventArgsBase(RequestStateBase state, ProxyEndPoint endPoint, TcpClientConnection clientConnection, HttpClientStream clientStream, ConnectRequest? connectRequest, Request request, CancellationTokenSource cancellationTokenSource) + :base(state) { + var server = state.Server; BufferPool = server.BufferPool; ExceptionFunc = server.ExceptionFunc; TimeLine["Session Created"] = DateTime.Now; diff --git a/src/Titanium.Web.Proxy/EventArguments/TunnelConnectEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/TunnelConnectEventArgs.cs index 494b51754..10393b656 100644 --- a/src/Titanium.Web.Proxy/EventArguments/TunnelConnectEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/TunnelConnectEventArgs.cs @@ -16,9 +16,9 @@ public class TunnelConnectSessionEventArgs : SessionEventArgsBase { private bool? isHttpsConnect; - internal TunnelConnectSessionEventArgs(ProxyServer server, ProxyEndPoint endPoint, ConnectRequest connectRequest, + internal TunnelConnectSessionEventArgs(RequestStateBase state, ProxyEndPoint endPoint, ConnectRequest connectRequest, TcpClientConnection clientConnection, HttpClientStream clientStream, CancellationTokenSource cancellationTokenSource) - : base(server, endPoint, clientConnection, clientStream, connectRequest, connectRequest, cancellationTokenSource) + : base(state, endPoint, clientConnection, clientStream, connectRequest, connectRequest, cancellationTokenSource) { } diff --git a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs index f78076a80..09a1b080a 100644 --- a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs @@ -22,8 +22,8 @@ namespace Titanium.Web.Proxy { - public partial class ProxyServer - { + public partial class ProxyServerBase + { /// /// 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 @@ -31,8 +31,9 @@ public partial class ProxyServer /// The explicit endpoint. /// The client connection. /// The task. - private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnection clientConnection) + internal async Task handleClient(ExplicitProxyEndPoint endPoint, RequestStateBase state) { + TcpClientConnection clientConnection = state; var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; @@ -66,7 +67,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect await HeaderParser.ReadHeaders(clientStream, connectRequest.Headers, cancellationToken); - connectArgs = new TunnelConnectSessionEventArgs(this, endPoint, connectRequest, + connectArgs = new TunnelConnectSessionEventArgs(state, endPoint, connectRequest, clientConnection, clientStream, cancellationTokenSource); clientStream.DataRead += (o, args) => connectArgs.OnDataSent(args.Buffer, args.Offset, args.Count); clientStream.DataWrite += (o, args) => connectArgs.OnDataReceived(args.Buffer, args.Offset, args.Count); @@ -137,7 +138,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect try { // todo: this is a hack, because Titanium does not support HTTP protocol changing currently - var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, + var connection = await tcpConnectionFactory.GetServerConnection(state, connectArgs, true, SslExtensions.Http2ProtocolAsList, true, cancellationToken); @@ -167,7 +168,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect { // don't pass cancellation token here // it could cause floating server connections when client exits - prefetchConnectionTask = tcpConnectionFactory.GetServerConnection(this, connectArgs, + prefetchConnectionTask = tcpConnectionFactory.GetServerConnection(state, connectArgs, true, null, false, CancellationToken.None); } @@ -252,7 +253,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect // create new connection to server. // If we detected that client tunnel CONNECTs without SSL by checking for empty client hello then // this connection should not be HTTPS. - var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, + var connection = await tcpConnectionFactory.GetServerConnection(state, connectArgs, true, SslExtensions.Http2ProtocolAsList, true, cancellationToken); @@ -329,7 +330,7 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received"); } - var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, + var connection = await tcpConnectionFactory.GetServerConnection(state, connectArgs, true, SslExtensions.Http2ProtocolAsList, true, cancellationToken); try @@ -338,7 +339,7 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, var connectionPreface = new ReadOnlyMemory(Http2Helper.ConnectionPreface); await connection.Stream.WriteAsync(connectionPreface, cancellationToken); await Http2Helper.SendHttp2(clientStream, connection.Stream, - () => new SessionEventArgs(this, endPoint, clientConnection, clientStream, connectArgs?.HttpClient.ConnectRequest, cancellationTokenSource) + () => new SessionEventArgs(state, endPoint, clientConnection, clientStream, connectArgs?.HttpClient.ConnectRequest, cancellationTokenSource) { UserData = connectArgs?.UserData }, @@ -357,7 +358,7 @@ await Http2Helper.SendHttp2(clientStream, connection.Stream, calledRequestHandler = true; // Now create the request - await handleHttpSessionRequest(endPoint, clientConnection, clientStream, cancellationTokenSource, connectArgs, prefetchConnectionTask); + await handleHttpSessionRequest(endPoint, state, clientStream, cancellationTokenSource, connectArgs, prefetchConnectionTask); } catch (ProxyException e) { diff --git a/src/Titanium.Web.Proxy/Helpers/ProxyInfo.cs b/src/Titanium.Web.Proxy/Helpers/ProxyInfo.cs index 600e3a9d9..5a9006d3d 100644 --- a/src/Titanium.Web.Proxy/Helpers/ProxyInfo.cs +++ b/src/Titanium.Web.Proxy/Helpers/ProxyInfo.cs @@ -140,11 +140,11 @@ private static string convertRegexReservedChars(string rawString) } ProxyProtocolType? protocolType = null; - if (protocolTypeStr.Equals(Proxy.ProxyServer.UriSchemeHttp, StringComparison.InvariantCultureIgnoreCase)) + if (protocolTypeStr.Equals(Proxy.ProxyServerBase.UriSchemeHttp, StringComparison.InvariantCultureIgnoreCase)) { protocolType = ProxyProtocolType.Http; } - else if (protocolTypeStr.Equals(Proxy.ProxyServer.UriSchemeHttps, + else if (protocolTypeStr.Equals(Proxy.ProxyServerBase.UriSchemeHttps, StringComparison.InvariantCultureIgnoreCase)) { protocolType = ProxyProtocolType.Https; diff --git a/src/Titanium.Web.Proxy/Helpers/SystemProxy.cs b/src/Titanium.Web.Proxy/Helpers/SystemProxy.cs index 81ea5224b..a38dc3946 100644 --- a/src/Titanium.Web.Proxy/Helpers/SystemProxy.cs +++ b/src/Titanium.Web.Proxy/Helpers/SystemProxy.cs @@ -21,10 +21,10 @@ public override string ToString() switch (ProtocolType) { case ProxyProtocolType.Http: - protocol = ProxyServer.UriSchemeHttp; + protocol = ProxyServerBase.UriSchemeHttp; break; case ProxyProtocolType.Https: - protocol = ProxyServer.UriSchemeHttps; + protocol = ProxyServerBase.UriSchemeHttps; break; default: throw new Exception("Unsupported protocol type"); diff --git a/src/Titanium.Web.Proxy/Http/Request.cs b/src/Titanium.Web.Proxy/Http/Request.cs index f7d42c099..f4191e0d4 100644 --- a/src/Titanium.Web.Proxy/Http/Request.cs +++ b/src/Titanium.Web.Proxy/Http/Request.cs @@ -35,7 +35,7 @@ internal ByteString RequestUriString8 var scheme = getUriScheme(value); if (scheme.Length > 0) { - IsHttps = scheme.Equals(ProxyServer.UriSchemeHttps8); + IsHttps = scheme.Equals(ProxyServerBase.UriSchemeHttps8); } } } diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs index f86898cbb..b030b000e 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs @@ -263,7 +263,7 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, request.HttpVersion = HttpVersion.Version20; request.Method = method.GetString(); - request.IsHttps = headerListener.Scheme == ProxyServer.UriSchemeHttps; + request.IsHttps = headerListener.Scheme == ProxyServerBase.UriSchemeHttps; request.Authority = headerListener.Authority; request.RequestUriString8 = path; @@ -592,14 +592,14 @@ public string Scheme { get { - if (scheme.Equals(ProxyServer.UriSchemeHttp8)) + if (scheme.Equals(ProxyServerBase.UriSchemeHttp8)) { - return ProxyServer.UriSchemeHttp; + return ProxyServerBase.UriSchemeHttp; } - if (scheme.Equals(ProxyServer.UriSchemeHttps8)) + if (scheme.Equals(ProxyServerBase.UriSchemeHttps8)) { - return ProxyServer.UriSchemeHttps; + return ProxyServerBase.UriSchemeHttps; } return string.Empty; diff --git a/src/Titanium.Web.Proxy/Models/ExplicitProxyEndPoint.cs b/src/Titanium.Web.Proxy/Models/ExplicitProxyEndPoint.cs index a364a9b2d..4c6a01ecf 100644 --- a/src/Titanium.Web.Proxy/Models/ExplicitProxyEndPoint.cs +++ b/src/Titanium.Web.Proxy/Models/ExplicitProxyEndPoint.cs @@ -42,7 +42,7 @@ public ExplicitProxyEndPoint(IPAddress ipAddress, int port, bool decryptSsl = tr /// public event AsyncEventHandler? BeforeTunnelConnectResponse; - internal async Task InvokeBeforeTunnelConnectRequest(ProxyServer proxyServer, + internal async Task InvokeBeforeTunnelConnectRequest(ProxyServerBase proxyServer, TunnelConnectSessionEventArgs connectArgs, ExceptionHandler exceptionFunc) { if (BeforeTunnelConnectRequest != null) @@ -51,7 +51,7 @@ internal async Task InvokeBeforeTunnelConnectRequest(ProxyServer proxyServer, } } - internal async Task InvokeBeforeTunnelConnectResponse(ProxyServer proxyServer, + internal async Task InvokeBeforeTunnelConnectResponse(ProxyServerBase proxyServer, TunnelConnectSessionEventArgs connectArgs, ExceptionHandler exceptionFunc, bool isClientHello = false) { if (BeforeTunnelConnectResponse != null) diff --git a/src/Titanium.Web.Proxy/Models/TransparentProxyEndPoint.cs b/src/Titanium.Web.Proxy/Models/TransparentProxyEndPoint.cs index 8b0599e8e..e2572debf 100644 --- a/src/Titanium.Web.Proxy/Models/TransparentProxyEndPoint.cs +++ b/src/Titanium.Web.Proxy/Models/TransparentProxyEndPoint.cs @@ -36,7 +36,7 @@ public TransparentProxyEndPoint(IPAddress ipAddress, int port, bool decryptSsl = /// public event AsyncEventHandler? BeforeSslAuthenticate; - internal async Task InvokeBeforeSslAuthenticate(ProxyServer proxyServer, + internal async Task InvokeBeforeSslAuthenticate(ProxyServerBase proxyServer, BeforeSslAuthenticateEventArgs connectArgs, ExceptionHandler exceptionFunc) { if (BeforeSslAuthenticate != null) diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs index 22d09da03..ef05a1ccd 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs @@ -15,15 +15,20 @@ namespace Titanium.Web.Proxy.Network.Tcp /// An object that holds TcpConnection to a particular server and port /// internal class TcpClientConnection : IDisposable - { - internal TcpClientConnection(ProxyServer proxyServer, TcpClient tcpClient) + { + public static implicit operator TcpClientConnection(RequestStateBase state) + { + return state.ClientConnection; + } + + internal TcpClientConnection(ProxyServerBase proxyServer, TcpClient tcpClient) { this.tcpClient = tcpClient; this.proxyServer = proxyServer; this.proxyServer.UpdateClientConnectionCount(true); } - private ProxyServer proxyServer { get; } + private ProxyServerBase proxyServer { get; } public Guid Id { get; } = Guid.NewGuid(); diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs index 6d3c4ae79..da6427cbd 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs @@ -39,13 +39,13 @@ private readonly ConcurrentDictionary await clearOutdatedConnections()); } - internal ProxyServer Server { get; } + internal ProxyServerBase Server { get; } internal string GetConnectionCacheKey(string remoteHostName, int remotePort, bool isHttps, List? applicationProtocols, @@ -106,7 +106,7 @@ internal string GetConnectionCacheKey(string remoteHostName, int remotePort, /// The session event arguments. /// The application protocol. /// - internal async Task GetConnectionCacheKey(ProxyServer server, SessionEventArgsBase session, + internal async Task GetConnectionCacheKey(ProxyServerBase server, SessionEventArgsBase session, SslApplicationProtocol applicationProtocol) { List? applicationProtocols = null; @@ -145,7 +145,7 @@ internal async Task GetConnectionCacheKey(ProxyServer server, SessionEve /// if set to true [no cache]. /// The cancellation token for this async task. /// - internal Task GetServerConnection(ProxyServer server, SessionEventArgsBase session, bool isConnect, + internal Task GetServerConnection(RequestStateBase state, SessionEventArgsBase session, bool isConnect, SslApplicationProtocol applicationProtocol, bool noCache, CancellationToken cancellationToken) { List? applicationProtocols = null; @@ -154,7 +154,7 @@ internal Task GetServerConnection(ProxyServer server, Sessi applicationProtocols = new List { applicationProtocol }; } - return GetServerConnection(server, session, isConnect, applicationProtocols, noCache, cancellationToken); + return GetServerConnection(state, session, isConnect, applicationProtocols, noCache, cancellationToken); } /// @@ -167,9 +167,10 @@ internal Task GetServerConnection(ProxyServer server, Sessi /// if set to true [no cache]. /// The cancellation token for this async task. /// - internal async Task GetServerConnection(ProxyServer server, SessionEventArgsBase session, bool isConnect, + internal async Task GetServerConnection(RequestStateBase state, SessionEventArgsBase session, bool isConnect, List? applicationProtocols, bool noCache, CancellationToken cancellationToken) { + var server = state.Server; IExternalProxy? customUpStreamProxy = null; bool isHttps = session.IsHttps; @@ -210,7 +211,7 @@ internal async Task GetServerConnection(ProxyServer server, port, session.HttpClient.Request.HttpVersion, isHttps, applicationProtocols, isConnect, - server, session, session.HttpClient.UpStreamEndPoint ?? server.UpStreamEndPoint, + state, session, session.HttpClient.UpStreamEndPoint ?? server.UpStreamEndPoint, customUpStreamProxy ?? (isHttps ? server.UpStreamHttpsProxy : server.UpStreamHttpProxy), noCache, cancellationToken); } @@ -233,9 +234,11 @@ internal async Task GetServerConnection(ProxyServer server, /// internal async Task GetServerConnection(string remoteHostName, int remotePort, Version httpVersion, bool isHttps, List? applicationProtocols, bool isConnect, - ProxyServer proxyServer, SessionEventArgsBase? session, IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy, + RequestStateBase state, SessionEventArgsBase? session, IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy, bool noCache, CancellationToken cancellationToken) { + var proxyServer = state.Server; + var sslProtocol = session?.ClientConnection.SslProtocol ?? SslProtocols.None; var cacheKey = GetConnectionCacheKey(remoteHostName, remotePort, isHttps, applicationProtocols, upStreamEndPoint, externalProxy); @@ -260,10 +263,10 @@ internal async Task GetServerConnection(string remoteHostNa } } } - } - + } + var connection = await createServerConnection(remoteHostName, remotePort, httpVersion, isHttps, sslProtocol, - applicationProtocols, isConnect, proxyServer, session, upStreamEndPoint, externalProxy, cacheKey, cancellationToken); + applicationProtocols, isConnect, state, session, upStreamEndPoint, externalProxy, cacheKey, cancellationToken); return connection; } @@ -287,9 +290,10 @@ internal async Task GetServerConnection(string remoteHostNa /// private async Task createServerConnection(string remoteHostName, int remotePort, Version httpVersion, bool isHttps, SslProtocols sslProtocol, List? applicationProtocols, bool isConnect, - ProxyServer proxyServer, SessionEventArgsBase? session, IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy, string cacheKey, + RequestStateBase state, SessionEventArgsBase? session, IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy, string cacheKey, CancellationToken cancellationToken) { + var proxyServer = state.Server; // deny connection to proxy end points to avoid infinite connection loop. if (Server.ProxyEndPoints.Any(x => x.Port == remotePort) && NetworkHelper.IsLocalIpAddress(remoteHostName)) @@ -432,7 +436,7 @@ private async Task createServerConnection(string remoteHost { session.CustomUpStreamProxyUsed = newUpstreamProxy; session.TimeLine["Retrying Upstream Proxy Connection"] = DateTime.Now; - return await createServerConnection(remoteHostName, remotePort, httpVersion, isHttps, sslProtocol, applicationProtocols, isConnect, proxyServer, session, upStreamEndPoint, externalProxy, cacheKey, cancellationToken); + return await createServerConnection(remoteHostName, remotePort, httpVersion, isHttps, sslProtocol, applicationProtocols, isConnect, state, session, upStreamEndPoint, externalProxy, cacheKey, cancellationToken); } } @@ -481,8 +485,8 @@ private async Task createServerConnection(string remoteHost if (isHttps) { - var sslStream = new SslStream(stream, false, proxyServer.ValidateServerCertificate, - proxyServer.SelectClientCertificate); + var sslStream = new SslStream(stream, false, state.ValidateServerCertificate, + state.SelectClientCertificate); stream = new HttpServerStream(sslStream, proxyServer.BufferPool); var options = new SslClientAuthenticationOptions diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpServerConnection.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpServerConnection.cs index c924ba40e..7c6319176 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpServerConnection.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpServerConnection.cs @@ -17,7 +17,7 @@ internal class TcpServerConnection : IDisposable { public Guid Id { get; } = Guid.NewGuid(); - internal TcpServerConnection(ProxyServer proxyServer, TcpClient tcpClient, HttpServerStream stream, + internal TcpServerConnection(ProxyServerBase proxyServer, TcpClient tcpClient, HttpServerStream stream, string hostName, int port, bool isHttps, SslApplicationProtocol negotiatedApplicationProtocol, Version version, IExternalProxy? upStreamProxy, IPEndPoint? upStreamEndPoint, string cacheKey) { @@ -37,7 +37,7 @@ internal TcpServerConnection(ProxyServer proxyServer, TcpClient tcpClient, HttpS CacheKey = cacheKey; } - private ProxyServer proxyServer { get; } + private ProxyServerBase proxyServer { get; } internal bool IsClosed => Stream.IsClosed; diff --git a/src/Titanium.Web.Proxy/ProxyAuthorizationHandler.cs b/src/Titanium.Web.Proxy/ProxyAuthorizationHandler.cs index a70eda3fd..be8b7c26f 100644 --- a/src/Titanium.Web.Proxy/ProxyAuthorizationHandler.cs +++ b/src/Titanium.Web.Proxy/ProxyAuthorizationHandler.cs @@ -10,7 +10,7 @@ namespace Titanium.Web.Proxy { - public partial class ProxyServer + public partial class ProxyServerBase { /// /// Callback to authorize clients of this proxy instance. diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index 64ea30829..0b8374aa5 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -18,50 +18,149 @@ using Titanium.Web.Proxy.StreamExtended.Network; namespace Titanium.Web.Proxy -{ +{ + public class RequestState : RequestState + { + + } + public class ProxyServer : RequestState.ProxyServer + { + + } + public abstract partial class RequestStateBase + { + public abstract ProxyServerBase Server { get; } + internal abstract TcpClientConnection ClientConnection { get; } + + public virtual async Task OnErrorAsync(Exception e) + { + Server.ExceptionFunc?.Invoke(e); + } + public virtual void OnError(Exception e) + { + Server.ExceptionFunc?.Invoke(e); + } + + } + public class RequestState : RequestStateBase, IDisposable + where TRequestState : RequestState, new() + { + ProxyServer server; + TcpClientConnection clientConnection; + public override ProxyServerBase Server => server; + internal override TcpClientConnection ClientConnection => clientConnection; + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + ClientConnection?.Dispose(); + // dispose managed state (managed objects). + } + + // free unmanaged resources (unmanaged objects) and override a finalizer below. + // set large fields to null. + + disposedValue = true; + } + } + + // override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~RequestState() + // { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + public class ProxyServer : ProxyServerBase + { + protected override async Task handleClient(TcpClient tcpClient, ProxyEndPoint endPoint) + { + tcpClient.ReceiveTimeout = ConnectionTimeOutSeconds * 1000; + tcpClient.SendTimeout = ConnectionTimeOutSeconds * 1000; + + tcpClient.LingerState = new LingerOption(true, TcpTimeWaitSeconds); + + await InvokeConnectionCreateEvent(tcpClient, true); + using (var requestState = new TRequestState()) + { + var clientConnection = new TcpClientConnection(this, tcpClient); + requestState.server = this; + requestState.clientConnection = clientConnection; + + if (endPoint is TransparentProxyEndPoint tep) + { + await handleClient(tep, requestState); + } + else + { + await handleClient((ExplicitProxyEndPoint)endPoint, requestState); + } + } + } + } + } /// /// /// This class is the backbone of proxy. One can create as many instances as needed. /// However care should be taken to avoid using the same listening ports across multiple instances. /// - public partial class ProxyServer : IDisposable - { + public abstract partial class ProxyServerBase : IDisposable + { + public static implicit operator ProxyServerBase(RequestStateBase state) + { + return state.Server; + } /// /// HTTP & HTTPS scheme shorthands. /// - internal static readonly string UriSchemeHttp = Uri.UriSchemeHttp; - internal static readonly string UriSchemeHttps = Uri.UriSchemeHttps; - - internal static ByteString UriSchemeHttp8 = (ByteString)UriSchemeHttp; - internal static ByteString UriSchemeHttps8 = (ByteString)UriSchemeHttps; - - + internal static readonly string UriSchemeHttp = Uri.UriSchemeHttp; + internal static readonly string UriSchemeHttps = Uri.UriSchemeHttps; + + internal static ByteString UriSchemeHttp8 = (ByteString)UriSchemeHttp; + internal static ByteString UriSchemeHttps8 = (ByteString)UriSchemeHttps; + + /// /// A default exception log func. /// - private readonly ExceptionHandler defaultExceptionFunc = e => { }; - + private readonly ExceptionHandler defaultExceptionFunc = e => { }; + /// /// Backing field for exposed public property. /// - private int clientConnectionCount; - + private int clientConnectionCount; + /// /// Backing field for exposed public property. /// - private ExceptionHandler? exceptionFunc; - + private ExceptionHandler? exceptionFunc; + /// /// Backing field for exposed public property. /// - private int serverConnectionCount; - + private int serverConnectionCount; + /// /// Upstream proxy manager. /// - private WinHttpWebProxyFinder? systemProxyResolver; - - + private WinHttpWebProxyFinder? systemProxyResolver; + + /// /// /// Initializes a new instance of ProxyServer class with provided parameters. @@ -75,12 +174,12 @@ public partial class ProxyServer : IDisposable /// Should we attempt to trust certificates with elevated permissions by /// prompting for UAC if required? /// - public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, - bool trustRootCertificateAsAdmin = false) : this(null, null, userTrustRootCertificate, - machineTrustRootCertificate, trustRootCertificateAsAdmin) - { - } - + public ProxyServerBase(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, + bool trustRootCertificateAsAdmin = false) : this(null, null, userTrustRootCertificate, + machineTrustRootCertificate, trustRootCertificateAsAdmin) + { + } + /// /// Initializes a new instance of ProxyServer class with provided parameters. /// @@ -95,48 +194,48 @@ public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCe /// Should we attempt to trust certificates with elevated permissions by /// prompting for UAC if required? /// - public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerName, - bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, - bool trustRootCertificateAsAdmin = false) - { - BufferPool = new DefaultBufferPool(); - ProxyEndPoints = new List(); - tcpConnectionFactory = new TcpConnectionFactory(this); - if (RunTime.IsWindows && !RunTime.IsUwpOnWindows) - { - systemProxySettingsManager = new SystemProxyManager(); - } - - CertificateManager = new CertificateManager(rootCertificateName, rootCertificateIssuerName, - userTrustRootCertificate, machineTrustRootCertificate, trustRootCertificateAsAdmin, ExceptionFunc); - } - + public ProxyServerBase(string? rootCertificateName, string? rootCertificateIssuerName, + bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, + bool trustRootCertificateAsAdmin = false) + { + BufferPool = new DefaultBufferPool(); + ProxyEndPoints = new List(); + tcpConnectionFactory = new TcpConnectionFactory(this); + if (RunTime.IsWindows && !RunTime.IsUwpOnWindows) + { + systemProxySettingsManager = new SystemProxyManager(); + } + + CertificateManager = new CertificateManager(rootCertificateName, rootCertificateIssuerName, + userTrustRootCertificate, machineTrustRootCertificate, trustRootCertificateAsAdmin, ExceptionFunc); + } + /// /// An factory that creates tcp connection to server. /// - private TcpConnectionFactory tcpConnectionFactory { get; } - + internal TcpConnectionFactory tcpConnectionFactory { get; } + /// /// Manage system proxy settings. /// - private SystemProxyManager? systemProxySettingsManager { get; } - + private SystemProxyManager? systemProxySettingsManager { get; } + /// /// Number of exception retries when connection pool is enabled. /// - private int retries => EnableConnectionPool ? MaxCachedConnections : 0; - + private int retries => EnableConnectionPool ? MaxCachedConnections : 0; + /// /// Is the proxy currently running? /// - public bool ProxyRunning { get; private set; } - + public bool ProxyRunning { get; private set; } + /// /// Gets or sets a value indicating whether requests will be chained to upstream gateway. /// Defaults to false. /// - public bool ForwardToUpstreamGateway { get; set; } - + public bool ForwardToUpstreamGateway { get; set; } + /// /// Enable disable Windows Authentication (NTLM/Kerberos). /// Note: NTLM/Kerberos will always send local credentials of current user @@ -144,35 +243,35 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam /// in middle attack with Windows domain authentication is not currently supported. /// Defaults to false. /// - public bool EnableWinAuth { get; set; } - + public bool EnableWinAuth { get; set; } + /// /// Enable disable HTTP/2 support. /// Warning: HTTP/2 support is very limited /// - only enabled when both client and server supports it (no protocol changing in proxy) /// - cannot modify the request/response (e.g header modifications in BeforeRequest/Response events are ignored) /// - public bool EnableHttp2 { get; set; } = false; - + public bool EnableHttp2 { get; set; } = false; + /// /// Should we check for certificate revocation during SSL authentication to servers /// Note: If enabled can reduce performance. Defaults to false. /// - public X509RevocationMode CheckCertificateRevocation { get; set; } - + public X509RevocationMode CheckCertificateRevocation { get; set; } + /// /// Does this proxy uses the HTTP protocol 100 continue behaviour strictly? /// Broken 100 continue implementations on server/client may cause problems if enabled. /// Defaults to false. /// - public bool Enable100ContinueBehaviour { get; set; } - + public bool Enable100ContinueBehaviour { get; set; } + /// /// Should we enable experimental server connection pool? /// Defaults to true. /// - public bool EnableConnectionPool { get; set; } = true; - + public bool EnableConnectionPool { get; set; } = true; + /// /// Should we enable tcp server connection prefetching? /// When enabled, as soon as we receive a client connection we concurrently initiate @@ -181,731 +280,712 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam /// If a server connection is available in cache then this prefetch task will immediately return with the available connection from cache. /// Defaults to true. /// - public bool EnableTcpServerConnectionPrefetch { get; set; } = true; - + public bool EnableTcpServerConnectionPrefetch { get; set; } = true; + /// /// Gets or sets a Boolean value that specifies whether server and client stream Sockets are using the Nagle algorithm. /// Defaults to true, no nagle algorithm is used. /// - public bool NoDelay { get; set; } = true; - + public bool NoDelay { get; set; } = true; + /// /// Seconds client/server connection are to be kept alive when waiting for read/write to complete. /// This will also determine the pool eviction time when connection pool is enabled. /// Default value is 60 seconds. /// - public int ConnectionTimeOutSeconds { get; set; } = 60; - + public int ConnectionTimeOutSeconds { get; set; } = 60; + /// /// Seconds server connection are to wait for connection to be established. /// Default value is 20 seconds. /// - public int ConnectTimeOutSeconds { get; set; } = 20; - + public int ConnectTimeOutSeconds { get; set; } = 20; + /// /// Maximum number of concurrent connections per remote host in cache. /// Only valid when connection pooling is enabled. /// Default value is 2. /// - public int MaxCachedConnections { get; set; } = 2; - + public int MaxCachedConnections { get; set; } = 2; + /// /// Number of seconds to linger when Tcp connection is in TIME_WAIT state. /// Default value is 30. /// - public int TcpTimeWaitSeconds { get; set; } = 30; - + public int TcpTimeWaitSeconds { get; set; } = 30; + /// /// Should we reuse client/server tcp sockets. /// Default is true (disabled for linux/macOS due to bug in .Net core). /// - public bool ReuseSocket { get; set; } = true; - + public bool ReuseSocket { get; set; } = true; + /// /// Total number of active client connections. /// - public int ClientConnectionCount => clientConnectionCount; - + public int ClientConnectionCount => clientConnectionCount; + /// /// Total number of active server connections. /// - public int ServerConnectionCount => serverConnectionCount; - + public int ServerConnectionCount => serverConnectionCount; + /// /// Realm used during Proxy Basic Authentication. /// - public string ProxyAuthenticationRealm { get; set; } = "TitaniumProxy"; - + public string ProxyAuthenticationRealm { get; set; } = "TitaniumProxy"; + /// /// List of supported Ssl versions. /// #pragma warning disable 618 - public SslProtocols SupportedSslProtocols { get; set; } = SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; + public SslProtocols SupportedSslProtocols { get; set; } = SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; #pragma warning restore 618 - + /// /// The buffer pool used throughout this proxy instance. /// Set custom implementations by implementing this interface. /// By default this uses DefaultBufferPool implementation available in StreamExtended library package. /// Buffer size should be at least 10 bytes. /// - public IBufferPool BufferPool { get; set; } - + public IBufferPool BufferPool { get; set; } + /// /// Manages certificates used by this proxy. /// - public CertificateManager CertificateManager { get; } - + public CertificateManager CertificateManager { get; } + /// /// External proxy used for Http requests. /// - public IExternalProxy? UpStreamHttpProxy { get; set; } - + public IExternalProxy? UpStreamHttpProxy { get; set; } + /// /// External proxy used for Https requests. /// - public IExternalProxy? UpStreamHttpsProxy { get; set; } - + public IExternalProxy? UpStreamHttpsProxy { get; set; } + /// /// Local adapter/NIC endpoint where proxy makes request via. /// Defaults via any IP addresses of this machine. /// - public IPEndPoint? UpStreamEndPoint { get; set; } - + public IPEndPoint? UpStreamEndPoint { get; set; } + /// /// A list of IpAddress and port this proxy is listening to. /// - public List ProxyEndPoints { get; set; } - + public List ProxyEndPoints { get; set; } + /// /// A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests. /// User should return the ExternalProxy object with valid credentials. /// - public Func>? GetCustomUpStreamProxyFunc { get; set; } - + public Func>? GetCustomUpStreamProxyFunc { get; set; } + /// /// A callback to provide a chance for an upstream proxy failure to be handled by a new upstream proxy. /// User should return the ExternalProxy object with valid credentials or null. /// - public Func>? CustomUpStreamProxyFailureFunc { get; set; } - + public Func>? CustomUpStreamProxyFailureFunc { get; set; } + /// /// Callback for error events in this proxy instance. /// - public ExceptionHandler ExceptionFunc - { - get => exceptionFunc ?? defaultExceptionFunc; - set - { - exceptionFunc = value; - CertificateManager.ExceptionFunc = value; - } - } - + public ExceptionHandler ExceptionFunc + { + get => exceptionFunc ?? defaultExceptionFunc; + set + { + exceptionFunc = value; + CertificateManager.ExceptionFunc = value; + } + } + /// + /// Handle exception. + /// + /// The client stream. + /// The exception. + private void onException(HttpClientStream clientStream, Exception exception) + { + ExceptionFunc(exception); + } + + + /// /// A callback to authenticate proxy clients via basic authentication. /// Parameters are username and password as provided by client. /// Should return true for successful authentication. /// - public Func>? ProxyBasicAuthenticateFunc { get; set; } - + public Func>? ProxyBasicAuthenticateFunc { get; set; } + /// /// A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc. /// Parameters are current working session, schemeType, and token as provided by a calling client. /// Should return success for successful authentication, continuation if the package requests, or failure. /// - public Func>? ProxySchemeAuthenticateFunc { get; set; } - + public Func>? ProxySchemeAuthenticateFunc { get; set; } + /// /// A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required. /// Works in relation with ProxySchemeAuthenticateFunc. /// - public IEnumerable ProxyAuthenticationSchemes { get; set; } = new string[0]; - + public IEnumerable ProxyAuthenticationSchemes { get; set; } = new string[0]; + /// /// Event occurs when client connection count changed. /// - public event EventHandler? ClientConnectionCountChanged; - + public event EventHandler? ClientConnectionCountChanged; + /// /// Event occurs when server connection count changed. /// - public event EventHandler? ServerConnectionCountChanged; - + public event EventHandler? ServerConnectionCountChanged; + /// /// Event to override the default verification logic of remote SSL certificate received during authentication. /// - public event AsyncEventHandler? ServerCertificateValidationCallback; - + public event AsyncEventHandler? ServerCertificateValidationCallback; + /// /// Event to override client certificate selection during mutual SSL authentication. /// - public event AsyncEventHandler? ClientCertificateSelectionCallback; - + public event AsyncEventHandler? ClientCertificateSelectionCallback; + /// /// Intercept request event to server. /// - public event AsyncEventHandler? BeforeRequest; - + public event AsyncEventHandler? BeforeRequest; + /// /// Intercept response event from server. /// - public event AsyncEventHandler? BeforeResponse; - + public event AsyncEventHandler? BeforeResponse; + /// /// Intercept after response event from server. /// - public event AsyncEventHandler? AfterResponse; - + public event AsyncEventHandler? AfterResponse; + /// /// Customize TcpClient used for client connection upon create. /// - public event AsyncEventHandler? OnClientConnectionCreate; - + public event AsyncEventHandler? OnClientConnectionCreate; + /// /// Customize TcpClient used for server connection upon create. /// - public event AsyncEventHandler? OnServerConnectionCreate; - + public event AsyncEventHandler? OnServerConnectionCreate; + /// /// Customize the minimum ThreadPool size (increase it on a server) /// - public int ThreadPoolWorkerThread { get; set; } = Environment.ProcessorCount; - + public int ThreadPoolWorkerThread { get; set; } = Environment.ProcessorCount; + /// /// Add a proxy end point. /// /// The proxy endpoint. - public void AddEndPoint(ProxyEndPoint endPoint) - { - if (ProxyEndPoints.Any(x => - x.IpAddress.Equals(endPoint.IpAddress) && endPoint.Port != 0 && x.Port == endPoint.Port)) - { - throw new Exception("Cannot add another endpoint to same port & ip address"); - } - - ProxyEndPoints.Add(endPoint); - - if (ProxyRunning) - { - listen(endPoint); - } - } - + public void AddEndPoint(ProxyEndPoint endPoint) + { + if (ProxyEndPoints.Any(x => + x.IpAddress.Equals(endPoint.IpAddress) && endPoint.Port != 0 && x.Port == endPoint.Port)) + { + throw new Exception("Cannot add another endpoint to same port & ip address"); + } + + ProxyEndPoints.Add(endPoint); + + if (ProxyRunning) + { + listen(endPoint); + } + } + /// /// Remove a proxy end point. /// Will throw error if the end point doesn't exist. /// /// The existing endpoint to remove. - public void RemoveEndPoint(ProxyEndPoint endPoint) - { - if (ProxyEndPoints.Contains(endPoint) == false) - { - throw new Exception("Cannot remove endPoints not added to proxy"); - } - - ProxyEndPoints.Remove(endPoint); - - if (ProxyRunning) - { - quitListen(endPoint); - } - } - + public void RemoveEndPoint(ProxyEndPoint endPoint) + { + if (ProxyEndPoints.Contains(endPoint) == false) + { + throw new Exception("Cannot remove endPoints not added to proxy"); + } + + ProxyEndPoints.Remove(endPoint); + + if (ProxyRunning) + { + quitListen(endPoint); + } + } + /// /// Set the given explicit end point as the default proxy server for current machine. /// /// The explicit endpoint. - public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint) - { - SetAsSystemProxy(endPoint, ProxyProtocolType.Http); - } - + public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint) + { + SetAsSystemProxy(endPoint, ProxyProtocolType.Http); + } + /// /// Set the given explicit end point as the default proxy server for current machine. /// /// The explicit endpoint. - public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) - { - SetAsSystemProxy(endPoint, ProxyProtocolType.Https); - } - + public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) + { + SetAsSystemProxy(endPoint, ProxyProtocolType.Https); + } + /// /// Set the given explicit end point as the default proxy server for current machine. /// /// The explicit endpoint. /// The proxy protocol type. - public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType) - { - if (systemProxySettingsManager == null) - { + public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType) + { + if (systemProxySettingsManager == null) + { throw new NotSupportedException(@"Setting system proxy settings are only supported in Windows. - Please manually confugure you operating system to use this proxy's port and address."); - } - - validateEndPointAsSystemProxy(endPoint); - - bool isHttp = (protocolType & ProxyProtocolType.Http) > 0; - bool isHttps = (protocolType & ProxyProtocolType.Https) > 0; - - if (isHttps) - { - CertificateManager.EnsureRootCertificate(); - - // If certificate was trusted by the machine - if (!CertificateManager.CertValidated) - { - protocolType = protocolType & ~ProxyProtocolType.Https; - isHttps = false; - } - } - - // clear any settings previously added - if (isHttp) - { - ProxyEndPoints.OfType().ToList().ForEach(x => x.IsSystemHttpProxy = false); - } - - if (isHttps) - { - ProxyEndPoints.OfType().ToList().ForEach(x => x.IsSystemHttpsProxy = false); - } - - systemProxySettingsManager.SetProxy( - Equals(endPoint.IpAddress, IPAddress.Any) | - Equals(endPoint.IpAddress, IPAddress.Loopback) - ? "localhost" - : endPoint.IpAddress.ToString(), - endPoint.Port, - protocolType); - - if (isHttp) - { - endPoint.IsSystemHttpProxy = true; - } - - if (isHttps) - { - endPoint.IsSystemHttpsProxy = true; - } - - string? proxyType = null; - switch (protocolType) - { - case ProxyProtocolType.Http: - proxyType = "HTTP"; - break; - case ProxyProtocolType.Https: - proxyType = "HTTPS"; - break; - case ProxyProtocolType.AllHttp: - proxyType = "HTTP and HTTPS"; - break; - } - - if (protocolType != ProxyProtocolType.None) - { - Console.WriteLine("Set endpoint at Ip {0} and port: {1} as System {2} Proxy", endPoint.IpAddress, - endPoint.Port, proxyType); - } - } - + Please manually confugure you operating system to use this proxy's port and address."); + } + + validateEndPointAsSystemProxy(endPoint); + + bool isHttp = (protocolType & ProxyProtocolType.Http) > 0; + bool isHttps = (protocolType & ProxyProtocolType.Https) > 0; + + if (isHttps) + { + CertificateManager.EnsureRootCertificate(); + + // If certificate was trusted by the machine + if (!CertificateManager.CertValidated) + { + protocolType = protocolType & ~ProxyProtocolType.Https; + isHttps = false; + } + } + + // clear any settings previously added + if (isHttp) + { + ProxyEndPoints.OfType().ToList().ForEach(x => x.IsSystemHttpProxy = false); + } + + if (isHttps) + { + ProxyEndPoints.OfType().ToList().ForEach(x => x.IsSystemHttpsProxy = false); + } + + systemProxySettingsManager.SetProxy( + Equals(endPoint.IpAddress, IPAddress.Any) | + Equals(endPoint.IpAddress, IPAddress.Loopback) + ? "localhost" + : endPoint.IpAddress.ToString(), + endPoint.Port, + protocolType); + + if (isHttp) + { + endPoint.IsSystemHttpProxy = true; + } + + if (isHttps) + { + endPoint.IsSystemHttpsProxy = true; + } + + string? proxyType = null; + switch (protocolType) + { + case ProxyProtocolType.Http: + proxyType = "HTTP"; + break; + case ProxyProtocolType.Https: + proxyType = "HTTPS"; + break; + case ProxyProtocolType.AllHttp: + proxyType = "HTTP and HTTPS"; + break; + } + + if (protocolType != ProxyProtocolType.None) + { + Console.WriteLine("Set endpoint at Ip {0} and port: {1} as System {2} Proxy", endPoint.IpAddress, + endPoint.Port, proxyType); + } + } + /// /// Clear HTTP proxy settings of current machine. /// - public void DisableSystemHttpProxy() - { - DisableSystemProxy(ProxyProtocolType.Http); - } - + public void DisableSystemHttpProxy() + { + DisableSystemProxy(ProxyProtocolType.Http); + } + /// /// Clear HTTPS proxy settings of current machine. /// - public void DisableSystemHttpsProxy() - { - DisableSystemProxy(ProxyProtocolType.Https); - } - + public void DisableSystemHttpsProxy() + { + DisableSystemProxy(ProxyProtocolType.Https); + } + /// /// Restores the original proxy settings. /// - public void RestoreOriginalProxySettings() - { - if (systemProxySettingsManager == null) - { + public void RestoreOriginalProxySettings() + { + if (systemProxySettingsManager == null) + { throw new NotSupportedException(@"Setting system proxy settings are only supported in Windows. - Please manually configure your operating system to use this proxy's port and address."); - } - - systemProxySettingsManager.RestoreOriginalSettings(); - } - + Please manually configure your operating system to use this proxy's port and address."); + } + + systemProxySettingsManager.RestoreOriginalSettings(); + } + /// /// Clear the specified proxy setting for current machine. /// - public void DisableSystemProxy(ProxyProtocolType protocolType) - { - if (systemProxySettingsManager == null) - { + public void DisableSystemProxy(ProxyProtocolType protocolType) + { + if (systemProxySettingsManager == null) + { throw new NotSupportedException(@"Setting system proxy settings are only supported in Windows. - Please manually configure your operating system to use this proxy's port and address."); - } - - systemProxySettingsManager.RemoveProxy(protocolType); - } - + Please manually configure your operating system to use this proxy's port and address."); + } + + systemProxySettingsManager.RemoveProxy(protocolType); + } + /// /// Clear all proxy settings for current machine. /// - public void DisableAllSystemProxies() - { - if (systemProxySettingsManager == null) - { + public void DisableAllSystemProxies() + { + if (systemProxySettingsManager == null) + { throw new NotSupportedException(@"Setting system proxy settings are only supported in Windows. - Please manually confugure you operating system to use this proxy's port and address."); - } - - systemProxySettingsManager.DisableAllProxy(); - } - + Please manually confugure you operating system to use this proxy's port and address."); + } + + systemProxySettingsManager.DisableAllProxy(); + } + /// /// Start this proxy server instance. /// - public void Start() - { - if (ProxyRunning) - { - throw new Exception("Proxy is already running."); - } - - setThreadPoolMinThread(ThreadPoolWorkerThread); - - if (ProxyEndPoints.OfType().Any(x => x.GenericCertificate == null)) - { - CertificateManager.EnsureRootCertificate(); - } - - // clear any system proxy settings which is pointing to our own endpoint (causing a cycle) - // due to ungracious proxy shutdown before or something else - if (systemProxySettingsManager != null && RunTime.IsWindows && !RunTime.IsUwpOnWindows) - { - var proxyInfo = systemProxySettingsManager.GetProxyInfoFromRegistry(); - if (proxyInfo?.Proxies != null) - { - var protocolToRemove = ProxyProtocolType.None; - foreach (var proxy in proxyInfo.Proxies.Values) - { - if (NetworkHelper.IsLocalIpAddress(proxy.HostName) - && ProxyEndPoints.Any(x => x.Port == proxy.Port)) - { - protocolToRemove |= proxy.ProtocolType; - } - } - - if (protocolToRemove != ProxyProtocolType.None) - { - systemProxySettingsManager.RemoveProxy(protocolToRemove, false); - } - } - } - - if (ForwardToUpstreamGateway && GetCustomUpStreamProxyFunc == null && systemProxySettingsManager != null) - { - // Use WinHttp to handle PAC/WAPD scripts. - systemProxyResolver = new WinHttpWebProxyFinder(); - systemProxyResolver.LoadFromIE(); - - GetCustomUpStreamProxyFunc = getSystemUpStreamProxy; - } - - ProxyRunning = true; - - CertificateManager.ClearIdleCertificates(); - - foreach (var endPoint in ProxyEndPoints) - { - listen(endPoint); - } - } - + public void Start() + { + if (ProxyRunning) + { + throw new Exception("Proxy is already running."); + } + + setThreadPoolMinThread(ThreadPoolWorkerThread); + + if (ProxyEndPoints.OfType().Any(x => x.GenericCertificate == null)) + { + CertificateManager.EnsureRootCertificate(); + } + + // clear any system proxy settings which is pointing to our own endpoint (causing a cycle) + // due to ungracious proxy shutdown before or something else + if (systemProxySettingsManager != null && RunTime.IsWindows && !RunTime.IsUwpOnWindows) + { + var proxyInfo = systemProxySettingsManager.GetProxyInfoFromRegistry(); + if (proxyInfo?.Proxies != null) + { + var protocolToRemove = ProxyProtocolType.None; + foreach (var proxy in proxyInfo.Proxies.Values) + { + if (NetworkHelper.IsLocalIpAddress(proxy.HostName) + && ProxyEndPoints.Any(x => x.Port == proxy.Port)) + { + protocolToRemove |= proxy.ProtocolType; + } + } + + if (protocolToRemove != ProxyProtocolType.None) + { + systemProxySettingsManager.RemoveProxy(protocolToRemove, false); + } + } + } + + if (ForwardToUpstreamGateway && GetCustomUpStreamProxyFunc == null && systemProxySettingsManager != null) + { + // Use WinHttp to handle PAC/WAPD scripts. + systemProxyResolver = new WinHttpWebProxyFinder(); + systemProxyResolver.LoadFromIE(); + + GetCustomUpStreamProxyFunc = getSystemUpStreamProxy; + } + + ProxyRunning = true; + + CertificateManager.ClearIdleCertificates(); + + foreach (var endPoint in ProxyEndPoints) + { + listen(endPoint); + } + } + /// /// Stop this proxy server instance. /// - public void Stop() - { - if (!ProxyRunning) - { - throw new Exception("Proxy is not running."); - } - - if (systemProxySettingsManager != null) - { - bool setAsSystemProxy = ProxyEndPoints.OfType() - .Any(x => x.IsSystemHttpProxy || x.IsSystemHttpsProxy); - - if (setAsSystemProxy) - { - systemProxySettingsManager.RestoreOriginalSettings(); - } - } - - foreach (var endPoint in ProxyEndPoints) - { - quitListen(endPoint); - } - - ProxyEndPoints.Clear(); - - CertificateManager?.StopClearIdleCertificates(); - tcpConnectionFactory.Dispose(); - - ProxyRunning = false; - } - + public void Stop() + { + if (!ProxyRunning) + { + throw new Exception("Proxy is not running."); + } + + if (systemProxySettingsManager != null) + { + bool setAsSystemProxy = ProxyEndPoints.OfType() + .Any(x => x.IsSystemHttpProxy || x.IsSystemHttpsProxy); + + if (setAsSystemProxy) + { + systemProxySettingsManager.RestoreOriginalSettings(); + } + } + + foreach (var endPoint in ProxyEndPoints) + { + quitListen(endPoint); + } + + ProxyEndPoints.Clear(); + + CertificateManager?.StopClearIdleCertificates(); + tcpConnectionFactory.Dispose(); + + ProxyRunning = false; + } + /// /// Listen on given end point of local machine. /// /// The end point to listen. - private void listen(ProxyEndPoint endPoint) - { - endPoint.Listener = new TcpListener(endPoint.IpAddress, endPoint.Port); - - if (ReuseSocket && RunTime.IsSocketReuseAvailable) - { - endPoint.Listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - - try - { - endPoint.Listener.Start(); - - endPoint.Port = ((IPEndPoint)endPoint.Listener.LocalEndpoint).Port; - - // accept clients asynchronously - endPoint.Listener.BeginAcceptTcpClient(onAcceptConnection, endPoint); - } - catch (SocketException ex) - { - var pex = new Exception( - $"Endpoint {endPoint} failed to start. Check inner exception and exception data for details.", ex); - pex.Data.Add("ipAddress", endPoint.IpAddress); - pex.Data.Add("port", endPoint.Port); - throw pex; - } - } - + private void listen(ProxyEndPoint endPoint) + { + endPoint.Listener = new TcpListener(endPoint.IpAddress, endPoint.Port); + + if (ReuseSocket && RunTime.IsSocketReuseAvailable) + { + endPoint.Listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + } + + try + { + endPoint.Listener.Start(); + + endPoint.Port = ((IPEndPoint)endPoint.Listener.LocalEndpoint).Port; + + // accept clients asynchronously + endPoint.Listener.BeginAcceptTcpClient(onAcceptConnection, endPoint); + } + catch (SocketException ex) + { + var pex = new Exception( + $"Endpoint {endPoint} failed to start. Check inner exception and exception data for details.", ex); + pex.Data.Add("ipAddress", endPoint.IpAddress); + pex.Data.Add("port", endPoint.Port); + throw pex; + } + } + /// /// Verify if its safe to set this end point as system proxy. /// /// The end point to validate. - private void validateEndPointAsSystemProxy(ExplicitProxyEndPoint endPoint) - { - if (endPoint == null) - { - throw new ArgumentNullException(nameof(endPoint)); - } - - if (ProxyEndPoints.Contains(endPoint) == false) - { - throw new Exception("Cannot set endPoints not added to proxy as system proxy"); - } - - if (!ProxyRunning) - { - throw new Exception("Cannot set system proxy settings before proxy has been started."); - } - } - + private void validateEndPointAsSystemProxy(ExplicitProxyEndPoint endPoint) + { + if (endPoint == null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + + if (ProxyEndPoints.Contains(endPoint) == false) + { + throw new Exception("Cannot set endPoints not added to proxy as system proxy"); + } + + if (!ProxyRunning) + { + throw new Exception("Cannot set system proxy settings before proxy has been started."); + } + } + /// /// Gets the system up stream proxy. /// /// The session. /// The external proxy as task result. - private Task getSystemUpStreamProxy(SessionEventArgsBase sessionEventArgs) - { - var proxy = systemProxyResolver!.GetProxy(sessionEventArgs.HttpClient.Request.RequestUri); - return Task.FromResult(proxy); - } - + private Task getSystemUpStreamProxy(SessionEventArgsBase sessionEventArgs) + { + var proxy = systemProxyResolver!.GetProxy(sessionEventArgs.HttpClient.Request.RequestUri); + return Task.FromResult(proxy); + } + /// /// Act when a connection is received from client. /// - private void onAcceptConnection(IAsyncResult asyn) - { - var endPoint = (ProxyEndPoint)asyn.AsyncState; - - TcpClient? tcpClient = null; - - try - { - // based on end point type call appropriate request handlers - tcpClient = endPoint.Listener.EndAcceptTcpClient(asyn); - tcpClient.NoDelay = NoDelay; - } - catch (ObjectDisposedException) - { - // The listener was Stop()'d, disposing the underlying socket and - // triggering the completion of the callback. We're already exiting, - // so just return. - return; - } - catch - { - // Other errors are discarded to keep proxy running - } - - if (tcpClient != null) - { - Task.Run(async () => - { - await handleClient(tcpClient, endPoint); - }); - } - - // Get the listener that handles the client request. - endPoint.Listener.BeginAcceptTcpClient(onAcceptConnection, endPoint); - } - - + private void onAcceptConnection(IAsyncResult asyn) + { + var endPoint = (ProxyEndPoint)asyn.AsyncState; + + TcpClient? tcpClient = null; + + try + { + // based on end point type call appropriate request handlers + tcpClient = endPoint.Listener.EndAcceptTcpClient(asyn); + tcpClient.NoDelay = NoDelay; + } + catch (ObjectDisposedException) + { + // The listener was Stop()'d, disposing the underlying socket and + // triggering the completion of the callback. We're already exiting, + // so just return. + return; + } + catch + { + // Other errors are discarded to keep proxy running + } + + if (tcpClient != null) + { + Task.Run(async () => + { + await handleClient(tcpClient, endPoint); + }); + } + + // Get the listener that handles the client request. + endPoint.Listener.BeginAcceptTcpClient(onAcceptConnection, endPoint); + } + + /// /// Change the ThreadPool.WorkerThread minThread /// /// minimum Threads allocated in the ThreadPool - private void setThreadPoolMinThread(int workerThreads) - { - ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads); - ThreadPool.GetMaxThreads(out int maxWorkerThreads, out _); - - minWorkerThreads = Math.Min(maxWorkerThreads, Math.Max(workerThreads, Environment.ProcessorCount)); - - ThreadPool.SetMinThreads(minWorkerThreads, minCompletionPortThreads); - } - - + private void setThreadPoolMinThread(int workerThreads) + { + ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads); + ThreadPool.GetMaxThreads(out int maxWorkerThreads, out _); + + minWorkerThreads = Math.Min(maxWorkerThreads, Math.Max(workerThreads, Environment.ProcessorCount)); + + ThreadPool.SetMinThreads(minWorkerThreads, minCompletionPortThreads); + } + + /// /// Handle the client. /// /// The client. /// The proxy endpoint. /// The task. - private async Task handleClient(TcpClient tcpClient, ProxyEndPoint endPoint) - { - tcpClient.ReceiveTimeout = ConnectionTimeOutSeconds * 1000; - tcpClient.SendTimeout = ConnectionTimeOutSeconds * 1000; - - tcpClient.LingerState = new LingerOption(true, TcpTimeWaitSeconds); - - await InvokeConnectionCreateEvent(tcpClient, true); - - using (var clientConnection = new TcpClientConnection(this, tcpClient)) - { - if (endPoint is TransparentProxyEndPoint tep) - { - await handleClient(tep, clientConnection); - } - else - { - await handleClient((ExplicitProxyEndPoint)endPoint, clientConnection); - } - } - } - - /// - /// Handle exception. - /// - /// The client stream. - /// The exception. - private void onException(HttpClientStream clientStream, Exception exception) - { - ExceptionFunc(exception); - } - + protected abstract Task handleClient(TcpClient tcpClient, ProxyEndPoint endPoint); + /// /// Quit listening on the given end point. /// - private void quitListen(ProxyEndPoint endPoint) - { - endPoint.Listener.Stop(); - endPoint.Listener.Server.Dispose(); - } - + private void quitListen(ProxyEndPoint endPoint) + { + endPoint.Listener.Stop(); + endPoint.Listener.Server.Dispose(); + } + /// /// Update client connection count. /// /// Should we increment/decrement? - internal void UpdateClientConnectionCount(bool increment) - { - if (increment) - { - Interlocked.Increment(ref clientConnectionCount); - } - else - { - Interlocked.Decrement(ref clientConnectionCount); - } - - ClientConnectionCountChanged?.Invoke(this, EventArgs.Empty); - } - + internal void UpdateClientConnectionCount(bool increment) + { + if (increment) + { + Interlocked.Increment(ref clientConnectionCount); + } + else + { + Interlocked.Decrement(ref clientConnectionCount); + } + + ClientConnectionCountChanged?.Invoke(this, EventArgs.Empty); + } + /// /// Update server connection count. /// /// Should we increment/decrement? - internal void UpdateServerConnectionCount(bool increment) - { - if (increment) - { - Interlocked.Increment(ref serverConnectionCount); - } - else - { - Interlocked.Decrement(ref serverConnectionCount); - } - - ServerConnectionCountChanged?.Invoke(this, EventArgs.Empty); - } - + internal void UpdateServerConnectionCount(bool increment) + { + if (increment) + { + Interlocked.Increment(ref serverConnectionCount); + } + else + { + Interlocked.Decrement(ref serverConnectionCount); + } + + ServerConnectionCountChanged?.Invoke(this, EventArgs.Empty); + } + /// /// Invoke client/server tcp connection events if subscribed by API user. /// /// The TcpClient object. /// Is this a client connection created event? If not then we would assume that its a server connection create event. /// - internal async Task InvokeConnectionCreateEvent(TcpClient client, bool isClientConnection) - { - // client connection created - if (isClientConnection && OnClientConnectionCreate != null) - { - await OnClientConnectionCreate.InvokeAsync(this, client, ExceptionFunc); - } - - // server connection created - if (!isClientConnection && OnServerConnectionCreate != null) - { - await OnServerConnectionCreate.InvokeAsync(this, client, ExceptionFunc); - } - } - + internal async Task InvokeConnectionCreateEvent(TcpClient client, bool isClientConnection) + { + // client connection created + if (isClientConnection && OnClientConnectionCreate != null) + { + await OnClientConnectionCreate.InvokeAsync(this, client, ExceptionFunc); + } + + // server connection created + if (!isClientConnection && OnServerConnectionCreate != null) + { + await OnServerConnectionCreate.InvokeAsync(this, client, ExceptionFunc); + } + } + /// /// Connection retry policy when using connection pool. /// - private RetryPolicy retryPolicy() where T : Exception - { - return new RetryPolicy(retries, tcpConnectionFactory); - } - + private RetryPolicy retryPolicy() where T : Exception + { + return new RetryPolicy(retries, tcpConnectionFactory); + } + /// /// Dispose the Proxy instance. /// - public void Dispose() - { - if (ProxyRunning) - { - Stop(); - } - - CertificateManager?.Dispose(); - BufferPool?.Dispose(); - } + public void Dispose() + { + if (ProxyRunning) + { + Stop(); + } + + CertificateManager?.Dispose(); + BufferPool?.Dispose(); + } } } diff --git a/src/Titanium.Web.Proxy/RequestHandler.cs b/src/Titanium.Web.Proxy/RequestHandler.cs index be93160fb..7e6412604 100644 --- a/src/Titanium.Web.Proxy/RequestHandler.cs +++ b/src/Titanium.Web.Proxy/RequestHandler.cs @@ -21,7 +21,7 @@ namespace Titanium.Web.Proxy /// /// Handle the request /// - public partial class ProxyServer + public partial class ProxyServerBase { /// /// This is the core request handler method for a particular connection from client. @@ -34,10 +34,11 @@ public partial class ProxyServer /// The cancellation token source for this async task. /// The Connect request if this is a HTTPS request from explicit endpoint. /// Prefetched server connection for current client using Connect/SNI headers. - private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientConnection clientConnection, + private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, RequestStateBase state, HttpClientStream clientStream, CancellationTokenSource cancellationTokenSource, TunnelConnectSessionEventArgs? connectArgs = null, Task? prefetchConnectionTask = null) { + var clientConnection = state.ClientConnection; var connectRequest = connectArgs?.HttpClient.ConnectRequest; var prefetchTask = prefetchConnectionTask; @@ -64,7 +65,7 @@ private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientCon return; } - var args = new SessionEventArgs(this, endPoint, clientConnection, clientStream, connectRequest, cancellationTokenSource) + var args = new SessionEventArgs(state, endPoint, clientConnection, clientStream, connectRequest, cancellationTokenSource) { UserData = connectArgs?.UserData }; @@ -266,7 +267,7 @@ private async Task handleHttpSessionRequest(SessionEventArgs args, // a connection generator task with captured parameters via closure. Func> generator = () => - tcpConnectionFactory.GetServerConnection(this, + tcpConnectionFactory.GetServerConnection(args.State, args, false, sslApplicationProtocol, diff --git a/src/Titanium.Web.Proxy/ResponseHandler.cs b/src/Titanium.Web.Proxy/ResponseHandler.cs index 782ec0b0b..8ec4ab455 100644 --- a/src/Titanium.Web.Proxy/ResponseHandler.cs +++ b/src/Titanium.Web.Proxy/ResponseHandler.cs @@ -11,14 +11,14 @@ namespace Titanium.Web.Proxy /// /// Handle the response from server. /// - public partial class ProxyServer + public partial class ProxyServerBase { /// /// Called asynchronously when a request was successful and we received the response. /// /// The session event arguments. /// The task. - private async Task handleHttpSessionResponse(SessionEventArgs args) + protected async Task handleHttpSessionResponse(SessionEventArgs args) { var cancellationToken = args.CancellationTokenSource.Token; @@ -132,7 +132,7 @@ await serverStream.CopyBodyAsync(response, false, clientStream, TransformationMo /// /// /// - private async Task onBeforeResponse(SessionEventArgs args) + protected async Task onBeforeResponse(SessionEventArgs args) { if (BeforeResponse != null) { @@ -145,7 +145,7 @@ private async Task onBeforeResponse(SessionEventArgs args) /// /// /// - private async Task onAfterResponse(SessionEventArgs args) + protected async Task onAfterResponse(SessionEventArgs args) { if (AfterResponse != null) { diff --git a/src/Titanium.Web.Proxy/TransparentClientHandler.cs b/src/Titanium.Web.Proxy/TransparentClientHandler.cs index 02da48df6..c82523a62 100644 --- a/src/Titanium.Web.Proxy/TransparentClientHandler.cs +++ b/src/Titanium.Web.Proxy/TransparentClientHandler.cs @@ -19,8 +19,8 @@ namespace Titanium.Web.Proxy { - public partial class ProxyServer - { + public partial class ProxyServerBase + { /// /// 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 @@ -28,8 +28,9 @@ public partial class ProxyServer /// The transparent endpoint. /// The client connection. /// - private async Task handleClient(TransparentProxyEndPoint endPoint, TcpClientConnection clientConnection) + internal virtual async Task handleClient(TransparentProxyEndPoint endPoint, RequestStateBase state) { + var clientConnection = state.ClientConnection; var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; @@ -47,7 +48,7 @@ private async Task handleClient(TransparentProxyEndPoint endPoint, TcpClientConn { httpsHostName = clientHelloInfo.GetServerName() ?? endPoint.GenericCertificateName; - var args = new BeforeSslAuthenticateEventArgs(cancellationTokenSource, httpsHostName); + var args = new BeforeSslAuthenticateEventArgs(state,cancellationTokenSource, httpsHostName); await endPoint.InvokeBeforeSslAuthenticate(this, args, ExceptionFunc); @@ -80,18 +81,18 @@ private async Task handleClient(TransparentProxyEndPoint endPoint, TcpClientConn catch (Exception e) { var certName = certificate?.GetNameInfo(X509NameType.SimpleName, false); - var session = new SessionEventArgs(this, endPoint, clientConnection, clientStream, null, + var session = new SessionEventArgs(state, endPoint, clientConnection, clientStream, null, cancellationTokenSource); throw new ProxyConnectException( $"Couldn't authenticate host '{httpsHostName}' with certificate '{certName}'.", e, session); - } - + } + } else { var connection = await tcpConnectionFactory.GetServerConnection(httpsHostName, endPoint.Port, HttpHeader.VersionUnknown, false, null, - true, this, null, UpStreamEndPoint, + true, state, null, UpStreamEndPoint, UpStreamHttpsProxy, true, cancellationToken); try @@ -131,7 +132,7 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, // HTTPS server created - we can now decrypt the client's traffic // Now create the request - await handleHttpSessionRequest(endPoint, clientConnection, clientStream, cancellationTokenSource); + await handleHttpSessionRequest(endPoint, state, clientStream, cancellationTokenSource); } catch (ProxyException e) { diff --git a/src/Titanium.Web.Proxy/WebSocketHandler.cs b/src/Titanium.Web.Proxy/WebSocketHandler.cs index e0c7df4bf..87a02316e 100644 --- a/src/Titanium.Web.Proxy/WebSocketHandler.cs +++ b/src/Titanium.Web.Proxy/WebSocketHandler.cs @@ -10,7 +10,7 @@ namespace Titanium.Web.Proxy { - public partial class ProxyServer + public partial class ProxyServerBase { /// diff --git a/src/Titanium.Web.Proxy/WinAuthHandler.cs b/src/Titanium.Web.Proxy/WinAuthHandler.cs index c72d333e1..3671cf7fe 100644 --- a/src/Titanium.Web.Proxy/WinAuthHandler.cs +++ b/src/Titanium.Web.Proxy/WinAuthHandler.cs @@ -11,7 +11,7 @@ namespace Titanium.Web.Proxy { - public partial class ProxyServer + public partial class ProxyServerBase { /// /// possible header names. From 8dbb49966838998a2d4fd05410e3c27f81fa5524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D1=8C=D0=BA=D0=BE=20=D0=90?= =?UTF-8?q?=20=D0=92?= Date: Sun, 1 Dec 2019 09:40:12 +0300 Subject: [PATCH 8/9] fix merge error --- src/Titanium.Web.Proxy/ExplicitClientHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs index f22946262..afcee9491 100644 --- a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs @@ -151,7 +151,7 @@ internal async Task handleClient(ExplicitProxyEndPoint endPoint, RequestStateBas try { // todo: this is a hack, because Titanium does not support HTTP protocol changing currently - var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, + var connection = await tcpConnectionFactory.GetServerConnection(state, connectArgs, true, SslExtensions.Http2ProtocolAsList, true, cancellationToken); From b13b1ca84c41cb5a6a852441f707ccd464ca16dc Mon Sep 17 00:00:00 2001 From: volanavlad Date: Sun, 1 Dec 2019 10:17:47 +0300 Subject: [PATCH 9/9] WIP dotnetcore.yml work in progress --- .github/workflows/dotnetcore.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/dotnetcore.yml diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml new file mode 100644 index 000000000..0f9d2a677 --- /dev/null +++ b/.github/workflows/dotnetcore.yml @@ -0,0 +1,17 @@ +name: .NET Core + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 2.2.108 + - name: Build with dotnet + run: dotnet build --configuration Release