From c7490f8768e98f6deb6f8e8ac683ea0e6fb67d4a Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 29 Nov 2024 07:45:39 -0800 Subject: [PATCH] fix #1459 --- src/Listener/PodeHttpRequest.cs | 51 +++-- src/Listener/PodeRequest.cs | 316 ++++++++++++++---------------- src/Listener/PodeSignalRequest.cs | 34 +++- src/Listener/PodeSmtpRequest.cs | 43 +++- src/Listener/PodeTcpRequest.cs | 26 ++- 5 files changed, 271 insertions(+), 199 deletions(-) diff --git a/src/Listener/PodeHttpRequest.cs b/src/Listener/PodeHttpRequest.cs index 1742f0b17..dd5d6b091 100644 --- a/src/Listener/PodeHttpRequest.cs +++ b/src/Listener/PodeHttpRequest.cs @@ -412,22 +412,51 @@ public override void PartialDispose() base.PartialDispose(); } - public override void Dispose() + /* public override void Dispose() + { + RawBody = default; + _body = string.Empty; + + if (BodyStream != default(MemoryStream)) + { + BodyStream.Dispose(); + } + + if (Form != default(PodeForm)) + { + Form.Dispose(); + } + + base.Dispose(); + }*/ + + /// + /// Dispose managed and unmanaged resources. + /// + /// Indicates whether the method is called explicitly or by garbage collection. + protected override void Dispose(bool disposing) { - RawBody = default; - _body = string.Empty; - - if (BodyStream != default(MemoryStream)) + if (disposing) { - BodyStream.Dispose(); - } + // Custom cleanup logic for PodeHttpRequest + RawBody = default; + _body = string.Empty; - if (Form != default(PodeForm)) - { - Form.Dispose(); + if (BodyStream != null) + { + BodyStream.Dispose(); + BodyStream = null; + } + + if (Form != null) + { + Form.Dispose(); + Form = null; + } } - base.Dispose(); + // Call the base Dispose to clean up shared resources + base.Dispose(disposing); } } } \ No newline at end of file diff --git a/src/Listener/PodeRequest.cs b/src/Listener/PodeRequest.cs index f36a1d938..fe840e0b7 100644 --- a/src/Listener/PodeRequest.cs +++ b/src/Listener/PodeRequest.cs @@ -1,14 +1,13 @@ using System; using System.IO; using System.Net; -using System.Net.Http; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; namespace Pode { @@ -27,9 +26,8 @@ public class PodeRequest : PodeProtocol, IDisposable public bool IsKeepAlive { get; protected set; } // Flags indicating request characteristics and handling status - public virtual bool CloseImmediately { get => false; } - public virtual bool IsProcessable { get => true; } - + public virtual bool CloseImmediately => false; + public virtual bool IsProcessable => true; // Input stream for incoming request data public Stream InputStream { get; private set; } public PodeStreamState State { get; private set; } @@ -39,19 +37,17 @@ public class PodeRequest : PodeProtocol, IDisposable public X509Certificate Certificate { get; private set; } public bool AllowClientCertificate { get; private set; } public PodeTlsMode TlsMode { get; private set; } - public X509Certificate2 ClientCertificate { get; set; } - public SslPolicyErrors ClientCertificateErrors { get; set; } + public X509Certificate2 ClientCertificate { get; private set; } + public SslPolicyErrors ClientCertificateErrors { get; private set; } public SslProtocols Protocols { get; private set; } - - // Error handling for request processing public PodeRequestException Error { get; set; } public bool IsAborted => Error != default(PodeRequestException); public bool IsDisposed { get; private set; } // Address and Scheme properties for the request public virtual string Address => Context.PodeSocket.HasHostnames - ? $"{Context.PodeSocket.Hostname}:{((IPEndPoint)LocalEndPoint).Port}" - : $"{((IPEndPoint)LocalEndPoint).Address}:{((IPEndPoint)LocalEndPoint).Port}"; + ? $"{Context.PodeSocket.Hostname}:{((IPEndPoint)LocalEndPoint).Port}" + : $"{((IPEndPoint)LocalEndPoint).Address}:{((IPEndPoint)LocalEndPoint).Port}"; public virtual string Scheme => SslUpgraded ? $"{Context.PodeSocket.Type}s" : $"{Context.PodeSocket.Type}"; @@ -60,10 +56,15 @@ public class PodeRequest : PodeProtocol, IDisposable protected PodeContext Context; // Encoding and buffer for handling incoming data - protected static UTF8Encoding Encoding = new UTF8Encoding(); - private byte[] Buffer; + protected static readonly UTF8Encoding Encoding = new UTF8Encoding(); + + // A fixed buffer used to temporarily store data read from the input stream. + // This buffer is readonly to prevent reassignment and reduce memory allocations. + private readonly byte[] Buffer; + private MemoryStream BufferStream; private const int BufferSize = 16384; + private readonly SemaphoreSlim StreamLock = new SemaphoreSlim(1, 1); /// /// Initializes a new instance of the PodeRequest class. @@ -83,6 +84,7 @@ public PodeRequest(Socket socket, PodeSocket podeSocket, PodeContext context) Protocols = podeSocket.Protocols; Context = context; State = PodeStreamState.New; + Buffer = new byte[BufferSize]; // Allocate buffer once } /// @@ -145,6 +147,7 @@ public async Task Open(CancellationToken cancellationToken) } } + /// /// Upgrades the current connection to SSL/TLS. /// @@ -152,49 +155,62 @@ public async Task Open(CancellationToken cancellationToken) /// A Task representing the async operation. public async Task UpgradeToSSL(CancellationToken cancellationToken) { - if (SslUpgraded) + if (SslUpgraded || IsDisposed) { State = PodeStreamState.Open; return; // Already upgraded } // Create an SSL stream for secure communication - var ssl = new SslStream(InputStream, false, new RemoteCertificateValidationCallback(ValidateCertificateCallback)); + var ssl = new SslStream(InputStream, false, ValidateCertificateCallback); // Authenticate the SSL stream, handling cancellation and exceptions - using (cancellationToken.Register(() => ssl.Dispose())) + try { - try + using (cancellationToken.Register(() => { - // Authenticate the SSL stream - await ssl.AuthenticateAsServerAsync(Certificate, AllowClientCertificate, Protocols, false).ConfigureAwait(false); - - // Set InputStream to the upgraded SSL stream - InputStream = ssl; - SslUpgraded = true; - State = PodeStreamState.Open; - } - catch (Exception ex) when (ex is OperationCanceledException || ex is IOException || ex is ObjectDisposedException) - { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Verbose); - State = PodeStreamState.Error; - Error = new PodeRequestException(ex, 500); - } - catch (AuthenticationException ex) - { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Debug); - State = PodeStreamState.Error; - Error = new PodeRequestException(ex, 400); - } - catch (Exception ex) + if (ssl != null && !IsDisposed) + { + ssl.Dispose(); + } + })) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); - State = PodeStreamState.Error; - Error = new PodeRequestException(ex, 502); + + // Authenticate the SSL stream + await ssl.AuthenticateAsServerAsync(Certificate, AllowClientCertificate, Protocols, false) + .ConfigureAwait(false); } + + // Set InputStream to the upgraded SSL stream + InputStream = ssl; + SslUpgraded = true; + State = PodeStreamState.Open; + } + catch (Exception ex) when (ex is OperationCanceledException || ex is IOException || ex is ObjectDisposedException) + { + PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Verbose); + ssl?.Dispose(); + State = PodeStreamState.Error; + Error = new PodeRequestException(ex, 500); + } + + catch (AuthenticationException ex) + { + PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Debug); + ssl?.Dispose(); + State = PodeStreamState.Error; + Error = new PodeRequestException(ex, 400); + } + catch (Exception ex) + { + PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); + ssl?.Dispose(); + State = PodeStreamState.Error; + Error = new PodeRequestException(ex, 502); } } + /// /// Callback to validate client certificates during the SSL handshake. /// @@ -221,42 +237,50 @@ private bool ValidateCertificateCallback(object sender, X509Certificate certific /// A Task representing the async operation, with a boolean indicating whether the connection should be closed. public async Task Receive(CancellationToken cancellationToken) { - // Check if the stream is open - if (State != PodeStreamState.Open) - { - return false; - } - + await StreamLock.WaitAsync(cancellationToken); try { - Error = default; + if (State != PodeStreamState.Open || InputStream == null) + { + return false; + } + + Error = null; - Buffer = new byte[BufferSize]; using (BufferStream = new MemoryStream()) { var close = true; while (true) { -#if NETCOREAPP2_1_OR_GREATER - // Read data from the input stream - var read = await InputStream.ReadAsync(Buffer.AsMemory(0, BufferSize), cancellationToken).ConfigureAwait(false); - if (read <= 0) + if (InputStream == null || cancellationToken.IsCancellationRequested || IsDisposed) { break; } - // Write the data to the buffer stream - await BufferStream.WriteAsync(Buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); + int read = 0; + try + { + // Read data from the input stream +#if NETCOREAPP2_1_OR_GREATER + read = await InputStream.ReadAsync(Buffer.AsMemory(0, BufferSize), cancellationToken).ConfigureAwait(false); #else - // Read data from the input stream - var read = await InputStream.ReadAsync(Buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false); + read = await InputStream.ReadAsync(Buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false); +#endif + } + catch (Exception ex) when (ex is IOException || ex is ObjectDisposedException) + { + PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Debug); + break; + } if (read <= 0) { break; } - // Write the data to the buffer stream +#if NETCOREAPP2_1_OR_GREATER + await BufferStream.WriteAsync(Buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); +#else await BufferStream.WriteAsync(Buffer, 0, read, cancellationToken).ConfigureAwait(false); #endif @@ -292,6 +316,10 @@ public async Task Receive(CancellationToken cancellationToken) PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); Error = ex; } + catch (ObjectDisposedException ex) + { + PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); + } catch (Exception ex) { PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); @@ -299,101 +327,12 @@ public async Task Receive(CancellationToken cancellationToken) } finally { + StreamLock.Release(); PartialDispose(); } - return false; } - /// - /// Reads data from the input stream until the specified bytes are found. - /// - /// The bytes to check for in the input stream. - /// Token to monitor for cancellation requests. - /// A Task representing the async operation, with a string containing the data read. - public async Task Read(byte[] checkBytes, CancellationToken cancellationToken) - { - // Check if the stream is open - if (State != PodeStreamState.Open) - { - return string.Empty; - } - - // Read data from the input stream until the check bytes are found - var buffer = new byte[BufferSize]; - using (var bufferStream = new MemoryStream()) - { - while (true) - { -#if NETCOREAPP2_1_OR_GREATER - // Read data from the input stream - var read = await InputStream.ReadAsync(buffer.AsMemory(0, BufferSize), cancellationToken).ConfigureAwait(false); - if (read <= 0) - { - break; - } - - // Write the data to the buffer stream - await bufferStream.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); -#else - // Read data from the input stream - var read = await InputStream.ReadAsync(buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false); - if (read <= 0) - { - break; - } - - // Write the data to the buffer stream - await bufferStream.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); -#endif - // Validate the input data - if (Socket.Available > 0 || !ValidateInputInternal(bufferStream.ToArray(), checkBytes)) - { - continue; - } - - break; - } - - return Encoding.GetString(bufferStream.ToArray()).Trim(); - } - } - - /// - /// Validates the input bytes against the specified check bytes. - /// - /// The bytes to validate. - /// The bytes to check against. - /// True if validation is successful, otherwise false. - private static bool ValidateInputInternal(byte[] bytes, byte[] checkBytes) - { - if (bytes.Length == 0) - { - return false; // Need more bytes - } - - if (checkBytes == default(byte[]) || checkBytes.Length == 0) - { - return true; // No specific bytes to check - } - - if (bytes.Length < checkBytes.Length) - { - return false; // Not enough bytes - } - - // Check if the input ends with checkBytes - for (var i = 0; i < checkBytes.Length; i++) - { - if (bytes[bytes.Length - (checkBytes.Length - i)] != checkBytes[i]) - { - return false; - } - } - - return true; - } - /// /// Parses the received bytes. This method should be implemented in derived classes. /// @@ -421,41 +360,78 @@ protected virtual bool ValidateInput(byte[] bytes) /// public virtual void PartialDispose() { - if (BufferStream != default(MemoryStream)) + try { - BufferStream.Dispose(); - BufferStream = default; - } + if (BufferStream != default(MemoryStream)) + { + BufferStream.Dispose(); + BufferStream = default; + } - Buffer = default; + // Clear the contents of the Buffer array + if (Buffer != null) + { + Array.Clear(Buffer, 0, Buffer.Length); + } + + } + catch (Exception ex) + { + PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); + } } /// - /// Disposes of the request and its associated resources. + /// Dispose managed and unmanaged resources. /// - public virtual void Dispose() + /// Indicates if disposing is called manually or by garbage collection. + protected virtual void Dispose(bool disposing) { - if (IsDisposed) + if (!IsDisposed) { - return; - } + if (disposing) + { + // Dispose managed resources + if (InputStream != default(Stream)) + { + State = PodeStreamState.Closed; + InputStream.Dispose(); + InputStream = default; + } - IsDisposed = true; + if (Socket != default(Socket)) + { + PodeSocket.CloseSocket(Socket); + Socket = default; + } - if (Socket != default(Socket)) - { - PodeSocket.CloseSocket(Socket); - } + PartialDispose(); - if (InputStream != default(Stream)) - { - State = PodeStreamState.Closed; - InputStream.Dispose(); - InputStream = default; + // Dispose SemaphoreSlim + StreamLock.Dispose(); + } + + // Dispose unmanaged resources if any + IsDisposed = true; } + } + + /// + /// Disposes of the request and its associated resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - PartialDispose(); - PodeHelpers.WriteErrorMessage($"Request disposed", Context.Listener, PodeLoggingLevel.Verbose, Context); + /// + /// Finalizer to release unmanaged resources. + /// + ~PodeRequest() + { + Dispose(false); } + } } diff --git a/src/Listener/PodeSignalRequest.cs b/src/Listener/PodeSignalRequest.cs index f7acc49cb..dd593d471 100644 --- a/src/Listener/PodeSignalRequest.cs +++ b/src/Listener/PodeSignalRequest.cs @@ -127,18 +127,40 @@ protected override async Task Parse(byte[] bytes, CancellationToken cancel return true; } - public override void Dispose() + /* public override void Dispose() + { + // send close frame + if (!IsDisposed) + { + PodeHelpers.WriteErrorMessage($"Closing Websocket", Context.Listener, PodeLoggingLevel.Verbose, Context); + Context.Response.WriteFrame(string.Empty, PodeWsOpCode.Close).Wait(); + } + + // remove client, and dispose + Context.Listener.Signals.Remove(Signal.ClientId); + base.Dispose(); + }*/ + + /// + /// Dispose managed and unmanaged resources. + /// + /// Indicates if the method is called explicitly or by garbage collection. + protected override void Dispose(bool disposing) { - // send close frame - if (!IsDisposed) + if (disposing && !IsDisposed) { + // Send close frame PodeHelpers.WriteErrorMessage($"Closing Websocket", Context.Listener, PodeLoggingLevel.Verbose, Context); + + // Wait for the close frame to be sent Context.Response.WriteFrame(string.Empty, PodeWsOpCode.Close).Wait(); + + // Remove the client signal + Context.Listener.Signals.Remove(Signal.ClientId); } - // remove client, and dispose - Context.Listener.Signals.Remove(Signal.ClientId); - base.Dispose(); + // Call the base Dispose to clean up other resources + base.Dispose(disposing); } } diff --git a/src/Listener/PodeSmtpRequest.cs b/src/Listener/PodeSmtpRequest.cs index 219a88640..cb9c8b425 100644 --- a/src/Listener/PodeSmtpRequest.cs +++ b/src/Listener/PodeSmtpRequest.cs @@ -516,20 +516,47 @@ private string ConvertBodyType(byte[] bytes, string contentType) } } - public override void Dispose() - { - RawBody = default; - Body = string.Empty; + /* public override void Dispose() + { + RawBody = default; + Body = string.Empty; - if (Attachments != default(List)) + if (Attachments != default(List)) + { + foreach (var attachment in Attachments) + { + attachment.Dispose(); + } + } + + base.Dispose(); + }*/ + + /// + /// Dispose managed and unmanaged resources. + /// + /// Indicates if the method is called explicitly or by garbage collection. + protected override void Dispose(bool disposing) + { + if (disposing) { - foreach (var attachment in Attachments) + // Custom cleanup logic for PodeSmtpRequest + RawBody = default; + Body = string.Empty; + + if (Attachments != null) { - attachment.Dispose(); + foreach (var attachment in Attachments) + { + attachment.Dispose(); + } + + Attachments = null; } } - base.Dispose(); + // Call the base Dispose to clean up other resources + base.Dispose(disposing); } } } \ No newline at end of file diff --git a/src/Listener/PodeTcpRequest.cs b/src/Listener/PodeTcpRequest.cs index c48982538..df18de9bd 100644 --- a/src/Listener/PodeTcpRequest.cs +++ b/src/Listener/PodeTcpRequest.cs @@ -76,11 +76,29 @@ public void Close() Context.Dispose(true); } - public override void Dispose() + /* public override void Dispose() + { + RawBody = default; + _body = string.Empty; + base.Dispose(); + }*/ + + /// + /// Dispose managed and unmanaged resources. + /// + /// Indicates if the method is called explicitly or by garbage collection. + protected override void Dispose(bool disposing) { - RawBody = default; - _body = string.Empty; - base.Dispose(); + if (disposing) + { + // Custom cleanup logic for PodeTcpRequest + RawBody = default; + _body = string.Empty; + } + + // Call the base Dispose to clean up other resources + base.Dispose(disposing); } + } } \ No newline at end of file