mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-07-02 10:38:16 +00:00
Merge pull request #1416 from PepperDash/feat/load-webapi-without-config
Feat/load webapi without config
This commit is contained in:
commit
f3ea9e1d5a
3 changed files with 126 additions and 42 deletions
|
|
@ -15,10 +15,12 @@ using Org.BouncyCastle.Asn1.X509;
|
||||||
using Org.BouncyCastle.Crypto;
|
using Org.BouncyCastle.Crypto;
|
||||||
using Org.BouncyCastle.Crypto.Generators;
|
using Org.BouncyCastle.Crypto.Generators;
|
||||||
using Org.BouncyCastle.Crypto.Operators;
|
using Org.BouncyCastle.Crypto.Operators;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
using Org.BouncyCastle.Math;
|
using Org.BouncyCastle.Math;
|
||||||
using Org.BouncyCastle.Pkcs;
|
using Org.BouncyCastle.Pkcs;
|
||||||
using Org.BouncyCastle.Security;
|
using Org.BouncyCastle.Security;
|
||||||
using Org.BouncyCastle.X509;
|
using Org.BouncyCastle.X509;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using Serilog.Formatting;
|
using Serilog.Formatting;
|
||||||
using Serilog.Formatting.Json;
|
using Serilog.Formatting.Json;
|
||||||
|
|
||||||
|
|
@ -172,7 +174,15 @@ namespace PepperDash.Core
|
||||||
|
|
||||||
using (var ms = new MemoryStream())
|
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());
|
File.WriteAllBytes(outputPath, ms.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,23 +225,70 @@ namespace PepperDash.Core
|
||||||
|
|
||||||
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
|
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
|
||||||
{
|
{
|
||||||
|
if (!File.Exists(certPath))
|
||||||
|
CreateCert();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
|
return LoadCertFromBouncyCastle(certPath, certPassword);
|
||||||
// key-container persistence failures, and avoids the private key export restriction.
|
|
||||||
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
|
// Cert is corrupt or was written by an incompatible tool — delete and regenerate once.
|
||||||
// Delete it, regenerate with the BCL path, and retry once.
|
|
||||||
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
|
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
|
||||||
try { File.Delete(certPath); } catch { }
|
try { File.Delete(certPath); } catch { }
|
||||||
CreateCert();
|
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();
|
||||||
|
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 = "")
|
private void Start(int port, string certPath = "", string certPassword = "")
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ namespace PepperDash.Essentials.Core.Web
|
||||||
private readonly WebApiServer _debugServer;
|
private readonly WebApiServer _debugServer;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///<example>
|
///<example>
|
||||||
/// http(s)://{ipaddress}/cws/{basePath}
|
/// http(s)://{ipaddress}/cws/{basePath}
|
||||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||||
|
|
@ -301,7 +303,15 @@ namespace PepperDash.Essentials.Core.Web
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
|
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));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ namespace PepperDash.Essentials
|
||||||
private CEvent _initializeEvent;
|
private CEvent _initializeEvent;
|
||||||
private const long StartupTime = 500;
|
private const long StartupTime = 500;
|
||||||
|
|
||||||
private const string minimumFirmwareVersion = "2.8006.00110";
|
// private const string minimumFirmwareVersion = "2.8006.00110";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the ControlSystem class
|
/// Initializes a new instance of the ControlSystem class
|
||||||
|
|
@ -50,21 +50,21 @@ namespace PepperDash.Essentials
|
||||||
{
|
{
|
||||||
|
|
||||||
// Get FW version and stop if it's too low to run this version of Essentials. Must be greater than v2.8006.00110
|
// 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;
|
// var fwVersion = InitialParametersClass.FirmwareVersion;
|
||||||
|
|
||||||
Debug.LogInformation("Control System Hardware Version: {fwVersion}", fwVersion);
|
// Debug.LogInformation("Control System Hardware Version: {fwVersion}", fwVersion);
|
||||||
|
|
||||||
// split the version into parts and compare against minimumFirmwareVersion
|
// // split the version into parts and compare against minimumFirmwareVersion
|
||||||
var versionParts = fwVersion.Split('.').Select(int.Parse).ToArray();
|
// var versionParts = fwVersion.Split('.').Select(int.Parse).ToArray();
|
||||||
var minParts = minimumFirmwareVersion.Split('.').Select(int.Parse).ToArray();
|
// var minParts = minimumFirmwareVersion.Split('.').Select(int.Parse).ToArray();
|
||||||
if (versionParts.Length < minParts.Length
|
// if (versionParts.Length < minParts.Length
|
||||||
|| versionParts[0] < minParts[0]
|
// || versionParts[0] < minParts[0]
|
||||||
|| (versionParts[0] == minParts[0] && versionParts[1] < minParts[1])
|
// || (versionParts[0] == minParts[0] && versionParts[1] < minParts[1])
|
||||||
|| (versionParts[0] == minParts[0] && versionParts[1] == minParts[1] && versionParts[2] <= minParts[2]))
|
// || (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);
|
// Debug.LogFatal("Firmware version {fwVersion} is too low to run this version of Essentials. Please upgrade to greater than v{minimumFirmwareVersion}.", fwVersion, minimumFirmwareVersion);
|
||||||
return;
|
// 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
|
// 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.
|
// to allow any HD-BaseT DM endpoints to register first.
|
||||||
|
|
@ -130,14 +130,8 @@ namespace PepperDash.Essentials
|
||||||
(ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n"));
|
(ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n"));
|
||||||
}, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator);
|
}, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(s =>
|
CrestronConsole.AddNewConsoleCommand(
|
||||||
CrestronConsole.ConsoleCommandResponse(
|
PrintPortalInfo,
|
||||||
"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),
|
|
||||||
"portalinfo",
|
"portalinfo",
|
||||||
"Shows portal URLS from configuration",
|
"Shows portal URLS from configuration",
|
||||||
ConsoleAccessLevelEnum.AccessOperator);
|
ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
@ -160,6 +154,29 @@ namespace PepperDash.Essentials
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
/// <summary>
|
||||||
/// DeterminePlatform method
|
/// DeterminePlatform method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -257,11 +274,6 @@ namespace PepperDash.Essentials
|
||||||
PluginLoader.AddProgramAssemblies();
|
PluginLoader.AddProgramAssemblies();
|
||||||
|
|
||||||
_ = new Core.DeviceFactory();
|
_ = new Core.DeviceFactory();
|
||||||
// _ = new Devices.Common.DeviceFactory();
|
|
||||||
// _ = new DeviceFactory();
|
|
||||||
|
|
||||||
// _ = new ProcessorExtensionDeviceFactory();
|
|
||||||
// _ = new MobileControlFactory();
|
|
||||||
|
|
||||||
LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix);
|
LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix);
|
||||||
|
|
||||||
|
|
@ -274,10 +286,9 @@ namespace PepperDash.Essentials
|
||||||
PluginLoader.LoadPlugins();
|
PluginLoader.LoadPlugins();
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config...");
|
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");
|
Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Load();
|
Load();
|
||||||
|
|
@ -399,6 +410,12 @@ namespace PepperDash.Essentials
|
||||||
new Core.Monitoring.SystemMonitorController("systemMonitor"));
|
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)
|
foreach (var devConf in ConfigReader.ConfigObject.Devices)
|
||||||
{
|
{
|
||||||
IKeyed newDev = null;
|
IKeyed newDev = null;
|
||||||
|
|
@ -452,7 +469,7 @@ namespace PepperDash.Essentials
|
||||||
|
|
||||||
var tlc = TieLineCollection.Default;
|
var tlc = TieLineCollection.Default;
|
||||||
|
|
||||||
if (ConfigReader.ConfigObject.TieLines == null)
|
if (ConfigReader.ConfigObject?.TieLines == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -749,7 +766,7 @@ namespace PepperDash.Essentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void LoadRooms()
|
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.");
|
Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -786,13 +803,13 @@ namespace PepperDash.Essentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void LoadLogoServer()
|
void LoadLogoServer()
|
||||||
{
|
{
|
||||||
if (ConfigReader.ConfigObject.Rooms == null)
|
if (ConfigReader.ConfigObject?.Rooms == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup.");
|
Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (ConfigReader.ConfigObject?.Rooms == null ||
|
||||||
!ConfigReader.ConfigObject.Rooms.Any(
|
!ConfigReader.ConfigObject.Rooms.Any(
|
||||||
CheckRoomConfig))
|
CheckRoomConfig))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue