diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
index e083da2da..8986fcb4c 100644
--- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
+++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
@@ -15,10 +15,12 @@
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
+using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
+using System.Security.Cryptography;
using Serilog.Formatting;
using Serilog.Formatting.Json;
@@ -172,7 +174,15 @@ private static void CreateCert()
using (var ms = new MemoryStream())
{
- pkcs12Store.Save(ms, _certificatePassword.ToCharArray(), random);
+ var passwordChars = _certificatePassword.ToCharArray();
+ try
+ {
+ pkcs12Store.Save(ms, passwordChars, random);
+ }
+ finally
+ {
+ Array.Clear(passwordChars, 0, passwordChars.Length);
+ }
File.WriteAllBytes(outputPath, ms.ToArray());
}
@@ -215,21 +225,68 @@ public void StartServerAndSetPort(int port)
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
{
+ if (!File.Exists(certPath))
+ CreateCert();
+
try
{
- // EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
- // key-container persistence failures, and avoids the private key export restriction.
- return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
+ return LoadCertFromBouncyCastle(certPath, certPassword);
}
catch (Exception ex)
{
- // Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
- // Delete it, regenerate with the BCL path, and retry once.
+ // 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 new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
+ return LoadCertFromBouncyCastle(certPath, certPassword);
+ }
+ }
+
+ ///
+ /// Loads a PKCS#12 file written by BouncyCastle and returns an with
+ /// private key attached via .
+ /// Using BouncyCastle's own reader avoids the .NET/Mono PFX parser, which can reject
+ /// BouncyCastle-generated archives on the Crestron runtime.
+ ///
+ 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);
+
+ foreach (string alias in store.Aliases)
+ {
+ if (!store.IsKeyEntry(alias)) continue;
+
+ var keyEntry = store.GetKey(alias);
+ var certChain = store.GetCertificateChain(alias);
+ if (certChain == null || certChain.Length == 0) continue;
+
+ // Build X509Certificate2 from raw DER — no PFX parsing by .NET needed.
+ var cert = new X509Certificate2(certChain[0].Certificate.GetEncoded());
+
+ // Attach the private key via RSACryptoServiceProvider (available on all target runtimes).
+ var rsaParams = DotNetUtilities.ToRSAParameters(
+ (RsaPrivateCrtKeyParameters)keyEntry.Key);
+ var rsa = new RSACryptoServiceProvider();
+ rsa.PersistKeyInCsp = false;
+ rsa.ImportParameters(rsaParams);
+ cert.PrivateKey = rsa;
+
+ return cert;
+ }
+ }
}
+ finally
+ {
+ Array.Clear(passwordChars, 0, passwordChars.Length);
+ }
+
+ throw new InvalidOperationException("No key entry found in PKCS#12 store: " + certPath);
}
private void Start(int port, string certPath = "", string certPassword = "")
diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
index 9e781883e..3cdb8433a 100644
--- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
+++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
@@ -20,6 +20,8 @@ public class EssentialsWebApi : EssentialsDevice
private readonly WebApiServer _debugServer;
+
+
///
/// http(s)://{ipaddress}/cws/{basePath}
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
@@ -260,7 +262,7 @@ WEBSERVER [ON | OFF | TIMEOUT | MAXSESSIONSPERUSER
/// Initializes a new instance of the ControlSystem class
@@ -50,21 +50,21 @@ public override void InitializeSystem()
{
// Get FW version and stop if it's too low to run this version of Essentials. Must be greater than v2.8006.00110
- var fwVersion = InitialParametersClass.FirmwareVersion;
-
- Debug.LogInformation("Control System Hardware Version: {fwVersion}", fwVersion);
-
- // split the version into parts and compare against minimumFirmwareVersion
- var versionParts = fwVersion.Split('.').Select(int.Parse).ToArray();
- var minParts = minimumFirmwareVersion.Split('.').Select(int.Parse).ToArray();
- if (versionParts.Length < minParts.Length
- || versionParts[0] < minParts[0]
- || (versionParts[0] == minParts[0] && versionParts[1] < minParts[1])
- || (versionParts[0] == minParts[0] && versionParts[1] == minParts[1] && versionParts[2] <= minParts[2]))
- {
- Debug.LogFatal("Firmware version {fwVersion} is too low to run this version of Essentials. Please upgrade to greater than v{minimumFirmwareVersion}.", fwVersion, minimumFirmwareVersion);
- return;
- }
+ // var fwVersion = InitialParametersClass.FirmwareVersion;
+
+ // Debug.LogInformation("Control System Hardware Version: {fwVersion}", fwVersion);
+
+ // // split the version into parts and compare against minimumFirmwareVersion
+ // var versionParts = fwVersion.Split('.').Select(int.Parse).ToArray();
+ // var minParts = minimumFirmwareVersion.Split('.').Select(int.Parse).ToArray();
+ // if (versionParts.Length < minParts.Length
+ // || versionParts[0] < minParts[0]
+ // || (versionParts[0] == minParts[0] && versionParts[1] < minParts[1])
+ // || (versionParts[0] == minParts[0] && versionParts[1] == minParts[1] && versionParts[2] <= minParts[2]))
+ // {
+ // Debug.LogFatal("Firmware version {fwVersion} is too low to run this version of Essentials. Please upgrade to greater than v{minimumFirmwareVersion}.", fwVersion, minimumFirmwareVersion);
+ // return;
+ // }
// If the control system is a DMPS type, we need to wait to exit this method until all devices have had time to activate
// to allow any HD-BaseT DM endpoints to register first.
@@ -130,14 +130,8 @@ private void StartSystem(object preventInitialization)
(ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n"));
}, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator);
- CrestronConsole.AddNewConsoleCommand(s =>
- CrestronConsole.ConsoleCommandResponse(
- "This system can be found at the following URLs:{2}" +
- "System URL: {0}{2}" +
- "Template URL: {1}{2}",
- ConfigReader.ConfigObject.SystemUrl,
- ConfigReader.ConfigObject.TemplateUrl,
- CrestronEnvironment.NewLine),
+ CrestronConsole.AddNewConsoleCommand(
+ PrintPortalInfo,
"portalinfo",
"Shows portal URLS from configuration",
ConsoleAccessLevelEnum.AccessOperator);
@@ -160,6 +154,29 @@ private void StartSystem(object preventInitialization)
}
}
+ private void PrintPortalInfo(string args)
+ {
+ if(ConfigReader.ConfigObject == null)
+ {
+ CrestronConsole.ConsoleCommandResponse("No configuration loaded. Cannot show portal URLs.");
+ return;
+ }
+
+ if (string.IsNullOrEmpty(ConfigReader.ConfigObject.SystemUrl) && string.IsNullOrEmpty(ConfigReader.ConfigObject.TemplateUrl))
+ {
+ CrestronConsole.ConsoleCommandResponse("No portal URLs defined in config.");
+ return;
+ }
+
+ CrestronConsole.ConsoleCommandResponse(
+ "This system can be found at the following URLs:{2}" +
+ "System URL: {0}{2}" +
+ "Template URL: {1}{2}",
+ ConfigReader.ConfigObject?.SystemUrl,
+ ConfigReader.ConfigObject?.TemplateUrl,
+ CrestronEnvironment.NewLine);
+ }
+
///
/// DeterminePlatform method
///
@@ -257,11 +274,6 @@ public void GoWithLoad()
PluginLoader.AddProgramAssemblies();
_ = new Core.DeviceFactory();
- // _ = new Devices.Common.DeviceFactory();
- // _ = new DeviceFactory();
-
- // _ = new ProcessorExtensionDeviceFactory();
- // _ = new MobileControlFactory();
LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix);
@@ -274,10 +286,9 @@ public void GoWithLoad()
PluginLoader.LoadPlugins();
Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config...");
- if (!ConfigReader.LoadConfig2())
+ if (!ConfigReader.LoadConfig2() || ConfigReader.ConfigObject == null)
{
- Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors");
- return;
+ Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file.");
}
Load();
@@ -399,6 +410,12 @@ public void LoadDevices()
new Core.Monitoring.SystemMonitorController("systemMonitor"));
}
+ if (ConfigReader.ConfigObject is null)
+ {
+ Debug.LogMessage(LogEventLevel.Warning, "LoadDevices: ConfigObject is null. Cannot load devices.");
+ return;
+ }
+
foreach (var devConf in ConfigReader.ConfigObject.Devices)
{
IKeyed newDev = null;
@@ -452,7 +469,7 @@ public void LoadTieLines()
var tlc = TieLineCollection.Default;
- if (ConfigReader.ConfigObject.TieLines == null)
+ if (ConfigReader.ConfigObject?.TieLines == null)
{
return;
}
@@ -749,7 +766,7 @@ private string GetSwitchDescription(RouteSwitchDescriptor route)
///
public void LoadRooms()
{
- if (ConfigReader.ConfigObject.Rooms == null)
+ if (ConfigReader.ConfigObject?.Rooms == null)
{
Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration.");
return;
@@ -786,13 +803,13 @@ public void LoadRooms()
///
void LoadLogoServer()
{
- if (ConfigReader.ConfigObject.Rooms == null)
+ if (ConfigReader.ConfigObject?.Rooms == null)
{
Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup.");
return;
}
- if (
+ if (ConfigReader.ConfigObject?.Rooms == null ||
!ConfigReader.ConfigObject.Rooms.Any(
CheckRoomConfig))
{