diff --git a/src/PepperDash.Essentials.Core/Extensions/IpAddressExtensions.cs b/src/PepperDash.Essentials.Core/Extensions/IpAddressExtensions.cs
new file mode 100644
index 00000000..35ec1b8a
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/Extensions/IpAddressExtensions.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PepperDash.Essentials.Core
+{
+ ///
+ /// Extensions for IPAddress to provide additional functionality such as getting broadcast address, network address, and checking if two addresses are in the same subnet.
+ ///
+ public static class IPAddressExtensions
+ {
+ ///
+ /// Get the broadcast address for a given IP address and subnet mask.
+ ///
+ /// Address to check
+ /// Subnet mask in a.b.c.d format
+ /// Broadcast address
+ ///
+ /// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the broadcast address will be 192.168.1.255
+ ///
+ ///
+ public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask)
+ {
+ byte[] ipAdressBytes = address.GetAddressBytes();
+ byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
+
+ if (ipAdressBytes.Length != subnetMaskBytes.Length)
+ throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
+
+ byte[] broadcastAddress = new byte[ipAdressBytes.Length];
+ for (int i = 0; i < broadcastAddress.Length; i++)
+ {
+ broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255));
+ }
+ return new IPAddress(broadcastAddress);
+ }
+
+ ///
+ /// Get the network address for a given IP address and subnet mask.
+ ///
+ /// Address to check
+ /// Subnet mask in a.b.c.d
+ /// Network Address
+ /// ///
+ /// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the network address will be 192.168.1.0
+ ///
+ ///
+ public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask)
+ {
+ byte[] ipAdressBytes = address.GetAddressBytes();
+ byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
+
+ if (ipAdressBytes.Length != subnetMaskBytes.Length)
+ throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
+
+ byte[] broadcastAddress = new byte[ipAdressBytes.Length];
+ for (int i = 0; i < broadcastAddress.Length; i++)
+ {
+ broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
+ }
+ return new IPAddress(broadcastAddress);
+ }
+
+ ///
+ /// Determine if two IP addresses are in the same subnet.
+ ///
+ /// Address to check
+ /// Second address to check
+ /// Subnet mask to use to compare the 2 IP Address
+ /// True if addresses are in the same subnet
+ ///
+ /// If the input IP addresses are 192.168.1.100 and 192.168.1.200, and the subnet mask is 255.255.255.0, this will return true.
+ /// If the input IP addresses are 10.1.1.100 and 192.168.1.100, and the subnet mask is 255.255.255.0, this will return false.
+ ///
+ public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask)
+ {
+ IPAddress network1 = address.GetNetworkAddress(subnetMask);
+ IPAddress network2 = address2.GetNetworkAddress(subnetMask);
+
+ return network1.Equals(network2);
+ }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
index cd75dcb7..859a7c7b 100644
--- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
+++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
@@ -1,1381 +1,1281 @@
-using Crestron.SimplSharp;
-using Crestron.SimplSharp.WebScripting;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using PepperDash.Core;
-using PepperDash.Core.Logging;
-using PepperDash.Essentials.AppServer.Messengers;
-using PepperDash.Essentials.Core;
-using PepperDash.Essentials.Core.DeviceTypeInterfaces;
-using PepperDash.Essentials.Core.Web;
-using PepperDash.Essentials.RoomBridges;
-using PepperDash.Essentials.WebApiHandlers;
-using Serilog.Events;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net.Http;
-using System.Text;
-using System.Text.RegularExpressions;
-using WebSocketSharp;
-using WebSocketSharp.Net;
-using WebSocketSharp.Server;
-using ErrorEventArgs = WebSocketSharp.ErrorEventArgs;
-
-
-namespace PepperDash.Essentials.WebSocketServer
-{
- ///
- /// Represents the behaviour to associate with a UiClient for WebSocket communication
- ///
- public class UiClient : WebSocketBehavior
- {
- public MobileControlSystemController Controller { get; set; }
-
- public string RoomKey { get; set; }
-
- private string _clientId;
-
- private DateTime _connectionTime;
-
- public TimeSpan ConnectedDuration
- {
- get
- {
- if (Context.WebSocket.IsAlive)
- {
- return DateTime.Now - _connectionTime;
- }
- else
- {
- return new TimeSpan(0);
- }
- }
- }
-
- public UiClient()
- {
-
- }
-
- protected override void OnOpen()
- {
- base.OnOpen();
-
- var url = Context.WebSocket.Url;
- Debug.LogMessage(LogEventLevel.Verbose, "New WebSocket Connection from: {0}", null, url);
-
- var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)");
-
- if (!match.Success)
- {
- _connectionTime = DateTime.Now;
- return;
- }
-
- var clientId = match.Groups[1].Value;
- _clientId = clientId;
-
- if (Controller == null)
- {
- Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null");
- _connectionTime = DateTime.Now;
- }
-
- var clientJoinedMessage = new MobileControlMessage
- {
- Type = "/system/clientJoined",
- Content = JToken.FromObject(new
- {
- clientId,
- roomKey = RoomKey,
- })
- };
-
- Controller.HandleClientMessage(JsonConvert.SerializeObject(clientJoinedMessage));
-
- var bridge = Controller.GetRoomBridge(RoomKey);
-
- if (bridge == null) return;
-
- SendUserCodeToClient(bridge, clientId);
-
- bridge.UserCodeChanged -= Bridge_UserCodeChanged;
- bridge.UserCodeChanged += Bridge_UserCodeChanged;
-
- // TODO: Future: Check token to see if there's already an open session using that token and reject/close the session
- }
-
- private void Bridge_UserCodeChanged(object sender, EventArgs e)
- {
- SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId);
- }
-
- private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId)
- {
- var content = new
- {
- userCode = bridge.UserCode,
- qrUrl = bridge.QrCodeUrl,
- };
-
- var message = new MobileControlMessage
- {
- Type = "/system/userCodeChanged",
- ClientId = clientId,
- Content = JToken.FromObject(content)
- };
-
- Controller.SendMessageObjectToDirectClient(message);
- }
-
- protected override void OnMessage(MessageEventArgs e)
- {
- base.OnMessage(e);
-
- if (e.IsText && e.Data.Length > 0 && Controller != null)
- {
- // Forward the message to the controller to be put on the receive queue
- Controller.HandleClientMessage(e.Data);
- }
- }
-
- protected override void OnClose(CloseEventArgs e)
- {
- base.OnClose(e);
-
- Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Closing: {0} reason: {1}", null, e.Code, e.Reason);
- }
-
- protected override void OnError(ErrorEventArgs e)
- {
- base.OnError(e);
-
- Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Error: {exception} message: {message}", e.Exception, e.Message);
- }
- }
-
- public class MobileControlWebsocketServer : EssentialsDevice
- {
- private readonly string userAppPath = Global.FilePathPrefix + "mcUserApp" + Global.DirectorySeparator;
-
- private readonly string localConfigFolderName = "_local-config";
-
- private readonly string appConfigFileName = "_config.local.json";
-
- ///
- /// Where the key is the join token and the value is the room key
- ///
- //private Dictionary _joinTokens;
-
- private HttpServer _server;
-
- public HttpServer Server => _server;
-
- public Dictionary UiClients { get; private set; }
-
- private readonly MobileControlSystemController _parent;
-
- private WebSocketServerSecretProvider _secretProvider;
-
- private ServerTokenSecrets _secret;
-
- private static readonly HttpClient LogClient = new HttpClient();
-
- private string SecretProviderKey
- {
- get
- {
- return string.Format("{0}:{1}-tokens", Global.ControlSystem.ProgramNumber, Key);
- }
- }
-
- ///
- /// The path for the WebSocket messaging
- ///
- private readonly string _wsPath = "/mc/api/ui/join/";
-
- public string WsPath => _wsPath;
-
- ///
- /// The path to the location of the files for the user app (single page Angular app)
- ///
- private readonly string _appPath = string.Format("{0}mcUserApp", Global.FilePathPrefix);
-
- ///
- /// The base HREF that the user app uses
- ///
- private string _userAppBaseHref = "/mc/app";
-
- ///
- /// The prot the server will run on
- ///
- public int Port { get; private set; }
-
- public string UserAppUrlPrefix
- {
- get
- {
- return string.Format("http://{0}:{1}{2}?token=",
- CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
- Port,
- _userAppBaseHref);
-
- }
- }
-
- public int ConnectedUiClientsCount
- {
- get
- {
- var count = 0;
-
- foreach (var client in UiClients)
- {
- if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive)
- {
- count++;
- }
- }
-
- return count;
- }
- }
-
- public MobileControlWebsocketServer(string key, int customPort, MobileControlSystemController parent)
- : base(key)
- {
- _parent = parent;
-
- // Set the default port to be 50000 plus the slot number of the program
- Port = 50000 + (int)Global.ControlSystem.ProgramNumber;
-
- if (customPort != 0)
- {
- Port = customPort;
- }
-
- if (parent.Config.DirectServer.AutomaticallyForwardPortToCSLAN == true)
- {
- try
- {
- CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
-
-
- Debug.LogMessage(LogEventLevel.Information, "Automatically forwarding port {0} to CS LAN", Port);
-
- var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
- var csIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
-
- var result = CrestronEthernetHelper.AddPortForwarding((ushort)Port, (ushort)Port, csIp, CrestronEthernetHelper.ePortMapTransport.TCP);
-
- if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
- {
- Debug.LogMessage(LogEventLevel.Error, "Error adding port forwarding: {0}", result);
- }
- }
- catch (ArgumentException)
- {
- Debug.LogMessage(LogEventLevel.Information, "This processor does not have a CS LAN", this);
- }
- catch (Exception ex)
- {
- Debug.LogMessage(ex, "Error automatically forwarding port to CS LAN");
- }
- }
-
-
- UiClients = new Dictionary();
-
- //_joinTokens = new Dictionary();
-
- if (Global.Platform == eDevicePlatform.Appliance)
- {
- AddConsoleCommands();
- }
-
- AddPreActivationAction(() => AddWebApiPaths());
- }
-
- private void AddWebApiPaths()
- {
- var apiServer = DeviceManager.AllDevices.OfType().FirstOrDefault();
-
- if (apiServer == null)
- {
- this.LogInformation("No API Server available");
- return;
- }
-
- var routes = new List
- {
- new HttpCwsRoute($"devices/{Key}/client")
- {
- Name = "ClientHandler",
- RouteHandler = new UiClientHandler(this)
- },
- };
-
- apiServer.AddRoute(routes);
- }
-
- private void AddConsoleCommands()
- {
- CrestronConsole.AddNewConsoleCommand(GenerateClientTokenFromConsole, "MobileAddUiClient", "Adds a client and generates a token. ? for more help", ConsoleAccessLevelEnum.AccessOperator);
- CrestronConsole.AddNewConsoleCommand(RemoveToken, "MobileRemoveUiClient", "Removes a client. ? for more help", ConsoleAccessLevelEnum.AccessOperator);
- CrestronConsole.AddNewConsoleCommand((s) => PrintClientInfo(), "MobileGetClientInfo", "Displays the current client info", ConsoleAccessLevelEnum.AccessOperator);
- CrestronConsole.AddNewConsoleCommand(RemoveAllTokens, "MobileRemoveAllClients", "Removes all clients", ConsoleAccessLevelEnum.AccessOperator);
- }
-
-
- public override void Initialize()
- {
- try
- {
- base.Initialize();
-
- _server = new HttpServer(Port, false);
-
- _server.OnGet += Server_OnGet;
-
- _server.OnOptions += Server_OnOptions;
-
- if (_parent.Config.DirectServer.Logging.EnableRemoteLogging)
- {
- _server.OnPost += Server_OnPost;
- }
-
- CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
-
- _server.Start();
-
- if (_server.IsListening)
- {
- Debug.LogMessage(LogEventLevel.Information, "Mobile Control WebSocket Server listening on port {port}", this, _server.Port);
- }
-
- CrestronEnvironment.ProgramStatusEventHandler += OnProgramStop;
-
- RetrieveSecret();
-
- CreateFolderStructure();
-
- AddClientsForTouchpanels();
- }
- catch (Exception ex)
- {
- Debug.LogMessage(ex, "Exception intializing websocket server", this);
- }
- }
-
- private void AddClientsForTouchpanels()
- {
- var touchpanels = DeviceManager.AllDevices
- .OfType().Where(tp => tp.UseDirectServer);
-
-
- var touchpanelsToAdd = new List();
-
- if (_secret != null)
- {
- var newTouchpanels = touchpanels.Where(tp => !_secret.Tokens.Any(t => t.Value.TouchpanelKey != null && t.Value.TouchpanelKey.Equals(tp.Key, StringComparison.InvariantCultureIgnoreCase)));
-
- touchpanelsToAdd.AddRange(newTouchpanels);
- }
- else
- {
- touchpanelsToAdd.AddRange(touchpanels);
- }
-
- foreach (var client in touchpanelsToAdd)
- {
- var bridge = _parent.GetRoomBridge(client.DefaultRoomKey);
-
- if (bridge == null)
- {
- this.LogWarning("Unable to find room with key: {defaultRoomKey}", client.DefaultRoomKey);
- return;
- }
-
- var (key, path) = GenerateClientToken(bridge, client.Key);
-
- if (key == null)
- {
- this.LogWarning("Unable to generate a client for {clientKey}", client.Key);
- continue;
- }
- }
-
- var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter);
-
- var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
-
- this.LogVerbose("Processor IP: {processorIp}", processorIp);
-
- foreach (var touchpanel in touchpanels.Select(tp =>
- {
- var token = _secret.Tokens.FirstOrDefault((t) => t.Value.TouchpanelKey.Equals(tp.Key, StringComparison.InvariantCultureIgnoreCase));
-
- var messenger = _parent.GetRoomBridge(tp.DefaultRoomKey);
-
- return new { token.Key, Touchpanel = tp, Messenger = messenger };
- }))
- {
- if (touchpanel.Key == null)
- {
- this.LogWarning("Token for touchpanel {touchpanelKey} not found", touchpanel.Touchpanel.Key);
- continue;
- }
-
- if (touchpanel.Messenger == null)
- {
- this.LogWarning("Unable to find room messenger for {defaultRoomKey}", touchpanel.Touchpanel.DefaultRoomKey);
- continue;
- }
-
- var appUrl = $"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
-
- this.LogVerbose("Sending URL {appUrl}", appUrl);
-
- touchpanel.Messenger.UpdateAppUrl($"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
- }
- }
-
- private void OnProgramStop(eProgramStatusEventType programEventType)
- {
- switch (programEventType)
- {
- case eProgramStatusEventType.Stopping:
- _server.Stop();
- break;
- }
- }
-
- private void CreateFolderStructure()
- {
- if (!Directory.Exists(userAppPath))
- {
- Directory.CreateDirectory(userAppPath);
- }
-
- if (!Directory.Exists($"{userAppPath}{localConfigFolderName}"))
- {
- Directory.CreateDirectory($"{userAppPath}{localConfigFolderName}");
- }
-
- using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigFileName}", FileMode.Create, FileAccess.ReadWrite)))
- {
- var config = GetApplicationConfig();
-
- var contents = JsonConvert.SerializeObject(config, Formatting.Indented);
-
- sw.Write(contents);
- }
- }
-
- private MobileControlApplicationConfig GetApplicationConfig()
- {
- MobileControlApplicationConfig config = null;
-
- var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter);
-
- var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
-
- try
- {
- if (_parent.Config.ApplicationConfig == null)
- {
- config = new MobileControlApplicationConfig
- {
- ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port),
- GatewayAppPath = "",
- LogoPath = "logo/logo.png",
- EnableDev = false,
- IconSet = MCIconSet.GOOGLE,
- LoginMode = "room-list",
- Modes = new Dictionary
- {
- {
- "room-list",
- new McMode{
- ListPageText= "Please select your room",
- LoginHelpText = "Please select your room from the list, then enter the code shown on the display.",
- PasscodePageText = "Please enter the code shown on this room's display"
- }
- }
- },
- Logging = _parent.Config.DirectServer.Logging.EnableRemoteLogging,
- };
- }
- else
- {
- config = new MobileControlApplicationConfig
- {
- ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port),
- GatewayAppPath = "",
- LogoPath = _parent.Config.ApplicationConfig.LogoPath ?? "logo/logo.png",
- EnableDev = _parent.Config.ApplicationConfig.EnableDev ?? false,
- IconSet = _parent.Config.ApplicationConfig.IconSet ?? MCIconSet.GOOGLE,
- LoginMode = _parent.Config.ApplicationConfig.LoginMode ?? "room-list",
- Modes = _parent.Config.ApplicationConfig.Modes ?? new Dictionary
- {
- {
- "room-list",
- new McMode {
- ListPageText = "Please select your room",
- LoginHelpText = "Please select your room from the list, then enter the code shown on the display.",
- PasscodePageText = "Please enter the code shown on this room's display"
- }
- }
- },
- Logging = _parent.Config.ApplicationConfig.Logging,
- PartnerMetadata = _parent.Config.ApplicationConfig.PartnerMetadata ?? new List()
- };
- }
- }
- catch (Exception ex)
- {
- Debug.LogMessage(ex, "Error getting application configuration", this);
-
- Debug.LogMessage(LogEventLevel.Verbose, "Config Object: {config} from {parentConfig}", this, config, _parent.Config);
- }
-
- return config;
- }
-
- ///
- /// Attempts to retrieve secrets previously stored in memory
- ///
- private void RetrieveSecret()
- {
- try
- {
- // Add secret provider
- _secretProvider = new WebSocketServerSecretProvider(SecretProviderKey);
-
- // Check for existing secrets
- var secret = _secretProvider.GetSecret(SecretProviderKey);
-
- if (secret != null)
- {
- Debug.LogMessage(LogEventLevel.Information, "Secret successfully retrieved", this);
-
- Debug.LogMessage(LogEventLevel.Debug, "Secret: {0}", this, secret.Value.ToString());
-
-
- // populate the local secrets object
- _secret = JsonConvert.DeserializeObject(secret.Value.ToString());
-
- if (_secret != null && _secret.Tokens != null)
- {
- // populate the _uiClient collection
- foreach (var token in _secret.Tokens)
- {
- if (token.Value == null)
- {
- Debug.LogMessage(LogEventLevel.Warning, "Token value is null", this);
- continue;
- }
-
- Debug.LogMessage(LogEventLevel.Information, "Adding token: {0} for room: {1}", this, token.Key, token.Value.RoomKey);
-
- if (UiClients == null)
- {
- Debug.LogMessage(LogEventLevel.Warning, "UiClients is null", this);
- UiClients = new Dictionary();
- }
-
- UiClients.Add(token.Key, new UiClientContext(token.Value));
- }
- }
-
- if (UiClients.Count > 0)
- {
- Debug.LogMessage(LogEventLevel.Information, "Restored {uiClientCount} UiClients from secrets data", this, UiClients.Count);
-
- foreach (var client in UiClients)
- {
- var key = client.Key;
- var path = _wsPath + key;
- var roomKey = client.Value.Token.RoomKey;
-
- _server.AddWebSocketService(path, () =>
- {
- var c = new UiClient();
- Debug.LogMessage(LogEventLevel.Debug, "Constructing UiClient with id: {key}", this, key);
-
- c.Controller = _parent;
- c.RoomKey = roomKey;
- UiClients[key].SetClient(c);
- return c;
- });
-
-
- //_server.WebSocketServices.AddService(path, (c) =>
- //{
- // Debug.Console(2, this, "Constructing UiClient with id: {0}", key);
- // c.Controller = _parent;
- // c.RoomKey = roomKey;
- // UiClients[key].SetClient(c);
- //});
- }
- }
- }
- else
- {
- Debug.LogMessage(LogEventLevel.Warning, "No secret found");
- }
-
- Debug.LogMessage(LogEventLevel.Debug, "{uiClientCount} UiClients restored from secrets data", this, UiClients.Count);
- }
- catch (Exception ex)
- {
- Debug.LogMessage(ex, "Exception retrieving secret", this);
- }
- }
-
- ///
- /// Stores secrets to memory to persist through reboot
- ///
- public void UpdateSecret()
- {
- try
- {
- if (_secret == null)
- {
- Debug.LogMessage(LogEventLevel.Error, "Secret is null", this);
-
- _secret = new ServerTokenSecrets(string.Empty);
- }
-
- _secret.Tokens.Clear();
-
- foreach (var uiClientContext in UiClients)
- {
- _secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token);
- }
-
- var serializedSecret = JsonConvert.SerializeObject(_secret);
-
- _secretProvider.SetSecret(SecretProviderKey, serializedSecret);
- }
- catch (Exception ex)
- {
- Debug.LogMessage(ex, "Exception updating secret", this);
- }
- }
-
- ///
- /// Generates a new token based on validating a room key and grant code passed in. If valid, returns a token and adds a service to the server for that token's path
- ///
- ///
- private void GenerateClientTokenFromConsole(string s)
- {
- if (s == "?" || string.IsNullOrEmpty(s))
- {
- CrestronConsole.ConsoleCommandResponse(@"[RoomKey] [GrantCode] Validates the room key against the grant code and returns a token for use in a UI client");
- return;
- }
-
- var values = s.Split(' ');
-
- if(values.Length < 2)
- {
- CrestronConsole.ConsoleCommandResponse("Invalid number of arguments. Please provide a room key and a grant code");
- return;
- }
-
-
- var roomKey = values[0];
- var grantCode = values[1];
-
- var bridge = _parent.GetRoomBridge(roomKey);
-
- if (bridge == null)
- {
- CrestronConsole.ConsoleCommandResponse(string.Format("Unable to find room with key: {0}", roomKey));
- return;
- }
-
- var (token, path) = ValidateGrantCode(grantCode, bridge);
-
- if (token == null)
- {
- CrestronConsole.ConsoleCommandResponse("Grant Code is not valid");
- return;
- }
-
- CrestronConsole.ConsoleCommandResponse($"Added new WebSocket UiClient service at path: {path}");
- CrestronConsole.ConsoleCommandResponse($"Token: {token}");
- }
-
- public (string, string) ValidateGrantCode(string grantCode, string roomKey)
- {
- var bridge = _parent.GetRoomBridge(roomKey);
-
- if (bridge == null)
- {
- this.LogWarning("Unable to find room with key: {roomKey}", roomKey);
- return (null, null);
- }
-
- return ValidateGrantCode(grantCode, bridge);
- }
-
- public (string, string) ValidateGrantCode(string grantCode, MobileControlBridgeBase bridge)
- {
- // TODO: Authenticate grant code passed in
- // For now, we just generate a random guid as the token and use it as the ClientId as well
- var grantCodeIsValid = true;
-
- if (grantCodeIsValid)
- {
- if (_secret == null)
- {
- _secret = new ServerTokenSecrets(grantCode);
- }
-
- return GenerateClientToken(bridge, "");
- }
- else
- {
- return (null, null);
- }
- }
-
- public (string, string) GenerateClientToken(MobileControlBridgeBase bridge, string touchPanelKey = "")
- {
- var key = Guid.NewGuid().ToString();
-
- var token = new JoinToken { Code = bridge.UserCode, RoomKey = bridge.RoomKey, Uuid = _parent.SystemUuid, TouchpanelKey = touchPanelKey };
-
- UiClients.Add(key, new UiClientContext(token));
-
- var path = _wsPath + key;
-
- _server.AddWebSocketService(path, () =>
- {
- var c = new UiClient();
- Debug.LogMessage(LogEventLevel.Verbose, "Constructing UiClient with id: {0}", this, key);
- c.Controller = _parent;
- c.RoomKey = bridge.RoomKey;
- UiClients[key].SetClient(c);
- return c;
- });
-
- Debug.LogMessage(LogEventLevel.Information, "Added new WebSocket UiClient service at path: {path}", this, path);
- Debug.LogMessage(LogEventLevel.Information, "Token: {@token}", this, token);
-
- Debug.LogMessage(LogEventLevel.Verbose, "{serviceCount} websocket services present", this, _server.WebSocketServices.Count);
-
- UpdateSecret();
-
- return (key, path);
- }
-
- ///
- /// Removes all clients from the server
- ///
- private void RemoveAllTokens(string s)
- {
- if (s == "?" || string.IsNullOrEmpty(s))
- {
- CrestronConsole.ConsoleCommandResponse(@"Removes all clients from the server. To execute add 'confirm' to command");
- return;
- }
-
- if (s != "confirm")
- {
- CrestronConsole.ConsoleCommandResponse(@"To remove all clients, add 'confirm' to the command");
- return;
- }
-
- foreach (var client in UiClients)
- {
- if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive)
- {
- client.Value.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down");
- }
-
- var path = _wsPath + client.Key;
- if (_server.RemoveWebSocketService(path))
- {
- CrestronConsole.ConsoleCommandResponse(string.Format("Client removed with token: {0}", client.Key));
- }
- else
- {
- CrestronConsole.ConsoleCommandResponse(string.Format("Unable to remove client with token : {0}", client.Key));
- }
- }
-
- UiClients.Clear();
-
- UpdateSecret();
- }
-
- ///
- /// Removes a client with the specified token value
- ///
- ///
- private void RemoveToken(string s)
- {
- if (s == "?" || string.IsNullOrEmpty(s))
- {
- CrestronConsole.ConsoleCommandResponse(@"[token] Removes the client with the specified token value");
- return;
- }
-
- var key = s;
-
- if (UiClients.ContainsKey(key))
- {
- var uiClientContext = UiClients[key];
-
- if (uiClientContext.Client != null && uiClientContext.Client.Context.WebSocket.IsAlive)
- {
- uiClientContext.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Token removed from server");
- }
-
- var path = _wsPath + key;
- if (_server.RemoveWebSocketService(path))
- {
- UiClients.Remove(key);
-
- UpdateSecret();
-
- CrestronConsole.ConsoleCommandResponse(string.Format("Client removed with token: {0}", key));
- }
- else
- {
- CrestronConsole.ConsoleCommandResponse(string.Format("Unable to remove client with token : {0}", key));
- }
- }
- else
- {
- CrestronConsole.ConsoleCommandResponse(string.Format("Unable to find client with token: {0}", key));
- }
- }
-
- ///
- /// Prints out info about current client IDs
- ///
- private void PrintClientInfo()
- {
- CrestronConsole.ConsoleCommandResponse("Mobile Control UI Client Info:\r");
-
- CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClients.Count));
-
- foreach (var client in UiClients)
- {
- CrestronConsole.ConsoleCommandResponse(string.Format("RoomKey: {0} Token: {1}\r", client.Value.Token.RoomKey, client.Key));
- }
- }
-
- private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
- {
- if (programEventType == eProgramStatusEventType.Stopping)
- {
- foreach (var client in UiClients.Values)
- {
- if (client.Client != null && client.Client.Context.WebSocket.IsAlive)
- {
- client.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down");
- }
- }
-
- StopServer();
- }
- }
-
- ///
- /// Handler for GET requests to server
- ///
- ///
- ///
- private void Server_OnGet(object sender, HttpRequestEventArgs e)
- {
- try
- {
- var req = e.Request;
- var res = e.Response;
- res.ContentEncoding = Encoding.UTF8;
-
- res.AddHeader("Access-Control-Allow-Origin", "*");
-
- var path = req.RawUrl;
-
- this.LogVerbose("GET Request received at path: {path}", path);
-
- // Call for user app to join the room with a token
- if (path.StartsWith("/mc/api/ui/joinroom"))
- {
- HandleJoinRequest(req, res);
- }
- // Call to get the server version
- else if (path.StartsWith("/mc/api/version"))
- {
- HandleVersionRequest(res);
- }
- else if (path.StartsWith("/mc/app/logo"))
- {
- HandleImageRequest(req, res);
- }
- // Call to serve the user app
- else if (path.StartsWith(_userAppBaseHref))
- {
- HandleUserAppRequest(req, res, path);
- }
- else
- {
- // All other paths
- res.StatusCode = 404;
- res.Close();
- }
- }
- catch (Exception ex)
- {
- Debug.LogMessage(ex, "Caught an exception in the OnGet handler", this);
- }
- }
-
- private async void Server_OnPost(object sender, HttpRequestEventArgs e)
- {
- try
- {
- var req = e.Request;
- var res = e.Response;
-
- res.AddHeader("Access-Control-Allow-Origin", "*");
-
- var path = req.RawUrl;
- var ip = req.RemoteEndPoint.Address.ToString();
-
- this.LogVerbose("POST Request received at path: {path} from host {host}", path, ip);
-
- var body = new StreamReader(req.InputStream).ReadToEnd();
-
- if (path.StartsWith("/mc/api/log"))
- {
- res.StatusCode = 200;
- res.Close();
-
- var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
- {
- Content = new StringContent(body, Encoding.UTF8, "application/json"),
- };
-
- logRequest.Headers.Add("x-pepperdash-host", ip);
-
- await LogClient.SendAsync(logRequest);
-
- this.LogVerbose("Log data sent to {host}:{port}", _parent.Config.DirectServer.Logging.Host, _parent.Config.DirectServer.Logging.Port);
- }
- else
- {
- res.StatusCode = 404;
- res.Close();
- }
- }
- catch (Exception ex)
- {
- this.LogException(ex, "Caught an exception in the OnPost handler");
- }
- }
-
- private void Server_OnOptions(object sender, HttpRequestEventArgs e)
- {
- try
- {
- var res = e.Response;
-
- res.AddHeader("Access-Control-Allow-Origin", "*");
- res.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
- res.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");
-
- res.StatusCode = 200;
- res.Close();
- }
- catch (Exception ex)
- {
- Debug.LogMessage(ex, "Caught an exception in the OnPost handler", this);
- }
- }
-
- ///
- /// Handle the request to join the room with a token
- ///
- ///
- ///
- private void HandleJoinRequest(HttpListenerRequest req, HttpListenerResponse res)
- {
- var qp = req.QueryString;
- var token = qp["token"];
-
- this.LogVerbose("Join Room Request with token: {token}", token);
-
-
- if (UiClients.TryGetValue(token, out UiClientContext clientContext))
- {
- var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey);
-
- if (bridge != null)
- {
- res.StatusCode = 200;
- res.ContentType = "application/json";
-
- // Construct the response object
- JoinResponse jRes = new JoinResponse
- {
- ClientId = token,
- RoomKey = bridge.RoomKey,
- SystemUuid = _parent.SystemUuid,
- RoomUuid = _parent.SystemUuid,
- Config = _parent.GetConfigWithPluginVersion(),
- CodeExpires = new DateTime().AddYears(1),
- UserCode = bridge.UserCode,
- UserAppUrl = string.Format("http://{0}:{1}/mc/app",
- CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
- Port),
- EnableDebug = false
- };
-
- // Serialize to JSON and convert to Byte[]
- var json = JsonConvert.SerializeObject(jRes);
- var body = Encoding.UTF8.GetBytes(json);
- res.ContentLength64 = body.LongLength;
-
- // Send the response
- res.Close(body, true);
- }
- else
- {
- var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey);
- res.StatusCode = 404;
- res.ContentType = "application/json";
- this.LogVerbose("{message}", message);
- var body = Encoding.UTF8.GetBytes(message);
- res.ContentLength64 = body.LongLength;
- res.Close(body, true);
-
- }
- }
- else
- {
- var message = "Token invalid or has expired";
- res.StatusCode = 401;
- res.ContentType = "application/json";
- this.LogVerbose("{message}", message);
- var body = Encoding.UTF8.GetBytes(message);
- res.ContentLength64 = body.LongLength;
- res.Close(body, true);
- }
- }
-
- ///
- /// Handles a server version request
- ///
- ///
- private void HandleVersionRequest(HttpListenerResponse res)
- {
- res.StatusCode = 200;
- res.ContentType = "application/json";
- var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
- var message = JsonConvert.SerializeObject(version);
- this.LogVerbose("{message}", message);
-
- var body = Encoding.UTF8.GetBytes(message);
- res.ContentLength64 = body.LongLength;
- res.Close(body, true);
- }
-
- ///
- /// Handler to return images requested by the user app
- ///
- ///
- ///
- private void HandleImageRequest(HttpListenerRequest req, HttpListenerResponse res)
- {
- var path = req.RawUrl;
-
- Debug.LogMessage(LogEventLevel.Verbose, "Requesting Image: {0}", this, path);
-
- var imageBasePath = Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo" + Global.DirectorySeparator;
-
- var image = path.Split('/').Last();
-
- var filePath = imageBasePath + image;
-
- Debug.LogMessage(LogEventLevel.Verbose, "Retrieving Image: {0}", this, filePath);
-
- if (File.Exists(filePath))
- {
- if (filePath.EndsWith(".png"))
- {
- res.ContentType = "image/png";
- }
- else if (filePath.EndsWith(".jpg"))
- {
- res.ContentType = "image/jpeg";
- }
- else if (filePath.EndsWith(".gif"))
- {
- res.ContentType = "image/gif";
- }
- else if (filePath.EndsWith(".svg"))
- {
- res.ContentType = "image/svg+xml";
- }
- byte[] contents = File.ReadAllBytes(filePath);
- res.ContentLength64 = contents.LongLength;
- res.Close(contents, true);
- }
- else
- {
- res.StatusCode = (int)HttpStatusCode.NotFound;
- res.Close();
- }
- }
-
- ///
- /// Handles requests to serve files for the Angular single page app
- ///
- ///
- ///
- ///
- private void HandleUserAppRequest(HttpListenerRequest req, HttpListenerResponse res, string path)
- {
- this.LogVerbose("Requesting User app file");
-
- string filePath = path.Split('?')[0];
-
- // remove the token from the path if found
- //string filePath = path.Replace(string.Format("?token={0}", token), "");
-
- // if there's no file suffix strip any extra path data after the base href
- if (filePath != _userAppBaseHref && !filePath.Contains(".") && (!filePath.EndsWith(_userAppBaseHref) || !filePath.EndsWith(_userAppBaseHref += "/")))
- {
- var suffix = filePath.Substring(_userAppBaseHref.Length, filePath.Length - _userAppBaseHref.Length);
- if (suffix != "/")
- {
- //Debug.Console(2, this, "Suffix: {0}", suffix);
- filePath = filePath.Replace(suffix, "");
- }
- }
-
- // swap the base href prefix for the file path prefix
- filePath = filePath.Replace(_userAppBaseHref, _appPath);
-
- this.LogVerbose("filepath: {filePath}", filePath);
-
-
- // append index.html if no specific file is specified
- if (!filePath.Contains("."))
- {
- if (filePath.EndsWith("/"))
- {
- filePath += "index.html";
- }
- else
- {
- filePath += "/index.html";
- }
- }
-
- // Set ContentType based on file type
- if (filePath.EndsWith(".html"))
- {
- this.LogVerbose("Client requesting User App");
-
- res.ContentType = "text/html";
- }
- else
- {
- if (path.EndsWith(".js"))
- {
- res.ContentType = "application/javascript";
- }
- else if (path.EndsWith(".css"))
- {
- res.ContentType = "text/css";
- }
- else if (path.EndsWith(".json"))
- {
- res.ContentType = "application/json";
- }
- }
-
- this.LogVerbose("Attempting to serve file: {filePath}", filePath);
-
- byte[] contents;
- if (File.Exists(filePath))
- {
- this.LogVerbose("File found: {filePath}", filePath);
- contents = File.ReadAllBytes(filePath);
- }
- else
- {
- this.LogVerbose("File not found: {filePath}", filePath);
- res.StatusCode = (int)HttpStatusCode.NotFound;
- res.Close();
- return;
- }
-
- res.ContentLength64 = contents.LongLength;
- res.Close(contents, true);
- }
-
- public void StopServer()
- {
- this.LogVerbose("Stopping WebSocket Server");
- _server.Stop(CloseStatusCode.Normal, "Server Shutting Down");
- }
-
- ///
- /// Sends a message to all connectd clients
- ///
- ///
- public void SendMessageToAllClients(string message)
- {
- foreach (var clientContext in UiClients.Values)
- {
- if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive)
- {
- clientContext.Client.Context.WebSocket.Send(message);
- }
- }
- }
-
- ///
- /// Sends a message to a specific client
- ///
- ///
- ///
- public void SendMessageToClient(object clientId, string message)
- {
- if (clientId == null)
- {
- return;
- }
-
- if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext))
- {
- if (clientContext.Client != null)
- {
- var socket = clientContext.Client.Context.WebSocket;
-
- if (socket.IsAlive)
- {
- socket.Send(message);
- }
- }
- }
- else
- {
- this.LogWarning("Unable to find client with ID: {clientId}", clientId);
- }
- }
- }
-
- ///
- /// Class to describe the server version info
- ///
- public class Version
- {
- [JsonProperty("serverVersion")]
- public string ServerVersion { get; set; }
-
- [JsonProperty("serverIsRunningOnProcessorHardware")]
- public bool ServerIsRunningOnProcessorHardware { get; private set; }
-
- public Version()
- {
- ServerIsRunningOnProcessorHardware = true;
- }
- }
-
- ///
- /// Represents an instance of a UiClient and the associated Token
- ///
- public class UiClientContext
- {
- public UiClient Client { get; private set; }
- public JoinToken Token { get; private set; }
-
- public UiClientContext(JoinToken token)
- {
- Token = token;
- }
-
- public void SetClient(UiClient client)
- {
- Client = client;
- }
-
- }
-
- ///
- /// Represents the data structure for the grant code and UiClient tokens to be stored in the secrets manager
- ///
- public class ServerTokenSecrets
- {
- public string GrantCode { get; set; }
-
- public Dictionary Tokens { get; set; }
-
- public ServerTokenSecrets(string grantCode)
- {
- GrantCode = grantCode;
- Tokens = new Dictionary();
- }
- }
-
- ///
- /// Represents a join token with the associated properties
- ///
- public class JoinToken
- {
- public string Code { get; set; }
-
- public string RoomKey { get; set; }
-
- public string Uuid { get; set; }
-
- public string TouchpanelKey { get; set; } = "";
-
- public string Token { get; set; } = null;
- }
-
- ///
- /// Represents the structure of the join response
- ///
- public class JoinResponse
- {
- [JsonProperty("clientId")]
- public string ClientId { get; set; }
-
- [JsonProperty("roomKey")]
- public string RoomKey { get; set; }
-
- [JsonProperty("systemUUid")]
- public string SystemUuid { get; set; }
-
- [JsonProperty("roomUUid")]
- public string RoomUuid { get; set; }
-
- [JsonProperty("config")]
- public object Config { get; set; }
-
- [JsonProperty("codeExpires")]
- public DateTime CodeExpires { get; set; }
-
- [JsonProperty("userCode")]
- public string UserCode { get; set; }
-
- [JsonProperty("userAppUrl")]
- public string UserAppUrl { get; set; }
-
- [JsonProperty("enableDebug")]
- public bool EnableDebug { get; set; }
- }
-}
+using Crestron.SimplSharp;
+using Crestron.SimplSharp.WebScripting;
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Core.Logging;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using PepperDash.Essentials.Core.Web;
+using PepperDash.Essentials.RoomBridges;
+using PepperDash.Essentials.WebApiHandlers;
+using Serilog.Events;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using WebSocketSharp;
+using WebSocketSharp.Net;
+using WebSocketSharp.Server;
+
+
+namespace PepperDash.Essentials.WebSocketServer
+{
+ public class MobileControlWebsocketServer : EssentialsDevice
+ {
+ private readonly string userAppPath = Global.FilePathPrefix + "mcUserApp" + Global.DirectorySeparator;
+
+ private readonly string localConfigFolderName = "_local-config";
+
+ private readonly string appConfigFileName = "_config.local.json";
+ private readonly string appConfigCsFileName = "_config.cs.json";
+
+ ///
+ /// Where the key is the join token and the value is the room key
+ ///
+ //private Dictionary _joinTokens;
+
+ private HttpServer _server;
+
+ public HttpServer Server => _server;
+
+ public Dictionary UiClients { get; private set; }
+
+ private readonly MobileControlSystemController _parent;
+
+ private WebSocketServerSecretProvider _secretProvider;
+
+ private ServerTokenSecrets _secret;
+
+ private static readonly HttpClient LogClient = new HttpClient();
+
+ private string SecretProviderKey
+ {
+ get
+ {
+ return string.Format("{0}:{1}-tokens", Global.ControlSystem.ProgramNumber, Key);
+ }
+ }
+
+ private string lanIpAddress => CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter));
+
+ private System.Net.IPAddress csIpAddress;
+
+ private System.Net.IPAddress csSubnetMask;
+
+ ///
+ /// The path for the WebSocket messaging
+ ///
+ private readonly string _wsPath = "/mc/api/ui/join/";
+
+ public string WsPath => _wsPath;
+
+ ///
+ /// The path to the location of the files for the user app (single page Angular app)
+ ///
+ private readonly string _appPath = string.Format("{0}mcUserApp", Global.FilePathPrefix);
+
+ ///
+ /// The base HREF that the user app uses
+ ///
+ private string _userAppBaseHref = "/mc/app";
+
+ ///
+ /// The port the server will run on
+ ///
+ public int Port { get; private set; }
+
+ public string UserAppUrlPrefix
+ {
+ get
+ {
+ return string.Format("http://{0}:{1}{2}?token=",
+ CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
+ Port,
+ _userAppBaseHref);
+
+ }
+ }
+
+ public int ConnectedUiClientsCount
+ {
+ get
+ {
+ var count = 0;
+
+ foreach (var client in UiClients)
+ {
+ if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+ }
+
+ public MobileControlWebsocketServer(string key, int customPort, MobileControlSystemController parent)
+ : base(key)
+ {
+ _parent = parent;
+
+ // Set the default port to be 50000 plus the slot number of the program
+ Port = 50000 + (int)Global.ControlSystem.ProgramNumber;
+
+ if (customPort != 0)
+ {
+ Port = customPort;
+ }
+
+ if (parent.Config.DirectServer.AutomaticallyForwardPortToCSLAN == true)
+ {
+ try
+ {
+ Debug.LogMessage(LogEventLevel.Information, "Automatically forwarding port {0} to CS LAN", Port);
+
+ var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
+ var csIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
+
+ var result = CrestronEthernetHelper.AddPortForwarding((ushort)Port, (ushort)Port, csIp, CrestronEthernetHelper.ePortMapTransport.TCP);
+
+ if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
+ {
+ Debug.LogMessage(LogEventLevel.Error, "Error adding port forwarding: {0}", result);
+ }
+ }
+ catch (ArgumentException)
+ {
+ Debug.LogMessage(LogEventLevel.Information, "This processor does not have a CS LAN", this);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Error automatically forwarding port to CS LAN");
+ }
+ }
+
+ try
+ {
+ var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
+ var csSubnetMask = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, csAdapterId);
+ var csIpAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
+
+ this.csSubnetMask = System.Net.IPAddress.Parse(csSubnetMask);
+ this.csIpAddress = System.Net.IPAddress.Parse(csIpAddress);
+ }
+ catch (ArgumentException)
+ {
+ if (parent.Config.DirectServer.AutomaticallyForwardPortToCSLAN == false)
+ {
+ Debug.LogMessage(LogEventLevel.Information, "This processor does not have a CS LAN", this);
+ }
+ }
+
+
+ UiClients = new Dictionary();
+
+ //_joinTokens = new Dictionary();
+
+ if (Global.Platform == eDevicePlatform.Appliance)
+ {
+ AddConsoleCommands();
+ }
+
+ AddPreActivationAction(() => AddWebApiPaths());
+ }
+
+ private void AddWebApiPaths()
+ {
+ var apiServer = DeviceManager.AllDevices.OfType().FirstOrDefault();
+
+ if (apiServer == null)
+ {
+ this.LogInformation("No API Server available");
+ return;
+ }
+
+ var routes = new List
+ {
+ new HttpCwsRoute($"devices/{Key}/client")
+ {
+ Name = "ClientHandler",
+ RouteHandler = new UiClientHandler(this)
+ },
+ };
+
+ apiServer.AddRoute(routes);
+ }
+
+ private void AddConsoleCommands()
+ {
+ CrestronConsole.AddNewConsoleCommand(GenerateClientTokenFromConsole, "MobileAddUiClient", "Adds a client and generates a token. ? for more help", ConsoleAccessLevelEnum.AccessOperator);
+ CrestronConsole.AddNewConsoleCommand(RemoveToken, "MobileRemoveUiClient", "Removes a client. ? for more help", ConsoleAccessLevelEnum.AccessOperator);
+ CrestronConsole.AddNewConsoleCommand((s) => PrintClientInfo(), "MobileGetClientInfo", "Displays the current client info", ConsoleAccessLevelEnum.AccessOperator);
+ CrestronConsole.AddNewConsoleCommand(RemoveAllTokens, "MobileRemoveAllClients", "Removes all clients", ConsoleAccessLevelEnum.AccessOperator);
+ }
+
+
+ public override void Initialize()
+ {
+ try
+ {
+ base.Initialize();
+
+ _server = new HttpServer(Port, false);
+
+ _server.OnGet += Server_OnGet;
+
+ _server.OnOptions += Server_OnOptions;
+
+ if (_parent.Config.DirectServer.Logging.EnableRemoteLogging)
+ {
+ _server.OnPost += Server_OnPost;
+ }
+
+ CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
+
+ _server.Start();
+
+ if (_server.IsListening)
+ {
+ Debug.LogMessage(LogEventLevel.Information, "Mobile Control WebSocket Server listening on port {port}", this, _server.Port);
+ }
+
+ CrestronEnvironment.ProgramStatusEventHandler += OnProgramStop;
+
+ RetrieveSecret();
+
+ CreateFolderStructure();
+
+ AddClientsForTouchpanels();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Exception intializing websocket server", this);
+ }
+ }
+
+ private void AddClientsForTouchpanels()
+ {
+ var touchpanels = DeviceManager.AllDevices
+ .OfType().Where(tp => tp.UseDirectServer);
+
+
+ var touchpanelsToAdd = new List();
+
+ if (_secret != null)
+ {
+ var newTouchpanels = touchpanels.Where(tp => !_secret.Tokens.Any(t => t.Value.TouchpanelKey != null && t.Value.TouchpanelKey.Equals(tp.Key, StringComparison.InvariantCultureIgnoreCase)));
+
+ touchpanelsToAdd.AddRange(newTouchpanels);
+ }
+ else
+ {
+ touchpanelsToAdd.AddRange(touchpanels);
+ }
+
+ foreach (var client in touchpanelsToAdd)
+ {
+ var bridge = _parent.GetRoomBridge(client.DefaultRoomKey);
+
+ if (bridge == null)
+ {
+ this.LogWarning("Unable to find room with key: {defaultRoomKey}", client.DefaultRoomKey);
+ return;
+ }
+
+ var (key, path) = GenerateClientToken(bridge, client.Key);
+
+ if (key == null)
+ {
+ this.LogWarning("Unable to generate a client for {clientKey}", client.Key);
+ continue;
+ }
+ }
+
+ var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter);
+
+ var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
+
+ this.LogVerbose("Processor IP: {processorIp}", processorIp);
+
+ foreach (var touchpanel in touchpanels.Select(tp =>
+ {
+ var token = _secret.Tokens.FirstOrDefault((t) => t.Value.TouchpanelKey.Equals(tp.Key, StringComparison.InvariantCultureIgnoreCase));
+
+ var messenger = _parent.GetRoomBridge(tp.DefaultRoomKey);
+
+ return new { token.Key, Touchpanel = tp, Messenger = messenger };
+ }))
+ {
+ if (touchpanel.Key == null)
+ {
+ this.LogWarning("Token for touchpanel {touchpanelKey} not found", touchpanel.Touchpanel.Key);
+ continue;
+ }
+
+ if (touchpanel.Messenger == null)
+ {
+ this.LogWarning("Unable to find room messenger for {defaultRoomKey}", touchpanel.Touchpanel.DefaultRoomKey);
+ continue;
+ }
+
+ var appUrl = $"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
+
+ this.LogVerbose("Sending URL {appUrl}", appUrl);
+
+ touchpanel.Messenger.UpdateAppUrl($"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
+ }
+ }
+
+ private void OnProgramStop(eProgramStatusEventType programEventType)
+ {
+ switch (programEventType)
+ {
+ case eProgramStatusEventType.Stopping:
+ _server.Stop();
+ break;
+ }
+ }
+
+ private void CreateFolderStructure()
+ {
+ if (!Directory.Exists(userAppPath))
+ {
+ Directory.CreateDirectory(userAppPath);
+ }
+
+ if (!Directory.Exists($"{userAppPath}{localConfigFolderName}"))
+ {
+ Directory.CreateDirectory($"{userAppPath}{localConfigFolderName}");
+ }
+
+ using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigFileName}", FileMode.Create, FileAccess.ReadWrite)))
+ {
+ // Write the LAN application configuration file. Used when a request comes in for the application config from the LAN
+ var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter);
+
+ this.LogDebug("LAN Adapter ID: {lanAdapterId}", lanAdapterId);
+
+ var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
+
+ var config = GetApplicationConfig(processorIp);
+
+ var contents = JsonConvert.SerializeObject(config, Formatting.Indented);
+
+ sw.Write(contents);
+ }
+
+ short csAdapterId;
+ try
+ {
+ csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
+ }
+ catch (ArgumentException)
+ {
+ this.LogDebug("This processor does not have a CS LAN");
+ return;
+ }
+
+ if(csAdapterId == -1)
+ {
+ this.LogDebug("CS LAN Adapter not found");
+ return;
+ }
+
+ this.LogDebug("CS LAN Adapter ID: {csAdapterId}. Adding CS Config", csAdapterId);
+
+ using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigCsFileName}", FileMode.Create, FileAccess.ReadWrite)))
+ {
+ // Write the CS application configuration file. Used when a request comes in for the application config from the CS
+ var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
+
+ var config = GetApplicationConfig(processorIp);
+
+ var contents = JsonConvert.SerializeObject(config, Formatting.Indented);
+
+ sw.Write(contents);
+ }
+ }
+
+ private MobileControlApplicationConfig GetApplicationConfig(string processorIp)
+ {
+ try
+ {
+ var config = new MobileControlApplicationConfig
+ {
+ ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port),
+ GatewayAppPath = "",
+ LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
+ EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
+ IconSet = _parent.Config.ApplicationConfig?.IconSet ?? MCIconSet.GOOGLE,
+ LoginMode = _parent.Config.ApplicationConfig?.LoginMode ?? "room-list",
+ Modes = _parent.Config.ApplicationConfig?.Modes ?? new Dictionary
+ {
+ {
+ "room-list",
+ new McMode {
+ ListPageText = "Please select your room",
+ LoginHelpText = "Please select your room from the list, then enter the code shown on the display.",
+ PasscodePageText = "Please enter the code shown on this room's display"
+ }
+ }
+ },
+ Logging = _parent.Config.ApplicationConfig?.Logging ?? false,
+ PartnerMetadata = _parent.Config.ApplicationConfig?.PartnerMetadata ?? new List()
+ };
+
+ return config;
+ }
+ catch (Exception ex)
+ {
+ this.LogError(ex, "Error getting application configuration");
+
+ return null;
+ }
+ }
+
+ ///
+ /// Attempts to retrieve secrets previously stored in memory
+ ///
+ private void RetrieveSecret()
+ {
+ try
+ {
+ // Add secret provider
+ _secretProvider = new WebSocketServerSecretProvider(SecretProviderKey);
+
+ // Check for existing secrets
+ var secret = _secretProvider.GetSecret(SecretProviderKey);
+
+ if (secret != null)
+ {
+ Debug.LogMessage(LogEventLevel.Information, "Secret successfully retrieved", this);
+
+ Debug.LogMessage(LogEventLevel.Debug, "Secret: {0}", this, secret.Value.ToString());
+
+
+ // populate the local secrets object
+ _secret = JsonConvert.DeserializeObject(secret.Value.ToString());
+
+ if (_secret != null && _secret.Tokens != null)
+ {
+ // populate the _uiClient collection
+ foreach (var token in _secret.Tokens)
+ {
+ if (token.Value == null)
+ {
+ Debug.LogMessage(LogEventLevel.Warning, "Token value is null", this);
+ continue;
+ }
+
+ Debug.LogMessage(LogEventLevel.Information, "Adding token: {0} for room: {1}", this, token.Key, token.Value.RoomKey);
+
+ if (UiClients == null)
+ {
+ Debug.LogMessage(LogEventLevel.Warning, "UiClients is null", this);
+ UiClients = new Dictionary();
+ }
+
+ UiClients.Add(token.Key, new UiClientContext(token.Value));
+ }
+ }
+
+ if (UiClients.Count > 0)
+ {
+ Debug.LogMessage(LogEventLevel.Information, "Restored {uiClientCount} UiClients from secrets data", this, UiClients.Count);
+
+ foreach (var client in UiClients)
+ {
+ var key = client.Key;
+ var path = _wsPath + key;
+ var roomKey = client.Value.Token.RoomKey;
+
+ _server.AddWebSocketService(path, () =>
+ {
+ var c = new UiClient();
+ Debug.LogMessage(LogEventLevel.Debug, "Constructing UiClient with id: {key}", this, key);
+
+ c.Controller = _parent;
+ c.RoomKey = roomKey;
+ UiClients[key].SetClient(c);
+ return c;
+ });
+
+
+ //_server.WebSocketServices.AddService(path, (c) =>
+ //{
+ // Debug.Console(2, this, "Constructing UiClient with id: {0}", key);
+ // c.Controller = _parent;
+ // c.RoomKey = roomKey;
+ // UiClients[key].SetClient(c);
+ //});
+ }
+ }
+ }
+ else
+ {
+ Debug.LogMessage(LogEventLevel.Warning, "No secret found");
+ }
+
+ Debug.LogMessage(LogEventLevel.Debug, "{uiClientCount} UiClients restored from secrets data", this, UiClients.Count);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Exception retrieving secret", this);
+ }
+ }
+
+ ///
+ /// Stores secrets to memory to persist through reboot
+ ///
+ public void UpdateSecret()
+ {
+ try
+ {
+ if (_secret == null)
+ {
+ Debug.LogMessage(LogEventLevel.Error, "Secret is null", this);
+
+ _secret = new ServerTokenSecrets(string.Empty);
+ }
+
+ _secret.Tokens.Clear();
+
+ foreach (var uiClientContext in UiClients)
+ {
+ _secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token);
+ }
+
+ var serializedSecret = JsonConvert.SerializeObject(_secret);
+
+ _secretProvider.SetSecret(SecretProviderKey, serializedSecret);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Exception updating secret", this);
+ }
+ }
+
+ ///
+ /// Generates a new token based on validating a room key and grant code passed in. If valid, returns a token and adds a service to the server for that token's path
+ ///
+ ///
+ private void GenerateClientTokenFromConsole(string s)
+ {
+ if (s == "?" || string.IsNullOrEmpty(s))
+ {
+ CrestronConsole.ConsoleCommandResponse(@"[RoomKey] [GrantCode] Validates the room key against the grant code and returns a token for use in a UI client");
+ return;
+ }
+
+ var values = s.Split(' ');
+
+ if(values.Length < 2)
+ {
+ CrestronConsole.ConsoleCommandResponse("Invalid number of arguments. Please provide a room key and a grant code");
+ return;
+ }
+
+
+ var roomKey = values[0];
+ var grantCode = values[1];
+
+ var bridge = _parent.GetRoomBridge(roomKey);
+
+ if (bridge == null)
+ {
+ CrestronConsole.ConsoleCommandResponse(string.Format("Unable to find room with key: {0}", roomKey));
+ return;
+ }
+
+ var (token, path) = ValidateGrantCode(grantCode, bridge);
+
+ if (token == null)
+ {
+ CrestronConsole.ConsoleCommandResponse("Grant Code is not valid");
+ return;
+ }
+
+ CrestronConsole.ConsoleCommandResponse($"Added new WebSocket UiClient service at path: {path}");
+ CrestronConsole.ConsoleCommandResponse($"Token: {token}");
+ }
+
+ public (string, string) ValidateGrantCode(string grantCode, string roomKey)
+ {
+ var bridge = _parent.GetRoomBridge(roomKey);
+
+ if (bridge == null)
+ {
+ this.LogWarning("Unable to find room with key: {roomKey}", roomKey);
+ return (null, null);
+ }
+
+ return ValidateGrantCode(grantCode, bridge);
+ }
+
+ public (string, string) ValidateGrantCode(string grantCode, MobileControlBridgeBase bridge)
+ {
+ // TODO: Authenticate grant code passed in
+ // For now, we just generate a random guid as the token and use it as the ClientId as well
+ var grantCodeIsValid = true;
+
+ if (grantCodeIsValid)
+ {
+ if (_secret == null)
+ {
+ _secret = new ServerTokenSecrets(grantCode);
+ }
+
+ return GenerateClientToken(bridge, "");
+ }
+ else
+ {
+ return (null, null);
+ }
+ }
+
+ public (string, string) GenerateClientToken(MobileControlBridgeBase bridge, string touchPanelKey = "")
+ {
+ var key = Guid.NewGuid().ToString();
+
+ var token = new JoinToken { Code = bridge.UserCode, RoomKey = bridge.RoomKey, Uuid = _parent.SystemUuid, TouchpanelKey = touchPanelKey };
+
+ UiClients.Add(key, new UiClientContext(token));
+
+ var path = _wsPath + key;
+
+ _server.AddWebSocketService(path, () =>
+ {
+ var c = new UiClient();
+ Debug.LogMessage(LogEventLevel.Verbose, "Constructing UiClient with id: {0}", this, key);
+ c.Controller = _parent;
+ c.RoomKey = bridge.RoomKey;
+ UiClients[key].SetClient(c);
+ return c;
+ });
+
+ Debug.LogMessage(LogEventLevel.Information, "Added new WebSocket UiClient service at path: {path}", this, path);
+ Debug.LogMessage(LogEventLevel.Information, "Token: {@token}", this, token);
+
+ Debug.LogMessage(LogEventLevel.Verbose, "{serviceCount} websocket services present", this, _server.WebSocketServices.Count);
+
+ UpdateSecret();
+
+ return (key, path);
+ }
+
+ ///
+ /// Removes all clients from the server
+ ///
+ private void RemoveAllTokens(string s)
+ {
+ if (s == "?" || string.IsNullOrEmpty(s))
+ {
+ CrestronConsole.ConsoleCommandResponse(@"Removes all clients from the server. To execute add 'confirm' to command");
+ return;
+ }
+
+ if (s != "confirm")
+ {
+ CrestronConsole.ConsoleCommandResponse(@"To remove all clients, add 'confirm' to the command");
+ return;
+ }
+
+ foreach (var client in UiClients)
+ {
+ if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive)
+ {
+ client.Value.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down");
+ }
+
+ var path = _wsPath + client.Key;
+ if (_server.RemoveWebSocketService(path))
+ {
+ CrestronConsole.ConsoleCommandResponse(string.Format("Client removed with token: {0}", client.Key));
+ }
+ else
+ {
+ CrestronConsole.ConsoleCommandResponse(string.Format("Unable to remove client with token : {0}", client.Key));
+ }
+ }
+
+ UiClients.Clear();
+
+ UpdateSecret();
+ }
+
+ ///
+ /// Removes a client with the specified token value
+ ///
+ ///
+ private void RemoveToken(string s)
+ {
+ if (s == "?" || string.IsNullOrEmpty(s))
+ {
+ CrestronConsole.ConsoleCommandResponse(@"[token] Removes the client with the specified token value");
+ return;
+ }
+
+ var key = s;
+
+ if (UiClients.ContainsKey(key))
+ {
+ var uiClientContext = UiClients[key];
+
+ if (uiClientContext.Client != null && uiClientContext.Client.Context.WebSocket.IsAlive)
+ {
+ uiClientContext.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Token removed from server");
+ }
+
+ var path = _wsPath + key;
+ if (_server.RemoveWebSocketService(path))
+ {
+ UiClients.Remove(key);
+
+ UpdateSecret();
+
+ CrestronConsole.ConsoleCommandResponse(string.Format("Client removed with token: {0}", key));
+ }
+ else
+ {
+ CrestronConsole.ConsoleCommandResponse(string.Format("Unable to remove client with token : {0}", key));
+ }
+ }
+ else
+ {
+ CrestronConsole.ConsoleCommandResponse(string.Format("Unable to find client with token: {0}", key));
+ }
+ }
+
+ ///
+ /// Prints out info about current client IDs
+ ///
+ private void PrintClientInfo()
+ {
+ CrestronConsole.ConsoleCommandResponse("Mobile Control UI Client Info:\r");
+
+ CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClients.Count));
+
+ foreach (var client in UiClients)
+ {
+ CrestronConsole.ConsoleCommandResponse(string.Format("RoomKey: {0} Token: {1}\r", client.Value.Token.RoomKey, client.Key));
+ }
+ }
+
+ private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+ if (programEventType == eProgramStatusEventType.Stopping)
+ {
+ foreach (var client in UiClients.Values)
+ {
+ if (client.Client != null && client.Client.Context.WebSocket.IsAlive)
+ {
+ client.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down");
+ }
+ }
+
+ StopServer();
+ }
+ }
+
+ ///
+ /// Handler for GET requests to server
+ ///
+ ///
+ ///
+ private void Server_OnGet(object sender, HttpRequestEventArgs e)
+ {
+ try
+ {
+ var req = e.Request;
+ var res = e.Response;
+ res.ContentEncoding = Encoding.UTF8;
+
+ res.AddHeader("Access-Control-Allow-Origin", "*");
+
+ var path = req.RawUrl;
+
+ this.LogVerbose("GET Request received at path: {path}", path);
+
+ // Call for user app to join the room with a token
+ if (path.StartsWith("/mc/api/ui/joinroom"))
+ {
+ HandleJoinRequest(req, res);
+ }
+ // Call to get the server version
+ else if (path.StartsWith("/mc/api/version"))
+ {
+ HandleVersionRequest(res);
+ }
+ else if (path.StartsWith("/mc/app/logo"))
+ {
+ HandleImageRequest(req, res);
+ }
+ // Call to serve the user app
+ else if (path.StartsWith(_userAppBaseHref))
+ {
+ HandleUserAppRequest(req, res, path);
+ }
+ else
+ {
+ // All other paths
+ res.StatusCode = 404;
+ res.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Caught an exception in the OnGet handler", this);
+ }
+ }
+
+ private async void Server_OnPost(object sender, HttpRequestEventArgs e)
+ {
+ try
+ {
+ var req = e.Request;
+ var res = e.Response;
+
+ res.AddHeader("Access-Control-Allow-Origin", "*");
+
+ var path = req.RawUrl;
+ var ip = req.RemoteEndPoint.Address.ToString();
+
+ this.LogVerbose("POST Request received at path: {path} from host {host}", path, ip);
+
+ var body = new StreamReader(req.InputStream).ReadToEnd();
+
+ if (path.StartsWith("/mc/api/log"))
+ {
+ res.StatusCode = 200;
+ res.Close();
+
+ var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
+ {
+ Content = new StringContent(body, Encoding.UTF8, "application/json"),
+ };
+
+ logRequest.Headers.Add("x-pepperdash-host", ip);
+
+ await LogClient.SendAsync(logRequest);
+
+ this.LogVerbose("Log data sent to {host}:{port}", _parent.Config.DirectServer.Logging.Host, _parent.Config.DirectServer.Logging.Port);
+ }
+ else
+ {
+ res.StatusCode = 404;
+ res.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ this.LogException(ex, "Caught an exception in the OnPost handler");
+ }
+ }
+
+ private void Server_OnOptions(object sender, HttpRequestEventArgs e)
+ {
+ try
+ {
+ var res = e.Response;
+
+ res.AddHeader("Access-Control-Allow-Origin", "*");
+ res.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+ res.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");
+
+ res.StatusCode = 200;
+ res.Close();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Caught an exception in the OnPost handler", this);
+ }
+ }
+
+ ///
+ /// Handle the request to join the room with a token
+ ///
+ ///
+ ///
+ private void HandleJoinRequest(HttpListenerRequest req, HttpListenerResponse res)
+ {
+ var qp = req.QueryString;
+ var token = qp["token"];
+
+ this.LogVerbose("Join Room Request with token: {token}", token);
+
+
+ if (UiClients.TryGetValue(token, out UiClientContext clientContext))
+ {
+ var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey);
+
+ if (bridge != null)
+ {
+ res.StatusCode = 200;
+ res.ContentType = "application/json";
+
+ // Construct the response object
+ JoinResponse jRes = new JoinResponse
+ {
+ ClientId = token,
+ RoomKey = bridge.RoomKey,
+ SystemUuid = _parent.SystemUuid,
+ RoomUuid = _parent.SystemUuid,
+ Config = _parent.GetConfigWithPluginVersion(),
+ CodeExpires = new DateTime().AddYears(1),
+ UserCode = bridge.UserCode,
+ UserAppUrl = string.Format("http://{0}:{1}/mc/app",
+ CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
+ Port),
+ EnableDebug = false
+ };
+
+ // Serialize to JSON and convert to Byte[]
+ var json = JsonConvert.SerializeObject(jRes);
+ var body = Encoding.UTF8.GetBytes(json);
+ res.ContentLength64 = body.LongLength;
+
+ // Send the response
+ res.Close(body, true);
+ }
+ else
+ {
+ var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey);
+ res.StatusCode = 404;
+ res.ContentType = "application/json";
+ this.LogVerbose("{message}", message);
+ var body = Encoding.UTF8.GetBytes(message);
+ res.ContentLength64 = body.LongLength;
+ res.Close(body, true);
+
+ }
+ }
+ else
+ {
+ var message = "Token invalid or has expired";
+ res.StatusCode = 401;
+ res.ContentType = "application/json";
+ this.LogVerbose("{message}", message);
+ var body = Encoding.UTF8.GetBytes(message);
+ res.ContentLength64 = body.LongLength;
+ res.Close(body, true);
+ }
+ }
+
+ ///
+ /// Handles a server version request
+ ///
+ ///
+ private void HandleVersionRequest(HttpListenerResponse res)
+ {
+ res.StatusCode = 200;
+ res.ContentType = "application/json";
+ var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
+ var message = JsonConvert.SerializeObject(version);
+ this.LogVerbose("{message}", message);
+
+ var body = Encoding.UTF8.GetBytes(message);
+ res.ContentLength64 = body.LongLength;
+ res.Close(body, true);
+ }
+
+ ///
+ /// Handler to return images requested by the user app
+ ///
+ ///
+ ///
+ private void HandleImageRequest(HttpListenerRequest req, HttpListenerResponse res)
+ {
+ var path = req.RawUrl;
+
+ Debug.LogMessage(LogEventLevel.Verbose, "Requesting Image: {0}", this, path);
+
+ var imageBasePath = Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo" + Global.DirectorySeparator;
+
+ var image = path.Split('/').Last();
+
+ var filePath = imageBasePath + image;
+
+ Debug.LogMessage(LogEventLevel.Verbose, "Retrieving Image: {0}", this, filePath);
+
+ if (File.Exists(filePath))
+ {
+ if (filePath.EndsWith(".png"))
+ {
+ res.ContentType = "image/png";
+ }
+ else if (filePath.EndsWith(".jpg"))
+ {
+ res.ContentType = "image/jpeg";
+ }
+ else if (filePath.EndsWith(".gif"))
+ {
+ res.ContentType = "image/gif";
+ }
+ else if (filePath.EndsWith(".svg"))
+ {
+ res.ContentType = "image/svg+xml";
+ }
+ byte[] contents = File.ReadAllBytes(filePath);
+ res.ContentLength64 = contents.LongLength;
+ res.Close(contents, true);
+ }
+ else
+ {
+ res.StatusCode = (int)HttpStatusCode.NotFound;
+ res.Close();
+ }
+ }
+
+ ///
+ /// Handles requests to serve files for the Angular single page app
+ ///
+ ///
+ ///
+ ///
+ private void HandleUserAppRequest(HttpListenerRequest req, HttpListenerResponse res, string path)
+ {
+ this.LogVerbose("Requesting User app file");
+
+ string filePath = path.Split('?')[0];
+
+ // remove the token from the path if found
+ //string filePath = path.Replace(string.Format("?token={0}", token), "");
+
+ // if there's no file suffix strip any extra path data after the base href
+ if (filePath != _userAppBaseHref && !filePath.Contains(".") && (!filePath.EndsWith(_userAppBaseHref) || !filePath.EndsWith(_userAppBaseHref += "/")))
+ {
+ var suffix = filePath.Substring(_userAppBaseHref.Length, filePath.Length - _userAppBaseHref.Length);
+ if (suffix != "/")
+ {
+ //Debug.Console(2, this, "Suffix: {0}", suffix);
+ filePath = filePath.Replace(suffix, "");
+ }
+ }
+
+ // swap the base href prefix for the file path prefix
+ filePath = filePath.Replace(_userAppBaseHref, _appPath);
+
+ this.LogVerbose("filepath: {filePath}", filePath);
+
+
+ // append index.html if no specific file is specified
+ if (!filePath.Contains("."))
+ {
+ if (filePath.EndsWith("/"))
+ {
+ filePath += "index.html";
+ }
+ else
+ {
+ filePath += "/index.html";
+ }
+ }
+
+ // Set ContentType based on file type
+ if (filePath.EndsWith(".html"))
+ {
+ this.LogVerbose("Client requesting User App");
+
+ res.ContentType = "text/html";
+ }
+ else
+ {
+ if (path.EndsWith(".js"))
+ {
+ res.ContentType = "application/javascript";
+ }
+ else if (path.EndsWith(".css"))
+ {
+ res.ContentType = "text/css";
+ }
+ else if (path.EndsWith(".json"))
+ {
+ res.ContentType = "application/json";
+ }
+ }
+
+ this.LogVerbose("Attempting to serve file: {filePath}", filePath);
+
+ var remoteIp = req.RemoteEndPoint.Address;
+
+ // Check if the request is coming from the CS LAN and if so, send the CS config instead of the LAN config
+ if (csSubnetMask != null && csIpAddress != null && remoteIp.IsInSameSubnet(csIpAddress, csSubnetMask) && filePath.Contains(appConfigFileName))
+ {
+ filePath = filePath.Replace(appConfigFileName, appConfigCsFileName);
+ }
+
+ byte[] contents;
+ if (File.Exists(filePath))
+ {
+ this.LogVerbose("File found: {filePath}", filePath);
+ contents = File.ReadAllBytes(filePath);
+ }
+ else
+ {
+ this.LogVerbose("File not found: {filePath}", filePath);
+ res.StatusCode = (int)HttpStatusCode.NotFound;
+ res.Close();
+ return;
+ }
+
+ res.ContentLength64 = contents.LongLength;
+ res.Close(contents, true);
+ }
+
+ public void StopServer()
+ {
+ this.LogVerbose("Stopping WebSocket Server");
+ _server.Stop(CloseStatusCode.Normal, "Server Shutting Down");
+ }
+
+ ///
+ /// Sends a message to all connectd clients
+ ///
+ ///
+ public void SendMessageToAllClients(string message)
+ {
+ foreach (var clientContext in UiClients.Values)
+ {
+ if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive)
+ {
+ clientContext.Client.Context.WebSocket.Send(message);
+ }
+ }
+ }
+
+ ///
+ /// Sends a message to a specific client
+ ///
+ ///
+ ///
+ public void SendMessageToClient(object clientId, string message)
+ {
+ if (clientId == null)
+ {
+ return;
+ }
+
+ if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext))
+ {
+ if (clientContext.Client != null)
+ {
+ var socket = clientContext.Client.Context.WebSocket;
+
+ if (socket.IsAlive)
+ {
+ socket.Send(message);
+ }
+ }
+ }
+ else
+ {
+ this.LogWarning("Unable to find client with ID: {clientId}", clientId);
+ }
+ }
+ }
+
+ ///
+ /// Class to describe the server version info
+ ///
+ public class Version
+ {
+ [JsonProperty("serverVersion")]
+ public string ServerVersion { get; set; }
+
+ [JsonProperty("serverIsRunningOnProcessorHardware")]
+ public bool ServerIsRunningOnProcessorHardware { get; private set; }
+
+ public Version()
+ {
+ ServerIsRunningOnProcessorHardware = true;
+ }
+ }
+
+ ///
+ /// Represents an instance of a UiClient and the associated Token
+ ///
+ public class UiClientContext
+ {
+ public UiClient Client { get; private set; }
+ public JoinToken Token { get; private set; }
+
+ public UiClientContext(JoinToken token)
+ {
+ Token = token;
+ }
+
+ public void SetClient(UiClient client)
+ {
+ Client = client;
+ }
+
+ }
+
+ ///
+ /// Represents the data structure for the grant code and UiClient tokens to be stored in the secrets manager
+ ///
+ public class ServerTokenSecrets
+ {
+ public string GrantCode { get; set; }
+
+ public Dictionary Tokens { get; set; }
+
+ public ServerTokenSecrets(string grantCode)
+ {
+ GrantCode = grantCode;
+ Tokens = new Dictionary();
+ }
+ }
+
+ ///
+ /// Represents a join token with the associated properties
+ ///
+ public class JoinToken
+ {
+ public string Code { get; set; }
+
+ public string RoomKey { get; set; }
+
+ public string Uuid { get; set; }
+
+ public string TouchpanelKey { get; set; } = "";
+
+ public string Token { get; set; } = null;
+ }
+
+ ///
+ /// Represents the structure of the join response
+ ///
+ public class JoinResponse
+ {
+ [JsonProperty("clientId")]
+ public string ClientId { get; set; }
+
+ [JsonProperty("roomKey")]
+ public string RoomKey { get; set; }
+
+ [JsonProperty("systemUUid")]
+ public string SystemUuid { get; set; }
+
+ [JsonProperty("roomUUid")]
+ public string RoomUuid { get; set; }
+
+ [JsonProperty("config")]
+ public object Config { get; set; }
+
+ [JsonProperty("codeExpires")]
+ public DateTime CodeExpires { get; set; }
+
+ [JsonProperty("userCode")]
+ public string UserCode { get; set; }
+
+ [JsonProperty("userAppUrl")]
+ public string UserAppUrl { get; set; }
+
+ [JsonProperty("enableDebug")]
+ public bool EnableDebug { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs
new file mode 100644
index 00000000..eb1cf7a1
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs
@@ -0,0 +1,145 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.RoomBridges;
+using Serilog.Events;
+using System;
+using System.Text.RegularExpressions;
+using WebSocketSharp;
+using WebSocketSharp.Server;
+using ErrorEventArgs = WebSocketSharp.ErrorEventArgs;
+
+
+namespace PepperDash.Essentials.WebSocketServer
+{
+ ///
+ /// Represents the behaviour to associate with a UiClient for WebSocket communication
+ ///
+ public class UiClient : WebSocketBehavior
+ {
+ public MobileControlSystemController Controller { get; set; }
+
+ public string RoomKey { get; set; }
+
+ private string _clientId;
+
+ private DateTime _connectionTime;
+
+ public TimeSpan ConnectedDuration
+ {
+ get
+ {
+ if (Context.WebSocket.IsAlive)
+ {
+ return DateTime.Now - _connectionTime;
+ }
+ else
+ {
+ return new TimeSpan(0);
+ }
+ }
+ }
+
+ public UiClient()
+ {
+
+ }
+
+ protected override void OnOpen()
+ {
+ base.OnOpen();
+
+ var url = Context.WebSocket.Url;
+ Debug.LogMessage(LogEventLevel.Verbose, "New WebSocket Connection from: {0}", null, url);
+
+ var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)");
+
+ if (!match.Success)
+ {
+ _connectionTime = DateTime.Now;
+ return;
+ }
+
+ var clientId = match.Groups[1].Value;
+ _clientId = clientId;
+
+ if (Controller == null)
+ {
+ Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null");
+ _connectionTime = DateTime.Now;
+ }
+
+ var clientJoinedMessage = new MobileControlMessage
+ {
+ Type = "/system/clientJoined",
+ Content = JToken.FromObject(new
+ {
+ clientId,
+ roomKey = RoomKey,
+ })
+ };
+
+ Controller.HandleClientMessage(JsonConvert.SerializeObject(clientJoinedMessage));
+
+ var bridge = Controller.GetRoomBridge(RoomKey);
+
+ if (bridge == null) return;
+
+ SendUserCodeToClient(bridge, clientId);
+
+ bridge.UserCodeChanged -= Bridge_UserCodeChanged;
+ bridge.UserCodeChanged += Bridge_UserCodeChanged;
+
+ // TODO: Future: Check token to see if there's already an open session using that token and reject/close the session
+ }
+
+ private void Bridge_UserCodeChanged(object sender, EventArgs e)
+ {
+ SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId);
+ }
+
+ private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId)
+ {
+ var content = new
+ {
+ userCode = bridge.UserCode,
+ qrUrl = bridge.QrCodeUrl,
+ };
+
+ var message = new MobileControlMessage
+ {
+ Type = "/system/userCodeChanged",
+ ClientId = clientId,
+ Content = JToken.FromObject(content)
+ };
+
+ Controller.SendMessageObjectToDirectClient(message);
+ }
+
+ protected override void OnMessage(MessageEventArgs e)
+ {
+ base.OnMessage(e);
+
+ if (e.IsText && e.Data.Length > 0 && Controller != null)
+ {
+ // Forward the message to the controller to be put on the receive queue
+ Controller.HandleClientMessage(e.Data);
+ }
+ }
+
+ protected override void OnClose(CloseEventArgs e)
+ {
+ base.OnClose(e);
+
+ Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Closing: {0} reason: {1}", null, e.Code, e.Reason);
+ }
+
+ protected override void OnError(ErrorEventArgs e)
+ {
+ base.OnError(e);
+
+ Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Error: {exception} message: {message}", e.Exception, e.Message);
+ }
+ }
+}