diff --git a/Certstream/Certstream.csproj b/Certstream/Certstream.csproj index 5122089..5ff26ae 100644 --- a/Certstream/Certstream.csproj +++ b/Certstream/Certstream.csproj @@ -7,7 +7,7 @@ Certstream akac akac - A C# library for processing newly issued SSL certificates in real time using the Certstream API. + C# library for real-time SSL certificate processing using the Calidog Certstream API. certstream; certificate-transparency; transparency; x509; security; ssl; tls; certificate; letsencrypt; cloudflare; digicert; verisign icon.png NuGet.md @@ -18,8 +18,8 @@ 1.2.2 - https://github.com/actually-akac/Certstream - https://github.com/actually-akac/Certstream + https://github.com/akacdev/Certstream + https://github.com/akacdev/Certstream git true diff --git a/Certstream/CertstreamClient.cs b/Certstream/CertstreamClient.cs index 3599b1c..62bfff2 100644 --- a/Certstream/CertstreamClient.cs +++ b/Certstream/CertstreamClient.cs @@ -3,10 +3,10 @@ using Microsoft.Extensions.Logging.Abstractions; using System; using System.Net.WebSockets; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Timer = System.Timers.Timer; namespace Certstream { @@ -15,45 +15,14 @@ namespace Certstream /// public class CertstreamClient { - /// - /// Logger - /// - private readonly ILogger _logger; - - /// - /// How often should the WebSocket connection be pinged, in milliseconds. - /// + private readonly string _hostname; private readonly int _pingInterval; - - /// - /// The delay between reconnecting a closed WebSocket connection, in milliseconds. - /// private readonly int _reconnectionDelay; - - /// - /// The maximum limit of consecutive reconnection fails until an exception is thrown. - /// - private readonly int _maxRetries; - - /// - /// The hostname to connect to. - /// - public readonly string _hostname; - - /// - /// The current amount of consecutive reconnection fails. - /// private int _retries = 0; - - /// - /// The calculated connection Uri. - /// + private readonly int _maxRetries; private readonly Uri _connectionUri; - - /// - /// The connection type to use in this instance. - /// private readonly ConnectionType _connectionType; + private readonly Timer _pingTimer = new(); private ClientWebSocket _webSocket; private CancellationTokenSource _cancellationTokenSource; @@ -61,6 +30,8 @@ public class CertstreamClient private EventHandler _fullHandler; private EventHandler _domainsOnlyHandler; + private readonly ILogger _logger; + /// /// Fired whenever a new SSL certificate is issued and received from the WebSocket connection. /// Requires to be used. @@ -70,9 +41,7 @@ public event EventHandler CertificateIssued add { if (_connectionType != ConnectionType.Full) - { throw new("Only available in connection type: Full."); - } _fullHandler += value; } @@ -83,17 +52,15 @@ public event EventHandler CertificateIssued } /// - /// Fired whenever a new hostname is spotted and received from the WebSocket connection. + /// Fired whenever a new hostname is received from the WebSocket connection. /// Requires to be used. /// - public event EventHandler HostnameSpotted + public event EventHandler HostnameReceived { add { if (_connectionType != ConnectionType.DomainsOnly) - { throw new("Only available in connection type: Domains-Only."); - } _domainsOnlyHandler += value; } @@ -107,25 +74,27 @@ public event EventHandler HostnameSpotted /// Create a new instance of the Certstream client. /// /// The type of connection to use in this instance. - /// Use WebSocket Secure + /// Whether to connect using WebSocket secure scheme. /// The Certstream server hostname to connect to. Use this if you want to connect to your own instance. + /// The interval of sending ping messages to the server. /// The maximum limit of consecutive reconnection fails until an exception is thrown.Set to a negative value for no limit. /// The delay to wait before attempting to reconnect. - /// The delay to wait before attempting to reconnect. + /// The logger to use for advanced logging. /// public CertstreamClient( ConnectionType connectionType = Constants.ConnectionType, bool secureWebsocket = true, string hostname = Constants.Hostname, + int pingInterval = Constants.PingInterval, int maxRetries = Constants.MaxRetries, int reconnectionDelay = Constants.ReconnectionDelay, ILogger logger = default) { _connectionType = connectionType; _hostname = hostname; + _pingInterval = pingInterval; _maxRetries = maxRetries; _reconnectionDelay = reconnectionDelay; - _logger = logger ?? new NullLogger(); string path = connectionType switch @@ -135,60 +104,44 @@ public CertstreamClient( _ => throw new NotImplementedException() }; - string rawUrl; - if (secureWebsocket) - { - rawUrl = string.Concat("wss://", hostname, path); - } - else - { - rawUrl = string.Concat("ws://", hostname, path); - } - + string rawUrl = secureWebsocket ? string.Concat("wss://", hostname, path) : string.Concat("ws://", hostname, path); if (!Uri.TryCreate(rawUrl, UriKind.Absolute, out Uri uri)) - { throw new ArgumentException("Invalid hostname provided.", nameof(hostname)); - } _connectionUri = uri; } /// - /// Start receive certificate data + /// Start receiveing certificate data. /// public Task StartAsync(CancellationToken cancellationToken = default) { if (!_cancellationTokenSource?.IsCancellationRequested ?? false) - { return Task.FromResult(false); - } _cancellationTokenSource?.Dispose(); _cancellationTokenSource = new(); - _ = Task.Run(async () => await MessageProcessorAsync(_cancellationTokenSource.Token), cancellationToken); + _ = Task.Run(async () => await ProcessMessagesAsync(_cancellationTokenSource.Token), cancellationToken); + + StartPinging(cancellationToken); return Task.FromResult(true); } /// - /// Stop receive certificate data + /// Stop receiving certificate data. /// - public Task StopAsync(CancellationToken cancellationToken = default) + public Task StopAsync() { - if (_cancellationTokenSource == null) - { + if (_cancellationTokenSource is null || _cancellationTokenSource.IsCancellationRequested) return Task.FromResult(false); - } - - if (_cancellationTokenSource.IsCancellationRequested) - { - return Task.FromResult(false); - } _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); + StopPinging(); + return Task.FromResult(true); } @@ -201,9 +154,9 @@ private void CreateWebsocket() /// /// The main method that connects to the WebSocket and listens for new messages. /// - private async Task MessageProcessorAsync(CancellationToken cancellationToken) + private async Task ProcessMessagesAsync(CancellationToken cancellationToken) { - this._logger.LogInformation($"{nameof(MessageProcessorAsync)} - Start"); + _logger.LogInformation($"{nameof(ProcessMessagesAsync)} - Starting"); byte[] receiveBuffer = new byte[Constants.BufferSize]; int offset = 0; @@ -224,27 +177,27 @@ private async Task MessageProcessorAsync(CancellationToken cancellationToken) } catch (Exception exception) { - this._logger.LogError(exception, $"{nameof(MessageProcessorAsync)} - Websocket cleanup failure"); + _logger.LogError(exception, $"{nameof(ProcessMessagesAsync)} - WebSocket cleanup failure"); } } if (_webSocket.State != WebSocketState.Open) { if (_maxRetries >= 0 && _retries >= _maxRetries) - { throw new CertstreamException($"Failed to connect to Certstream after {_retries} retries."); - } try { await _webSocket.ConnectAsync(_connectionUri, cancellationToken); _retries = 0; + + _logger.LogInformation($"{nameof(ProcessMessagesAsync)} - Connected"); continue; } catch (Exception exception) { - this._logger.LogError(exception, $"{nameof(MessageProcessorAsync)} - Connect failure"); - await Task.Delay(_reconnectionDelay); + _logger.LogError(exception, $"{nameof(ProcessMessagesAsync)} - Connect failure"); + await Task.Delay(_reconnectionDelay, cancellationToken); _retries++; } } @@ -254,14 +207,10 @@ private async Task MessageProcessorAsync(CancellationToken cancellationToken) try { int remainingBufferSpace = receiveBuffer.Length - offset; - if (remainingBufferSpace <= 0) - { throw new InvalidOperationException("Buffer overflow: The receive buffer is full."); - } ArraySegment bytesReceived = new(receiveBuffer, offset, remainingBufferSpace); - WebSocketReceiveResult result = await _webSocket.ReceiveAsync(bytesReceived, cancellationToken); offset += result.Count; @@ -274,7 +223,7 @@ private async Task MessageProcessorAsync(CancellationToken cancellationToken) } catch (Exception exception) { - this._logger.LogError(exception, $"{nameof(MessageProcessorAsync)} - Process Data"); + _logger.LogError(exception, $"{nameof(ProcessMessagesAsync)} - Process Data"); break; } } @@ -283,76 +232,109 @@ private async Task MessageProcessorAsync(CancellationToken cancellationToken) { try { - OnMessage(this, Encoding.UTF8.GetString(receiveBuffer, 0, offset)); + HandleMessage(this, new ReadOnlySpan(receiveBuffer, 0, offset)); offset = 0; } catch (Exception exception) { - this._logger.LogError(exception, $"{nameof(MessageProcessorAsync)} - Distribute Message"); + _logger.LogError(exception, $"{nameof(ProcessMessagesAsync)} - Distribute Message"); } } } } finally { - this._logger.LogInformation($"{nameof(MessageProcessorAsync)} - Stop"); + _logger.LogInformation($"{nameof(ProcessMessagesAsync)} - Stopping"); } } /// /// Called whenever a WebSocket message is received. /// - /// - /// - private void OnMessage(object sender, string message) + private void HandleMessage(object sender, ReadOnlySpan message) { switch (_connectionType) { case ConnectionType.Full: { - CertificateMessage certMessage; - - try - { - certMessage = JsonSerializer.Deserialize(message); - } - catch (Exception exception) - { - this._logger.LogError(exception, $"{nameof(OnMessage)} - ConnectionType Full"); - return; - }; - - if (certMessage.MessageType != Constants.TargetMessageType) - { - return; - } - - _fullHandler.Invoke(sender, certMessage.Data.Leaf); - + HandleFullMessage(sender, message); break; } case ConnectionType.DomainsOnly: { - DomainsOnlyMessage domainsOnlyMessage; + HandleDomainOnlyMessage(sender, message); + break; + } + } + } - try - { - domainsOnlyMessage = JsonSerializer.Deserialize(message); - } - catch (Exception exception) - { - this._logger.LogError(exception, $"{nameof(OnMessage)} - ConnectionType DomainsOnly"); - return; - }; + private void HandleFullMessage(object sender, ReadOnlySpan message) + { + CertificateMessage certMessage; - foreach (string hostname in domainsOnlyMessage.Hostnames) - { - _domainsOnlyHandler.Invoke(sender, hostname); - } + try + { + certMessage = JsonSerializer.Deserialize(message); + } + catch (Exception exception) + { + _logger.LogError(exception, $"{nameof(HandleMessage)} - ConnectionType Full"); + return; + } - break; - } + if (certMessage.MessageType != Constants.TargetMessageType) return; + + _fullHandler?.Invoke(sender, certMessage.Data.Leaf); + } + + private void HandleDomainOnlyMessage(object sender, ReadOnlySpan message) + { + DomainsOnlyMessage domainsOnlyMessage; + + try + { + domainsOnlyMessage = JsonSerializer.Deserialize(message); + } + catch (Exception exception) + { + _logger.LogError(exception, $"{nameof(HandleMessage)} - ConnectionType DomainsOnly"); + return; + } + + foreach (string hostname in domainsOnlyMessage.Hostnames) + _domainsOnlyHandler?.Invoke(sender, hostname); + } + + private void StartPinging(CancellationToken cancellationToken) + { + _pingTimer.Interval = _pingInterval; + _pingTimer.Elapsed += async (sender, e) => await SendPingAsync(cancellationToken); + _pingTimer.AutoReset = true; + _pingTimer.Start(); + } + + private void StopPinging() + { + if (_pingTimer is null) return; + + _pingTimer.Stop(); + _pingTimer.Dispose(); + } + + private async Task SendPingAsync(CancellationToken cancellationToken) + { + if (_webSocket?.State != WebSocketState.Open) return; + + try + { + await _webSocket.SendAsync(Constants.PingBytes, WebSocketMessageType.Text, true, cancellationToken); + + _logger.LogInformation($"{nameof(SendPingAsync)} - Ping message sent"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"{nameof(SendPingAsync)} - Failed to send ping message"); } } } -} +} \ No newline at end of file diff --git a/Certstream/Constants.cs b/Certstream/Constants.cs index 96bd372..fd6f082 100644 --- a/Certstream/Constants.cs +++ b/Certstream/Constants.cs @@ -9,7 +9,7 @@ internal class Constants /// /// The User-Agent header value to send when connecting. /// - public const string UserAgent = "C# Certstream Client - https://github.com/actually-akac/Certstream"; + public const string UserAgent = "C# Certstream Client - https://github.com/akacdev/Certstream"; /// /// The buffer size for holding incoming messages. - Default: 16 KiB @@ -41,10 +41,10 @@ internal class Constants /// public const int MaxRetries = 10; - ///// - ///// How often should the WebSocket connection be pinged, in milliseconds. - ///// - //public const int PingInterval = 5000; + /// + /// How often should the WebSocket connection be pinged, in milliseconds. + /// + public const int PingInterval = 5000; /// /// The delay between reconnecting a closed WebSocket connection, in milliseconds. diff --git a/Certstream/NuGet.md b/Certstream/NuGet.md index cc362bf..33ec4ab 100644 --- a/Certstream/NuGet.md +++ b/Certstream/NuGet.md @@ -1,8 +1,8 @@ # Certstream -![](https://raw.githubusercontent.com/actually-akac/Certstream/master/Certstream/icon.png) +![](https://raw.githubusercontent.com/akacdev/Certstream/master/Certstream/icon.png) -A C# library for processing newly issued SSL certificates in real time using the Certstream API. +C# library for real-time SSL certificate processing using the Calidog Certstream API. ## Usage This library provides an easy interface for interacting with the Certstream API, allowing you to process newly issued SSL/TLS certificates in real time. diff --git a/Example/Program.cs b/Example/Program.cs index d78bfd6..b36e756 100644 --- a/Example/Program.cs +++ b/Example/Program.cs @@ -2,28 +2,36 @@ using Certstream.Models; using Microsoft.Extensions.Logging; using System; +using System.Threading.Tasks; -using var loggerFactory = LoggerFactory.Create(builder => +namespace Example { - builder.SetMinimumLevel(LogLevel.Debug); - builder.AddConsole(); -}); + public class Example + { + public static async Task Main() + { + using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Debug); + builder.AddConsole(); + }); -var logger = loggerFactory.CreateLogger(); + ILogger logger = loggerFactory.CreateLogger(); -var certstreamClient = new CertstreamClient(ConnectionType.Full, logger: logger); -await certstreamClient.StartAsync(); + CertstreamClient client = new(ConnectionType.Full, logger: logger); + await client.StartAsync(); -certstreamClient.CertificateIssued += (sender, cert) => -{ - foreach (string domain in cert.AllDomains) - { - Console.WriteLine($"{cert.Issuer.O ?? cert.Issuer.CN} issued a SSL certificate for {domain}"); - } -}; + client.CertificateIssued += (sender, cert) => + { + foreach (string domain in cert.AllDomains) + Console.WriteLine($"{cert.Issuer.O ?? cert.Issuer.CN} issued a SSL certificate for {domain}"); + }; -Console.ReadKey(); + Console.ReadKey(true); -await certstreamClient.StopAsync(); + await client.StopAsync(); -Console.ReadKey(); + Console.ReadKey(true); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 040ad21..8048418 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Certstream
- +
- A C# library for processing newly issued SSL certificates in real time using the Certstream API. + C# library for real-time SSL certificate processing using the Calidog Certstream API.
## Usage