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