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