diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs new file mode 100644 index 00000000..887f1789 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs @@ -0,0 +1,23 @@ +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +{ + /// + /// Defines the contract for IMobileControlMessenger + /// + public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger + { + /// + /// Unsubscribe a client from this messenger + /// + /// + void UnsubscribeClient(string clientId); + + /// + /// Register this messenger with the AppServerController + /// + /// parent for this messenger + /// Enable messenger subscriptions + void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index 9ed9029b..a7210a13 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using Crestron.SimplSharp.Net; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Core.Logging; @@ -13,13 +13,26 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Provides a messaging bridge /// - public abstract class MessengerBase : EssentialsDevice, IMobileControlMessenger + public abstract class MessengerBase : EssentialsDevice, IMobileControlMessengerWithSubscriptions { /// /// The device this messenger is associated with /// protected IKeyName _device; + /// + /// Enable subscriptions + /// + protected bool enableMessengerSubscriptions; + + /// + /// List of clients subscribed to this messenger + /// + /// + /// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection. + /// + protected HashSet SubscriberIds = new HashSet(); + private readonly List _deviceInterfaces; private readonly Dictionary> _actions = new Dictionary>(); @@ -93,6 +106,21 @@ namespace PepperDash.Essentials.AppServer.Messengers RegisterActions(); } + /// + /// Register this messenger with appserver controller + /// + /// Parent controller for this messenger + /// Enable subscriptions + public void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions) + { + this.enableMessengerSubscriptions = enableMessengerSubscriptions; + AppServerController = appServerController ?? throw new ArgumentNullException("appServerController"); + + AppServerController.AddAction(this, HandleMessage); + + RegisterActions(); + } + private void HandleMessage(string path, string id, JToken content) { // replace base path with empty string. Should leave something like /fullStatus @@ -103,7 +131,7 @@ namespace PepperDash.Essentials.AppServer.Messengers return; } - Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Executing action for path {path}", this, path); + this.LogDebug("Executing action for path {path}", path); action(id, content); } @@ -117,7 +145,6 @@ namespace PepperDash.Essentials.AppServer.Messengers { if (_actions.ContainsKey(path)) { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Messenger {Key} already has action registered at {path}", this); return; } @@ -154,6 +181,52 @@ namespace PepperDash.Essentials.AppServer.Messengers } + /// + /// Add client to the susbscription list for unsolicited feedback + /// + /// Client ID to add + protected void SubscribeClient(string clientId) + { + if (!enableMessengerSubscriptions) + { + this.LogWarning("Messenger subscriptions not enabled"); + return; + } + + if (SubscriberIds.Any(id => id == clientId)) + { + this.LogVerbose("Client {clientId} already subscribed", clientId); + return; + } + + SubscriberIds.Add(clientId); + + this.LogDebug("Client {clientId} subscribed", clientId); + } + + /// + /// Remove a client from the subscription list + /// + /// Client ID to remove + public void UnsubscribeClient(string clientId) + { + if (!enableMessengerSubscriptions) + { + this.LogWarning("Messenger subscriptions not enabled"); + return; + } + + if (!SubscriberIds.Any(i => i == clientId)) + { + this.LogVerbose("Client with ID {clientId} is not subscribed", clientId); + return; + } + + SubscriberIds.RemoveWhere((i) => i == clientId); + + this.LogInformation("Client with ID {clientId} unsubscribed", clientId); + } + /// /// Helper for posting status message /// @@ -228,11 +301,33 @@ namespace PepperDash.Essentials.AppServer.Messengers { try { + // Allow for legacy method to continue without subscriptions + if (!enableMessengerSubscriptions) + { + AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content }); + return; + } + + // handle subscription feedback + // If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties + if (string.IsNullOrEmpty(clientId)) + { + foreach (var client in SubscriberIds) + { + AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content }); + } + + return; + } + + SubscribeClient(clientId); + AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content }); } catch (Exception ex) { - Debug.LogMessage(ex, "Exception posting status message", this); + this.LogError("Exception posting status message: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); } } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs index 6c130f6d..ec7219a3 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs @@ -38,6 +38,12 @@ namespace PepperDash.Essentials /// [JsonProperty("enableApiServer")] public bool EnableApiServer { get; set; } = true; + + /// + /// Enable subscriptions for Messengers + /// + [JsonProperty("enableMessengerSubscriptions")] + public bool EnableMessengerSubscriptions { get; set; } } /// @@ -78,6 +84,15 @@ namespace PepperDash.Essentials [JsonProperty("csLanUiDeviceKeys")] public List CSLanUiDeviceKeys { get; set; } + /// + /// Get or set the Secure property + /// + /// + /// Indicates whether the connection is secure (HTTPS). + /// + [JsonProperty("Secure")] + public bool Secure { get; set; } + /// /// Initializes a new instance of the MobileControlDirectServerPropertiesConfig class. /// diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 736bf69c..bd04b0e3 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -54,7 +54,10 @@ namespace PepperDash.Essentials StringComparer.InvariantCultureIgnoreCase ); - public Dictionary> ActionDictionary => _actionDictionary; + /// + /// Actions + /// + public ReadOnlyDictionary> ActionDictionary => new ReadOnlyDictionary>(_actionDictionary); private readonly GenericQueue _receiveQueue; private readonly List _roomBridges = @@ -66,6 +69,16 @@ namespace PepperDash.Essentials private readonly Dictionary _defaultMessengers = new Dictionary(); + /// + /// Get the custom messengers with subscriptions + /// + public ReadOnlyDictionary Messengers => new ReadOnlyDictionary(_messengers.Values.OfType().ToDictionary(k => k.Key, v => v)); + + /// + /// Get the default messengers + /// + public ReadOnlyDictionary DefaultMessengers => new ReadOnlyDictionary(_defaultMessengers.Values.OfType().ToDictionary(k => k.Key, v => v)); + private readonly GenericQueue _transmitToServerQueue; private readonly GenericQueue _transmitToClientsQueue; @@ -1199,8 +1212,7 @@ namespace PepperDash.Essentials if (_initialized) { - this.LogDebug("Registering messenger {messengerKey} AFTER initialization", messenger.Key); - messenger.RegisterWithAppServer(this); + RegisterMessengerWithServer(messenger); } } @@ -1241,6 +1253,12 @@ namespace PepperDash.Essentials messenger.MessagePath ); + if (messenger is IMobileControlMessengerWithSubscriptions subMessenger) + { + subMessenger.RegisterWithAppServer(this, Config.EnableMessengerSubscriptions); + return; + } + messenger.RegisterWithAppServer(this); } diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index f2f5003a..eb60857e 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -4,6 +4,8 @@ using System.ComponentModel; using System.IO; using System.Linq; using System.Net.Http; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; @@ -35,6 +37,10 @@ namespace PepperDash.Essentials.WebSocketServer private readonly string appConfigFileName = "_config.local.json"; private readonly string appConfigCsFileName = "_config.cs.json"; + private const string certificateName = "selfCres"; + + private const string certificatePassword = "cres12345"; + /// /// Where the key is the join token and the value is the room key /// @@ -260,7 +266,41 @@ namespace PepperDash.Essentials.WebSocketServer _server.OnPost += Server_OnPost; } - _server.Log.Output = (data, level) => this.LogInformation("WebSocket Server Log [{level}]: {data}", level, data); + if (_parent.Config.DirectServer.Secure) + { + this.LogInformation("Adding SSL Configuration to server"); + _server.SslConfiguration = new ServerSslConfiguration(new X509Certificate2($"\\user\\{certificateName}.pfx", certificatePassword)) + { + ClientCertificateRequired = false, + CheckCertificateRevocation = false, + EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 + }; + } + + _server.Log.Output = (data, message) => + { + switch (data.Level) + { + case LogLevel.Trace: + this.LogVerbose(data.Message); + break; + case LogLevel.Debug: + this.LogDebug(data.Message); + break; + case LogLevel.Info: + this.LogInformation(data.Message); + break; + case LogLevel.Warn: + this.LogWarning(data.Message); + break; + case LogLevel.Error: + this.LogError(data.Message); + break; + case LogLevel.Fatal: + this.LogFatal(data.Message); + break; + } + }; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index d1d4524d..3cdd183b 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -22,14 +22,29 @@ namespace PepperDash.Essentials.WebSocketServer /// public string Key { get; private set; } + /// + /// Gets or sets the mobile control system controller that handles this client's messages + /// public MobileControlSystemController Controller { get; set; } + /// + /// Gets or sets the room key that this client is associated with + /// public string RoomKey { get; set; } + /// + /// The unique identifier for this client instance + /// private string _clientId; + /// + /// The timestamp when this client connection was established + /// private DateTime _connectionTime; + /// + /// Gets the duration that this client has been connected. Returns zero if not currently connected. + /// public TimeSpan ConnectedDuration { get @@ -45,6 +60,10 @@ namespace PepperDash.Essentials.WebSocketServer } } + /// + /// Initializes a new instance of the UiClient class with the specified key + /// + /// The unique key to identify this client public UiClient(string key) { Key = key; @@ -99,11 +118,21 @@ namespace PepperDash.Essentials.WebSocketServer // TODO: Future: Check token to see if there's already an open session using that token and reject/close the session } + /// + /// Handles the UserCodeChanged event from a room bridge and sends the updated user code to the client + /// + /// The room bridge that raised the event + /// Event arguments private void Bridge_UserCodeChanged(object sender, EventArgs e) { SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId); } + /// + /// Sends the current user code and QR code URL to the specified client + /// + /// The room bridge containing the user code information + /// The ID of the client to send the information to private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId) { var content = new @@ -140,6 +169,16 @@ namespace PepperDash.Essentials.WebSocketServer base.OnClose(e); this.LogInformation("WebSocket UiClient Closing: {code} reason: {reason}", e.Code, e.Reason); + + foreach (var messenger in Controller.Messengers) + { + messenger.Value.UnsubscribeClient(_clientId); + } + + foreach (var messenger in Controller.DefaultMessengers) + { + messenger.Value.UnsubscribeClient(_clientId); + } } ///