Skip to content

Commit f3ea9e1

Browse files
authored
Merge pull request #1416 from PepperDash/feat/load-webapi-without-config
Feat/load webapi without config
2 parents 841279e + 404728c commit f3ea9e1

3 files changed

Lines changed: 128 additions & 44 deletions

File tree

src/PepperDash.Core/Logging/DebugWebsocketSink.cs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
using Org.BouncyCastle.Crypto;
1616
using Org.BouncyCastle.Crypto.Generators;
1717
using Org.BouncyCastle.Crypto.Operators;
18+
using Org.BouncyCastle.Crypto.Parameters;
1819
using Org.BouncyCastle.Math;
1920
using Org.BouncyCastle.Pkcs;
2021
using Org.BouncyCastle.Security;
2122
using Org.BouncyCastle.X509;
23+
using System.Security.Cryptography;
2224
using Serilog.Formatting;
2325
using Serilog.Formatting.Json;
2426

@@ -172,7 +174,15 @@ private static void CreateCert()
172174

173175
using (var ms = new MemoryStream())
174176
{
175-
pkcs12Store.Save(ms, _certificatePassword.ToCharArray(), random);
177+
var passwordChars = _certificatePassword.ToCharArray();
178+
try
179+
{
180+
pkcs12Store.Save(ms, passwordChars, random);
181+
}
182+
finally
183+
{
184+
Array.Clear(passwordChars, 0, passwordChars.Length);
185+
}
176186
File.WriteAllBytes(outputPath, ms.ToArray());
177187
}
178188

@@ -215,21 +225,68 @@ public void StartServerAndSetPort(int port)
215225

216226
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
217227
{
228+
if (!File.Exists(certPath))
229+
CreateCert();
230+
218231
try
219232
{
220-
// EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
221-
// key-container persistence failures, and avoids the private key export restriction.
222-
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
233+
return LoadCertFromBouncyCastle(certPath, certPassword);
223234
}
224235
catch (Exception ex)
225236
{
226-
// Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
227-
// Delete it, regenerate with the BCL path, and retry once.
237+
// Cert is corrupt or was written by an incompatible tool — delete and regenerate once.
228238
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
229239
try { File.Delete(certPath); } catch { }
230240
CreateCert();
231-
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
241+
return LoadCertFromBouncyCastle(certPath, certPassword);
242+
}
243+
}
244+
245+
/// <summary>
246+
/// Loads a PKCS#12 file written by BouncyCastle and returns an <see cref="X509Certificate2"/> with
247+
/// private key attached via <see cref="RSACryptoServiceProvider"/>.
248+
/// Using BouncyCastle's own reader avoids the .NET/Mono PFX parser, which can reject
249+
/// BouncyCastle-generated archives on the Crestron runtime.
250+
/// </summary>
251+
private static X509Certificate2 LoadCertFromBouncyCastle(string certPath, string certPassword)
252+
{
253+
var passwordChars = certPassword.ToCharArray();
254+
try
255+
{
256+
using (var stream = File.OpenRead(certPath))
257+
{
258+
var store = new Pkcs12StoreBuilder().Build();
259+
store.Load(stream, passwordChars);
260+
261+
foreach (string alias in store.Aliases)
262+
{
263+
if (!store.IsKeyEntry(alias)) continue;
264+
265+
var keyEntry = store.GetKey(alias);
266+
var certChain = store.GetCertificateChain(alias);
267+
if (certChain == null || certChain.Length == 0) continue;
268+
269+
// Build X509Certificate2 from raw DER — no PFX parsing by .NET needed.
270+
var cert = new X509Certificate2(certChain[0].Certificate.GetEncoded());
271+
272+
// Attach the private key via RSACryptoServiceProvider (available on all target runtimes).
273+
var rsaParams = DotNetUtilities.ToRSAParameters(
274+
(RsaPrivateCrtKeyParameters)keyEntry.Key);
275+
var rsa = new RSACryptoServiceProvider();
276+
rsa.PersistKeyInCsp = false;
277+
rsa.ImportParameters(rsaParams);
278+
cert.PrivateKey = rsa;
279+
280+
return cert;
281+
}
282+
}
232283
}
284+
finally
285+
{
286+
Array.Clear(passwordChars, 0, passwordChars.Length);
287+
}
288+
289+
throw new InvalidOperationException("No key entry found in PKCS#12 store: " + certPath);
233290
}
234291

235292
private void Start(int port, string certPath = "", string certPassword = "")

src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class EssentialsWebApi : EssentialsDevice
2020
private readonly WebApiServer _debugServer;
2121

2222

23+
24+
2325
///<example>
2426
/// http(s)://{ipaddress}/cws/{basePath}
2527
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
@@ -260,7 +262,7 @@ WEBSERVER [ON | OFF | TIMEOUT <VALUE IN SECONDS> | MAXSESSIONSPERUSER <Number of
260262

261263
_server.Start();
262264
_debugServer.Start();
263-
265+
264266
GetPaths();
265267
}
266268

@@ -301,7 +303,15 @@ public void GetPaths()
301303
{
302304
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
303305
}
306+
Debug.LogInformation(this, "Web API initialized and ready to accept requests");
307+
304308
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
309+
310+
var debugAppUrl = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
311+
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws/debug"
312+
: $"https://{currentIp}/cws/debug";
313+
314+
Debug.LogMessage(LogEventLevel.Information, this, "Developer Tools Web App available at: {debugAppUrl:l}", debugAppUrl);
305315
}
306316
}
307317
}

src/PepperDash.Essentials/ControlSystem.cs

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
2727
private CEvent _initializeEvent;
2828
private const long StartupTime = 500;
2929

30-
private const string minimumFirmwareVersion = "2.8006.00110";
30+
// private const string minimumFirmwareVersion = "2.8006.00110";
3131

3232
/// <summary>
3333
/// Initializes a new instance of the ControlSystem class
@@ -50,21 +50,21 @@ public override void InitializeSystem()
5050
{
5151

5252
// Get FW version and stop if it's too low to run this version of Essentials. Must be greater than v2.8006.00110
53-
var fwVersion = InitialParametersClass.FirmwareVersion;
54-
55-
Debug.LogInformation("Control System Hardware Version: {fwVersion}", fwVersion);
56-
57-
// split the version into parts and compare against minimumFirmwareVersion
58-
var versionParts = fwVersion.Split('.').Select(int.Parse).ToArray();
59-
var minParts = minimumFirmwareVersion.Split('.').Select(int.Parse).ToArray();
60-
if (versionParts.Length < minParts.Length
61-
|| versionParts[0] < minParts[0]
62-
|| (versionParts[0] == minParts[0] && versionParts[1] < minParts[1])
63-
|| (versionParts[0] == minParts[0] && versionParts[1] == minParts[1] && versionParts[2] <= minParts[2]))
64-
{
65-
Debug.LogFatal("Firmware version {fwVersion} is too low to run this version of Essentials. Please upgrade to greater than v{minimumFirmwareVersion}.", fwVersion, minimumFirmwareVersion);
66-
return;
67-
}
53+
// var fwVersion = InitialParametersClass.FirmwareVersion;
54+
55+
// Debug.LogInformation("Control System Hardware Version: {fwVersion}", fwVersion);
56+
57+
// // split the version into parts and compare against minimumFirmwareVersion
58+
// var versionParts = fwVersion.Split('.').Select(int.Parse).ToArray();
59+
// var minParts = minimumFirmwareVersion.Split('.').Select(int.Parse).ToArray();
60+
// if (versionParts.Length < minParts.Length
61+
// || versionParts[0] < minParts[0]
62+
// || (versionParts[0] == minParts[0] && versionParts[1] < minParts[1])
63+
// || (versionParts[0] == minParts[0] && versionParts[1] == minParts[1] && versionParts[2] <= minParts[2]))
64+
// {
65+
// Debug.LogFatal("Firmware version {fwVersion} is too low to run this version of Essentials. Please upgrade to greater than v{minimumFirmwareVersion}.", fwVersion, minimumFirmwareVersion);
66+
// return;
67+
// }
6868

6969
// If the control system is a DMPS type, we need to wait to exit this method until all devices have had time to activate
7070
// to allow any HD-BaseT DM endpoints to register first.
@@ -130,14 +130,8 @@ private void StartSystem(object preventInitialization)
130130
(ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n"));
131131
}, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator);
132132

133-
CrestronConsole.AddNewConsoleCommand(s =>
134-
CrestronConsole.ConsoleCommandResponse(
135-
"This system can be found at the following URLs:{2}" +
136-
"System URL: {0}{2}" +
137-
"Template URL: {1}{2}",
138-
ConfigReader.ConfigObject.SystemUrl,
139-
ConfigReader.ConfigObject.TemplateUrl,
140-
CrestronEnvironment.NewLine),
133+
CrestronConsole.AddNewConsoleCommand(
134+
PrintPortalInfo,
141135
"portalinfo",
142136
"Shows portal URLS from configuration",
143137
ConsoleAccessLevelEnum.AccessOperator);
@@ -160,6 +154,29 @@ private void StartSystem(object preventInitialization)
160154
}
161155
}
162156

157+
private void PrintPortalInfo(string args)
158+
{
159+
if(ConfigReader.ConfigObject == null)
160+
{
161+
CrestronConsole.ConsoleCommandResponse("No configuration loaded. Cannot show portal URLs.");
162+
return;
163+
}
164+
165+
if (string.IsNullOrEmpty(ConfigReader.ConfigObject.SystemUrl) && string.IsNullOrEmpty(ConfigReader.ConfigObject.TemplateUrl))
166+
{
167+
CrestronConsole.ConsoleCommandResponse("No portal URLs defined in config.");
168+
return;
169+
}
170+
171+
CrestronConsole.ConsoleCommandResponse(
172+
"This system can be found at the following URLs:{2}" +
173+
"System URL: {0}{2}" +
174+
"Template URL: {1}{2}",
175+
ConfigReader.ConfigObject?.SystemUrl,
176+
ConfigReader.ConfigObject?.TemplateUrl,
177+
CrestronEnvironment.NewLine);
178+
}
179+
163180
/// <summary>
164181
/// DeterminePlatform method
165182
/// </summary>
@@ -257,11 +274,6 @@ public void GoWithLoad()
257274
PluginLoader.AddProgramAssemblies();
258275

259276
_ = new Core.DeviceFactory();
260-
// _ = new Devices.Common.DeviceFactory();
261-
// _ = new DeviceFactory();
262-
263-
// _ = new ProcessorExtensionDeviceFactory();
264-
// _ = new MobileControlFactory();
265277

266278
LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix);
267279

@@ -274,10 +286,9 @@ public void GoWithLoad()
274286
PluginLoader.LoadPlugins();
275287

276288
Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config...");
277-
if (!ConfigReader.LoadConfig2())
289+
if (!ConfigReader.LoadConfig2() || ConfigReader.ConfigObject == null)
278290
{
279-
Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors");
280-
return;
291+
Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file.");
281292
}
282293

283294
Load();
@@ -399,6 +410,12 @@ public void LoadDevices()
399410
new Core.Monitoring.SystemMonitorController("systemMonitor"));
400411
}
401412

413+
if (ConfigReader.ConfigObject is null)
414+
{
415+
Debug.LogMessage(LogEventLevel.Warning, "LoadDevices: ConfigObject is null. Cannot load devices.");
416+
return;
417+
}
418+
402419
foreach (var devConf in ConfigReader.ConfigObject.Devices)
403420
{
404421
IKeyed newDev = null;
@@ -452,7 +469,7 @@ public void LoadTieLines()
452469

453470
var tlc = TieLineCollection.Default;
454471

455-
if (ConfigReader.ConfigObject.TieLines == null)
472+
if (ConfigReader.ConfigObject?.TieLines == null)
456473
{
457474
return;
458475
}
@@ -749,7 +766,7 @@ private string GetSwitchDescription(RouteSwitchDescriptor route)
749766
/// </summary>
750767
public void LoadRooms()
751768
{
752-
if (ConfigReader.ConfigObject.Rooms == null)
769+
if (ConfigReader.ConfigObject?.Rooms == null)
753770
{
754771
Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration.");
755772
return;
@@ -786,13 +803,13 @@ public void LoadRooms()
786803
/// </summary>
787804
void LoadLogoServer()
788805
{
789-
if (ConfigReader.ConfigObject.Rooms == null)
806+
if (ConfigReader.ConfigObject?.Rooms == null)
790807
{
791808
Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup.");
792809
return;
793810
}
794811

795-
if (
812+
if (ConfigReader.ConfigObject?.Rooms == null ||
796813
!ConfigReader.ConfigObject.Rooms.Any(
797814
CheckRoomConfig))
798815
{

0 commit comments

Comments
 (0)