From 453feabf2a71dc22f3c5c3074a92d919ba497003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graziano?= Date: Thu, 18 Apr 2019 17:38:31 +0200 Subject: [PATCH 1/5] Previous code with "continue" generates a memory leak on 'connection' for an unknown reason. In any case, this code is easier to read. --- .../Network/Tcp/TcpConnectionFactory.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs index 2f726bf21..04d68deee 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs @@ -494,11 +494,12 @@ private async Task clearOutdatedConnections() || connection.LastAccess < cutOff) { disposalBag.Add(connection); - continue; } - - queue.Enqueue(connection); - break; + else + { + queue.Enqueue(connection); + break; + } } } } From 4c954df6ca3712d17f28423f9962391e2c7ab7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graziano?= Date: Tue, 30 Apr 2019 11:14:00 +0200 Subject: [PATCH 2/5] Optimisation : Rewrite startsWith with PeekBytesAsync instead of looping on PeekByteAsync --- .../ExplicitClientHandler.cs | 2 +- src/Titanium.Web.Proxy/Helpers/HttpHelper.cs | 46 +++++++------------ 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs index 3cbab765b..c05888280 100644 --- a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs @@ -49,7 +49,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect TunnelConnectSessionEventArgs connectArgs = null; // Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) - if (await HttpHelper.IsConnectMethod(clientStream) == 1) + if (await HttpHelper.IsConnectMethod(clientStream, cancellationToken) == 1) { // read the first line HTTP command string httpCmd = await clientStream.ReadLineAsync(cancellationToken); diff --git a/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs b/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs index c13dc0c07..487df0ec8 100644 --- a/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs +++ b/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs @@ -1,5 +1,7 @@ using System; using System.Text; +using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using StreamExtended.Network; using Titanium.Web.Proxy.Extensions; @@ -120,9 +122,9 @@ internal static string GetWildCardDomainName(string hostname) /// /// The client stream reader. /// 1: when CONNECT, 0: when valid HTTP method, -1: otherwise - internal static Task IsConnectMethod(ICustomStreamReader clientStreamReader) + internal static Task IsConnectMethod(ICustomStreamReader clientStreamReader, CancellationToken cancellationToken = default(CancellationToken)) { - return startsWith(clientStreamReader, "CONNECT"); + return startsWith(clientStreamReader, "CONNECT", cancellationToken); } /// @@ -130,9 +132,9 @@ internal static Task IsConnectMethod(ICustomStreamReader clientStreamReader /// /// The client stream reader. /// 1: when PRI, 0: when valid HTTP method, -1: otherwise - internal static Task IsPriMethod(ICustomStreamReader clientStreamReader) + internal static Task IsPriMethod(ICustomStreamReader clientStreamReader, CancellationToken cancellationToken = default(CancellationToken)) { - return startsWith(clientStreamReader, "PRI"); + return startsWith(clientStreamReader, "PRI", cancellationToken); } /// @@ -143,37 +145,23 @@ internal static Task IsPriMethod(ICustomStreamReader clientStreamReader) /// /// 1: when starts with the given string, 0: when valid HTTP method, -1: otherwise /// - private static async Task startsWith(ICustomStreamReader clientStreamReader, string expectedStart) + private static async Task startsWith(ICustomStreamReader clientStreamReader, string expectedStart, CancellationToken cancellationToken = default(CancellationToken)) { - bool isExpected = true; + int iRet = -1; int lengthToCheck = 10; - for (int i = 0; i < lengthToCheck; i++) - { - int b = await clientStreamReader.PeekByteAsync(i); - if (b == -1) - { - return -1; - } - - if (b == ' ' && i > 2) - { - return isExpected ? 1 : 0; - } - char ch = (char)b; - if (!char.IsLetter(ch)) - { - return -1; - } + var vBuffer = await clientStreamReader.PeekBytesAsync(0, lengthToCheck, cancellationToken); + if (vBuffer != null) + { + var httpMethod = defaultEncoding.GetString(vBuffer); - if (i >= expectedStart.Length || ch != expectedStart[i]) - { - isExpected = false; - } + if (httpMethod.StartsWith(expectedStart)) + iRet = 1; + else if (Regex.Match(httpMethod, @"^[a-z]{3,} ", RegexOptions.IgnoreCase).Success) //valid HTTP requests start by at least 3 letters + space + iRet = 0; } - // only letters - return isExpected ? 1 : 0; + return iRet; } } } From 36171d185eb8e45bc885122e543dadc3c6eedf55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graziano?= Date: Tue, 30 Apr 2019 11:16:21 +0200 Subject: [PATCH 3/5] Allow async read/write for .netstandard 2.0 --- src/StreamExtended/Network/CustomBufferedStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StreamExtended/Network/CustomBufferedStream.cs b/src/StreamExtended/Network/CustomBufferedStream.cs index 9c815141c..75d47e464 100644 --- a/src/StreamExtended/Network/CustomBufferedStream.cs +++ b/src/StreamExtended/Network/CustomBufferedStream.cs @@ -605,7 +605,7 @@ private static void ResizeBuffer(ref byte[] buffer, long size) buffer = newBuffer; } -#if NET45 +#if NET45 || NETSTANDARD2_0 /// /// Base Stream.BeginRead will call this.Read and block thread (we don't want this, Network stream handles async) From 712da855949eda4bc84ae075df760de1e1a23673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graziano?= Date: Thu, 2 May 2019 17:39:36 +0200 Subject: [PATCH 4/5] Mixed implementation for startWith + PeekBytesAsync with the use of bufferpool (to get the best of the two worlds) --- src/StreamExtended/Network/CopyStream.cs | 4 +- .../Network/CustomBufferedPeekStream.cs | 6 +- .../Network/CustomBufferedStream.cs | 12 ++-- .../Network/ICustomStreamReader.cs | 4 +- .../ExplicitClientHandler.cs | 6 +- src/Titanium.Web.Proxy/Helpers/HttpHelper.cs | 55 ++++++++++++++----- 6 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/StreamExtended/Network/CopyStream.cs b/src/StreamExtended/Network/CopyStream.cs index 041eecffe..a337c11fb 100644 --- a/src/StreamExtended/Network/CopyStream.cs +++ b/src/StreamExtended/Network/CopyStream.cs @@ -55,9 +55,9 @@ public byte PeekByteFromBuffer(int index) return reader.PeekByteAsync(index, cancellationToken); } - public Task PeekBytesAsync(int index, int size, CancellationToken cancellationToken = default(CancellationToken)) + public Task PeekBytesAsync(byte[] buffer, int offset, int index, int size, CancellationToken cancellationToken = default(CancellationToken)) { - return reader.PeekBytesAsync(index, size, cancellationToken); + return reader.PeekBytesAsync(buffer, offset, index, size, cancellationToken); } public void Flush() diff --git a/src/StreamExtended/Network/CustomBufferedPeekStream.cs b/src/StreamExtended/Network/CustomBufferedPeekStream.cs index ab3d86299..43a9d2ace 100644 --- a/src/StreamExtended/Network/CustomBufferedPeekStream.cs +++ b/src/StreamExtended/Network/CustomBufferedPeekStream.cs @@ -88,12 +88,14 @@ byte ICustomStreamReader.PeekByteFromBuffer(int index) /// /// Peeks bytes asynchronous. /// + /// The buffer to copy. + /// The offset where copying. /// The index. /// The cancellation token. /// - Task ICustomStreamReader.PeekBytesAsync(int index, int size, CancellationToken cancellationToken) + Task ICustomStreamReader.PeekBytesAsync(byte[] buffer, int offset, int index, int size, CancellationToken cancellationToken) { - return baseStream.PeekBytesAsync(index, size, cancellationToken); + return baseStream.PeekBytesAsync(buffer, offset, index, size, cancellationToken); } /// diff --git a/src/StreamExtended/Network/CustomBufferedStream.cs b/src/StreamExtended/Network/CustomBufferedStream.cs index 75d47e464..9b93b51d1 100644 --- a/src/StreamExtended/Network/CustomBufferedStream.cs +++ b/src/StreamExtended/Network/CustomBufferedStream.cs @@ -252,10 +252,12 @@ public override int ReadByte() /// /// Peeks bytes asynchronous. /// + /// The buffer to copy. + /// The offset where copying. /// The index. /// The cancellation token. /// - public async Task PeekBytesAsync(int index, int size, CancellationToken cancellationToken = default(CancellationToken)) + public async Task PeekBytesAsync(byte[] buffer, int offset, int index, int size, CancellationToken cancellationToken = default(CancellationToken)) { if (Available <= index) { @@ -270,12 +272,12 @@ public override int ReadByte() if (Available <= (index + size)) { - return null; + return -1; } - var vRet = new byte[size]; - Array.Copy(streamBuffer, index, vRet, 0, size); - return vRet; + Buffer.BlockCopy(streamBuffer, index, buffer, offset, size); + + return size; } /// diff --git a/src/StreamExtended/Network/ICustomStreamReader.cs b/src/StreamExtended/Network/ICustomStreamReader.cs index 89d327806..3d9848e34 100644 --- a/src/StreamExtended/Network/ICustomStreamReader.cs +++ b/src/StreamExtended/Network/ICustomStreamReader.cs @@ -40,10 +40,12 @@ public interface ICustomStreamReader /// /// Peeks bytes asynchronous. /// + /// The buffer to copy. + /// The offset where copying. /// The index. /// The cancellation token. /// - Task PeekBytesAsync(int index, int size, CancellationToken cancellationToken = default(CancellationToken)); + Task PeekBytesAsync(byte[] buffer, int offset, int index, int size, CancellationToken cancellationToken = default(CancellationToken)); byte ReadByteFromBuffer(); diff --git a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs index c05888280..9ef639a5a 100644 --- a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs @@ -49,7 +49,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect TunnelConnectSessionEventArgs connectArgs = null; // Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) - if (await HttpHelper.IsConnectMethod(clientStream, cancellationToken) == 1) + if (await HttpHelper.IsConnectMethod(clientStream, BufferPool, BufferSize, cancellationToken) == 1) { // read the first line HTTP command string httpCmd = await clientStream.ReadLineAsync(cancellationToken); @@ -220,7 +220,7 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, $"Couldn't authenticate host '{connectHostname}' with certificate '{certName}'.", e, connectArgs); } - if (await HttpHelper.IsConnectMethod(clientStream) == -1) + if (await HttpHelper.IsConnectMethod(clientStream, BufferPool, BufferSize, cancellationToken) == -1) { decryptSsl = false; } @@ -292,7 +292,7 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, BufferSize, } } - if (connectArgs != null && await HttpHelper.IsPriMethod(clientStream) == 1) + if (connectArgs != null && await HttpHelper.IsPriMethod(clientStream, BufferPool, BufferSize, cancellationToken) == 1) { // todo string httpCmd = await clientStream.ReadLineAsync(cancellationToken); diff --git a/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs b/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs index 487df0ec8..73d738a6c 100644 --- a/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs +++ b/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using StreamExtended; using StreamExtended.Network; using Titanium.Web.Proxy.Extensions; using Titanium.Web.Proxy.Http; @@ -122,9 +123,9 @@ internal static string GetWildCardDomainName(string hostname) /// /// The client stream reader. /// 1: when CONNECT, 0: when valid HTTP method, -1: otherwise - internal static Task IsConnectMethod(ICustomStreamReader clientStreamReader, CancellationToken cancellationToken = default(CancellationToken)) + internal static Task IsConnectMethod(ICustomStreamReader clientStreamReader, IBufferPool bufferPool, int bufferSize, CancellationToken cancellationToken = default(CancellationToken)) { - return startsWith(clientStreamReader, "CONNECT", cancellationToken); + return startsWith(clientStreamReader, bufferPool, bufferSize, "CONNECT", cancellationToken); } /// @@ -132,9 +133,9 @@ internal static string GetWildCardDomainName(string hostname) /// /// The client stream reader. /// 1: when PRI, 0: when valid HTTP method, -1: otherwise - internal static Task IsPriMethod(ICustomStreamReader clientStreamReader, CancellationToken cancellationToken = default(CancellationToken)) + internal static Task IsPriMethod(ICustomStreamReader clientStreamReader, IBufferPool bufferPool, int bufferSize, CancellationToken cancellationToken = default(CancellationToken)) { - return startsWith(clientStreamReader, "PRI", cancellationToken); + return startsWith(clientStreamReader, bufferPool, bufferSize, "PRI", cancellationToken); } /// @@ -145,22 +146,46 @@ internal static string GetWildCardDomainName(string hostname) /// /// 1: when starts with the given string, 0: when valid HTTP method, -1: otherwise /// - private static async Task startsWith(ICustomStreamReader clientStreamReader, string expectedStart, CancellationToken cancellationToken = default(CancellationToken)) + private static async Task startsWith(ICustomStreamReader clientStreamReader, IBufferPool bufferPool, int bufferSize, string expectedStart, CancellationToken cancellationToken = default(CancellationToken)) { int iRet = -1; - int lengthToCheck = 10; - - var vBuffer = await clientStreamReader.PeekBytesAsync(0, lengthToCheck, cancellationToken); - if (vBuffer != null) + const int lengthToCheck = 10; + byte[] buffer = null; + try { - var httpMethod = defaultEncoding.GetString(vBuffer); + buffer = bufferPool.GetBuffer(Math.Max(bufferSize, lengthToCheck)); - if (httpMethod.StartsWith(expectedStart)) - iRet = 1; - else if (Regex.Match(httpMethod, @"^[a-z]{3,} ", RegexOptions.IgnoreCase).Success) //valid HTTP requests start by at least 3 letters + space - iRet = 0; - } + int peeked = await clientStreamReader.PeekBytesAsync(buffer, 0, 0, lengthToCheck, cancellationToken); + + if (peeked > 0) + { + bool isExpected = true; + + for (int i = 0; i < lengthToCheck; i++) + { + int b = buffer[i]; + if (b == ' ' && i > 2) + return isExpected ? 1 : 0; + else + { + char ch = (char)b; + if (!char.IsLetter(ch)) + return -1; + else if (i >= expectedStart.Length || ch != expectedStart[i]) + isExpected = false; + } + } + + // only letters + iRet = isExpected ? 1 : 0; + } + } + finally + { + bufferPool.ReturnBuffer(buffer); + buffer = null; + } return iRet; } } From 715a26d3948d2294de332d8d345fd005cd02ca80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graziano?= Date: Tue, 7 May 2019 11:27:29 +0200 Subject: [PATCH 5/5] clientStream were undisposed. --------------------------- //1st clientStream var clientStream = new CustomBufferedStream(clientConnection.GetStream(), BufferPool, BufferSize); sslStream = new SslStream(clientStream, false); //false => do not dispose 1st ClientStream //2nd clientStream clientStream = new CustomBufferedStream(sslStream, BufferPool, BufferSize); ... sslStream?.Dispose(); // dispose sslStream but not 1st ClientStream clientStream.Dispose(); // dispose 2nd ClientStream, sslStream (already disposed) //here 1st clientStream is not disposed --------------------------- --- src/Titanium.Web.Proxy/ExplicitClientHandler.cs | 2 +- src/Titanium.Web.Proxy/TransparentClientHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs index 9ef639a5a..a4fd23498 100644 --- a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs @@ -182,7 +182,7 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, X509Certificate2 certificate = null; try { - sslStream = new SslStream(clientStream, true); + sslStream = new SslStream(clientStream, false); string certName = HttpHelper.GetWildCardDomainName(connectHostname); certificate = endPoint.GenericCertificate ?? diff --git a/src/Titanium.Web.Proxy/TransparentClientHandler.cs b/src/Titanium.Web.Proxy/TransparentClientHandler.cs index b5e429be8..c929c567d 100644 --- a/src/Titanium.Web.Proxy/TransparentClientHandler.cs +++ b/src/Titanium.Web.Proxy/TransparentClientHandler.cs @@ -67,7 +67,7 @@ private async Task handleClient(TransparentProxyEndPoint endPoint, TcpClientConn X509Certificate2 certificate = null; try { - sslStream = new SslStream(clientStream, true); + sslStream = new SslStream(clientStream, false); string certName = HttpHelper.GetWildCardDomainName(httpsHostName); certificate = endPoint.GenericCertificate ??