diff --git a/examples/Web-SelfSigned.ps1 b/examples/Web-SelfSigned.ps1 new file mode 100644 index 000000000..94a2e6356 --- /dev/null +++ b/examples/Web-SelfSigned.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a HTTPS Pode server with a self-sign certificate + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 in HTTPS + +.EXAMPLE + To run the sample: ./Web-SelfSigned.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-SelfSigned.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer -Threads 6 { + Add-PodeEndpoint -Address localhost -Port '8081' -Protocol 'Https' -SelfSigned + + New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging + New-PodeLoggingMethod -File -Name 'errors' | Enable-PodeErrorLogging + + Add-PodeRoute -Method Get -Path / -ScriptBlock { + Write-PodeTextResponse -Value 'Test' + } +} \ No newline at end of file diff --git a/src/Listener/PodeHttpRequest.cs b/src/Listener/PodeHttpRequest.cs index 1742f0b17..6c3eee6b5 100644 --- a/src/Listener/PodeHttpRequest.cs +++ b/src/Listener/PodeHttpRequest.cs @@ -412,22 +412,35 @@ public override void PartialDispose() base.PartialDispose(); } - public override void 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 (IsDisposed) return; - 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 != default(MemoryStream)) + { + BodyStream.Dispose(); + BodyStream = default; + } + + if (Form != default(PodeForm)) + { + Form.Dispose(); + Form = default; + } } - 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..7fd7e4461 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,8 +56,12 @@ 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; @@ -83,6 +83,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 +146,7 @@ public async Task Open(CancellationToken cancellationToken) } } + /// /// Upgrades the current connection to SSL/TLS. /// @@ -152,49 +154,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 +236,49 @@ 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; - } - 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 @@ -301,10 +323,10 @@ public async Task Receive(CancellationToken cancellationToken) { PartialDispose(); } - return false; } + /// /// Reads data from the input stream until the specified bytes are found. /// @@ -421,41 +443,62 @@ 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) - { - return; - } + if (IsDisposed) return; IsDisposed = true; - if (Socket != default(Socket)) + if (disposing) { - PodeSocket.CloseSocket(Socket); - } + if (InputStream != default(Stream)) + { + InputStream.Dispose(); + InputStream = default; + } - if (InputStream != default(Stream)) - { - State = PodeStreamState.Closed; - InputStream.Dispose(); - InputStream = default; + if (Socket != default(Socket)) + { + PodeSocket.CloseSocket(Socket); + Socket = default; + } + + PartialDispose(); } + } - PartialDispose(); - PodeHelpers.WriteErrorMessage($"Request disposed", Context.Listener, PodeLoggingLevel.Verbose, Context); + /// + /// Disposes of the request and its associated resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } } } diff --git a/src/Listener/PodeSignalRequest.cs b/src/Listener/PodeSignalRequest.cs index f7acc49cb..13e2fa420 100644 --- a/src/Listener/PodeSignalRequest.cs +++ b/src/Listener/PodeSignalRequest.cs @@ -127,18 +127,28 @@ protected override async Task Parse(byte[] bytes, CancellationToken cancel return true; } - public override void 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 (IsDisposed) return; + + if (disposing) { + // 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 5e30f2893..1c3bfca08 100644 --- a/src/Listener/PodeSmtpRequest.cs +++ b/src/Listener/PodeSmtpRequest.cs @@ -50,7 +50,7 @@ public PodeSmtpRequest(Socket socket, PodeSocket podeSocket, PodeContext context Type = PodeProtocolType.Smtp; } - private bool IsCommand(string content, string command) + private static bool IsCommand(string content, string command) { if (string.IsNullOrWhiteSpace(content)) { @@ -291,7 +291,7 @@ public void Reset() Attachments = new List(); } - private string ParseEmail(string value) + private static string ParseEmail(string value) { var parts = value.Split(':'); if (parts.Length > 1) @@ -364,7 +364,7 @@ private Hashtable ParseHeaders(string value) return headers; } - private bool IsBodyValid(string value) + private static bool IsBodyValid(string value) { var lines = value.Split(new string[] { PodeHelpers.NEW_LINE }, StringSplitOptions.None); return Array.LastIndexOf(lines, ".") > -1; @@ -423,7 +423,7 @@ private void ParseBoundary() } } - private string ParseBody(string value, string boundary = null) + private static string ParseBody(string value, string boundary = null) { // split the message up var lines = value.Split(new string[] { PodeHelpers.NEW_LINE }, StringSplitOptions.None); @@ -455,7 +455,7 @@ private string ParseBody(string value, string boundary = null) return body; } - private byte[] ConvertBodyEncoding(string body, string contentEncoding) + private static byte[] ConvertBodyEncoding(string body, string contentEncoding) { switch (contentEncoding.ToLowerInvariant()) { @@ -476,7 +476,7 @@ private byte[] ConvertBodyEncoding(string body, string contentEncoding) } } - private string ConvertBodyType(byte[] bytes, string contentType) + private static string ConvertBodyType(byte[] bytes, string contentType) { if (bytes == default(byte[]) || bytes.Length == 0) { @@ -516,20 +516,33 @@ private string ConvertBodyType(byte[] bytes, string contentType) } } - public override void 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; - - if (Attachments != default(List)) + if (IsDisposed) return; + + 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..debd03b94 100644 --- a/src/Listener/PodeTcpRequest.cs +++ b/src/Listener/PodeTcpRequest.cs @@ -76,11 +76,23 @@ public void Close() Context.Dispose(true); } - public override void 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 (IsDisposed) return; + 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