From 652b4445699b0087999601ac68083873dcd9d10c Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 16 May 2017 18:42:39 -0400 Subject: [PATCH 1/5] move around methods based on logical flow --- Titanium.Web.Proxy/RequestHandler.cs | 338 ++++++++++++++------------- 1 file changed, 179 insertions(+), 159 deletions(-) diff --git a/Titanium.Web.Proxy/RequestHandler.cs b/Titanium.Web.Proxy/RequestHandler.cs index 99918465d..d1a7a28d0 100644 --- a/Titanium.Web.Proxy/RequestHandler.cs +++ b/Titanium.Web.Proxy/RequestHandler.cs @@ -23,8 +23,13 @@ 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) { var disposed = false; @@ -139,7 +144,7 @@ await sslStream.AuthenticateAsServerAsync(certificate, false, { //Siphon out CONNECT request headers await clientStreamReader.ReadAndIgnoreAllLinesAsync(); - + //write back successfull CONNECT response await WriteConnectResponse(clientStreamWriter, version); @@ -170,8 +175,13 @@ await TcpHelper.SendRaw(this, } } - //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) { bool disposed = false; @@ -219,162 +229,10 @@ await sslStream.AuthenticateAsServerAsync(certificate, false, } } - /// - /// 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); - } - - - 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; - } - /// /// 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 /// /// /// @@ -533,6 +391,168 @@ await TcpHelper.SendRaw(this, 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 /// From f1e3741c096bedf3e60fbf39a9d1d2e20deca920 Mon Sep 17 00:00:00 2001 From: Honfika Date: Wed, 17 May 2017 16:48:13 +0200 Subject: [PATCH 2/5] Allow to query the root certificate status (IsRootCertificateTrusted and IsRootCertificateMachineTrusted) Allow to machine trust the root certificate (it will show the UAC dialog when required) --- .../Network/CertificateManager.cs | 101 ++++++++++++++++-- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/Titanium.Web.Proxy/Network/CertificateManager.cs b/Titanium.Web.Proxy/Network/CertificateManager.cs index 828e4f8fa..befb2d91b 100644 --- a/Titanium.Web.Proxy/Network/CertificateManager.cs +++ b/Titanium.Web.Proxy/Network/CertificateManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -30,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 { @@ -147,7 +148,7 @@ private string GetRootCertificatePath() return fileName; } - internal X509Certificate2 LoadRootCertificate() + private X509Certificate2 LoadRootCertificate() { var fileName = GetRootCertificatePath(); if (!File.Exists(fileName)) return null; @@ -218,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 -Enterprise -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. /// @@ -230,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)) { @@ -317,7 +399,7 @@ internal async void ClearIdleCertificates(int certificateCacheTimeOutMinutes) /// /// /// - internal void TrustRootCertificate(StoreLocation storeLocation) + private void TrustRootCertificate(StoreLocation storeLocation) { if (RootCertificate == null) { @@ -328,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 @@ -357,7 +439,7 @@ internal void TrustRootCertificate(StoreLocation storeLocation) /// /// /// - internal void RemoveTrustedRootCertificates(StoreLocation storeLocation) + private void RemoveTrustedRootCertificates(StoreLocation storeLocation) { if (RootCertificate == null) { @@ -368,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 @@ -382,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 @@ -392,6 +474,9 @@ internal void RemoveTrustedRootCertificates(StoreLocation storeLocation) } } + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// public void Dispose() { } From cad1b6d100d8bc02fc902f26d43a1331305502d5 Mon Sep 17 00:00:00 2001 From: "PDR\\jehonathan.thomas" Date: Wed, 17 May 2017 11:18:38 -0400 Subject: [PATCH 3/5] Not sure why this was added in PR #187 --- Titanium.Web.Proxy/ProxyServer.cs | 41 ------------------------------- 1 file changed, 41 deletions(-) diff --git a/Titanium.Web.Proxy/ProxyServer.cs b/Titanium.Web.Proxy/ProxyServer.cs index a912b0d53..fefd59b4d 100644 --- a/Titanium.Web.Proxy/ProxyServer.cs +++ b/Titanium.Web.Proxy/ProxyServer.cs @@ -647,46 +647,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); - } } } From 03e69d2e2c2f52f975b44ad56de7b7d538a184cd Mon Sep 17 00:00:00 2001 From: Honfika Date: Wed, 17 May 2017 17:51:26 +0200 Subject: [PATCH 4/5] Enterprise parameter removed --- Titanium.Web.Proxy/Network/CertificateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Titanium.Web.Proxy/Network/CertificateManager.cs b/Titanium.Web.Proxy/Network/CertificateManager.cs index befb2d91b..ddc9cd2ed 100644 --- a/Titanium.Web.Proxy/Network/CertificateManager.cs +++ b/Titanium.Web.Proxy/Network/CertificateManager.cs @@ -237,7 +237,7 @@ public bool TrustRootCertificateAsAdministrator() var info = new ProcessStartInfo { FileName = "certutil.exe", - Arguments = "-importPFX -Enterprise -p \"\" -f \"" + fileName + "\"", + Arguments = "-importPFX -p \"\" -f \"" + fileName + "\"", CreateNoWindow = true, UseShellExecute = true, Verb = "runas", From 10e5e3175c4eae810018dd6ac6e882bdaa353a59 Mon Sep 17 00:00:00 2001 From: "PDR\\jehonathan.thomas" Date: Wed, 17 May 2017 15:53:37 -0400 Subject: [PATCH 5/5] move dispose; remove references to actice objects after dispose --- .../EventArguments/SessionEventArgs.cs | 6 +++++- Titanium.Web.Proxy/Http/HttpWebClient.cs | 10 +++------- Titanium.Web.Proxy/Http/Request.cs | 17 ++++++++++++++++- Titanium.Web.Proxy/Http/Response.cs | 17 ++++++++++++++++- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs b/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs index 9f2824575..869dcf62d 100644 --- a/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs +++ b/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs @@ -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 @@ -522,6 +522,10 @@ public async Task Respond(Response response) /// public void Dispose() { + httpResponseHandler = null; + CustomUpStreamHttpProxyUsed = null; + CustomUpStreamHttpsProxyUsed = null; + WebSession.Dispose(); } } diff --git a/Titanium.Web.Proxy/Http/HttpWebClient.cs b/Titanium.Web.Proxy/Http/HttpWebClient.cs index bd4278320..ac61bbea1 100644 --- a/Titanium.Web.Proxy/Http/HttpWebClient.cs +++ b/Titanium.Web.Proxy/Http/HttpWebClient.cs @@ -214,14 +214,10 @@ internal async Task ReceiveResponse() /// public void Dispose() { - //not really needed since GC will collect it - //but just to be on safe side - Request.RequestBody = null; - Response.ResponseBody = null; - - Request.RequestBodyString = null; - Response.ResponseBodyString = null; + ConnectHeaders = null; + Request.Dispose(); + Response.Dispose(); } } } diff --git a/Titanium.Web.Proxy/Http/Request.cs b/Titanium.Web.Proxy/Http/Request.cs index 0afdea85c..2784511a9 100644 --- a/Titanium.Web.Proxy/Http/Request.cs +++ b/Titanium.Web.Proxy/Http/Request.cs @@ -9,7 +9,7 @@ namespace Titanium.Web.Proxy.Http /// /// A HTTP(S) request object /// - public class Request + public class Request : IDisposable { /// /// Request Method @@ -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 0393f4875..82b5fdd6c 100644 --- a/Titanium.Web.Proxy/Http/Response.cs +++ b/Titanium.Web.Proxy/Http/Response.cs @@ -10,7 +10,7 @@ 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; + } } }