(_path);
+ _httpsServer.OnGet += HandleHttpGet;
+ Debug.LogInformation("Assigning Log Info");
+ _httpsServer.Log.Level = LogLevel.Trace;
+ _httpsServer.Log.Output = WriteWebSocketInternalLog;
+ Debug.LogInformation("Starting");
+
+ _httpsServer.Start();
+ Debug.LogInformation("Ready");
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message);
+ Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
+ // Null out the server so callers can detect failure via IsRunning / Url null guards.
+ _httpsServer = null;
+ }
+ }
+
+ private void HandleHttpGet(object sender, HttpRequestEventArgs e)
+ {
+ var res = e.Response;
+ var body = System.Text.Encoding.UTF8.GetBytes(
+ "Certificate accepted.
You can close this tab and return to the application.
");
+ res.ContentType = "text/html";
+ res.ContentLength64 = body.Length;
+ res.Close(body, true);
+ }
+
+ ///
+ /// Stops the WebSocket server if it is currently running.
+ ///
+ /// This method halts the WebSocket server and releases any associated resources. After
+ /// calling this method, the server will no longer accept or process incoming connections.
+ public void StopServer()
+ {
+ Debug.LogInformation("Stopping Websocket Server");
+
+ try
+ {
+ if (_httpsServer == null || !_httpsServer.IsListening)
+ {
+ return;
+ }
+
+ // Prevent close-sequence internal websocket logs from re-entering the logging pipeline.
+ _httpsServer.Log.Output = (d, s) => { };
+
+ var serviceHost = _httpsServer.WebSocketServices[_path];
+
+ if (serviceHost == null)
+ {
+ _httpsServer.Stop();
+ _httpsServer = null;
+ return;
+ }
+
+ serviceHost.Sessions.Broadcast("Server is stopping");
+
+ foreach (var session in serviceHost.Sessions.Sessions)
+ {
+ if (session?.Context?.WebSocket != null && session.Context.WebSocket.IsAlive)
+ {
+ session.Context.WebSocket.Close(1001, "Server is stopping");
+ }
+ }
+
+ _httpsServer.Stop();
+
+ _httpsServer = null;
+
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError(ex, "WebSocket Failed to stop gracefully {0}", ex.Message);
+ Debug.LogVerbose("Stack Trace\r\n{0}", ex.StackTrace);
+ }
+ }
+
+ private static void WriteWebSocketInternalLog(LogData data, string supplemental)
+ {
+ try
+ {
+ if (data == null)
+ {
+ return;
+ }
+
+ var message = string.IsNullOrWhiteSpace(data.Message) ? "" : data.Message;
+ var details = string.IsNullOrWhiteSpace(supplemental) ? string.Empty : string.Format(" | details: {0}", supplemental);
+
+ // Use direct console output to avoid recursive log sink calls.
+ CrestronConsole.PrintLine(string.Format("WS[{0}] {1} | message: {2}{3}", data.Level, data.Date, message, details));
+ }
+ catch
+ {
+ // Never throw from websocket log callback.
+ }
+ }
+
+}
+
+///
+/// Configures the logger to write log events to a debug WebSocket sink.
+///
+/// This extension method allows you to direct log events to a WebSocket sink for debugging
+/// purposes.
+public static class DebugWebsocketSinkExtensions
{
///
- /// Represents a DebugWebsocketSink
+ /// Configures a logger to write log events to a debug WebSocket sink.
///
- public class DebugWebsocketSink : ILogEventSink
+ /// This method adds a sink that writes log events to a WebSocket for debugging purposes.
+ /// It is typically used during development to stream log events in real-time.
+ /// The logger sink configuration to apply the WebSocket sink to.
+ /// An optional text formatter to format the log events. If not provided, a default formatter will be used.
+ /// A object that can be used to further configure the logger.
+ public static LoggerConfiguration DebugWebsocketSink(
+ this LoggerSinkConfiguration loggerConfiguration,
+ ITextFormatter formatProvider = null)
{
- private HttpServer _httpsServer;
-
- private string _path = "/debug/join/";
- private const string _certificateName = "selfCres";
- private const string _certificatePassword = "cres12345";
-
- private static string CertPath =>
- $"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx";
-
-
- public int Port
- {
- get
- {
-
- if (_httpsServer == null) return 0;
- return _httpsServer.Port;
- }
- }
-
- public string Url
- {
- get
- {
- if (_httpsServer == null || !_httpsServer.IsListening) return "";
- var service = _httpsServer.WebSocketServices[_path];
- if (service == null) return "";
-
- // Use CSLAN IP if available, otherwise fallback to primary IP. This ensures we provide a reachable URL in dual-stack environments.
- var cslanIp = CrestronEthernetHelper.GetEthernetParameter(
- CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1);
- if (!string.IsNullOrEmpty(cslanIp) && cslanIp != "Invalid Value")
- return $"wss://{cslanIp}:{_httpsServer.Port}{service.Path}";
- else
- return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}";
- }
- }
-
- ///
- /// Gets or sets the IsRunning
- ///
- public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
-
- ///
- /// Gets a value indicating whether there are active WebSocket connections.
- ///
- public bool HasActiveConnections
- {
- get
- {
- if (_httpsServer == null || !_httpsServer.IsListening) return false;
- var service = _httpsServer.WebSocketServices[_path];
- if (service == null) return false;
- return service.Sessions.Count > 0;
- }
- }
-
-
- private readonly ITextFormatter _textFormatter;
-
- ///
- /// Initializes a new instance of the class with the specified text formatter.
- ///
- /// This constructor initializes the WebSocket sink and ensures that a certificate is
- /// available for secure communication. If the required certificate does not exist, it will be created
- /// automatically. Additionally, the sink is configured to stop the server when the program is
- /// stopping.
- /// The text formatter used to format log messages. If null, a default JSON formatter is used.
- public DebugWebsocketSink(ITextFormatter formatProvider)
- {
-
- _textFormatter = formatProvider ?? new JsonFormatter();
-
- if (!File.Exists(CertPath))
- CreateCert();
-
- try
- {
- CrestronEnvironment.ProgramStatusEventHandler += type =>
- {
- if (type == eProgramStatusEventType.Stopping)
- StopServer();
- };
- }
- catch
- {
- // CrestronEnvironment is not available in test / dev environments — safe to skip.
- }
- }
-
- private static void CreateCert()
- {
- // NOTE: This method is called from the constructor, which is itself called during Debug's static
- // constructor before _logger is assigned. Do NOT call any Debug.Log* methods here — use
- // CrestronConsole.PrintLine only, to avoid a NullReferenceException that would poison the Debug type.
- try
- {
- var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
- var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
- var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0);
-
- CrestronConsole.PrintLine(string.Format("CreateCert: DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
-
- var subjectName = string.Format("CN={0}.{1}", hostName, domainName);
- var fqdn = string.Format("{0}.{1}", hostName, domainName);
-
- var random = new SecureRandom();
-
- // Generate RSA 2048 key pair
- var keyPairGenerator = new RsaKeyPairGenerator();
- keyPairGenerator.Init(new KeyGenerationParameters(random, 2048));
- var keyPair = keyPairGenerator.GenerateKeyPair();
-
- // Build certificate
- var certGenerator = new X509V3CertificateGenerator();
- certGenerator.SetSerialNumber(BigInteger.ValueOf(Math.Abs(DateTime.UtcNow.Ticks)));
- certGenerator.SetIssuerDN(new X509Name(subjectName));
- certGenerator.SetSubjectDN(new X509Name(subjectName));
- certGenerator.SetNotBefore(DateTime.UtcNow);
- certGenerator.SetNotAfter(DateTime.UtcNow.AddYears(2));
- certGenerator.SetPublicKey(keyPair.Public);
-
- // Extended Key Usage: server + client auth
- certGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, false,
- new ExtendedKeyUsage(new[] { KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth }));
-
- // Subject Alternative Names: DNS + IP
- System.Net.IPAddress parsedIp;
- if (System.Net.IPAddress.TryParse(ipAddress, out parsedIp))
- {
- certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false,
- new GeneralNames(new GeneralName[] {
- new GeneralName(GeneralName.DnsName, fqdn),
- new GeneralName(GeneralName.IPAddress, ipAddress)
- }));
- }
- else
- {
- certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false,
- new GeneralNames(new GeneralName(GeneralName.DnsName, fqdn)));
- }
-
- // Sign with SHA256withRSA
- var signatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", keyPair.Private, random);
- var certificate = certGenerator.Generate(signatureFactory);
-
- // Export as PKCS12/PFX
- var pkcs12Store = new Pkcs12StoreBuilder().Build();
- var certEntry = new X509CertificateEntry(certificate);
- pkcs12Store.SetCertificateEntry(_certificateName, certEntry);
- pkcs12Store.SetKeyEntry(_certificateName, new AsymmetricKeyEntry(keyPair.Private), new[] { certEntry });
-
- var separator = Path.DirectorySeparatorChar;
- var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName);
-
- using (var ms = new MemoryStream())
- {
- var passwordChars = _certificatePassword.ToCharArray();
- try
- {
- pkcs12Store.Save(ms, passwordChars, random);
- }
- finally
- {
- Array.Clear(passwordChars, 0, passwordChars.Length);
- }
- File.WriteAllBytes(outputPath, ms.ToArray());
- }
-
- CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath));
- }
- catch (Exception ex)
- {
- CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed: {0}\r\n{1}", ex.Message, ex.StackTrace));
- }
- }
-
- ///
- /// Emit method
- ///
- public void Emit(LogEvent logEvent)
- {
- if (_httpsServer == null || !_httpsServer.IsListening) return;
-
- var sw = new StringWriter();
- _textFormatter.Format(logEvent, sw);
-
- _httpsServer.WebSocketServices.Broadcast(sw.ToString());
-
- }
-
- ///
- /// Starts the WebSocket server on the specified port and configures it with the appropriate certificate.
- ///
- /// This method initializes the WebSocket server and binds it to the specified port. It
- /// also applies the server's certificate for secure communication. Ensure that the port is not already in use
- /// and that the certificate file is accessible.
- /// The port number on which the WebSocket server will listen. Must be a valid, non-negative port number.
- public void StartServerAndSetPort(int port)
- {
- Debug.LogInformation("Starting Websocket Server on port: {0}", port);
-
-
-
-
- Start(port, CertPath, _certificatePassword);
- }
-
- private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
- {
- if (!File.Exists(certPath))
- CreateCert();
-
- try
- {
- return LoadCertFromBouncyCastle(certPath, certPassword);
- }
- catch (Exception ex)
- {
- // Cert is corrupt or was written by an incompatible tool — delete and regenerate once.
- CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
- try { File.Delete(certPath); } catch { }
- CreateCert();
- return LoadCertFromBouncyCastle(certPath, certPassword);
- }
- }
-
- ///
- /// Loads a PKCS#12 file written by BouncyCastle and returns an with
- /// private key attached.
- /// The PFX is parsed and re-encoded by BouncyCastle (ensuring format compatibility), then passed as
- /// raw bytes to so neither RSACryptoServiceProvider nor the
- /// EphemeralKeySet flag (unsupported on the Crestron/Mono runtime) is needed.
- ///
- private static X509Certificate2 LoadCertFromBouncyCastle(string certPath, string certPassword)
- {
- var passwordChars = certPassword.ToCharArray();
- try
- {
- using (var stream = File.OpenRead(certPath))
- {
- var store = new Pkcs12StoreBuilder().Build();
- store.Load(stream, passwordChars);
-
- // Re-encode through BouncyCastle to guarantee PKCS#12 format compatibility,
- // then hand raw bytes to X509Certificate2 — no RSACryptoServiceProvider needed.
- using (var ms = new MemoryStream())
- {
- store.Save(ms, passwordChars, new SecureRandom());
- var cert = new X509Certificate2(ms.ToArray(), certPassword);
-
- if (!cert.HasPrivateKey)
- throw new InvalidOperationException(
- string.Format("Certificate loaded from '{0}' does not contain a private key and cannot be used as a server certificate.", certPath));
-
- return cert;
- }
- }
- }
- finally
- {
- Array.Clear(passwordChars, 0, passwordChars.Length);
- }
- }
-
- private void Start(int port, string certPath = "", string certPassword = "")
- {
- try
- {
- _httpsServer = new HttpServer(port, true);
-
- if (!string.IsNullOrWhiteSpace(certPath))
- {
- Debug.LogInformation("Assigning SSL Configuration");
-
- _httpsServer.SslConfiguration.ServerCertificate = LoadOrRecreateCert(certPath, certPassword);
- _httpsServer.SslConfiguration.ClientCertificateRequired = false;
- _httpsServer.SslConfiguration.CheckCertificateRevocation = false;
- _httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12;
- //this is just to test, you might want to actually validate
- _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
- {
- Debug.LogInformation("HTTPS ClientCerticateValidation Callback triggered");
- return true;
- };
- }
- Debug.LogInformation("Adding Debug Client Service");
- _httpsServer.AddWebSocketService(_path);
- Debug.LogInformation("Assigning Log Info");
- _httpsServer.Log.Level = LogLevel.Trace;
- _httpsServer.Log.Output = WriteWebSocketInternalLog;
- Debug.LogInformation("Starting");
-
- _httpsServer.Start();
- Debug.LogInformation("Ready");
- }
- catch (Exception ex)
- {
- Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message);
- Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
- // Null out the server so callers can detect failure via IsRunning / Url null guards.
- _httpsServer = null;
- }
- }
-
- ///
- /// StopServer method
- ///
- public void StopServer()
- {
- Debug.LogInformation("Stopping Websocket Server");
-
- try
- {
- if (_httpsServer == null || !_httpsServer.IsListening)
- {
- return;
- }
-
- // Prevent close-sequence internal websocket logs from re-entering the logging pipeline.
- _httpsServer.Log.Output = (d, s) => { };
-
- var serviceHost = _httpsServer.WebSocketServices[_path];
-
- if (serviceHost == null)
- {
- _httpsServer.Stop();
- _httpsServer = null;
- return;
- }
-
- serviceHost.Sessions.Broadcast("Server is stopping");
-
- foreach (var session in serviceHost.Sessions.Sessions)
- {
- if (session?.Context?.WebSocket != null && session.Context.WebSocket.IsAlive)
- {
- session.Context.WebSocket.Close(1001, "Server is stopping");
- }
- }
-
- _httpsServer.Stop();
-
- _httpsServer = null;
-
- }
- catch (Exception ex)
- {
- Debug.LogError(ex, "WebSocket Failed to stop gracefully {0}", ex.Message);
- Debug.LogVerbose("Stack Trace\r\n{0}", ex.StackTrace);
- }
- }
-
- private static void WriteWebSocketInternalLog(LogData data, string supplemental)
- {
- try
- {
- if (data == null)
- {
- return;
- }
-
- var message = string.IsNullOrWhiteSpace(data.Message) ? "" : data.Message;
- var details = string.IsNullOrWhiteSpace(supplemental) ? string.Empty : string.Format(" | details: {0}", supplemental);
-
- // Use direct console output to avoid recursive log sink calls.
- CrestronConsole.PrintLine(string.Format("WS[{0}] {1} | message: {2}{3}", data.Level, data.Date, message, details));
- }
- catch
- {
- // Never throw from websocket log callback.
- }
- }
+ return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider));
}
+}
- public static class DebugWebsocketSinkExtensions
+///
+/// Represents a WebSocket client for debugging purposes, providing connection lifecycle management and message
+/// handling functionality.
+///
+/// The class extends to handle
+/// WebSocket connections, including events for opening, closing, receiving messages, and errors. It tracks the
+/// duration of the connection and logs relevant events for debugging.
+public class DebugClient : WebSocketBehavior
+{
+ private DateTime _connectionTime;
+
+ ///
+ /// Gets the duration of time the WebSocket connection has been active.
+ ///
+ public TimeSpan ConnectedDuration
{
- ///
- /// DebugWebsocketSink method
- ///
- public static LoggerConfiguration DebugWebsocketSink(
- this LoggerSinkConfiguration loggerConfiguration,
- ITextFormatter formatProvider = null)
+ get
{
- return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider));
+ if (Context.WebSocket.IsAlive)
+ {
+ return DateTime.Now - _connectionTime;
+ }
+ else
+ {
+ return new TimeSpan(0);
+ }
}
}
///
- /// Represents a DebugClient
+ /// Initializes a new instance of the class.
///
- public class DebugClient : WebSocketBehavior
+ public DebugClient()
{
- private DateTime _connectionTime;
+ Debug.LogInformation("DebugClient Created");
+ }
- public TimeSpan ConnectedDuration
- {
- get
- {
- if (Context.WebSocket.IsAlive)
- {
- return DateTime.Now - _connectionTime;
- }
- else
- {
- return new TimeSpan(0);
- }
- }
- }
+ ///
+ protected override void OnOpen()
+ {
+ base.OnOpen();
- public DebugClient()
- {
- Debug.Console(0, "DebugClient Created");
- }
+ var url = Context.WebSocket.Url;
+ Debug.LogInformation("New WebSocket Connection from: {0}", url);
- protected override void OnOpen()
- {
- base.OnOpen();
+ _connectionTime = DateTime.Now;
+ }
- var url = Context.WebSocket.Url;
- Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url);
+ ///
+ protected override void OnMessage(MessageEventArgs e)
+ {
+ base.OnMessage(e);
- _connectionTime = DateTime.Now;
- }
+ Debug.LogVerbose("WebSocket UiClient Message: {0}", e.Data);
+ }
- protected override void OnMessage(MessageEventArgs e)
- {
- base.OnMessage(e);
+ ///
+ protected override void OnClose(CloseEventArgs e)
+ {
+ base.OnClose(e);
- Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data);
- }
+ Debug.LogDebug("WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason);
+ }
- protected override void OnClose(CloseEventArgs e)
- {
- base.OnClose(e);
+ ///
+ protected override void OnError(WebSocketSharp.ErrorEventArgs e)
+ {
+ base.OnError(e);
- Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason);
-
- }
-
- protected override void OnError(WebSocketSharp.ErrorEventArgs e)
- {
- base.OnError(e);
-
- Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message);
- }
+ Debug.LogError(e.Exception, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message);
+ Debug.LogVerbose("Stack Trace:\r{0}", e.Exception.StackTrace);
}
}
diff --git a/src/PepperDash.Core/Network/DiscoveryThings.cs b/src/PepperDash.Core/Network/DiscoveryThings.cs
index 973c03a4..c01613b7 100644
--- a/src/PepperDash.Core/Network/DiscoveryThings.cs
+++ b/src/PepperDash.Core/Network/DiscoveryThings.cs
@@ -4,19 +4,17 @@ using System.Linq;
using System.Text;
using Crestron.SimplSharp;
-namespace PepperDash.Core
-{
+namespace PepperDash.Core;
+
+///
+/// Not in use
+///
+ public static class NetworkComm
+ {
///
/// Not in use
///
- public static class NetworkComm
- {
- ///
- /// Not in use
- ///
static NetworkComm()
{
}
- }
-
-}
\ No newline at end of file
+ }
\ No newline at end of file
diff --git a/src/PepperDash.Core/PasswordManagement/Config.cs b/src/PepperDash.Core/PasswordManagement/Config.cs
index 22aa4881..a5f071a4 100644
--- a/src/PepperDash.Core/PasswordManagement/Config.cs
+++ b/src/PepperDash.Core/PasswordManagement/Config.cs
@@ -4,8 +4,8 @@ using System.Linq;
using System.Text;
using Crestron.SimplSharp;
-namespace PepperDash.Core.PasswordManagement
-{
+namespace PepperDash.Core.PasswordManagement;
+
///
/// JSON password configuration
///
@@ -22,5 +22,4 @@ namespace PepperDash.Core.PasswordManagement
{
}
- }
-}
\ No newline at end of file
+ }
\ No newline at end of file
diff --git a/src/PepperDash.Core/PasswordManagement/Constants.cs b/src/PepperDash.Core/PasswordManagement/Constants.cs
index 65a1bf45..d4cf1e0b 100644
--- a/src/PepperDash.Core/PasswordManagement/Constants.cs
+++ b/src/PepperDash.Core/PasswordManagement/Constants.cs
@@ -4,8 +4,8 @@ using System.Linq;
using System.Text;
using Crestron.SimplSharp;
-namespace PepperDash.Core.PasswordManagement
-{
+namespace PepperDash.Core.PasswordManagement;
+
///
/// Constants
///
@@ -53,5 +53,4 @@ namespace PepperDash.Core.PasswordManagement
/// Generic string value change constant
///