Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 64 additions & 7 deletions src/PepperDash.Core/Logging/DebugWebsocketSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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);
}
}

/// <summary>
/// Loads a PKCS#12 file written by BouncyCastle and returns an <see cref="X509Certificate2"/> with
/// private key attached via <see cref="RSACryptoServiceProvider"/>.
/// Using BouncyCastle's own reader avoids the .NET/Mono PFX parser, which can reject
/// BouncyCastle-generated archives on the Crestron runtime.
/// </summary>
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();
Comment thread
ndorin marked this conversation as resolved.
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 = "")
Expand Down
12 changes: 11 additions & 1 deletion src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class EssentialsWebApi : EssentialsDevice
private readonly WebApiServer _debugServer;




Comment thread
ndorin marked this conversation as resolved.
///<example>
/// http(s)://{ipaddress}/cws/{basePath}
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
Expand Down Expand Up @@ -260,7 +262,7 @@ WEBSERVER [ON | OFF | TIMEOUT <VALUE IN SECONDS> | MAXSESSIONSPERUSER <Number of

_server.Start();
_debugServer.Start();

GetPaths();
}

Expand Down Expand Up @@ -301,7 +303,15 @@ public void GetPaths()
{
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
}
Debug.LogInformation(this, "Web API initialized and ready to accept requests");

Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));

var debugAppUrl = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws/debug"
: $"https://{currentIp}/cws/debug";

Debug.LogMessage(LogEventLevel.Information, this, "Developer Tools Web App available at: {debugAppUrl:l}", debugAppUrl);
}
}
}
89 changes: 53 additions & 36 deletions src/PepperDash.Essentials/ControlSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
private CEvent _initializeEvent;
private const long StartupTime = 500;

private const string minimumFirmwareVersion = "2.8006.00110";
// private const string minimumFirmwareVersion = "2.8006.00110";

/// <summary>
/// Initializes a new instance of the ControlSystem class
Expand All @@ -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;
// }
Comment thread
ndorin marked this conversation as resolved.

// 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.
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

/// <summary>
/// DeterminePlatform method
/// </summary>
Expand Down Expand Up @@ -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);

Expand All @@ -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.");
}
Comment thread
ndorin marked this conversation as resolved.

Load();
Comment thread
ndorin marked this conversation as resolved.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -452,7 +469,7 @@ public void LoadTieLines()

var tlc = TieLineCollection.Default;

if (ConfigReader.ConfigObject.TieLines == null)
if (ConfigReader.ConfigObject?.TieLines == null)
{
return;
}
Expand Down Expand Up @@ -749,7 +766,7 @@ private string GetSwitchDescription(RouteSwitchDescriptor route)
/// </summary>
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;
Expand Down Expand Up @@ -786,13 +803,13 @@ public void LoadRooms()
/// </summary>
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))
{
Expand Down
Loading