diff --git a/Examples/Titanium.Web.Proxy.Examples.Basic/Helpers/ConsoleHelper.cs b/Examples/Titanium.Web.Proxy.Examples.Basic/Helpers/ConsoleHelper.cs new file mode 100644 index 000000000..86b06a857 --- /dev/null +++ b/Examples/Titanium.Web.Proxy.Examples.Basic/Helpers/ConsoleHelper.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.InteropServices; + +namespace Titanium.Web.Proxy.Examples.Basic.Helpers +{ + /// + /// Adapated from + /// http://stackoverflow.com/questions/13656846/how-to-programmatic-disable-c-sharp-console-applications-quick-edit-mode + /// + internal static class ConsoleHelper + { + const uint ENABLE_QUICK_EDIT = 0x0040; + + // STD_INPUT_HANDLE (DWORD): -10 is the standard input device. + const int STD_INPUT_HANDLE = -10; + + [DllImport("kernel32.dll", SetLastError = true)] + static extern IntPtr GetStdHandle(int nStdHandle); + + [DllImport("kernel32.dll")] + static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); + + [DllImport("kernel32.dll")] + static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); + + internal static bool DisableQuickEditMode() + { + + IntPtr consoleHandle = GetStdHandle(STD_INPUT_HANDLE); + + // get current console mode + uint consoleMode; + if (!GetConsoleMode(consoleHandle, out consoleMode)) + { + // ERROR: Unable to get console mode. + return false; + } + + // Clear the quick edit bit in the mode flags + consoleMode &= ~ENABLE_QUICK_EDIT; + + // set the new mode + if (!SetConsoleMode(consoleHandle, consoleMode)) + { + // ERROR: Unable to set console mode + return false; + } + + return true; + } + } +} diff --git a/Examples/Titanium.Web.Proxy.Examples.Basic/Program.cs b/Examples/Titanium.Web.Proxy.Examples.Basic/Program.cs index 13cc0fe86..bbfb5a25f 100644 --- a/Examples/Titanium.Web.Proxy.Examples.Basic/Program.cs +++ b/Examples/Titanium.Web.Proxy.Examples.Basic/Program.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.Runtime.InteropServices; +using Titanium.Web.Proxy.Examples.Basic.Helpers; namespace Titanium.Web.Proxy.Examples.Basic { @@ -9,11 +11,13 @@ public class Program public static void Main(string[] args) { + //fix console hang due to QuickEdit mode + ConsoleHelper.DisableQuickEditMode(); + //On Console exit make sure we also exit the proxy NativeMethods.Handler = ConsoleEventCallback; NativeMethods.SetConsoleCtrlHandler(NativeMethods.Handler, true); - //Start proxy controller controller.StartProxy(); @@ -50,5 +54,7 @@ internal static class NativeMethods // Pinvoke internal delegate bool ConsoleEventDelegate(int eventType); + } -} \ No newline at end of file + +} diff --git a/Examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs b/Examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs index fe9f16c1c..c88d2c483 100644 --- a/Examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs +++ b/Examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Net; +using System.Net.Security; using System.Threading.Tasks; using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Models; @@ -47,18 +47,18 @@ public void StartProxy() //Exclude Https addresses you don't want to proxy //Useful for clients that use certificate pinning //for example google.com and dropbox.com - ExcludedHttpsHostNameRegex = new List() { "dropbox.com" } + ExcludedHttpsHostNameRegex = new List { "dropbox.com" } //Include Https addresses you want to proxy (others will be excluded) //for example github.com - // IncludedHttpsHostNameRegex = new List() { "github.com" } + //IncludedHttpsHostNameRegex = new List { "github.com" } //You can set only one of the ExcludedHttpsHostNameRegex and IncludedHttpsHostNameRegex properties, otherwise ArgumentException will be thrown //Use self-issued generic certificate on all https requests //Optimizes performance by not creating a certificate for each https-enabled domain //Useful when certificate trust is not required by proxy clients - // GenericCertificate = new X509Certificate2(Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "genericcert.pfx"), "password") + //GenericCertificate = new X509Certificate2(Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "genericcert.pfx"), "password") }; //An explicit endpoint is where the client knows about the existence of a proxy @@ -107,7 +107,7 @@ public void Stop() //intecept & cancel redirect or update requests public async Task OnRequest(object sender, SessionEventArgs e) { - Console.WriteLine("Active Client Connections:" + ((ProxyServer) sender).ClientConnectionCount); + Console.WriteLine("Active Client Connections:" + ((ProxyServer)sender).ClientConnectionCount); Console.WriteLine(e.WebSession.Request.Url); //read request headers @@ -150,7 +150,7 @@ public async Task OnRequest(object sender, SessionEventArgs e) //Modify response public async Task OnResponse(object sender, SessionEventArgs e) { - Console.WriteLine("Active Server Connections:" + (sender as ProxyServer).ServerConnectionCount); + Console.WriteLine("Active Server Connections:" + ((ProxyServer)sender).ServerConnectionCount); if (requestBodyHistory.ContainsKey(e.Id)) { @@ -189,7 +189,7 @@ public async Task OnResponse(object sender, SessionEventArgs e) public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e) { //set IsValid to true/false based on Certificate Errors - if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None) + if (e.SslPolicyErrors == SslPolicyErrors.None) { e.IsValid = true; } diff --git a/Examples/Titanium.Web.Proxy.Examples.Basic/Titanium.Web.Proxy.Examples.Basic.csproj b/Examples/Titanium.Web.Proxy.Examples.Basic/Titanium.Web.Proxy.Examples.Basic.csproj index 84c0b9f89..3ebd19cb6 100644 --- a/Examples/Titanium.Web.Proxy.Examples.Basic/Titanium.Web.Proxy.Examples.Basic.csproj +++ b/Examples/Titanium.Web.Proxy.Examples.Basic/Titanium.Web.Proxy.Examples.Basic.csproj @@ -55,6 +55,7 @@ + diff --git a/README.md b/README.md index b7e366ee7..f7ae16734 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ Features * Safely relays Web Socket requests over HTTP * Support mutual SSL authentication * Fully asynchronous proxy -* Supports proxy authentication - +* Supports proxy authentication & automatic proxy detection Usage ===== @@ -204,10 +203,8 @@ public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs ``` Future road map (Pull requests are welcome!) ============ +* Implement Kerberos/NTLM authentication over HTTP protocols for windows domain * Support Server Name Indication (SNI) for transparent endpoints * Support HTTP 2.0 -* Support upstream AutoProxy detection * Support SOCKS protocol -* Implement Kerberos/NTLM authentication over HTTP protocols for windows domain - diff --git a/Tests/Titanium.Web.Proxy.IntegrationTests/Properties/AssemblyInfo.cs b/Tests/Titanium.Web.Proxy.IntegrationTests/Properties/AssemblyInfo.cs index e2f3d0432..2b5ed878b 100644 --- a/Tests/Titanium.Web.Proxy.IntegrationTests/Properties/AssemblyInfo.cs +++ b/Tests/Titanium.Web.Proxy.IntegrationTests/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/Tests/Titanium.Web.Proxy.IntegrationTests/SslTests.cs b/Tests/Titanium.Web.Proxy.IntegrationTests/SslTests.cs index c9f07f40c..1f851580e 100644 --- a/Tests/Titanium.Web.Proxy.IntegrationTests/SslTests.cs +++ b/Tests/Titanium.Web.Proxy.IntegrationTests/SslTests.cs @@ -1,11 +1,12 @@ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Diagnostics; using System.Net; +using System.Net.Http; +using System.Net.Security; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Models; -using System.Net.Http; -using System.Diagnostics; namespace Titanium.Web.Proxy.IntegrationTests { @@ -13,7 +14,6 @@ namespace Titanium.Web.Proxy.IntegrationTests public class SslTests { [TestMethod] - public void TestSsl() { //expand this to stress test to find @@ -103,7 +103,7 @@ public async Task OnResponse(object sender, SessionEventArgs e) public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e) { //set IsValid to true/false based on Certificate Errors - if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None) + if (e.SslPolicyErrors == SslPolicyErrors.None) { e.IsValid = true; } diff --git a/Tests/Titanium.Web.Proxy.UnitTests/CertificateManagerTests.cs b/Tests/Titanium.Web.Proxy.UnitTests/CertificateManagerTests.cs index 651142271..e4388e2e5 100644 --- a/Tests/Titanium.Web.Proxy.UnitTests/CertificateManagerTests.cs +++ b/Tests/Titanium.Web.Proxy.UnitTests/CertificateManagerTests.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Titanium.Web.Proxy.Network; -using System.Threading.Tasks; -using System.Collections.Generic; namespace Titanium.Web.Proxy.UnitTests { @@ -10,8 +10,7 @@ namespace Titanium.Web.Proxy.UnitTests public class CertificateManagerTests { private static readonly string[] hostNames - = new string[] { "facebook.com", "youtube.com", "google.com", - "bing.com", "yahoo.com"}; + = { "facebook.com", "youtube.com", "google.com", "bing.com", "yahoo.com" }; private readonly Random random = new Random(); @@ -36,16 +35,13 @@ public async Task Simple_Create_Certificate_Stress_Test() var certificate = mgr.CreateCertificate(host, false); Assert.IsNotNull(certificate); - })); - } } await Task.WhenAll(tasks.ToArray()); mgr.StopClearIdleCertificates(); - } } } diff --git a/Titanium.Web.Proxy.sln.DotSettings b/Titanium.Web.Proxy.sln.DotSettings index 1fc658854..5d870b7df 100644 --- a/Titanium.Web.Proxy.sln.DotSettings +++ b/Titanium.Web.Proxy.sln.DotSettings @@ -1,8 +1,21 @@  True + False + True + 240 BC + CN + DN + EKU + KU + MTA + OID + OIDS <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="Property (private)"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> \ No newline at end of file + <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="Property (private)"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + True + True + True \ No newline at end of file diff --git a/Titanium.Web.Proxy/CertificateHandler.cs b/Titanium.Web.Proxy/CertificateHandler.cs index 134798937..b13ccfd82 100644 --- a/Titanium.Web.Proxy/CertificateHandler.cs +++ b/Titanium.Web.Proxy/CertificateHandler.cs @@ -1,8 +1,8 @@ using System; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; using Titanium.Web.Proxy.EventArguments; +using Titanium.Web.Proxy.Extensions; namespace Titanium.Web.Proxy { @@ -32,17 +32,8 @@ internal bool ValidateServerCertificate( SslPolicyErrors = sslPolicyErrors }; - - Delegate[] invocationList = ServerCertificateValidationCallback.GetInvocationList(); - Task[] handlerTasks = new Task[invocationList.Length]; - - for (int i = 0; i < invocationList.Length; i++) - { - handlerTasks[i] = ((Func) invocationList[i])(null, args); - } - - Task.WhenAll(handlerTasks).Wait(); - + //why is the sender null? + ServerCertificateValidationCallback.InvokeParallel(this, args); return args.IsValid; } @@ -108,17 +99,8 @@ internal X509Certificate SelectClientCertificate( ClientCertificate = clientCertificate }; - - Delegate[] invocationList = ClientCertificateSelectionCallback.GetInvocationList(); - Task[] handlerTasks = new Task[invocationList.Length]; - - for (int i = 0; i < invocationList.Length; i++) - { - handlerTasks[i] = ((Func) invocationList[i])(null, args); - } - - Task.WhenAll(handlerTasks).Wait(); - + //why is the sender null? + ClientCertificateSelectionCallback.InvokeParallel(this, args); return args.ClientCertificate; } diff --git a/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs b/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs index f70bf8550..869dcf62d 100644 --- a/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs +++ b/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net; using System.Text; -using Titanium.Web.Proxy.Exceptions; +using System.Threading.Tasks; using Titanium.Web.Proxy.Decompression; +using Titanium.Web.Proxy.Exceptions; +using Titanium.Web.Proxy.Extensions; using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Http.Responses; -using Titanium.Web.Proxy.Extensions; -using System.Threading.Tasks; -using Titanium.Web.Proxy.Network; -using System.Net; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Network; namespace Titanium.Web.Proxy.EventArguments { @@ -30,7 +30,7 @@ public class SessionEventArgs : EventArgs, IDisposable /// /// Holds a reference to proxy response handler method /// - private readonly Func httpResponseHandler; + private Func httpResponseHandler; /// /// Holds a reference to client @@ -56,7 +56,7 @@ public class SessionEventArgs : EventArgs, IDisposable /// /// Client End Point. /// - public IPEndPoint ClientEndPoint => (IPEndPoint) ProxyClient.TcpClient.Client.RemoteEndPoint; + public IPEndPoint ClientEndPoint => (IPEndPoint)ProxyClient.TcpClient.Client.RemoteEndPoint; /// /// A web session corresponding to a single request/response sequence @@ -158,7 +158,7 @@ private async Task ReadResponseBody() await WebSession.ServerConnection.StreamReader.CopyBytesToStream(bufferSize, responseBodyStream, WebSession.Response.ContentLength); } - else if ((WebSession.Response.HttpVersion.Major == 1 && WebSession.Response.HttpVersion.Minor == 0) || WebSession.Response.ContentLength == -1) + else if (WebSession.Response.HttpVersion.Major == 1 && WebSession.Response.HttpVersion.Minor == 0 || WebSession.Response.ContentLength == -1) { await WebSession.ServerConnection.StreamReader.CopyBytesToStream(bufferSize, responseBodyStream, long.MaxValue); } @@ -522,6 +522,11 @@ public async Task Respond(Response response) /// public void Dispose() { + httpResponseHandler = null; + CustomUpStreamHttpProxyUsed = null; + CustomUpStreamHttpsProxyUsed = null; + + WebSession.Dispose(); } } } diff --git a/Titanium.Web.Proxy/Extensions/FuncExtensions.cs b/Titanium.Web.Proxy/Extensions/FuncExtensions.cs new file mode 100644 index 000000000..cfc1a2c78 --- /dev/null +++ b/Titanium.Web.Proxy/Extensions/FuncExtensions.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace Titanium.Web.Proxy.Extensions +{ + internal static class FuncExtensions + { + public static void InvokeParallel(this Func callback, object sender, T args) + { + Delegate[] invocationList = callback.GetInvocationList(); + Task[] handlerTasks = new Task[invocationList.Length]; + + for (int i = 0; i < invocationList.Length; i++) + { + handlerTasks[i] = ((Func)invocationList[i])(sender, args); + } + + Task.WhenAll(handlerTasks).Wait(); + } + + public static async Task InvokeParallelAsync(this Func callback, object sender, T args) + { + Delegate[] invocationList = callback.GetInvocationList(); + Task[] handlerTasks = new Task[invocationList.Length]; + + for (int i = 0; i < invocationList.Length; i++) + { + handlerTasks[i] = ((Func)invocationList[i])(sender, args); + } + + await Task.WhenAll(handlerTasks); + } + } +} diff --git a/Titanium.Web.Proxy/Extensions/StreamExtensions.cs b/Titanium.Web.Proxy/Extensions/StreamExtensions.cs index 4cf6be2bc..289e02290 100644 --- a/Titanium.Web.Proxy/Extensions/StreamExtensions.cs +++ b/Titanium.Web.Proxy/Extensions/StreamExtensions.cs @@ -136,7 +136,7 @@ internal static async Task WriteResponseBody(this CustomBinaryReader inStreamRea if (contentLength < bufferSize) { - bytesToRead = (int) contentLength; + bytesToRead = (int)contentLength; } var buffer = new byte[bufferSize]; @@ -153,8 +153,8 @@ internal static async Task WriteResponseBody(this CustomBinaryReader inStreamRea break; bytesRead = 0; - var remainingBytes = (contentLength - totalBytesRead); - bytesToRead = remainingBytes > (long) bufferSize ? bufferSize : (int) remainingBytes; + var remainingBytes = contentLength - totalBytesRead; + bytesToRead = remainingBytes > (long)bufferSize ? bufferSize : (int)remainingBytes; } } else diff --git a/Titanium.Web.Proxy/Helpers/CustomBinaryReader.cs b/Titanium.Web.Proxy/Helpers/CustomBinaryReader.cs index 6d10c3d91..7217a2719 100644 --- a/Titanium.Web.Proxy/Helpers/CustomBinaryReader.cs +++ b/Titanium.Web.Proxy/Helpers/CustomBinaryReader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Text; @@ -18,10 +19,18 @@ internal class CustomBinaryReader : IDisposable private readonly byte[] staticBuffer; private readonly Encoding encoding; + private static readonly ConcurrentQueue buffers + = new ConcurrentQueue(); + + private volatile bool disposed; + internal CustomBinaryReader(CustomBufferedStream stream, int bufferSize) { this.stream = stream; - staticBuffer = new byte[bufferSize]; + if (!buffers.TryDequeue(out staticBuffer) || staticBuffer.Length != bufferSize) + { + staticBuffer = new byte[bufferSize]; + } this.bufferSize = bufferSize; @@ -112,7 +121,7 @@ internal async Task ReadBytesAsync(long totalBytesToRead) var buffer = staticBuffer; if (totalBytesToRead < bufferSize) { - bytesToRead = (int) totalBytesToRead; + bytesToRead = (int)totalBytesToRead; buffer = new byte[bytesToRead]; } @@ -127,7 +136,7 @@ internal async Task ReadBytesAsync(long totalBytesToRead) break; var remainingBytes = totalBytesToRead - totalBytesRead; - bytesToRead = Math.Min(bufferSize, (int) remainingBytes); + bytesToRead = Math.Min(bufferSize, (int)remainingBytes); if (totalBytesRead + bytesToRead > buffer.Length) { @@ -148,8 +157,18 @@ internal async Task ReadBytesAsync(long totalBytesToRead) public void Dispose() { + if (!disposed) + { + disposed = true; + buffers.Enqueue(staticBuffer); + } } + /// + /// Increase size of buffer and copy existing content to new buffer + /// + /// + /// private void ResizeBuffer(ref byte[] buffer, long size) { var newBuffer = new byte[size]; diff --git a/Titanium.Web.Proxy/Helpers/CustomBufferedStream.cs b/Titanium.Web.Proxy/Helpers/CustomBufferedStream.cs index 78fa6db21..98281be68 100644 --- a/Titanium.Web.Proxy/Helpers/CustomBufferedStream.cs +++ b/Titanium.Web.Proxy/Helpers/CustomBufferedStream.cs @@ -198,7 +198,7 @@ public override int EndRead(IAsyncResult asyncResult) { if (asyncResult is ReadAsyncResult) { - return ((ReadAsyncResult) asyncResult).ReadBytes; + return ((ReadAsyncResult)asyncResult).ReadBytes; } return baseStream.EndRead(asyncResult); diff --git a/Titanium.Web.Proxy/Helpers/Network.cs b/Titanium.Web.Proxy/Helpers/Network.cs index 2d77e0bf7..7ee2d9a55 100644 --- a/Titanium.Web.Proxy/Helpers/Network.cs +++ b/Titanium.Web.Proxy/Helpers/Network.cs @@ -1,78 +1,78 @@ -using System.Linq; -using System.Net; -using System.Net.Sockets; - -namespace Titanium.Web.Proxy.Helpers -{ - internal class NetworkHelper - { - private static int FindProcessIdFromLocalPort(int port, IpVersion ipVersion) - { - var tcpRow = TcpHelper.GetTcpRowByLocalPort(ipVersion, port); - - return tcpRow?.ProcessId ?? 0; - } - - internal static int GetProcessIdFromPort(int port, bool ipV6Enabled) - { - var processId = FindProcessIdFromLocalPort(port, IpVersion.Ipv4); - - if (processId > 0 && !ipV6Enabled) - { - return processId; - } - - return FindProcessIdFromLocalPort(port, IpVersion.Ipv6); - } - - /// - /// Adapated from below link - /// http://stackoverflow.com/questions/11834091/how-to-check-if-localhost - /// - /// - /// - internal static bool IsLocalIpAddress(IPAddress address) - { - // get local IP addresses - var localIPs = Dns.GetHostAddresses(Dns.GetHostName()); - // test if any host IP equals to any local IP or to localhost - return IPAddress.IsLoopback(address) || localIPs.Contains(address); - } - - internal static bool IsLocalIpAddress(string hostName) - { - bool isLocalhost = false; - - IPHostEntry localhost = Dns.GetHostEntry("127.0.0.1"); - if (hostName == localhost.HostName) - { - IPHostEntry hostEntry = Dns.GetHostEntry(hostName); - isLocalhost = hostEntry.AddressList.Any(IPAddress.IsLoopback); - } - - if (!isLocalhost) - { - localhost = Dns.GetHostEntry(Dns.GetHostName()); - - IPAddress ipAddress; - - if (IPAddress.TryParse(hostName, out ipAddress)) - isLocalhost = localhost.AddressList.Any(x => x.Equals(ipAddress)); - - if (!isLocalhost) - { - try - { - var hostEntry = Dns.GetHostEntry(hostName); - isLocalhost = localhost.AddressList.Any(x => hostEntry.AddressList.Any(x.Equals)); - } - catch (SocketException) - { - } - } - } - - return isLocalhost; - } - } -} +using System.Linq; +using System.Net; +using System.Net.Sockets; + +namespace Titanium.Web.Proxy.Helpers +{ + internal class NetworkHelper + { + private static int FindProcessIdFromLocalPort(int port, IpVersion ipVersion) + { + var tcpRow = TcpHelper.GetTcpRowByLocalPort(ipVersion, port); + + return tcpRow?.ProcessId ?? 0; + } + + internal static int GetProcessIdFromPort(int port, bool ipV6Enabled) + { + var processId = FindProcessIdFromLocalPort(port, IpVersion.Ipv4); + + if (processId > 0 && !ipV6Enabled) + { + return processId; + } + + return FindProcessIdFromLocalPort(port, IpVersion.Ipv6); + } + + /// + /// Adapated from below link + /// http://stackoverflow.com/questions/11834091/how-to-check-if-localhost + /// + /// + /// + internal static bool IsLocalIpAddress(IPAddress address) + { + // get local IP addresses + var localIPs = Dns.GetHostAddresses(Dns.GetHostName()); + // test if any host IP equals to any local IP or to localhost + return IPAddress.IsLoopback(address) || localIPs.Contains(address); + } + + internal static bool IsLocalIpAddress(string hostName) + { + bool isLocalhost = false; + + IPHostEntry localhost = Dns.GetHostEntry("127.0.0.1"); + if (hostName == localhost.HostName) + { + IPHostEntry hostEntry = Dns.GetHostEntry(hostName); + isLocalhost = hostEntry.AddressList.Any(IPAddress.IsLoopback); + } + + if (!isLocalhost) + { + localhost = Dns.GetHostEntry(Dns.GetHostName()); + + IPAddress ipAddress; + + if (IPAddress.TryParse(hostName, out ipAddress)) + isLocalhost = localhost.AddressList.Any(x => x.Equals(ipAddress)); + + if (!isLocalhost) + { + try + { + var hostEntry = Dns.GetHostEntry(hostName); + isLocalhost = localhost.AddressList.Any(x => hostEntry.AddressList.Any(x.Equals)); + } + catch (SocketException) + { + } + } + } + + return isLocalhost; + } + } +} diff --git a/Titanium.Web.Proxy/Helpers/SystemProxy.cs b/Titanium.Web.Proxy/Helpers/SystemProxy.cs index ec348319f..1edeb91b9 100644 --- a/Titanium.Web.Proxy/Helpers/SystemProxy.cs +++ b/Titanium.Web.Proxy/Helpers/SystemProxy.cs @@ -1,9 +1,9 @@ using System; -using System.Runtime.InteropServices; -using Microsoft.Win32; -using System.Text.RegularExpressions; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Microsoft.Win32; // Helper classes for setting system proxy settings namespace Titanium.Web.Proxy.Helpers @@ -84,7 +84,7 @@ private void SetProxy(string hostname, int port, ProxyProtocolType protocolType) var exisitingContent = reg.GetValue("ProxyServer") as string; var existingSystemProxyValues = GetSystemProxyValues(exisitingContent); existingSystemProxyValues.RemoveAll(x => protocolType == ProxyProtocolType.Https ? x.IsHttps : !x.IsHttps); - existingSystemProxyValues.Add(new HttpSystemProxyValue() + existingSystemProxyValues.Add(new HttpSystemProxyValue { HostName = hostname, IsHttps = protocolType == ProxyProtocolType.Https, diff --git a/Titanium.Web.Proxy/Helpers/Tcp.cs b/Titanium.Web.Proxy/Helpers/Tcp.cs index e64da17b6..226a26e00 100644 --- a/Titanium.Web.Proxy/Helpers/Tcp.cs +++ b/Titanium.Web.Proxy/Helpers/Tcp.cs @@ -3,10 +3,9 @@ using System.IO; using System.Linq; using System.Net.NetworkInformation; -using System.Net.Security; using System.Runtime.InteropServices; -using System.Security.Authentication; using System.Text; +using System.Threading; using System.Threading.Tasks; using Titanium.Web.Proxy.Extensions; using Titanium.Web.Proxy.Models; @@ -15,8 +14,6 @@ namespace Titanium.Web.Proxy.Helpers { - using System.Net; - internal enum IpVersion { Ipv4 = 1, @@ -232,7 +229,7 @@ internal static async Task SendRaw(ProxyServer server, finally { tcpConnection.Dispose(); - server.ServerConnectionCount--; + Interlocked.Decrement(ref server.serverConnectionCount); } } } diff --git a/Titanium.Web.Proxy/Http/HeaderParser.cs b/Titanium.Web.Proxy/Http/HeaderParser.cs index 27d8e051a..bf69aecbb 100644 --- a/Titanium.Web.Proxy/Http/HeaderParser.cs +++ b/Titanium.Web.Proxy/Http/HeaderParser.cs @@ -8,8 +8,8 @@ namespace Titanium.Web.Proxy.Http { internal static class HeaderParser { - internal static async Task ReadHeaders(CustomBinaryReader reader, - Dictionary> nonUniqueResponseHeaders, + internal static async Task ReadHeaders(CustomBinaryReader reader, + Dictionary> nonUniqueResponseHeaders, Dictionary headers) { string tmpLine; diff --git a/Titanium.Web.Proxy/Http/HttpWebClient.cs b/Titanium.Web.Proxy/Http/HttpWebClient.cs index 46aa379c7..ac61bbea1 100644 --- a/Titanium.Web.Proxy/Http/HttpWebClient.cs +++ b/Titanium.Web.Proxy/Http/HttpWebClient.cs @@ -11,7 +11,7 @@ namespace Titanium.Web.Proxy.Http /// /// Used to communicate with the server over HTTP(S) /// - public class HttpWebClient + public class HttpWebClient : IDisposable { /// /// Connection to server @@ -79,7 +79,7 @@ internal async Task SendRequest(bool enable100ContinueBehaviour) var requestLines = new StringBuilder(); //prepare the request & headers - if ((ServerConnection.UpStreamHttpProxy != null && ServerConnection.IsHttps == false) || (ServerConnection.UpStreamHttpsProxy != null && ServerConnection.IsHttps)) + if (ServerConnection.UpStreamHttpProxy != null && ServerConnection.IsHttps == false || ServerConnection.UpStreamHttpsProxy != null && ServerConnection.IsHttps) { requestLines.AppendLine($"{Request.Method} {Request.RequestUri.AbsoluteUri} HTTP/{Request.HttpVersion.Major}.{Request.HttpVersion.Minor}"); } @@ -93,7 +93,7 @@ internal async Task SendRequest(bool enable100ContinueBehaviour) { requestLines.AppendLine("Proxy-Connection: keep-alive"); requestLines.AppendLine("Proxy-Authorization" + ": Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes( - $"{ServerConnection.UpStreamHttpProxy.UserName}:{ServerConnection.UpStreamHttpProxy.Password}"))); + $"{ServerConnection.UpStreamHttpProxy.UserName}:{ServerConnection.UpStreamHttpProxy.Password}"))); } //write request headers foreach (var headerItem in Request.RequestHeaders) @@ -208,5 +208,16 @@ internal async Task ReceiveResponse() //Read the response headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(ServerConnection.StreamReader, Response.NonUniqueResponseHeaders, Response.ResponseHeaders); } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + ConnectHeaders = null; + + Request.Dispose(); + Response.Dispose(); + } } } diff --git a/Titanium.Web.Proxy/Http/Request.cs b/Titanium.Web.Proxy/Http/Request.cs index 949cd5570..2784511a9 100644 --- a/Titanium.Web.Proxy/Http/Request.cs +++ b/Titanium.Web.Proxy/Http/Request.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; using System.Text; -using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Extensions; +using Titanium.Web.Proxy.Models; namespace Titanium.Web.Proxy.Http { /// /// A HTTP(S) request object /// - public class Request + public class Request : IDisposable { /// /// Request Method @@ -241,7 +241,7 @@ public bool ExpectContinue /// request body as string /// internal string RequestBodyString { get; set; } - + internal bool RequestBodyRead { get; set; } internal bool RequestLocked { get; set; } @@ -294,5 +294,20 @@ public Request() RequestHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); NonUniqueRequestHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); } + + /// + /// Dispose off + /// + public void Dispose() + { + //not really needed since GC will collect it + //but just to be on safe side + + RequestHeaders = null; + NonUniqueRequestHeaders = null; + + RequestBody = null; + RequestBody = null; + } } } diff --git a/Titanium.Web.Proxy/Http/Response.cs b/Titanium.Web.Proxy/Http/Response.cs index 08163a4b9..82b5fdd6c 100644 --- a/Titanium.Web.Proxy/Http/Response.cs +++ b/Titanium.Web.Proxy/Http/Response.cs @@ -1,16 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Text; -using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Extensions; -using System; +using Titanium.Web.Proxy.Models; namespace Titanium.Web.Proxy.Http { /// /// Http(s) response object /// - public class Response + public class Response : IDisposable { /// /// Response Status Code. @@ -234,5 +234,20 @@ public Response() ResponseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); NonUniqueResponseHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); } + + /// + /// Dispose off + /// + public void Dispose() + { + //not really needed since GC will collect it + //but just to be on safe side + + ResponseHeaders = null; + NonUniqueResponseHeaders = null; + + ResponseBody = null; + ResponseBodyString = null; + } } } diff --git a/Titanium.Web.Proxy/Http/Responses/GenericResponse.cs b/Titanium.Web.Proxy/Http/Responses/GenericResponse.cs index bf3a0753b..d2ad39f05 100644 --- a/Titanium.Web.Proxy/Http/Responses/GenericResponse.cs +++ b/Titanium.Web.Proxy/Http/Responses/GenericResponse.cs @@ -13,7 +13,7 @@ public class GenericResponse : Response /// public GenericResponse(HttpStatusCode status) { - ResponseStatusCode = ((int) status).ToString(); + ResponseStatusCode = ((int)status).ToString(); ResponseStatusDescription = status.ToString(); } diff --git a/Titanium.Web.Proxy/Models/ExternalProxy.cs b/Titanium.Web.Proxy/Models/ExternalProxy.cs index e1516a477..5922c3a47 100644 --- a/Titanium.Web.Proxy/Models/ExternalProxy.cs +++ b/Titanium.Web.Proxy/Models/ExternalProxy.cs @@ -21,7 +21,7 @@ public class ExternalProxy /// /// Bypass this proxy for connections to localhost? /// - public bool BypassForLocalhost { get; set; } + public bool BypassLocalhost { get; set; } /// /// Username. diff --git a/Titanium.Web.Proxy/Network/Certificate/BCCertificateMaker.cs b/Titanium.Web.Proxy/Network/Certificate/BCCertificateMaker.cs index e7a8b7eb0..fba922e72 100644 --- a/Titanium.Web.Proxy/Network/Certificate/BCCertificateMaker.cs +++ b/Titanium.Web.Proxy/Network/Certificate/BCCertificateMaker.cs @@ -110,7 +110,7 @@ private static X509Certificate2 GenerateCertificate(string hostName, // Corresponding private key var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private); - var seq = (Asn1Sequence) Asn1Object.FromByteArray(privateKeyInfo.ParsePrivateKey().GetDerEncoded()); + var seq = (Asn1Sequence)Asn1Object.FromByteArray(privateKeyInfo.ParsePrivateKey().GetDerEncoded()); if (seq.Count != 9) { diff --git a/Titanium.Web.Proxy/Network/Certificate/WinCertificateMaker.cs b/Titanium.Web.Proxy/Network/Certificate/WinCertificateMaker.cs index fed9f2ecb..42a5db01e 100644 --- a/Titanium.Web.Proxy/Network/Certificate/WinCertificateMaker.cs +++ b/Titanium.Web.Proxy/Network/Certificate/WinCertificateMaker.cs @@ -89,7 +89,7 @@ private X509Certificate2 MakeCertificate(bool isRoot, string subject, string ful } var x500CertDN = Activator.CreateInstance(typeX500DN); - var typeValue = new object[] {fullSubject, 0}; + var typeValue = new object[] { fullSubject, 0 }; typeX500DN.InvokeMember("Encode", BindingFlags.InvokeMethod, null, x500CertDN, typeValue); var x500RootCertDN = Activator.CreateInstance(typeX500DN); @@ -110,16 +110,16 @@ private X509Certificate2 MakeCertificate(bool isRoot, string subject, string ful if (sharedPrivateKey == null) { sharedPrivateKey = Activator.CreateInstance(typeX509PrivateKey); - typeValue = new object[] {sProviderName}; + typeValue = new object[] { sProviderName }; typeX509PrivateKey.InvokeMember("ProviderName", BindingFlags.PutDispProperty, null, sharedPrivateKey, typeValue); typeValue[0] = 2; typeX509PrivateKey.InvokeMember("ExportPolicy", BindingFlags.PutDispProperty, null, sharedPrivateKey, typeValue); - typeValue = new object[] {(isRoot ? 2 : 1)}; + typeValue = new object[] { isRoot ? 2 : 1 }; typeX509PrivateKey.InvokeMember("KeySpec", BindingFlags.PutDispProperty, null, sharedPrivateKey, typeValue); if (!isRoot) { - typeValue = new object[] {176}; + typeValue = new object[] { 176 }; typeX509PrivateKey.InvokeMember("KeyUsage", BindingFlags.PutDispProperty, null, sharedPrivateKey, typeValue); } @@ -149,9 +149,9 @@ private X509Certificate2 MakeCertificate(bool isRoot, string subject, string ful var requestCert = Activator.CreateInstance(typeRequestCert); - typeValue = new[] {1, sharedPrivateKey, string.Empty}; + typeValue = new[] { 1, sharedPrivateKey, string.Empty }; typeRequestCert.InvokeMember("InitializeFromPrivateKey", BindingFlags.InvokeMethod, null, requestCert, typeValue); - typeValue = new[] {x500CertDN}; + typeValue = new[] { x500CertDN }; typeRequestCert.InvokeMember("Subject", BindingFlags.PutDispProperty, null, requestCert, typeValue); typeValue[0] = x500RootCertDN; typeRequestCert.InvokeMember("Issuer", BindingFlags.PutDispProperty, null, requestCert, typeValue); @@ -186,14 +186,14 @@ private X509Certificate2 MakeCertificate(bool isRoot, string subject, string ful var extNames = Activator.CreateInstance(typeExtNames); var altDnsNames = Activator.CreateInstance(typeCAlternativeName); - typeValue = new object[] {3, subject}; + typeValue = new object[] { 3, subject }; typeCAlternativeName.InvokeMember("InitializeFromString", BindingFlags.InvokeMethod, null, altDnsNames, typeValue); - typeValue = new[] {altDnsNames}; + typeValue = new[] { altDnsNames }; typeAltNamesCollection.InvokeMember("Add", BindingFlags.InvokeMethod, null, altNameCollection, typeValue); - typeValue = new[] {altNameCollection}; + typeValue = new[] { altNameCollection }; typeExtNames.InvokeMember("InitializeEncode", BindingFlags.InvokeMethod, null, extNames, typeValue); typeValue[0] = extNames; @@ -204,27 +204,27 @@ private X509Certificate2 MakeCertificate(bool isRoot, string subject, string ful { var signerCertificate = Activator.CreateInstance(typeSignerCertificate); - typeValue = new object[] {0, 0, 12, signingCertificate.Thumbprint}; + typeValue = new object[] { 0, 0, 12, signingCertificate.Thumbprint }; typeSignerCertificate.InvokeMember("Initialize", BindingFlags.InvokeMethod, null, signerCertificate, typeValue); - typeValue = new[] {signerCertificate}; + typeValue = new[] { signerCertificate }; typeRequestCert.InvokeMember("SignerCertificate", BindingFlags.PutDispProperty, null, requestCert, typeValue); } else { var basicConstraints = Activator.CreateInstance(typeBasicConstraints); - typeValue = new object[] {"true", "0"}; + typeValue = new object[] { "true", "0" }; typeBasicConstraints.InvokeMember("InitializeEncode", BindingFlags.InvokeMethod, null, basicConstraints, typeValue); - typeValue = new[] {basicConstraints}; + typeValue = new[] { basicConstraints }; typeX509Extensions.InvokeMember("Add", BindingFlags.InvokeMethod, null, certificate, typeValue); } oid = Activator.CreateInstance(typeOID); - typeValue = new object[] {1, 0, 0, hashAlg}; + typeValue = new object[] { 1, 0, 0, hashAlg }; typeOID.InvokeMember("InitializeFromAlgorithmName", BindingFlags.InvokeMethod, null, oid, typeValue); - typeValue = new[] {oid}; + typeValue = new[] { oid }; typeRequestCert.InvokeMember("HashAlgorithm", BindingFlags.PutDispProperty, null, requestCert, typeValue); typeRequestCert.InvokeMember("Encode", BindingFlags.InvokeMethod, null, requestCert, null); @@ -243,15 +243,15 @@ private X509Certificate2 MakeCertificate(bool isRoot, string subject, string ful typeValue[0] = 0; - var createCertRequest = typeX509Enrollment.InvokeMember("CreateRequest", BindingFlags.InvokeMethod, null, x509Enrollment, typeValue); - typeValue = new[] {2, createCertRequest, 0, string.Empty}; + var createCertRequest = typeX509Enrollment.InvokeMember("CreateRequest", BindingFlags.InvokeMethod, null, x509Enrollment, typeValue); + typeValue = new[] { 2, createCertRequest, 0, string.Empty }; typeX509Enrollment.InvokeMember("InstallResponse", BindingFlags.InvokeMethod, null, x509Enrollment, typeValue); - typeValue = new object[] {null, 0, 1}; + typeValue = new object[] { null, 0, 1 }; try { - var empty = (string) typeX509Enrollment.InvokeMember("CreatePFX", BindingFlags.InvokeMethod, null, x509Enrollment, typeValue); + var empty = (string)typeX509Enrollment.InvokeMember("CreatePFX", BindingFlags.InvokeMethod, null, x509Enrollment, typeValue); return new X509Certificate2(Convert.FromBase64String(empty), string.Empty, X509KeyStorageFlags.Exportable); } catch (Exception) @@ -293,8 +293,7 @@ private X509Certificate2 MakeCertificateInternal(string sSubjectCN, bool isRoot, var graceTime = DateTime.Now.AddDays(GraceDays); var now = DateTime.Now; - rCert = !isRoot ? MakeCertificate(false, sSubjectCN, fullSubject, keyLength, HashAlgo, graceTime, now.AddDays(ValidDays), signingCert) : - MakeCertificate(true, sSubjectCN, fullSubject, keyLength, HashAlgo, graceTime, now.AddDays(ValidDays), null); + rCert = MakeCertificate(isRoot, sSubjectCN, fullSubject, keyLength, HashAlgo, graceTime, now.AddDays(ValidDays), isRoot ? null : signingCert); return rCert; } } diff --git a/Titanium.Web.Proxy/Network/CertificateManager.cs b/Titanium.Web.Proxy/Network/CertificateManager.cs index d3e7547de..ddc9cd2ed 100644 --- a/Titanium.Web.Proxy/Network/CertificateManager.cs +++ b/Titanium.Web.Proxy/Network/CertificateManager.cs @@ -1,12 +1,14 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using System.Linq; -using System.Collections.Concurrent; -using System.IO; -using Titanium.Web.Proxy.Network.Certificate; using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Network.Certificate; namespace Titanium.Web.Proxy.Network { @@ -29,7 +31,7 @@ public enum CertificateEngine /// /// A class to manage SSL certificates used by this proxy server /// - public class CertificateManager : IDisposable + public sealed class CertificateManager : IDisposable { internal CertificateEngine Engine { @@ -51,7 +53,7 @@ internal CertificateEngine Engine if (certEngine == null) { certEngine = engine == CertificateEngine.BouncyCastle - ? (ICertificateMaker) new BCCertificateMaker() + ? (ICertificateMaker)new BCCertificateMaker() : new WinCertificateMaker(); } } @@ -132,12 +134,12 @@ private void ClearRootCertificate() private string GetRootCertificatePath() { - var assemblyLocation = System.Reflection.Assembly.GetExecutingAssembly().Location; + var assemblyLocation = Assembly.GetExecutingAssembly().Location; // dynamically loaded assemblies returns string.Empty location if (assemblyLocation == string.Empty) { - assemblyLocation = System.Reflection.Assembly.GetEntryAssembly().Location; + assemblyLocation = Assembly.GetEntryAssembly().Location; } var path = Path.GetDirectoryName(assemblyLocation); @@ -146,7 +148,7 @@ private string GetRootCertificatePath() return fileName; } - internal X509Certificate2 LoadRootCertificate() + private X509Certificate2 LoadRootCertificate() { var fileName = GetRootCertificatePath(); if (!File.Exists(fileName)) return null; @@ -217,6 +219,51 @@ public void TrustRootCertificate() TrustRootCertificate(StoreLocation.LocalMachine); } + /// + /// Puts the certificate to the local machine's certificate store. + /// Needs elevated permission. Works only on Windows. + /// + /// + public bool TrustRootCertificateAsAdministrator() + { + if (RunTime.IsRunningOnMono()) + { + return false; + } + + var fileName = Path.GetTempFileName(); + File.WriteAllBytes(fileName, RootCertificate.Export(X509ContentType.Pkcs12)); + + var info = new ProcessStartInfo + { + FileName = "certutil.exe", + Arguments = "-importPFX -p \"\" -f \"" + fileName + "\"", + CreateNoWindow = true, + UseShellExecute = true, + Verb = "runas", + ErrorDialog = false, + }; + + try + { + var process = Process.Start(info); + if (process == null) + { + return false; + } + + process.WaitForExit(); + + File.Delete(fileName); + } + catch + { + return false; + } + + return true; + } + /// /// Removes the trusted certificates. /// @@ -229,13 +276,49 @@ public void RemoveTrustedRootCertificates() RemoveTrustedRootCertificates(StoreLocation.LocalMachine); } + /// + /// Determines whether the root certificate is trusted. + /// + public bool IsRootCertificateTrusted() + { + return FindRootCertificate(StoreLocation.CurrentUser) || IsRootCertificateMachineTrusted(); + } + + /// + /// Determines whether the root certificate is machine trusted. + /// + public bool IsRootCertificateMachineTrusted() + { + return FindRootCertificate(StoreLocation.LocalMachine); + } + + private bool FindRootCertificate(StoreLocation storeLocation) + { + string value = $"{RootCertificate.Issuer}"; + return FindCertificates(StoreName.Root, storeLocation, value).Count > 0; + } + + private X509Certificate2Collection FindCertificates(StoreName storeName, StoreLocation storeLocation, string findValue) + { + X509Store x509Store = new X509Store(storeName, storeLocation); + try + { + x509Store.Open(OpenFlags.OpenExistingOnly); + return x509Store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, findValue, false); + } + finally + { + x509Store.Close(); + } + } + /// /// Create an SSL certificate /// /// /// /// - internal virtual X509Certificate2 CreateCertificate(string certificateName, bool isRootCertificate) + internal X509Certificate2 CreateCertificate(string certificateName, bool isRootCertificate) { if (certificateCache.ContainsKey(certificateName)) { @@ -264,7 +347,7 @@ internal virtual X509Certificate2 CreateCertificate(string certificateName, bool } if (certificate != null && !certificateCache.ContainsKey(certificateName)) { - certificateCache.Add(certificateName, new CachedCertificate {Certificate = certificate}); + certificateCache.Add(certificateName, new CachedCertificate { Certificate = certificate }); } } else @@ -316,7 +399,7 @@ internal async void ClearIdleCertificates(int certificateCacheTimeOutMinutes) /// /// /// - internal void TrustRootCertificate(StoreLocation storeLocation) + private void TrustRootCertificate(StoreLocation storeLocation) { if (RootCertificate == null) { @@ -327,7 +410,7 @@ internal void TrustRootCertificate(StoreLocation storeLocation) return; } - X509Store x509RootStore = new X509Store(StoreName.Root, storeLocation); + var x509RootStore = new X509Store(StoreName.Root, storeLocation); var x509PersonalStore = new X509Store(StoreName.My, storeLocation); try @@ -356,7 +439,7 @@ internal void TrustRootCertificate(StoreLocation storeLocation) /// /// /// - internal void RemoveTrustedRootCertificates(StoreLocation storeLocation) + private void RemoveTrustedRootCertificates(StoreLocation storeLocation) { if (RootCertificate == null) { @@ -367,7 +450,7 @@ internal void RemoveTrustedRootCertificates(StoreLocation storeLocation) return; } - X509Store x509RootStore = new X509Store(StoreName.Root, storeLocation); + var x509RootStore = new X509Store(StoreName.Root, storeLocation); var x509PersonalStore = new X509Store(StoreName.My, storeLocation); try @@ -381,7 +464,7 @@ internal void RemoveTrustedRootCertificates(StoreLocation storeLocation) catch (Exception e) { exceptionFunc( - new Exception("Failed to make system trust root certificate " + new Exception("Failed to remove root certificate trust " + $" for {storeLocation} store location. You may need admin rights.", e)); } finally @@ -391,6 +474,9 @@ internal void RemoveTrustedRootCertificates(StoreLocation storeLocation) } } + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// public void Dispose() { } diff --git a/Titanium.Web.Proxy/Network/Tcp/TcpConnection.cs b/Titanium.Web.Proxy/Network/Tcp/TcpConnection.cs index 70ced1fc0..ab621061f 100644 --- a/Titanium.Web.Proxy/Network/Tcp/TcpConnection.cs +++ b/Titanium.Web.Proxy/Network/Tcp/TcpConnection.cs @@ -58,7 +58,21 @@ public void Dispose() StreamReader?.Dispose(); - TcpClient?.Close(); + try + { + if (TcpClient != null) + { + //This line is important! + //contributors please don't remove it without discussion + //It helps to avoid eventual deterioration of performance due to TCP port exhaustion + //due to default TCP CLOSE_WAIT timeout for 4 minutes + TcpClient.LingerState = new LingerOption(true, 0); + TcpClient.Close(); + } + } + catch + { + } } } } diff --git a/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs b/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs index 84369afd7..7940afb19 100644 --- a/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs +++ b/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs @@ -1,146 +1,172 @@ -using System; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; -using System.IO; -using System.Net.Security; -using Titanium.Web.Proxy.Helpers; -using Titanium.Web.Proxy.Models; -using System.Linq; -using Titanium.Web.Proxy.Extensions; -using Titanium.Web.Proxy.Shared; - -namespace Titanium.Web.Proxy.Network.Tcp -{ - /// - /// A class that manages Tcp Connection to server used by this proxy server - /// - internal class TcpConnectionFactory - { - - /// - /// Creates a TCP connection to server - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - internal async Task CreateClient(ProxyServer server, - string remoteHostName, int remotePort, Version httpVersion, - bool isHttps, - ExternalProxy externalHttpProxy, ExternalProxy externalHttpsProxy, - Stream clientStream) - { - TcpClient client; - CustomBufferedStream stream; - - bool isLocalhost = (externalHttpsProxy == null && externalHttpProxy == null) ? false : NetworkHelper.IsLocalIpAddress(remoteHostName); - - bool useHttpsProxy = externalHttpsProxy != null && externalHttpsProxy.HostName != remoteHostName && (externalHttpsProxy.BypassForLocalhost && !isLocalhost); - bool useHttpProxy = externalHttpProxy != null && externalHttpProxy.HostName != remoteHostName && (externalHttpProxy.BypassForLocalhost && !isLocalhost); - - if (isHttps) - { - SslStream sslStream = null; - - //If this proxy uses another external proxy then create a tunnel request for HTTPS connections - if (useHttpsProxy) - { - client = new TcpClient(server.UpStreamEndPoint); - await client.ConnectAsync(externalHttpsProxy.HostName, externalHttpsProxy.Port); - stream = new CustomBufferedStream(client.GetStream(), server.BufferSize); - - using (var writer = new StreamWriter(stream, Encoding.ASCII, server.BufferSize, true) {NewLine = ProxyConstants.NewLine}) - { - await writer.WriteLineAsync($"CONNECT {remoteHostName}:{remotePort} HTTP/{httpVersion}"); - await writer.WriteLineAsync($"Host: {remoteHostName}:{remotePort}"); - await writer.WriteLineAsync("Connection: Keep-Alive"); - - if (!string.IsNullOrEmpty(externalHttpsProxy.UserName) && externalHttpsProxy.Password != null) - { - await writer.WriteLineAsync("Proxy-Connection: keep-alive"); - await writer.WriteLineAsync("Proxy-Authorization" + ": Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(externalHttpsProxy.UserName + ":" + externalHttpsProxy.Password))); - } - await writer.WriteLineAsync(); - await writer.FlushAsync(); - writer.Close(); - } - - using (var reader = new CustomBinaryReader(stream, server.BufferSize)) - { - var result = await reader.ReadLineAsync(); - - if (!new[] {"200 OK", "connection established"}.Any(s => result.ContainsIgnoreCase(s))) - { - throw new Exception("Upstream proxy failed to create a secure tunnel"); - } - - await reader.ReadAndIgnoreAllLinesAsync(); - } - } - else - { - client = new TcpClient(server.UpStreamEndPoint); - await client.ConnectAsync(remoteHostName, remotePort); - stream = new CustomBufferedStream(client.GetStream(), server.BufferSize); - } - - try - { - sslStream = new SslStream(stream, true, server.ValidateServerCertificate, - server.SelectClientCertificate); - - await sslStream.AuthenticateAsClientAsync(remoteHostName, null, server.SupportedSslProtocols, false); - - stream = new CustomBufferedStream(sslStream, server.BufferSize); - } - catch - { - sslStream?.Dispose(); - - throw; - } - } - else - { - if (useHttpProxy) - { - client = new TcpClient(server.UpStreamEndPoint); - await client.ConnectAsync(externalHttpProxy.HostName, externalHttpProxy.Port); - stream = new CustomBufferedStream(client.GetStream(), server.BufferSize); - } - else - { - client = new TcpClient(server.UpStreamEndPoint); - await client.ConnectAsync(remoteHostName, remotePort); - stream = new CustomBufferedStream(client.GetStream(), server.BufferSize); - } - } - - client.ReceiveTimeout = server.ConnectionTimeOutSeconds * 1000; - client.SendTimeout = server.ConnectionTimeOutSeconds * 1000; - - client.LingerState = new LingerOption(true, 0); - - server.ServerConnectionCount++; - - return new TcpConnection - { - UpStreamHttpProxy = externalHttpProxy, - UpStreamHttpsProxy = externalHttpsProxy, - HostName = remoteHostName, - Port = remotePort, - IsHttps = isHttps, - TcpClient = client, - StreamReader = new CustomBinaryReader(stream, server.BufferSize), - Stream = stream, - Version = httpVersion - }; - } - } -} +using System; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Titanium.Web.Proxy.Extensions; +using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Shared; + +namespace Titanium.Web.Proxy.Network.Tcp +{ + /// + /// A class that manages Tcp Connection to server used by this proxy server + /// + internal class TcpConnectionFactory + { + /// + /// Creates a TCP connection to server + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal async Task CreateClient(ProxyServer server, + string remoteHostName, int remotePort, Version httpVersion, + bool isHttps, + ExternalProxy externalHttpProxy, ExternalProxy externalHttpsProxy, + Stream clientStream) + { + TcpClient client; + CustomBufferedStream stream; + + + bool useHttpProxy = false; + + //check if external proxy is set for HTTP + if (!isHttps && externalHttpProxy != null + && externalHttpProxy.HostName != remoteHostName) + { + useHttpProxy = true; + + //check if we need to ByPass + if (externalHttpProxy.BypassLocalhost + && NetworkHelper.IsLocalIpAddress(remoteHostName)) + { + useHttpProxy = false; + } + } + + bool useHttpsProxy = false; + //check if external proxy is set for HTTPS + if (isHttps && externalHttpsProxy != null + && externalHttpsProxy.HostName != remoteHostName) + { + useHttpsProxy = true; + + //check if we need to ByPass + if (externalHttpsProxy.BypassLocalhost + && NetworkHelper.IsLocalIpAddress(remoteHostName)) + { + useHttpsProxy = false; + } + } + + if (isHttps) + { + SslStream sslStream = null; + + //If this proxy uses another external proxy then create a tunnel request for HTTPS connections + if (useHttpsProxy) + { + client = new TcpClient(server.UpStreamEndPoint); + await client.ConnectAsync(externalHttpsProxy.HostName, externalHttpsProxy.Port); + stream = new CustomBufferedStream(client.GetStream(), server.BufferSize); + + using (var writer = new StreamWriter(stream, Encoding.ASCII, server.BufferSize, true) { NewLine = ProxyConstants.NewLine }) + { + await writer.WriteLineAsync($"CONNECT {remoteHostName}:{remotePort} HTTP/{httpVersion}"); + await writer.WriteLineAsync($"Host: {remoteHostName}:{remotePort}"); + await writer.WriteLineAsync("Connection: Keep-Alive"); + + if (!string.IsNullOrEmpty(externalHttpsProxy.UserName) && externalHttpsProxy.Password != null) + { + await writer.WriteLineAsync("Proxy-Connection: keep-alive"); + await writer.WriteLineAsync("Proxy-Authorization" + ": Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(externalHttpsProxy.UserName + ":" + externalHttpsProxy.Password))); + } + await writer.WriteLineAsync(); + await writer.FlushAsync(); + writer.Close(); + } + + using (var reader = new CustomBinaryReader(stream, server.BufferSize)) + { + var result = await reader.ReadLineAsync(); + + if (!new[] { "200 OK", "connection established" }.Any(s => result.ContainsIgnoreCase(s))) + { + throw new Exception("Upstream proxy failed to create a secure tunnel"); + } + + await reader.ReadAndIgnoreAllLinesAsync(); + } + } + else + { + client = new TcpClient(server.UpStreamEndPoint); + await client.ConnectAsync(remoteHostName, remotePort); + stream = new CustomBufferedStream(client.GetStream(), server.BufferSize); + } + + try + { + sslStream = new SslStream(stream, true, server.ValidateServerCertificate, + server.SelectClientCertificate); + + await sslStream.AuthenticateAsClientAsync(remoteHostName, null, server.SupportedSslProtocols, server.CheckCertificateRevocation); + + stream = new CustomBufferedStream(sslStream, server.BufferSize); + } + catch + { + sslStream?.Close(); + sslStream?.Dispose(); + + throw; + } + } + else + { + if (useHttpProxy) + { + client = new TcpClient(server.UpStreamEndPoint); + await client.ConnectAsync(externalHttpProxy.HostName, externalHttpProxy.Port); + stream = new CustomBufferedStream(client.GetStream(), server.BufferSize); + } + else + { + client = new TcpClient(server.UpStreamEndPoint); + await client.ConnectAsync(remoteHostName, remotePort); + stream = new CustomBufferedStream(client.GetStream(), server.BufferSize); + } + } + + client.ReceiveTimeout = server.ConnectionTimeOutSeconds * 1000; + client.SendTimeout = server.ConnectionTimeOutSeconds * 1000; + + Interlocked.Increment(ref server.serverConnectionCount); + + return new TcpConnection + { + UpStreamHttpProxy = externalHttpProxy, + UpStreamHttpsProxy = externalHttpsProxy, + HostName = remoteHostName, + Port = remotePort, + IsHttps = isHttps, + TcpClient = client, + StreamReader = new CustomBinaryReader(stream, server.BufferSize), + Stream = stream, + Version = httpVersion + }; + } + } +} diff --git a/Titanium.Web.Proxy/ProxyAuthorizationHandler.cs b/Titanium.Web.Proxy/ProxyAuthorizationHandler.cs index c8279925c..ad03921fe 100644 --- a/Titanium.Web.Proxy/ProxyAuthorizationHandler.cs +++ b/Titanium.Web.Proxy/ProxyAuthorizationHandler.cs @@ -70,8 +70,8 @@ private async Task SendAuthentication407Response(StreamWriter clientStreamWriter { ResponseHeaders = new Dictionary { - {"Proxy-Authenticate", new HttpHeader("Proxy-Authenticate", "Basic realm=\"TitaniumProxy\"")}, - {"Proxy-Connection", new HttpHeader("Proxy-Connection", "close")} + { "Proxy-Authenticate", new HttpHeader("Proxy-Authenticate", "Basic realm=\"TitaniumProxy\"") }, + { "Proxy-Connection", new HttpHeader("Proxy-Connection", "close") } } }; await WriteResponseHeaders(clientStreamWriter, response); diff --git a/Titanium.Web.Proxy/ProxyServer.cs b/Titanium.Web.Proxy/ProxyServer.cs index de3a0d928..a9c3936ad 100644 --- a/Titanium.Web.Proxy/ProxyServer.cs +++ b/Titanium.Web.Proxy/ProxyServer.cs @@ -1,16 +1,17 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; using System.Threading.Tasks; using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Network; -using System.Linq; -using System.Security.Authentication; using Titanium.Web.Proxy.Network.Tcp; -using System.Security.Cryptography.X509Certificates; namespace Titanium.Web.Proxy { @@ -34,8 +35,21 @@ public partial class ProxyServer : IDisposable /// private Action exceptionFunc; + /// + /// Backing field for corresponding public property + /// private bool trustRootCertificate; + /// + /// Backing field for corresponding public property + /// + private int clientConnectionCount; + + /// + /// Backing field for corresponding public property + /// + internal int serverConnectionCount; + /// /// A object that creates tcp connection to server /// @@ -50,8 +64,7 @@ public partial class ProxyServer : IDisposable /// /// Set firefox to use default system proxy /// - private FireFoxProxySettingsManager firefoxProxySettingsManager - = new FireFoxProxySettingsManager(); + private FireFoxProxySettingsManager firefoxProxySettingsManager = new FireFoxProxySettingsManager(); #endif /// @@ -125,6 +138,12 @@ public CertificateEngine CertificateEngine set { CertificateManager.Engine = value; } } + /// + /// Should we check for certificare revocation during SSL authentication to servers + /// Note: If enabled can reduce performance (Default disabled) + /// + public bool CheckCertificateRevocation { get; set; } + /// /// Does this proxy uses the HTTP protocol 100 continue behaviour strictly? /// Broken 100 contunue implementations on server/client may cause problems if enabled @@ -224,18 +243,18 @@ public Action ExceptionFunc /// List of supported Ssl versions /// public SslProtocols SupportedSslProtocols { get; set; } = SslProtocols.Tls - | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Ssl3; + | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Ssl3; /// /// Total number of active client connections /// - public int ClientConnectionCount { get; private set; } + public int ClientConnectionCount => clientConnectionCount; /// /// Total number of active server connections /// - public int ServerConnectionCount { get; internal set; } + public int ServerConnectionCount => serverConnectionCount; /// /// Constructor @@ -381,8 +400,7 @@ public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) #if !DEBUG firefoxProxySettingsManager.AddFirefox(); #endif - Console.WriteLine("Set endpoint at Ip {0} and port: {1} as System HTTPS Proxy", - endPoint.IpAddress, endPoint.Port); + Console.WriteLine("Set endpoint at Ip {0} and port: {1} as System HTTPS Proxy", endPoint.IpAddress, endPoint.Port); } /// @@ -596,13 +614,10 @@ private void OnAcceptConnection(IAsyncResult asyn) { Task.Run(async () => { - ClientConnectionCount++; + Interlocked.Increment(ref clientConnectionCount); - //This line is important! - //contributors please don't remove it without discussion - //It helps to avoid eventual deterioration of performance due to TCP port exhaustion - //due to default TCP CLOSE_WAIT timeout for 4 minutes - tcpClient.LingerState = new LingerOption(true, 0); + tcpClient.ReceiveTimeout = ConnectionTimeOutSeconds * 1000; + tcpClient.SendTimeout = ConnectionTimeOutSeconds * 1000; try { @@ -617,8 +632,23 @@ private void OnAcceptConnection(IAsyncResult asyn) } finally { - ClientConnectionCount--; - tcpClient?.Close(); + Interlocked.Decrement(ref clientConnectionCount); + + try + { + if (tcpClient != null) + { + //This line is important! + //contributors please don't remove it without discussion + //It helps to avoid eventual deterioration of performance due to TCP port exhaustion + //due to default TCP CLOSE_WAIT timeout for 4 minutes + tcpClient.LingerState = new LingerOption(true, 0); + tcpClient.Close(); + } + } + catch + { + } } }); } @@ -637,46 +667,5 @@ private void QuitListen(ProxyEndPoint endPoint) endPoint.Listener.Server.Close(); endPoint.Listener.Server.Dispose(); } - - /// - /// Invocator for BeforeRequest event. - /// - /// - /// - protected virtual void OnBeforeRequest(object sender, SessionEventArgs e) - { - BeforeRequest?.Invoke(sender, e); - } - - /// - /// Invocator for BeforeResponse event. - /// - /// - /// - /// - protected virtual void OnBeforeResponse(object sender, SessionEventArgs e) - { - BeforeResponse?.Invoke(sender, e); - } - - /// - /// Invocator for ServerCertificateValidationCallback event. - /// - /// - /// - protected virtual void OnServerCertificateValidationCallback(object sender, CertificateValidationEventArgs e) - { - ServerCertificateValidationCallback?.Invoke(sender, e); - } - - /// - /// Invocator for ClientCertifcateSelectionCallback event. - /// - /// - /// - protected virtual void OnClientCertificateSelectionCallback(object sender, CertificateSelectionEventArgs e) - { - ClientCertificateSelectionCallback?.Invoke(sender, e); - } } } diff --git a/Titanium.Web.Proxy/RequestHandler.cs b/Titanium.Web.Proxy/RequestHandler.cs index 60f3975e8..38ee110ed 100644 --- a/Titanium.Web.Proxy/RequestHandler.cs +++ b/Titanium.Web.Proxy/RequestHandler.cs @@ -6,15 +6,15 @@ using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; -using Titanium.Web.Proxy.Exceptions; +using System.Threading.Tasks; using Titanium.Web.Proxy.EventArguments; +using Titanium.Web.Proxy.Exceptions; +using Titanium.Web.Proxy.Extensions; using Titanium.Web.Proxy.Helpers; -using Titanium.Web.Proxy.Models; -using Titanium.Web.Proxy.Shared; using Titanium.Web.Proxy.Http; -using System.Threading.Tasks; -using Titanium.Web.Proxy.Extensions; +using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Network.Tcp; +using Titanium.Web.Proxy.Shared; namespace Titanium.Web.Proxy { @@ -23,14 +23,18 @@ namespace Titanium.Web.Proxy /// partial class ProxyServer { - //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 + /// + /// This is called when client is aware of proxy + /// So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy + /// + /// + /// + /// private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient tcpClient) { - CustomBufferedStream clientStream = new CustomBufferedStream(tcpClient.GetStream(), BufferSize); + var disposed = false; - clientStream.ReadTimeout = ConnectionTimeOutSeconds * 1000; - clientStream.WriteTimeout = ConnectionTimeOutSeconds * 1000; + var clientStream = new CustomBufferedStream(tcpClient.GetStream(), BufferSize); var clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); var clientStreamWriter = new StreamWriter(clientStream) { NewLine = ProxyConstants.NewLine }; @@ -44,7 +48,6 @@ private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient tcpCli if (string.IsNullOrEmpty(httpCmd)) { - Dispose(clientStream, clientStreamReader, clientStreamWriter, null); return; } @@ -99,7 +102,6 @@ private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient tcpCli if (await CheckAuthorization(clientStreamWriter, connectRequestHeaders) == false) { - Dispose(clientStream, clientStreamReader, clientStreamWriter, null); return; } @@ -111,22 +113,23 @@ private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient tcpCli { sslStream = new SslStream(clientStream, true); - var certificate = endPoint.GenericCertificate ?? CertificateManager.CreateCertificate(httpRemoteUri.Host, false); + var certificate = endPoint.GenericCertificate ?? + CertificateManager.CreateCertificate(httpRemoteUri.Host, false); //Successfully managed to authenticate the client using the fake certificate await sslStream.AuthenticateAsServerAsync(certificate, false, SupportedSslProtocols, false); + //HTTPS server created - we can now decrypt the client's traffic clientStream = new CustomBufferedStream(sslStream, BufferSize); + clientStreamReader.Dispose(); clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); clientStreamWriter = new StreamWriter(clientStream) { NewLine = ProxyConstants.NewLine }; } catch { sslStream?.Dispose(); - - Dispose(clientStream, clientStreamReader, clientStreamWriter, null); return; } @@ -138,6 +141,7 @@ await sslStream.AuthenticateAsServerAsync(certificate, false, { //Siphon out CONNECT request headers await clientStreamReader.ReadAndIgnoreAllLinesAsync(); + //write back successfull CONNECT response await WriteConnectResponse(clientStreamWriter, version); @@ -147,243 +151,82 @@ await TcpHelper.SendRaw(this, false, clientStream, tcpConnectionFactory); - Dispose(clientStream, clientStreamReader, clientStreamWriter, null); return; } + //Now create the request - await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter, - httpRemoteUri.Scheme == Uri.UriSchemeHttps ? httpRemoteUri.Host : null, endPoint, connectRequestHeaders); + disposed = await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter, + httpRemoteUri.Scheme == Uri.UriSchemeHttps ? httpRemoteUri.Host : null, endPoint, + connectRequestHeaders); + } + catch (Exception e) + { + ExceptionFunc(new Exception("Error whilst authorizing request", e)); } - catch (Exception) + finally { - Dispose(clientStream, - clientStreamReader, - clientStreamWriter, null); + if (!disposed) + { + Dispose(clientStream, clientStreamReader, clientStreamWriter, null); + } } } - //This is called when this proxy acts as a reverse proxy (like a real http server) - //So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client + /// + /// This is called when this proxy acts as a reverse proxy (like a real http server) + /// So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client + /// + /// + /// + /// private async Task HandleClient(TransparentProxyEndPoint endPoint, TcpClient tcpClient) { - CustomBufferedStream clientStream = new CustomBufferedStream(tcpClient.GetStream(), BufferSize); - - clientStream.ReadTimeout = ConnectionTimeOutSeconds * 1000; - clientStream.WriteTimeout = ConnectionTimeOutSeconds * 1000; + bool disposed = false; + var clientStream = new CustomBufferedStream(tcpClient.GetStream(), BufferSize); CustomBinaryReader clientStreamReader = null; StreamWriter clientStreamWriter = null; - if (endPoint.EnableSsl) + try { - var sslStream = new SslStream(clientStream, true); + if (endPoint.EnableSsl) + { + var sslStream = new SslStream(clientStream, true); + clientStream = new CustomBufferedStream(sslStream, BufferSize); - //implement in future once SNI supported by SSL stream, for now use the same certificate - var certificate = CertificateManager.CreateCertificate(endPoint.GenericCertificateName, false); + //implement in future once SNI supported by SSL stream, for now use the same certificate + var certificate = CertificateManager.CreateCertificate(endPoint.GenericCertificateName, false); - try - { //Successfully managed to authenticate the client using the fake certificate await sslStream.AuthenticateAsServerAsync(certificate, false, SslProtocols.Tls, false); - clientStream = new CustomBufferedStream(sslStream, BufferSize); - clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); - clientStreamWriter = new StreamWriter(clientStream) { NewLine = ProxyConstants.NewLine }; //HTTPS server created - we can now decrypt the client's traffic } - catch (Exception) - { - sslStream.Dispose(); - - Dispose(sslStream, - clientStreamReader, - clientStreamWriter, null); - return; - } - } - else - { clientStreamReader = new CustomBinaryReader(clientStream, BufferSize); clientStreamWriter = new StreamWriter(clientStream) { NewLine = ProxyConstants.NewLine }; - } - - //now read the request line - var httpCmd = await clientStreamReader.ReadLineAsync(); - - //Now create the request - await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter, - endPoint.EnableSsl ? endPoint.GenericCertificateName : null, endPoint, null); - } - /// - /// Create a Server Connection - /// - /// - /// - private async Task GetServerConnection( - SessionEventArgs args) - { - ExternalProxy customUpStreamHttpProxy = null; - ExternalProxy customUpStreamHttpsProxy = null; + //now read the request line + var httpCmd = await clientStreamReader.ReadLineAsync(); - if (args.WebSession.Request.RequestUri.Scheme == "http") - { - if (GetCustomUpStreamHttpProxyFunc != null) - { - customUpStreamHttpProxy = await GetCustomUpStreamHttpProxyFunc(args); - } - } - else - { - if (GetCustomUpStreamHttpsProxyFunc != null) - { - customUpStreamHttpsProxy = await GetCustomUpStreamHttpsProxyFunc(args); - } + //Now create the request + disposed = await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter, + endPoint.EnableSsl ? endPoint.GenericCertificateName : null, endPoint, null); } - - args.CustomUpStreamHttpProxyUsed = customUpStreamHttpProxy; - args.CustomUpStreamHttpsProxyUsed = customUpStreamHttpsProxy; - - return await tcpConnectionFactory.CreateClient(this, - args.WebSession.Request.RequestUri.Host, - args.WebSession.Request.RequestUri.Port, - args.WebSession.Request.HttpVersion, - args.IsHttps, - customUpStreamHttpProxy ?? UpStreamHttpProxy, - customUpStreamHttpsProxy ?? UpStreamHttpsProxy, - args.ProxyClient.ClientStream); - } - - - private async Task HandleHttpSessionRequestInternal(TcpConnection connection, - SessionEventArgs args, bool closeConnection) - { - try + finally { - args.WebSession.Request.RequestLocked = true; - - //If request was cancelled by user then dispose the client - if (args.WebSession.Request.CancelRequest) - { - Dispose(args.ProxyClient.ClientStream, - args.ProxyClient.ClientStreamReader, - args.ProxyClient.ClientStreamWriter, - args.WebSession.ServerConnection); - - return false; - } - - //if expect continue is enabled then send the headers first - //and see if server would return 100 conitinue - if (args.WebSession.Request.ExpectContinue) - { - args.WebSession.SetConnection(connection); - await args.WebSession.SendRequest(Enable100ContinueBehaviour); - } - - //If 100 continue was the response inform that to the client - if (Enable100ContinueBehaviour) - { - if (args.WebSession.Request.Is100Continue) - { - await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100", - "Continue", args.ProxyClient.ClientStreamWriter); - await args.ProxyClient.ClientStreamWriter.WriteLineAsync(); - } - else if (args.WebSession.Request.ExpectationFailed) - { - await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417", - "Expectation Failed", args.ProxyClient.ClientStreamWriter); - await args.ProxyClient.ClientStreamWriter.WriteLineAsync(); - } - } - - //If expect continue is not enabled then set the connectio and send request headers - if (!args.WebSession.Request.ExpectContinue) + if (!disposed) { - args.WebSession.SetConnection(connection); - await args.WebSession.SendRequest(Enable100ContinueBehaviour); - } - - //If request was modified by user - if (args.WebSession.Request.RequestBodyRead) - { - if (args.WebSession.Request.ContentEncoding != null) - { - args.WebSession.Request.RequestBody = await GetCompressedResponseBody(args.WebSession.Request.ContentEncoding, args.WebSession.Request.RequestBody); - } - //chunked send is not supported as of now - args.WebSession.Request.ContentLength = args.WebSession.Request.RequestBody.Length; - - var newStream = args.WebSession.ServerConnection.Stream; - await newStream.WriteAsync(args.WebSession.Request.RequestBody, 0, args.WebSession.Request.RequestBody.Length); - } - else - { - if (!args.WebSession.Request.ExpectationFailed) - { - //If its a post/put/patch request, then read the client html body and send it to server - var method = args.WebSession.Request.Method.ToUpper(); - if (method == "POST" || method == "PUT" || method == "PATCH") - { - await SendClientRequestBody(args); - } - } - } - - //If not expectation failed response was returned by server then parse response - if (!args.WebSession.Request.ExpectationFailed) - { - var result = await HandleHttpSessionResponse(args); - - //already disposed inside above method - if (result == false) - { - return false; - } - } - - //if connection is closing exit - if (args.WebSession.Response.ResponseKeepAlive == false) - { - Dispose(args.ProxyClient.ClientStream, - args.ProxyClient.ClientStreamReader, - args.ProxyClient.ClientStreamWriter, - args.WebSession.ServerConnection); - - return false; + Dispose(clientStream, clientStreamReader, clientStreamWriter, null); } } - catch (Exception e) - { - ExceptionFunc(new ProxyHttpException("Error occured whilst handling session request (internal)", e, args)); - - Dispose(args.ProxyClient.ClientStream, - args.ProxyClient.ClientStreamReader, - args.ProxyClient.ClientStreamWriter, - args.WebSession.ServerConnection); - - return false; - } - - if (closeConnection) - { - //dispose - Dispose(args.ProxyClient.ClientStream, - args.ProxyClient.ClientStreamReader, - args.ProxyClient.ClientStreamWriter, - args.WebSession.ServerConnection); - - return false; - } - - return true; } /// /// This is the core request handler method for a particular connection from client + /// Will create new session (request/response) sequence until + /// client/server abruptly terminates connection or by normal HTTP termination /// /// /// @@ -393,13 +236,13 @@ await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417", /// /// /// - /// - /// /// - private async Task HandleHttpSessionRequest(TcpClient client, string httpCmd, Stream clientStream, + private async Task HandleHttpSessionRequest(TcpClient client, string httpCmd, Stream clientStream, CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, string httpsHostName, ProxyEndPoint endPoint, List connectHeaders) { + bool disposed = false; + TcpConnection connection = null; //Loop through each subsequest request on this particular client connection @@ -408,19 +251,14 @@ private async Task HandleHttpSessionRequest(TcpClient client, string httpCmd, St { if (string.IsNullOrEmpty(httpCmd)) { - Dispose(clientStream, - clientStreamReader, - clientStreamWriter, - connection); - break; } var args = new SessionEventArgs(BufferSize, HandleHttpSessionResponse) - { - ProxyClient = { TcpClient = client }, - WebSession = { ConnectHeaders = connectHeaders } - }; + { + ProxyClient = { TcpClient = client }, + WebSession = { ConnectHeaders = connectHeaders } + }; args.WebSession.ProcessId = new Lazy(() => { @@ -458,7 +296,8 @@ private async Task HandleHttpSessionRequest(TcpClient client, string httpCmd, St //Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.NonUniqueRequestHeaders, args.WebSession.Request.RequestHeaders); - var httpRemoteUri = new Uri(httpsHostName == null ? httpCmdSplit[1] + var httpRemoteUri = new Uri(httpsHostName == null + ? httpCmdSplit[1] : string.Concat("https://", args.WebSession.Request.Host ?? httpsHostName, httpCmdSplit[1])); args.WebSession.Request.RequestUri = httpRemoteUri; @@ -469,15 +308,11 @@ private async Task HandleHttpSessionRequest(TcpClient client, string httpCmd, St args.ProxyClient.ClientStreamReader = clientStreamReader; args.ProxyClient.ClientStreamWriter = clientStreamWriter; - if (httpsHostName == null && - await CheckAuthorization(clientStreamWriter, + if (httpsHostName == null && + await CheckAuthorization(clientStreamWriter, args.WebSession.Request.RequestHeaders.Values) == false) { - Dispose(clientStream, - clientStreamReader, - clientStreamWriter, - connection); - + args.Dispose(); break; } @@ -487,15 +322,7 @@ await CheckAuthorization(clientStreamWriter, //If user requested interception do it if (BeforeRequest != null) { - var invocationList = BeforeRequest.GetInvocationList(); - var handlerTasks = new Task[invocationList.Length]; - - for (var i = 0; i < invocationList.Length; i++) - { - handlerTasks[i] = ((Func)invocationList[i])(this, args); - } - - await Task.WhenAll(handlerTasks); + await BeforeRequest.InvokeParallelAsync(this, args); } //if upgrading to websocket then relay the requet without reading the contents @@ -506,11 +333,7 @@ await TcpHelper.SendRaw(this, httpCmd, httpVersion, args.WebSession.Request.RequestHeaders, args.IsHttps, clientStream, tcpConnectionFactory); - Dispose(clientStream, - clientStreamReader, - clientStreamWriter, - connection); - + args.Dispose(); break; } @@ -520,52 +343,210 @@ await TcpHelper.SendRaw(this, } //construct the web request that we are going to issue on behalf of the client. - var result = await HandleHttpSessionRequestInternal(connection, args, false); + disposed = await HandleHttpSessionRequestInternal(connection, args, false); - if (result == false) + if (disposed) { //already disposed inside above method + args.Dispose(); break; } if (args.WebSession.Request.CancelRequest) { - Dispose(clientStream, - clientStreamReader, - clientStreamWriter, - connection); - + args.Dispose(); break; } //if connection is closing exit if (args.WebSession.Response.ResponseKeepAlive == false) { - Dispose(clientStream, - clientStreamReader, - clientStreamWriter, - connection); - + args.Dispose(); break; } + args.Dispose(); + // read the next request httpCmd = await clientStreamReader.ReadLineAsync(); } catch (Exception e) { ExceptionFunc(new ProxyHttpException("Error occured whilst handling session request", e, args)); - - Dispose(clientStream, - clientStreamReader, - clientStreamWriter, - connection); break; } } + if (!disposed) + { + Dispose(clientStream, clientStreamReader, clientStreamWriter, connection); + } + + return true; + } + + /// + /// Handle a specific session (request/response sequence) + /// + /// + /// + /// + /// + private async Task HandleHttpSessionRequestInternal(TcpConnection connection, + SessionEventArgs args, bool closeConnection) + { + bool disposed = false; + bool keepAlive = false; + + try + { + args.WebSession.Request.RequestLocked = true; + + //If request was cancelled by user then dispose the client + if (args.WebSession.Request.CancelRequest) + { + return true; + } + + //if expect continue is enabled then send the headers first + //and see if server would return 100 conitinue + if (args.WebSession.Request.ExpectContinue) + { + args.WebSession.SetConnection(connection); + await args.WebSession.SendRequest(Enable100ContinueBehaviour); + } + + //If 100 continue was the response inform that to the client + if (Enable100ContinueBehaviour) + { + if (args.WebSession.Request.Is100Continue) + { + await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100", + "Continue", args.ProxyClient.ClientStreamWriter); + await args.ProxyClient.ClientStreamWriter.WriteLineAsync(); + } + else if (args.WebSession.Request.ExpectationFailed) + { + await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417", + "Expectation Failed", args.ProxyClient.ClientStreamWriter); + await args.ProxyClient.ClientStreamWriter.WriteLineAsync(); + } + } + + //If expect continue is not enabled then set the connectio and send request headers + if (!args.WebSession.Request.ExpectContinue) + { + args.WebSession.SetConnection(connection); + await args.WebSession.SendRequest(Enable100ContinueBehaviour); + } + + //If request was modified by user + if (args.WebSession.Request.RequestBodyRead) + { + if (args.WebSession.Request.ContentEncoding != null) + { + args.WebSession.Request.RequestBody = await GetCompressedResponseBody(args.WebSession.Request.ContentEncoding, args.WebSession.Request.RequestBody); + } + //chunked send is not supported as of now + args.WebSession.Request.ContentLength = args.WebSession.Request.RequestBody.Length; + + var newStream = args.WebSession.ServerConnection.Stream; + await newStream.WriteAsync(args.WebSession.Request.RequestBody, 0, args.WebSession.Request.RequestBody.Length); + } + else + { + if (!args.WebSession.Request.ExpectationFailed) + { + //If its a post/put/patch request, then read the client html body and send it to server + var method = args.WebSession.Request.Method.ToUpper(); + if (method == "POST" || method == "PUT" || method == "PATCH") + { + await SendClientRequestBody(args); + } + } + } + + //If not expectation failed response was returned by server then parse response + if (!args.WebSession.Request.ExpectationFailed) + { + disposed = await HandleHttpSessionResponse(args); + + //already disposed inside above method + if (disposed) + { + return true; + } + } + + //if connection is closing exit + if (args.WebSession.Response.ResponseKeepAlive == false) + { + return true; + } + + if (!closeConnection) + { + keepAlive = true; + return false; + } + } + catch (Exception e) + { + ExceptionFunc(new ProxyHttpException("Error occured whilst handling session request (internal)", e, args)); + return true; + } + finally + { + if (!disposed && !keepAlive) + { + //dispose + Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader, args.ProxyClient.ClientStreamWriter, args.WebSession.ServerConnection); + } + } + + return true; } + /// + /// Create a Server Connection + /// + /// + /// + private async Task GetServerConnection( + SessionEventArgs args) + { + ExternalProxy customUpStreamHttpProxy = null; + ExternalProxy customUpStreamHttpsProxy = null; + + if (args.WebSession.Request.RequestUri.Scheme == "http") + { + if (GetCustomUpStreamHttpProxyFunc != null) + { + customUpStreamHttpProxy = await GetCustomUpStreamHttpProxyFunc(args); + } + } + else + { + if (GetCustomUpStreamHttpsProxyFunc != null) + { + customUpStreamHttpsProxy = await GetCustomUpStreamHttpsProxyFunc(args); + } + } + + args.CustomUpStreamHttpProxyUsed = customUpStreamHttpProxy; + args.CustomUpStreamHttpsProxyUsed = customUpStreamHttpsProxy; + + return await tcpConnectionFactory.CreateClient(this, + args.WebSession.Request.RequestUri.Host, + args.WebSession.Request.RequestUri.Port, + args.WebSession.Request.HttpVersion, + args.IsHttps, + customUpStreamHttpProxy ?? UpStreamHttpProxy, + customUpStreamHttpsProxy ?? UpStreamHttpsProxy, + args.ProxyClient.ClientStream); + } + + /// /// Write successfull CONNECT response to client /// diff --git a/Titanium.Web.Proxy/ResponseHandler.cs b/Titanium.Web.Proxy/ResponseHandler.cs index 0fadbbee0..3a25aacb7 100644 --- a/Titanium.Web.Proxy/ResponseHandler.cs +++ b/Titanium.Web.Proxy/ResponseHandler.cs @@ -1,14 +1,15 @@ using System; using System.Collections.Generic; using System.IO; -using Titanium.Web.Proxy.EventArguments; -using Titanium.Web.Proxy.Models; -using Titanium.Web.Proxy.Compression; +using System.Threading; using System.Threading.Tasks; +using Titanium.Web.Proxy.Compression; +using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Exceptions; using Titanium.Web.Proxy.Extensions; -using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Http; +using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Network.Tcp; namespace Titanium.Web.Proxy @@ -22,7 +23,7 @@ partial class ProxyServer /// Called asynchronously when a request was successfully and we received the response /// /// - /// true if no errors + /// true if client/server connection was terminated (and disposed) private async Task HandleHttpSessionResponse(SessionEventArgs args) { try @@ -40,28 +41,20 @@ private async Task HandleHttpSessionResponse(SessionEventArgs args) //If user requested call back then do it if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked) { - Delegate[] invocationList = BeforeResponse.GetInvocationList(); - Task[] handlerTasks = new Task[invocationList.Length]; - - for (int i = 0; i < invocationList.Length; i++) - { - handlerTasks[i] = ((Func)invocationList[i])(this, args); - } - - await Task.WhenAll(handlerTasks); + await BeforeResponse.InvokeParallelAsync(this, args); } if (args.ReRequest) { - if(args.WebSession.ServerConnection != null) + if (args.WebSession.ServerConnection != null) { args.WebSession.ServerConnection.Dispose(); - ServerConnectionCount--; + Interlocked.Decrement(ref serverConnectionCount); } var connection = await GetServerConnection(args); - var result = await HandleHttpSessionRequestInternal(null, args, true); - return result; + var disposed = await HandleHttpSessionRequestInternal(null, args, true); + return disposed; } args.WebSession.Response.ResponseLocked = true; @@ -137,12 +130,10 @@ await args.WebSession.ServerConnection.StreamReader Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader, args.ProxyClient.ClientStreamWriter, args.WebSession.ServerConnection); - return false; + return true; } - args.Dispose(); - - return true; + return false; } /// @@ -240,15 +231,17 @@ private void Dispose(Stream clientStream, StreamWriter clientStreamWriter, TcpConnection serverConnection) { - ServerConnectionCount--; - clientStream?.Close(); clientStream?.Dispose(); clientStreamReader?.Dispose(); clientStreamWriter?.Dispose(); - serverConnection?.Dispose(); + if (serverConnection != null) + { + serverConnection.Dispose(); + Interlocked.Decrement(ref serverConnectionCount); + } } } } diff --git a/Titanium.Web.Proxy/Shared/ProxyConstants.cs b/Titanium.Web.Proxy/Shared/ProxyConstants.cs index 6e2a1f29b..d2535b651 100644 --- a/Titanium.Web.Proxy/Shared/ProxyConstants.cs +++ b/Titanium.Web.Proxy/Shared/ProxyConstants.cs @@ -7,9 +7,9 @@ namespace Titanium.Web.Proxy.Shared /// internal class ProxyConstants { - internal static readonly char[] SpaceSplit = {' '}; - internal static readonly char[] ColonSplit = {':'}; - internal static readonly char[] SemiColonSplit = {';'}; + internal static readonly char[] SpaceSplit = { ' ' }; + internal static readonly char[] ColonSplit = { ':' }; + internal static readonly char[] SemiColonSplit = { ';' }; internal static readonly byte[] NewLineBytes = Encoding.ASCII.GetBytes(NewLine); diff --git a/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj b/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj index 6b186297c..0e211df08 100644 --- a/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj +++ b/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj @@ -73,6 +73,7 @@ +