diff --git a/src/Pepperdash Core/Config/PortalConfigReader.cs b/src/Pepperdash Core/Config/PortalConfigReader.cs index a75f17d..f639f0c 100644 --- a/src/Pepperdash Core/Config/PortalConfigReader.cs +++ b/src/Pepperdash Core/Config/PortalConfigReader.cs @@ -79,7 +79,7 @@ public static JObject MergeConfigs(JObject doubleConfig) merged.Add("info", template["info"]); merged.Add("devices", MergeArraysOnTopLevelProperty(template["devices"] as JArray, - system["devices"] as JArray, "uid", "devices")); + system["devices"] as JArray, "key", "devices")); if (system["rooms"] == null) merged.Add("rooms", template["rooms"]); @@ -117,7 +117,7 @@ public static JObject MergeConfigs(JObject doubleConfig) else merged.Add("global", template["global"]); - Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged); + //Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged); return merged; } diff --git a/src/Pepperdash Core/CoreInterfaces.cs b/src/Pepperdash Core/CoreInterfaces.cs index 3a5df42..c1432c2 100644 --- a/src/Pepperdash Core/CoreInterfaces.cs +++ b/src/Pepperdash Core/CoreInterfaces.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; +using Serilog; namespace PepperDash.Core { @@ -15,16 +16,17 @@ public interface IKeyed /// Unique Key /// string Key { get; } - } + } /// - /// Named Keyed device interface. Forces the devie to have a Unique Key and a name. + /// Named Keyed device interface. Forces the device to have a Unique Key and a name. /// public interface IKeyName : IKeyed - { + { /// /// Isn't it obvious :) /// string Name { get; } - } + } + } \ No newline at end of file diff --git a/src/Pepperdash Core/Device.cs b/src/Pepperdash Core/Device.cs index 55d4c15..98c4293 100644 --- a/src/Pepperdash Core/Device.cs +++ b/src/Pepperdash Core/Device.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Serilog; +using Serilog.Core; +using Serilog.Sinks.SystemConsole; namespace PepperDash.Core { @@ -10,6 +13,7 @@ namespace PepperDash.Core /// public class Device : IKeyName { + /// /// Unique Key /// @@ -51,7 +55,6 @@ public Device(string key) Key = key; if (key.Contains('.')) Debug.Console(0, this, "WARNING: Device name's should not include '.'"); Name = ""; - } /// diff --git a/src/Pepperdash Core/JsonToSimpl/JsonToSimplFileMaster.cs b/src/Pepperdash Core/JsonToSimpl/JsonToSimplFileMaster.cs index 84762b9..411fbdc 100644 --- a/src/Pepperdash Core/JsonToSimpl/JsonToSimplFileMaster.cs +++ b/src/Pepperdash Core/JsonToSimpl/JsonToSimplFileMaster.cs @@ -194,7 +194,7 @@ public void EvaluateFile(string filepath) /// Sets the debug level /// /// - public void setDebugLevel(int level) + public void setDebugLevel(uint level) { Debug.SetDebugLevel(level); } diff --git a/src/Pepperdash Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs b/src/Pepperdash Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs index 1c3edb3..c170a9a 100644 --- a/src/Pepperdash Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs +++ b/src/Pepperdash Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs @@ -128,7 +128,7 @@ FileInfo GetActualFileInfoFromPath(string path) /// /// /// - public void setDebugLevel(int level) + public void setDebugLevel(uint level) { Debug.SetDebugLevel(level); } diff --git a/src/Pepperdash Core/Logging/Debug.cs b/src/Pepperdash Core/Logging/Debug.cs index aee3ec1..4fec380 100644 --- a/src/Pepperdash Core/Logging/Debug.cs +++ b/src/Pepperdash Core/Logging/Debug.cs @@ -7,7 +7,12 @@ using Crestron.SimplSharp.CrestronIO; using Newtonsoft.Json; using PepperDash.Core.DebugThings; - +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting.Json; +using Crestron.SimplSharp.CrestronDataStore; +using System.Linq; namespace PepperDash.Core { @@ -16,6 +21,34 @@ namespace PepperDash.Core /// public static class Debug { + private static Dictionary _logLevels = new Dictionary() + { + {0, LogEventLevel.Information }, + {1, LogEventLevel.Warning }, + {2, LogEventLevel.Error }, + {3, LogEventLevel.Fatal }, + {4, LogEventLevel.Debug }, + {5, LogEventLevel.Verbose }, + }; + + private static Logger _logger; + + private static LoggingLevelSwitch _consoleLoggingLevelSwitch; + + private static LoggingLevelSwitch _websocketLoggingLevelSwitch; + + public static LogEventLevel WebsocketMinimumLogLevel + { + get { return _websocketLoggingLevelSwitch.MinimumLevel; } + } + + private static DebugWebsocketSink _websocketSink; + + public static DebugWebsocketSink WebsocketSink + { + get { return _websocketSink; } + } + /// /// Describes the folder location where a given program stores it's debug level memory. By default, the /// file written will be named appNdebug where N is 1-10. @@ -41,12 +74,14 @@ public static class Debug /// /// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal /// - public static bool DoNotLoadOnNextBoot { get; private set; } + public static bool DoNotLoadConfigOnNextBoot { get; private set; } private static DebugContextCollection _contexts; private const int SaveTimeoutMs = 30000; + public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; + /// /// Version for the currently loaded PepperDashCore dll /// @@ -66,6 +101,24 @@ public static class Debug static Debug() { + _consoleLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: LogEventLevel.Information); + _consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) => + { + Debug.Console(0, "Console debug level set to {0}", _consoleLoggingLevelSwitch.MinimumLevel); + }; + _websocketLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: LogEventLevel.Verbose); + _websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); + + // Instantiate the root logger + _logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Sink(new DebugConsoleSink(new JsonFormatter(renderMessage: true)), levelSwitch: _consoleLoggingLevelSwitch) + .WriteTo.Sink(_websocketSink, levelSwitch: _websocketLoggingLevelSwitch) + .WriteTo.File(@"\user\debug\global-log-{Date}.txt" + , rollingInterval: RollingInterval.Day + , restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug) + .CreateLogger(); + // Get the assembly version and print it to console and the log GetVersion(); @@ -94,7 +147,7 @@ static Debug() "donotloadonnextboot:P [true/false]: Should the application load on next boot", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", - "appdebug:P [0-2]: Sets the application's console debug message level", + "appdebug:P [0-5]: Sets the application's console debug message level", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", "appdebuglog:P [all] Use \"all\" for full log.", @@ -112,9 +165,9 @@ static Debug() var context = _contexts.GetOrCreateItem("DEFAULT"); Level = context.Level; - DoNotLoadOnNextBoot = context.DoNotLoadOnNextBoot; + DoNotLoadConfigOnNextBoot = context.DoNotLoadOnNextBoot; - if(DoNotLoadOnNextBoot) + if(DoNotLoadConfigOnNextBoot) CrestronConsole.PrintLine(string.Format("Program {0} will not load config after next boot. Use console command go:{0} to load the config manually", InitialParametersClass.ApplicationNumber)); try @@ -164,8 +217,11 @@ private static void GetVersion() /// static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) { + if (programEventType == eProgramStatusEventType.Stopping) { + Log.CloseAndFlush(); + if (_saveTimer != null) { _saveTimer.Stop(); @@ -184,20 +240,65 @@ public static void SetDebugFromConsole(string levelString) { try { + if (levelString.Trim() == "?") + { + CrestronConsole.ConsoleCommandResponse( + $@"Used to set the minimum level of debug messages to be printed to the console: +{_logLevels[0]} = 0 +{_logLevels[1]} = 1 +{_logLevels[2]} = 2 +{_logLevels[3]} = 3 +{_logLevels[4]} = 4 +{_logLevels[5]} = 5"); + return; + } + if (string.IsNullOrEmpty(levelString.Trim())) { - CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", Level); + CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", _consoleLoggingLevelSwitch.MinimumLevel); return; } - SetDebugLevel(Convert.ToInt32(levelString)); + var level = Convert.ToUInt32(levelString); + + if (_logLevels.ContainsKey(level)) + SetDebugLevel(level); } catch { - CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-2]"); + CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); } } + /// + /// Sets the debug level + /// + /// Valid values 0-5 + public static void SetDebugLevel(uint level) + { + if (_logLevels.ContainsKey(level)) + _consoleLoggingLevelSwitch.MinimumLevel = _logLevels[level]; + + CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}", + InitialParametersClass.ApplicationNumber, _consoleLoggingLevelSwitch.MinimumLevel); + + var err = CrestronDataStoreStatic.SetLocalUintValue("ConsoleDebugLevel", level); + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + CrestronConsole.PrintLine("Error saving console debug level setting: {0}", err); + } + + public static void SetWebSocketMinimumDebugLevel(LogEventLevel level) + { + _websocketLoggingLevelSwitch.MinimumLevel = level; + var levelInt = _logLevels.FirstOrDefault((l) => l.Value.Equals(level)).Key; + + var err = CrestronDataStoreStatic.SetLocalUintValue("WebsocketDebugLevel", levelInt); + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + Console(0, "Error saving websocket debug level setting: {0}", err); + + Console(0, "Websocket debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); + } + /// /// Callback for console command /// @@ -208,11 +309,11 @@ public static void SetDoNotLoadOnNextBootFromConsole(string stateString) { if (string.IsNullOrEmpty(stateString.Trim())) { - CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadOnNextBoot); + CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadConfigOnNextBoot); return; } - SetDoNotLoadOnNextBoot(Boolean.Parse(stateString)); + SetDoNotLoadConfigOnNextBoot(Boolean.Parse(stateString)); } catch { @@ -232,7 +333,7 @@ public static void SetDebugFilterFromConsole(string items) CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " + "+all: at beginning puts filter into 'default include' mode\r" + " All keys that follow will be excluded from output.\r" + - "-all: at beginning puts filter into 'default excluse all' mode.\r" + + "-all: at beginning puts filter into 'default exclude all' mode.\r" + " All keys that follow will be the only keys that are shown\r" + "+nokey: Enables messages with no key (default)\r" + "-nokey: Disables messages with no key.\r" + @@ -300,26 +401,7 @@ public static void SetDebugFilterFromConsole(string items) } - /// - /// Sets the debug level - /// - /// Valid values 0 (no debug), 1 (critical), 2 (all messages) - public static void SetDebugLevel(int level) - { - if (level <= 2) - { - Level = level; - _contexts.GetOrCreateItem("DEFAULT").Level = level; - SaveMemoryOnTimeout(); - - CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}", - InitialParametersClass.ApplicationNumber, Level); - //var err = CrestronDataStoreStatic.SetLocalUintValue("DebugLevel", level); - //if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - // CrestronConsole.PrintLine("Error saving console debug level setting: {0}", err); - } - } /// /// sets the settings for a device or creates a new entry @@ -347,14 +429,14 @@ public static object GetDeviceDebugSettingsForKey(string deviceKey) /// Sets the flag to prevent application starting on next boot /// /// - public static void SetDoNotLoadOnNextBoot(bool state) + public static void SetDoNotLoadConfigOnNextBoot(bool state) { - DoNotLoadOnNextBoot = state; + DoNotLoadConfigOnNextBoot = state; _contexts.GetOrCreateItem("DEFAULT").DoNotLoadOnNextBoot = state; SaveMemoryOnTimeout(); - CrestronConsole.ConsoleCommandResponse("[Application {0}], Do Not Start on Next Boot set to {1}", - InitialParametersClass.ApplicationNumber, DoNotLoadOnNextBoot); + CrestronConsole.ConsoleCommandResponse("[Application {0}], Do Not Load Config on Next Boot set to {1}", + InitialParametersClass.ApplicationNumber, DoNotLoadConfigOnNextBoot); } /// @@ -367,6 +449,26 @@ public static void ShowDebugLog(string s) CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); } + + private static void LogMessage(uint level, string format, params object[] items) + { + if (!_logLevels.ContainsKey(level)) return; + + var logLevel = _logLevels[level]; + _logger.Write(logLevel, format, items); + } + + private static void LogMessage(uint level, IKeyed keyed, string format, params object[] items) + { + if (!_logLevels.ContainsKey(level)) return; + + var logLevel = _logLevels[level]; + + var log = _logger.ForContext("Key", keyed.Key); + log.Write(logLevel, format, items); + } + + /// /// Prints message to console if current debug level is equal to or higher than the level of this message. /// Uses CrestronConsole.PrintLine. @@ -376,6 +478,9 @@ public static void ShowDebugLog(string s) /// Object parameters public static void Console(uint level, string format, params object[] items) { + + LogMessage(level, format, items); + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) { var logString = string.Format("[level {0}] {1}", level, string.Format(format, items)); @@ -384,13 +489,13 @@ public static void Console(uint level, string format, params object[] items) return; } - if(Level < level) - { - return; - } - - CrestronConsole.PrintLine("[{0}]App {1}:{2}", DateTime.Now.ToString("HH:mm:ss.fff"), InitialParametersClass.ApplicationNumber, - string.Format(format, items)); + //if (IsRunningOnAppliance) + //{ + // CrestronConsole.PrintLine("[{0}]App {1} Lvl {2}:{3}", DateTime.Now.ToString("HH:mm:ss.fff"), + // InitialParametersClass.ApplicationNumber, + // level, + // string.Format(format, items)); + //} } /// @@ -398,8 +503,10 @@ public static void Console(uint level, string format, params object[] items) /// public static void Console(uint level, IKeyed dev, string format, params object[] items) { - if (Level >= level) - Console(level, "[{0}] {1}", dev.Key, string.Format(format, items)); + LogMessage(level, dev, format, items); + + //if (Level >= level) + // Console(level, "[{0}] {1}", dev.Key, message); } /// @@ -409,20 +516,29 @@ public static void Console(uint level, IKeyed dev, string format, params object[ public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, string format, params object[] items) { + var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items)); if (errorLogLevel != ErrorLogLevel.None) { LogError(errorLogLevel, str); } - if (Level >= level) - { - Console(level, str); - } + + LogMessage(level, dev, format, items); + + //var log = _logger.ForContext("Key", dev.Key); + //var message = string.Format(format, items); + + //log.Write((LogEventLevel)level, message); + + //if (Level >= level) + //{ + // Console(level, str); + //} } - /// - /// Logs to Console when at-level, and all messages to error log - /// + /// + /// Logs to Console when at-level, and all messages to error log + /// public static void Console(uint level, ErrorLogLevel errorLogLevel, string format, params object[] items) { @@ -431,10 +547,12 @@ public static void Console(uint level, ErrorLogLevel errorLogLevel, { LogError(errorLogLevel, str); } - if (Level >= level) - { - Console(level, str); - } + + LogMessage(level, format, items); + //if (Level >= level) + //{ + // Console(level, str); + //} } /// @@ -444,9 +562,11 @@ public static void Console(uint level, ErrorLogLevel errorLogLevel, /// public static void ConsoleWithLog(uint level, string format, params object[] items) { + LogMessage(level, format, items); + var str = string.Format(format, items); - if (Level >= level) - CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); + //if (Level >= level) + // CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); CrestronLogger.WriteToLog(str, level); } @@ -457,9 +577,10 @@ public static void ConsoleWithLog(uint level, string format, params object[] ite /// public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items) { + LogMessage(level, dev, format, items); + var str = string.Format(format, items); - if (Level >= level) - ConsoleWithLog(level, "[{0}] {1}", dev.Key, str); + CrestronLogger.WriteToLog(string.Format("[{0}] {1}", dev.Key, str), level); } /// @@ -470,7 +591,7 @@ public static void ConsoleWithLog(uint level, IKeyed dev, string format, params public static void LogError(ErrorLogLevel errorLogLevel, string str) { - var msg = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str) : string.Format("Room {0}:{1}", InitialParametersClass.RoomId, str); + var msg = IsRunningOnAppliance ? string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str) : string.Format("Room {0}:{1}", InitialParametersClass.RoomId, str); switch (errorLogLevel) { case ErrorLogLevel.Error: diff --git a/src/Pepperdash Core/Logging/DebugConsoleSink.cs b/src/Pepperdash Core/Logging/DebugConsoleSink.cs new file mode 100644 index 0000000..e991429 --- /dev/null +++ b/src/Pepperdash Core/Logging/DebugConsoleSink.cs @@ -0,0 +1,50 @@ +using Crestron.SimplSharp; +using Serilog.Configuration; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Formatting.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using System.Text; +using System.Threading.Tasks; + +namespace PepperDash.Core +{ + internal class DebugConsoleSink : ILogEventSink + { + private readonly ITextFormatter _textFormatter; + + public void Emit(LogEvent logEvent) + { + if (!Debug.IsRunningOnAppliance) return; + + CrestronConsole.PrintLine("[{0}][App {1}][Lvl {2}]: {3}", logEvent.Timestamp, + InitialParametersClass.ApplicationNumber, + logEvent.Level, + logEvent.RenderMessage()); + } + + public DebugConsoleSink(ITextFormatter formatProvider) + { + + _textFormatter = formatProvider ?? new JsonFormatter(); + + } + + } + + public static class DebugConsoleSinkExtensions + { + public static LoggerConfiguration DebugConsoleSink( + this LoggerSinkConfiguration loggerConfiguration, + ITextFormatter formatProvider = null) + { + return loggerConfiguration.Sink(new DebugConsoleSink(formatProvider)); + } + } + +} diff --git a/src/Pepperdash Core/Logging/DebugWebsocketSink.cs b/src/Pepperdash Core/Logging/DebugWebsocketSink.cs new file mode 100644 index 0000000..f24a585 --- /dev/null +++ b/src/Pepperdash Core/Logging/DebugWebsocketSink.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Configuration; +using WebSocketSharp.Server; +using Crestron.SimplSharp; +using WebSocketSharp; +using System.Security.Authentication; +using WebSocketSharp.Net; +using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; +using System.IO; +using Org.BouncyCastle.Asn1.X509; +using Serilog.Formatting; +using Newtonsoft.Json.Linq; +using Serilog.Formatting.Json; + +namespace PepperDash.Core +{ + public class DebugWebsocketSink : ILogEventSink + { + private HttpServer _httpsServer; + + private string _path = "/debug/join/"; + private const string _certificateName = "selfCres"; + private const string _certificatePassword = "cres12345"; + + public int Port + { get + { + + if(_httpsServer == null) return 0; + return _httpsServer.Port; + } + } + + public string Url + { + get + { + if (_httpsServer == null) return ""; + return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}"; + } + } + + public bool IsRunning { get => _httpsServer?.IsListening ?? false; } + + + private readonly ITextFormatter _textFormatter; + + public DebugWebsocketSink(ITextFormatter formatProvider) + { + + _textFormatter = formatProvider ?? new JsonFormatter(); + + if (!File.Exists($"\\user\\{_certificateName}.pfx")) + CreateCert(null); + + CrestronEnvironment.ProgramStatusEventHandler += type => + { + if (type == eProgramStatusEventType.Stopping) + { + StopServer(); + } + }; + } + + private void CreateCert(string[] args) + { + try + { + //Debug.Console(0,"CreateCert Creating Utility"); + CrestronConsole.PrintLine("CreateCert Creating Utility"); + //var utility = new CertificateUtility(); + var utility = new BouncyCertificate(); + //Debug.Console(0, "CreateCert Calling CreateCert"); + CrestronConsole.PrintLine("CreateCert Calling CreateCert"); + //utility.CreateCert(); + 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); + + //Debug.Console(0, "DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress); + CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress)); + + var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), new[] { string.Format("{0}.{1}", hostName, domainName), ipAddress }, new[] { KeyPurposeID.IdKPServerAuth, KeyPurposeID.IdKPClientAuth }); + //Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested + //Debug.Print($"CreateCert Storing Certificate To My.LocalMachine"); + //utility.AddCertToStore(certificate, StoreName.My, StoreLocation.LocalMachine); + //Debug.Console(0, "CreateCert Saving Cert to \\user\\"); + CrestronConsole.PrintLine("CreateCert Saving Cert to \\user\\"); + utility.CertificatePassword = _certificatePassword; + utility.WriteCertificate(certificate, @"\user\", _certificateName); + //Debug.Console(0, "CreateCert Ending CreateCert"); + CrestronConsole.PrintLine("CreateCert Ending CreateCert"); + } + catch (Exception ex) + { + //Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); + CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace)); + } + } + + public void Emit(LogEvent logEvent) + { + if (_httpsServer == null || !_httpsServer.IsListening) return; + + var sw = new StringWriter(); + _textFormatter.Format(logEvent, sw); + + _httpsServer.WebSocketServices.Broadcast(sw.ToString()); + + } + + public void StartServerAndSetPort(int port) + { + Debug.Console(0, "Starting Websocket Server on port: {0}", port); + + + Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword); + } + + private void Start(int port, string certPath = "", string certPassword = "") + { + try + { + _httpsServer = new HttpServer(port, true); + + + if (!string.IsNullOrWhiteSpace(certPath)) + { + Debug.Console(0, "Assigning SSL Configuration"); + _httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword)) + { + ClientCertificateRequired = false, + CheckCertificateRevocation = false, + EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, + //this is just to test, you might want to actually validate + ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => + { + Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered"); + return true; + } + }; + } + Debug.Console(0, "Adding Debug Client Service"); + _httpsServer.AddWebSocketService(_path); + Debug.Console(0, "Assigning Log Info"); + _httpsServer.Log.Level = LogLevel.Trace; + _httpsServer.Log.Output = (d, s) => + { + uint level; + + switch(d.Level) + { + case WebSocketSharp.LogLevel.Fatal: + level = 3; + break; + case WebSocketSharp.LogLevel.Error: + level = 2; + break; + case WebSocketSharp.LogLevel.Warn: + level = 1; + break; + case WebSocketSharp.LogLevel.Info: + level = 0; + break; + case WebSocketSharp.LogLevel.Debug: + level = 4; + break; + case WebSocketSharp.LogLevel.Trace: + level = 5; + break; + default: + level = 4; + break; + } + + Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); + }; + Debug.Console(0, "Starting"); + + _httpsServer.Start(); + Debug.Console(0, "Ready"); + } + catch (Exception ex) + { + Debug.Console(0, "WebSocket Failed to start {0}", ex.Message); + } + } + + public void StopServer() + { + Debug.Console(0, "Stopping Websocket Server"); + _httpsServer?.Stop(); + + _httpsServer = null; + } + } + + public static class DebugWebsocketSinkExtensions + { + public static LoggerConfiguration DebugWebsocketSink( + this LoggerSinkConfiguration loggerConfiguration, + ITextFormatter formatProvider = null) + { + return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider)); + } + } + + public class DebugClient : WebSocketBehavior + { + private DateTime _connectionTime; + + public TimeSpan ConnectedDuration + { + get + { + if (Context.WebSocket.IsAlive) + { + return DateTime.Now - _connectionTime; + } + else + { + return new TimeSpan(0); + } + } + } + + public DebugClient() + { + Debug.Console(0, "DebugClient Created"); + } + + protected override void OnOpen() + { + base.OnOpen(); + + var url = Context.WebSocket.Url; + Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); + + _connectionTime = DateTime.Now; + } + + protected override void OnMessage(MessageEventArgs e) + { + base.OnMessage(e); + + Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data); + } + + protected override void OnClose(CloseEventArgs e) + { + base.OnClose(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); + } + } +} diff --git a/src/Pepperdash Core/PepperDash_Core.csproj b/src/Pepperdash Core/PepperDash_Core.csproj index d90f536..782dae7 100644 --- a/src/Pepperdash Core/PepperDash_Core.csproj +++ b/src/Pepperdash Core/PepperDash_Core.csproj @@ -14,6 +14,7 @@ https://github.com/PepperDash/PepperDashCore crestron;4series; $(Version) + $(Version) ../../package @@ -25,7 +26,18 @@ bin\4Series\$(Configuration)\PepperDashCore.xml + + + + + + + + + + + diff --git a/src/Pepperdash Core/Web/BouncyCertificate.cs b/src/Pepperdash Core/Web/BouncyCertificate.cs new file mode 100644 index 0000000..67c129f --- /dev/null +++ b/src/Pepperdash Core/Web/BouncyCertificate.cs @@ -0,0 +1,360 @@ +using Crestron.SimplSharp; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Prng; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.X509; +using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; +using X509KeyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags; +using X509ContentType = System.Security.Cryptography.X509Certificates.X509ContentType; +using System.Text; +using Org.BouncyCastle.Crypto.Operators; +using System.Numerics; +using System.Security.Cryptography.X509Certificates; +using BigInteger = Org.BouncyCastle.Math.BigInteger; +using X509Certificate = Org.BouncyCastle.X509.X509Certificate; + +namespace PepperDash.Core +{ + /// + /// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/ + /// + internal class BouncyCertificate + { + public string CertificatePassword { get; set; } = "password"; + public X509Certificate2 LoadCertificate(string issuerFileName, string password) + { + // We need to pass 'Exportable', otherwise we can't get the private key. + var issuerCertificate = new X509Certificate2(issuerFileName, password, X509KeyStorageFlags.Exportable); + return issuerCertificate; + } + + public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages) + { + // It's self-signed, so these are the same. + var issuerName = issuerCertificate.Subject; + + var random = GetSecureRandom(); + var subjectKeyPair = GenerateKeyPair(random, 2048); + + var issuerKeyPair = DotNetUtilities.GetKeyPair(issuerCertificate.PrivateKey); + + var serialNumber = GenerateSerialNumber(random); + var issuerSerialNumber = new BigInteger(issuerCertificate.GetSerialNumber()); + + const bool isCertificateAuthority = false; + var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, + subjectAlternativeNames, issuerName, issuerKeyPair, + issuerSerialNumber, isCertificateAuthority, + usages); + return ConvertCertificate(certificate, subjectKeyPair, random); + } + + public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages) + { + // It's self-signed, so these are the same. + var issuerName = subjectName; + + var random = GetSecureRandom(); + var subjectKeyPair = GenerateKeyPair(random, 2048); + + // It's self-signed, so these are the same. + var issuerKeyPair = subjectKeyPair; + + var serialNumber = GenerateSerialNumber(random); + var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number. + + const bool isCertificateAuthority = true; + var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, + subjectAlternativeNames, issuerName, issuerKeyPair, + issuerSerialNumber, isCertificateAuthority, + usages); + return ConvertCertificate(certificate, subjectKeyPair, random); + } + + public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages) + { + // It's self-signed, so these are the same. + var issuerName = subjectName; + + var random = GetSecureRandom(); + var subjectKeyPair = GenerateKeyPair(random, 2048); + + // It's self-signed, so these are the same. + var issuerKeyPair = subjectKeyPair; + + var serialNumber = GenerateSerialNumber(random); + var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number. + + const bool isCertificateAuthority = false; + var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, + subjectAlternativeNames, issuerName, issuerKeyPair, + issuerSerialNumber, isCertificateAuthority, + usages); + return ConvertCertificate(certificate, subjectKeyPair, random); + } + + private SecureRandom GetSecureRandom() + { + // Since we're on Windows, we'll use the CryptoAPI one (on the assumption + // that it might have access to better sources of entropy than the built-in + // Bouncy Castle ones): + var randomGenerator = new CryptoApiRandomGenerator(); + var random = new SecureRandom(randomGenerator); + return random; + } + + private X509Certificate GenerateCertificate(SecureRandom random, + string subjectName, + AsymmetricCipherKeyPair subjectKeyPair, + BigInteger subjectSerialNumber, + string[] subjectAlternativeNames, + string issuerName, + AsymmetricCipherKeyPair issuerKeyPair, + BigInteger issuerSerialNumber, + bool isCertificateAuthority, + KeyPurposeID[] usages) + { + var certificateGenerator = new X509V3CertificateGenerator(); + + certificateGenerator.SetSerialNumber(subjectSerialNumber); + + var issuerDN = new X509Name(issuerName); + certificateGenerator.SetIssuerDN(issuerDN); + + // Note: The subject can be omitted if you specify a subject alternative name (SAN). + var subjectDN = new X509Name(subjectName); + certificateGenerator.SetSubjectDN(subjectDN); + + // Our certificate needs valid from/to values. + var notBefore = DateTime.UtcNow.Date; + var notAfter = notBefore.AddYears(2); + + certificateGenerator.SetNotBefore(notBefore); + certificateGenerator.SetNotAfter(notAfter); + + // The subject's public key goes in the certificate. + certificateGenerator.SetPublicKey(subjectKeyPair.Public); + + AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber); + AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair); + //AddBasicConstraints(certificateGenerator, isCertificateAuthority); + + if (usages != null && usages.Any()) + AddExtendedKeyUsage(certificateGenerator, usages); + + if (subjectAlternativeNames != null && subjectAlternativeNames.Any()) + AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames); + + // Set the signature algorithm. This is used to generate the thumbprint which is then signed + // with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong. + const string signatureAlgorithm = "SHA256WithRSA"; + + // The certificate is signed with the issuer's private key. + ISignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private, random); + var certificate = certificateGenerator.Generate(signatureFactory); + return certificate; + } + + /// + /// The certificate needs a serial number. This is used for revocation, + /// and usually should be an incrementing index (which makes it easier to revoke a range of certificates). + /// Since we don't have anywhere to store the incrementing index, we can just use a random number. + /// + /// + /// + private BigInteger GenerateSerialNumber(SecureRandom random) + { + var serialNumber = + BigIntegers.CreateRandomInRange( + BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random); + return serialNumber; + } + + /// + /// Generate a key pair. + /// + /// The random number generator. + /// The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days. + /// + private AsymmetricCipherKeyPair GenerateKeyPair(SecureRandom random, int strength) + { + var keyGenerationParameters = new KeyGenerationParameters(random, strength); + + var keyPairGenerator = new RsaKeyPairGenerator(); + keyPairGenerator.Init(keyGenerationParameters); + var subjectKeyPair = keyPairGenerator.GenerateKeyPair(); + return subjectKeyPair; + } + + /// + /// Add the Authority Key Identifier. According to http://www.alvestrand.no/objectid/2.5.29.35.html, this + /// identifies the public key to be used to verify the signature on this certificate. + /// In a certificate chain, this corresponds to the "Subject Key Identifier" on the *issuer* certificate. + /// The Bouncy Castle documentation, at http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation, + /// shows how to create this from the issuing certificate. Since we're creating a self-signed certificate, we have to do this slightly differently. + /// + /// + /// + /// + /// + private void AddAuthorityKeyIdentifier(X509V3CertificateGenerator certificateGenerator, + X509Name issuerDN, + AsymmetricCipherKeyPair issuerKeyPair, + BigInteger issuerSerialNumber) + { + var authorityKeyIdentifierExtension = + new AuthorityKeyIdentifier( + SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public), + new GeneralNames(new GeneralName(issuerDN)), + issuerSerialNumber); + certificateGenerator.AddExtension( + X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension); + } + + /// + /// Add the "Subject Alternative Names" extension. Note that you have to repeat + /// the value from the "Subject Name" property. + /// + /// + /// + private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator, + IEnumerable subjectAlternativeNames) + { + var subjectAlternativeNamesExtension = + new DerSequence( + subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name)) + .ToArray()); + certificateGenerator.AddExtension( + X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension); + } + + /// + /// Add the "Extended Key Usage" extension, specifying (for example) "server authentication". + /// + /// + /// + private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages) + { + certificateGenerator.AddExtension( + X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages)); + } + + /// + /// Add the "Basic Constraints" extension. + /// + /// + /// + private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator, + bool isCertificateAuthority) + { + certificateGenerator.AddExtension( + X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority)); + } + + /// + /// Add the Subject Key Identifier. + /// + /// + /// + private void AddSubjectKeyIdentifier(X509V3CertificateGenerator certificateGenerator, + AsymmetricCipherKeyPair subjectKeyPair) + { + var subjectKeyIdentifierExtension = + new SubjectKeyIdentifier( + SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(subjectKeyPair.Public)); + certificateGenerator.AddExtension( + X509Extensions.SubjectKeyIdentifier.Id, false, subjectKeyIdentifierExtension); + } + + private X509Certificate2 ConvertCertificate(X509Certificate certificate, + AsymmetricCipherKeyPair subjectKeyPair, + SecureRandom random) + { + // Now to convert the Bouncy Castle certificate to a .NET certificate. + // See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx + // ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and private key to that. + var store = new Pkcs12Store(); + + // What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name". + string friendlyName = certificate.SubjectDN.ToString(); + + // Add the certificate. + var certificateEntry = new X509CertificateEntry(certificate); + store.SetCertificateEntry(friendlyName, certificateEntry); + + // Add the private key. + store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry }); + + // Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream. + // It needs a password. Since we'll remove this later, it doesn't particularly matter what we use. + + var stream = new MemoryStream(); + store.Save(stream, CertificatePassword.ToCharArray(), random); + + var convertedCertificate = + new X509Certificate2(stream.ToArray(), + CertificatePassword, + X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); + return convertedCertificate; + } + + public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName) + { + // This password is the one attached to the PFX file. Use 'null' for no password. + // Create PFX (PKCS #12) with private key + try + { + var pfx = certificate.Export(X509ContentType.Pfx, CertificatePassword); + File.WriteAllBytes(string.Format("{0}.pfx", Path.Combine(outputDirectory, certName)), pfx); + } + catch (Exception ex) + { + CrestronConsole.PrintLine(string.Format("Failed to write x509 cert pfx\r\n{0}", ex.Message)); + } + // Create Base 64 encoded CER (public key only) + using (var writer = new StreamWriter($"{Path.Combine(outputDirectory, certName)}.cer", false)) + { + try + { + var contents = string.Format("-----BEGIN CERTIFICATE-----\r\n{0}\r\n-----END CERTIFICATE-----", Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); + writer.Write(contents); + } + catch (Exception ex) + { + CrestronConsole.PrintLine(string.Format("Failed to write x509 cert cer\r\n{0}", ex.Message)); + } + } + } + public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl) + { + bool bRet = false; + + try + { + var store = new System.Security.Cryptography.X509Certificates.X509Store(st, sl); + store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadWrite); + store.Add(cert); + + store.Close(); + bRet = true; + } + catch (Exception ex) + { + CrestronConsole.PrintLine(string.Format("AddCertToStore Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace)); + } + + return bRet; + } + } +} \ No newline at end of file diff --git a/src/Pepperdash Core/Web/RequestHandlers/DefaultRequestHandler.cs b/src/Pepperdash Core/Web/RequestHandlers/DefaultRequestHandler.cs new file mode 100644 index 0000000..57fa1de --- /dev/null +++ b/src/Pepperdash Core/Web/RequestHandlers/DefaultRequestHandler.cs @@ -0,0 +1,16 @@ + +namespace PepperDash.Core.Web.RequestHandlers +{ + /// + /// Web API default request handler + /// + public class DefaultRequestHandler : WebApiBaseRequestHandler + { + /// + /// Constructor + /// + public DefaultRequestHandler() + : base(true) + { } + } +} diff --git a/src/Pepperdash Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs b/src/Pepperdash Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs new file mode 100644 index 0000000..caa6a7c --- /dev/null +++ b/src/Pepperdash Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp.WebScripting; + +namespace PepperDash.Core.Web.RequestHandlers +{ + /// + /// CWS Base Handler, implements IHttpCwsHandler + /// + public abstract class WebApiBaseRequestHandler : IHttpCwsHandler + { + private readonly Dictionary> _handlers; + protected readonly bool EnableCors; + + /// + /// Constructor + /// + protected WebApiBaseRequestHandler(bool enableCors) + { + EnableCors = enableCors; + + _handlers = new Dictionary> + { + {"CONNECT", HandleConnect}, + {"DELETE", HandleDelete}, + {"GET", HandleGet}, + {"HEAD", HandleHead}, + {"OPTIONS", HandleOptions}, + {"PATCH", HandlePatch}, + {"POST", HandlePost}, + {"PUT", HandlePut}, + {"TRACE", HandleTrace} + }; + } + + /// + /// Constructor + /// + protected WebApiBaseRequestHandler() + : this(false) + { + } + + /// + /// Handles CONNECT method requests + /// + /// + protected virtual void HandleConnect(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles DELETE method requests + /// + /// + protected virtual void HandleDelete(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles GET method requests + /// + /// + protected virtual void HandleGet(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles HEAD method requests + /// + /// + protected virtual void HandleHead(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles OPTIONS method requests + /// + /// + protected virtual void HandleOptions(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles PATCH method requests + /// + /// + protected virtual void HandlePatch(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles POST method requests + /// + /// + protected virtual void HandlePost(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles PUT method requests + /// + /// + protected virtual void HandlePut(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles TRACE method requests + /// + /// + protected virtual void HandleTrace(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Process request + /// + /// + public void ProcessRequest(HttpCwsContext context) + { + Action handler; + + if (!_handlers.TryGetValue(context.Request.HttpMethod, out handler)) + { + return; + } + + if (EnableCors) + { + context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); + context.Response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); + } + + handler(context); + } + } +} \ No newline at end of file diff --git a/src/Pepperdash Core/Web/WebApiServer.cs b/src/Pepperdash Core/Web/WebApiServer.cs new file mode 100644 index 0000000..17c737a --- /dev/null +++ b/src/Pepperdash Core/Web/WebApiServer.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharp; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core.Web.RequestHandlers; + +namespace PepperDash.Core.Web +{ + /// + /// Web API server + /// + public class WebApiServer : IKeyName + { + private const string SplusKey = "Uninitialized Web API Server"; + private const string DefaultName = "Web API Server"; + private const string DefaultBasePath = "/api"; + + private const uint DebugTrace = 0; + private const uint DebugInfo = 1; + private const uint DebugVerbose = 2; + + private readonly CCriticalSection _serverLock = new CCriticalSection(); + private HttpCwsServer _server; + + /// + /// Web API server key + /// + public string Key { get; private set; } + + /// + /// Web API server name + /// + public string Name { get; private set; } + + /// + /// CWS base path, will default to "/api" if not set via initialize method + /// + public string BasePath { get; private set; } + + /// + /// Indicates CWS is registered with base path + /// + public bool IsRegistered { get; private set; } + + /// + /// Http request handler + /// + //public IHttpCwsHandler HttpRequestHandler + //{ + // get { return _server.HttpRequestHandler; } + // set + // { + // if (_server == null) return; + // _server.HttpRequestHandler = value; + // } + //} + + /// + /// Received request event handler + /// + //public event EventHandler ReceivedRequestEvent + //{ + // add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); } + // remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); } + //} + + /// + /// Constructor for S+. Make sure to set necessary properties using init method + /// + public WebApiServer() + : this(SplusKey, DefaultName, null) + { + } + + /// + /// Constructor + /// + /// + /// + public WebApiServer(string key, string basePath) + : this(key, DefaultName, basePath) + { + } + + /// + /// Constructor + /// + /// + /// + /// + public WebApiServer(string key, string name, string basePath) + { + Key = key; + Name = string.IsNullOrEmpty(name) ? DefaultName : name; + BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; + + if (_server == null) _server = new HttpCwsServer(BasePath); + + _server.setProcessName(Key); + _server.HttpRequestHandler = new DefaultRequestHandler(); + + CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; + CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler; + } + + /// + /// Program status event handler + /// + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType != eProgramStatusEventType.Stopping) return; + + Debug.Console(DebugInfo, this, "Program stopping. stopping server"); + + Stop(); + } + + /// + /// Ethernet event handler + /// + /// + void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) + { + // Re-enable the server if the link comes back up and the status should be connected + if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered) + { + Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered."); + return; + } + + Debug.Console(DebugInfo, this, "Ethernet link up. Starting server"); + + Start(); + } + + /// + /// Initializes CWS class + /// + public void Initialize(string key, string basePath) + { + Key = key; + BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; + } + + /// + /// Adds a route to CWS + /// + public void AddRoute(HttpCwsRoute route) + { + if (route == null) + { + Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null"); + return; + } + + _server.Routes.Add(route); + + } + + /// + /// Removes a route from CWS + /// + /// + public void RemoveRoute(HttpCwsRoute route) + { + if (route == null) + { + Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null"); + return; + } + + _server.Routes.Remove(route); + } + + /// + /// Returns a list of the current routes + /// + public HttpCwsRouteCollection GetRouteCollection() + { + return _server.Routes; + } + + /// + /// Starts CWS instance + /// + public void Start() + { + try + { + _serverLock.Enter(); + + if (_server == null) + { + Debug.Console(DebugInfo, this, "Server is null, unable to start"); + return; + } + + if (IsRegistered) + { + Debug.Console(DebugInfo, this, "Server has already been started"); + return; + } + + IsRegistered = _server.Register(); + + Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed"); + } + catch (Exception ex) + { + Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message); + Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace); + if (ex.InnerException != null) + Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException); + } + finally + { + _serverLock.Leave(); + } + } + + /// + /// Stop CWS instance + /// + public void Stop() + { + try + { + _serverLock.Enter(); + + if (_server == null) + { + Debug.Console(DebugInfo, this, "Server is null or has already been stopped"); + return; + } + + IsRegistered = _server.Unregister() == false; + + Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful"); + + _server.Dispose(); + _server = null; + } + catch (Exception ex) + { + Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message); + Debug.Console(DebugVerbose, this, "Server Stop Exception StackTrace: {0}", ex.StackTrace); + if (ex.InnerException != null) + Debug.Console(DebugVerbose, this, "Server Stop Exception InnerException: {0}", ex.InnerException); + } + finally + { + _serverLock.Leave(); + } + } + + /// + /// Received request handler + /// + /// + /// This is here for development and testing + /// + /// + /// + public void ReceivedRequestEventHandler(object sender, HttpCwsRequestEventArgs args) + { + try + { + var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented); + Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j); + } + catch (Exception ex) + { + Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message); + Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace); + if (ex.InnerException != null) + Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException); + } + } + } +} \ No newline at end of file