Merge pull request #169 from PepperDash/feature-2/adds-web-classes

Update debugging and add web stuff
This commit is contained in:
Andrew Welker
2024-02-05 13:15:31 -06:00
committed by GitHub
13 changed files with 1353 additions and 69 deletions

View File

@@ -79,7 +79,7 @@ namespace PepperDash.Core.Config
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 @@ namespace PepperDash.Core.Config
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;
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Serilog;
namespace PepperDash.Core
{
@@ -18,7 +19,7 @@ namespace PepperDash.Core
}
/// <summary>
/// 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.
/// </summary>
public interface IKeyName : IKeyed
{
@@ -27,4 +28,5 @@ namespace PepperDash.Core
/// </summary>
string Name { get; }
}
}

View File

@@ -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
/// </summary>
public class Device : IKeyName
{
/// <summary>
/// Unique Key
/// </summary>
@@ -51,7 +55,6 @@ namespace PepperDash.Core
Key = key;
if (key.Contains('.')) Debug.Console(0, this, "WARNING: Device name's should not include '.'");
Name = "";
}
/// <summary>

View File

@@ -194,7 +194,7 @@ namespace PepperDash.Core.JsonToSimpl
/// Sets the debug level
/// </summary>
/// <param name="level"></param>
public void setDebugLevel(int level)
public void setDebugLevel(uint level)
{
Debug.SetDebugLevel(level);
}

View File

@@ -128,7 +128,7 @@ namespace PepperDash.Core.JsonToSimpl
///
/// </summary>
/// <param name="level"></param>
public void setDebugLevel(int level)
public void setDebugLevel(uint level)
{
Debug.SetDebugLevel(level);
}

View File

@@ -7,7 +7,12 @@ using Crestron.SimplSharp.CrestronLogger;
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
/// </summary>
public static class Debug
{
private static Dictionary<uint, LogEventLevel> _logLevels = new Dictionary<uint, LogEventLevel>()
{
{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; }
}
/// <summary>
/// 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 @@ namespace PepperDash.Core
/// <summary>
/// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal
/// </summary>
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;
/// <summary>
/// Version for the currently loaded PepperDashCore dll
/// </summary>
@@ -66,6 +101,24 @@ namespace PepperDash.Core
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 @@ namespace PepperDash.Core
"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 @@ namespace PepperDash.Core
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 @@ namespace PepperDash.Core
/// <param name="programEventType"></param>
static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
Log.CloseAndFlush();
if (_saveTimer != null)
{
_saveTimer.Stop();
@@ -184,20 +240,65 @@ namespace PepperDash.Core
{
try
{
if (string.IsNullOrEmpty(levelString.Trim()))
if (levelString.Trim() == "?")
{
CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", Level);
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;
}
SetDebugLevel(Convert.ToInt32(levelString));
if (string.IsNullOrEmpty(levelString.Trim()))
{
CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", _consoleLoggingLevelSwitch.MinimumLevel);
return;
}
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]");
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"> Valid values 0-5</param>
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);
}
/// <summary>
/// Callback for console command
/// </summary>
@@ -208,11 +309,11 @@ namespace PepperDash.Core
{
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 @@ namespace PepperDash.Core
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 @@ namespace PepperDash.Core
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"> Valid values 0 (no debug), 1 (critical), 2 (all messages)</param>
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);
}
}
/// <summary>
/// sets the settings for a device or creates a new entry
@@ -347,14 +429,14 @@ namespace PepperDash.Core
/// Sets the flag to prevent application starting on next boot
/// </summary>
/// <param name="state"></param>
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);
}
/// <summary>
@@ -367,6 +449,26 @@ namespace PepperDash.Core
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);
}
/// <summary>
/// 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 @@ namespace PepperDash.Core
/// <param name="items">Object parameters</param>
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 @@ namespace PepperDash.Core
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));
//}
}
/// <summary>
@@ -398,8 +503,10 @@ namespace PepperDash.Core
/// </summary>
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);
}
/// <summary>
@@ -409,15 +516,24 @@ namespace PepperDash.Core
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);
//}
}
/// <summary>
@@ -431,10 +547,12 @@ namespace PepperDash.Core
{
LogError(errorLogLevel, str);
}
if (Level >= level)
{
Console(level, str);
}
LogMessage(level, format, items);
//if (Level >= level)
//{
// Console(level, str);
//}
}
/// <summary>
@@ -444,9 +562,11 @@ namespace PepperDash.Core
/// </summary>
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 @@ namespace PepperDash.Core
/// </summary>
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);
}
/// <summary>
@@ -470,7 +591,7 @@ namespace PepperDash.Core
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:

View File

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

View File

@@ -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<DebugClient>(_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);
}
}
}

View File

@@ -14,6 +14,7 @@
<RepositoryUrl>https://github.com/PepperDash/PepperDashCore</RepositoryUrl>
<PackageTags>crestron;4series;</PackageTags>
<Version>$(Version)</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<PackageOutputPath>../../package</PackageOutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@@ -25,7 +26,18 @@
<DocumentationFile>bin\4Series\$(Configuration)\PepperDashCore.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle" Version="1.8.9" />
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.20.42" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Comm\._GenericSshClient.cs" />

View File

@@ -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
{
/// <summary>
/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="random"></param>
/// <returns></returns>
private BigInteger GenerateSerialNumber(SecureRandom random)
{
var serialNumber =
BigIntegers.CreateRandomInRange(
BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
return serialNumber;
}
/// <summary>
/// Generate a key pair.
/// </summary>
/// <param name="random">The random number generator.</param>
/// <param name="strength">The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days.</param>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="issuerDN"></param>
/// <param name="issuerKeyPair"></param>
/// <param name="issuerSerialNumber"></param>
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);
}
/// <summary>
/// Add the "Subject Alternative Names" extension. Note that you have to repeat
/// the value from the "Subject Name" property.
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="subjectAlternativeNames"></param>
private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator,
IEnumerable<string> subjectAlternativeNames)
{
var subjectAlternativeNamesExtension =
new DerSequence(
subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name))
.ToArray<Asn1Encodable>());
certificateGenerator.AddExtension(
X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension);
}
/// <summary>
/// Add the "Extended Key Usage" extension, specifying (for example) "server authentication".
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="usages"></param>
private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages)
{
certificateGenerator.AddExtension(
X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages));
}
/// <summary>
/// Add the "Basic Constraints" extension.
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="isCertificateAuthority"></param>
private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator,
bool isCertificateAuthority)
{
certificateGenerator.AddExtension(
X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority));
}
/// <summary>
/// Add the Subject Key Identifier.
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="subjectKeyPair"></param>
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;
}
}
}

View File

@@ -0,0 +1,16 @@
namespace PepperDash.Core.Web.RequestHandlers
{
/// <summary>
/// Web API default request handler
/// </summary>
public class DefaultRequestHandler : WebApiBaseRequestHandler
{
/// <summary>
/// Constructor
/// </summary>
public DefaultRequestHandler()
: base(true)
{ }
}
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp.WebScripting;
namespace PepperDash.Core.Web.RequestHandlers
{
/// <summary>
/// CWS Base Handler, implements IHttpCwsHandler
/// </summary>
public abstract class WebApiBaseRequestHandler : IHttpCwsHandler
{
private readonly Dictionary<string, Action<HttpCwsContext>> _handlers;
protected readonly bool EnableCors;
/// <summary>
/// Constructor
/// </summary>
protected WebApiBaseRequestHandler(bool enableCors)
{
EnableCors = enableCors;
_handlers = new Dictionary<string, Action<HttpCwsContext>>
{
{"CONNECT", HandleConnect},
{"DELETE", HandleDelete},
{"GET", HandleGet},
{"HEAD", HandleHead},
{"OPTIONS", HandleOptions},
{"PATCH", HandlePatch},
{"POST", HandlePost},
{"PUT", HandlePut},
{"TRACE", HandleTrace}
};
}
/// <summary>
/// Constructor
/// </summary>
protected WebApiBaseRequestHandler()
: this(false)
{
}
/// <summary>
/// Handles CONNECT method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleConnect(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles DELETE method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleDelete(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles GET method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleGet(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles HEAD method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleHead(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles OPTIONS method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleOptions(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles PATCH method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandlePatch(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles POST method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandlePost(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles PUT method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandlePut(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles TRACE method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleTrace(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Process request
/// </summary>
/// <param name="context"></param>
public void ProcessRequest(HttpCwsContext context)
{
Action<HttpCwsContext> 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);
}
}
}

View File

@@ -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
{
/// <summary>
/// Web API server
/// </summary>
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;
/// <summary>
/// Web API server key
/// </summary>
public string Key { get; private set; }
/// <summary>
/// Web API server name
/// </summary>
public string Name { get; private set; }
/// <summary>
/// CWS base path, will default to "/api" if not set via initialize method
/// </summary>
public string BasePath { get; private set; }
/// <summary>
/// Indicates CWS is registered with base path
/// </summary>
public bool IsRegistered { get; private set; }
/// <summary>
/// Http request handler
/// </summary>
//public IHttpCwsHandler HttpRequestHandler
//{
// get { return _server.HttpRequestHandler; }
// set
// {
// if (_server == null) return;
// _server.HttpRequestHandler = value;
// }
//}
/// <summary>
/// Received request event handler
/// </summary>
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
//{
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
// remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); }
//}
/// <summary>
/// Constructor for S+. Make sure to set necessary properties using init method
/// </summary>
public WebApiServer()
: this(SplusKey, DefaultName, null)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="basePath"></param>
public WebApiServer(string key, string basePath)
: this(key, DefaultName, basePath)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <param name="basePath"></param>
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;
}
/// <summary>
/// Program status event handler
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType != eProgramStatusEventType.Stopping) return;
Debug.Console(DebugInfo, this, "Program stopping. stopping server");
Stop();
}
/// <summary>
/// Ethernet event handler
/// </summary>
/// <param name="ethernetEventArgs"></param>
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();
}
/// <summary>
/// Initializes CWS class
/// </summary>
public void Initialize(string key, string basePath)
{
Key = key;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
}
/// <summary>
/// Adds a route to CWS
/// </summary>
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);
}
/// <summary>
/// Removes a route from CWS
/// </summary>
/// <param name="route"></param>
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);
}
/// <summary>
/// Returns a list of the current routes
/// </summary>
public HttpCwsRouteCollection GetRouteCollection()
{
return _server.Routes;
}
/// <summary>
/// Starts CWS instance
/// </summary>
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();
}
}
/// <summary>
/// Stop CWS instance
/// </summary>
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();
}
}
/// <summary>
/// Received request handler
/// </summary>
/// <remarks>
/// This is here for development and testing
/// </remarks>
/// <param name="sender"></param>
/// <param name="args"></param>
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);
}
}
}
}