using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Net.Http; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using Org.BouncyCastle.Crypto.Prng; 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 WebSocketSharp; using WebSocketSharp.Net; using WebSocketSharp.Server; namespace PepperDash.Essentials.WebSocketServer { /// /// Represents a MobileControlWebsocketServer /// 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"; private const string certificateName = "selfCres"; private const string certificatePassword = "cres12345"; /// /// Where the key is the join token and the value is the room key /// //private Dictionary _joinTokens; private HttpServer _server; /// /// Gets the HttpServer instance /// public HttpServer Server => _server; /// /// Gets the collection of UI client contexts /// public Dictionary UiClientContexts { get; private set; } private readonly Dictionary uiClients = new Dictionary(); /// /// Gets the collection of UI clients /// public ReadOnlyDictionary UiClients => new ReadOnlyDictionary(uiClients); 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 readonly System.Net.IPAddress csIpAddress; private readonly System.Net.IPAddress csSubnetMask; /// /// The path for the WebSocket messaging /// private readonly string _wsPath = "/mc/api/ui/join/"; /// /// Gets the WebSocket path /// 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"; /// /// Gets or sets the Port /// public int Port { get; private set; } /// /// Gets the user app URL prefix /// 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); } } /// /// Gets the count of connected UI clients /// public int ConnectedUiClientsCount { get { return uiClients.Values.Where(c => c.Context.WebSocket.IsAlive).Count(); } } /// /// Initializes a new instance of the MobileControlWebsocketServer class. /// 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 { this.LogInformation("Automatically forwarding port {port} 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) { this.LogError("Error adding port forwarding: {error}", result); } } catch (ArgumentException) { this.LogInformation("This processor does not have a CS LAN", this); } catch (Exception ex) { this.LogError("Error automatically forwarding port to CS LAN: {message}", ex.Message); this.LogDebug(ex, "Stack Trace"); } } 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) { this.LogInformation("This processor does not have a CS LAN"); } } UiClientContexts = 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); } /// /// Initialize method /// /// 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; } if (_parent.Config.DirectServer.Secure) { this.LogInformation("Adding SSL Configuration to server"); _server.SslConfiguration = new ServerSslConfiguration(new X509Certificate2($"\\user\\{certificateName}.pfx", certificatePassword)) { ClientCertificateRequired = false, CheckCertificateRevocation = false, EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 }; } _server.Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); // setting to trace to allow logging level to be controlled by appdebug _server.Log.Level = LogLevel.Trace; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; _server.Start(); if (_server.IsListening) { this.LogInformation("Mobile Control WebSocket Server listening on port {port}", _server.Port); } CrestronEnvironment.ProgramStatusEventHandler += OnProgramStop; RetrieveSecret(); CreateFolderStructure(); AddClientsForTouchpanels(); } catch (Exception ex) { this.LogError("Exception initializing direct server: {message}", ex.Message); this.LogDebug(ex, "Stack Trace"); } } /// /// Set the internal logging level for the Websocket Server /// public void SetWebsocketLogLevel(LogLevel level) { CrestronConsole.ConsoleCommandResponse($"Setting direct server debug level to {level}", level.ToString()); _server.Log.Level = level; } 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); 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; } string ip = processorIp; if (_parent.Config.DirectServer.CSLanUiDeviceKeys != null && _parent.Config.DirectServer.CSLanUiDeviceKeys.Any(k => k.Equals(touchpanel.Touchpanel.Key, StringComparison.InvariantCultureIgnoreCase)) && csIpAddress != null) { ip = csIpAddress.ToString(); } var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"; this.LogVerbose("Sending URL {appUrl}", appUrl); touchpanel.Messenger.UpdateAppUrl($"http://{ip}:{_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("Error getting application configuration: {message}", ex.Message); this.LogDebug(ex, "Stack Trace"); 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) { this.LogWarning("Token value is null"); continue; } this.LogInformation("Adding token: {key} for room: {roomKey}", token.Key, token.Value.RoomKey); if (UiClientContexts == null) { UiClientContexts = new Dictionary(); } UiClientContexts.Add(token.Key, new UiClientContext(token.Value)); } } if (UiClientContexts.Count > 0) { this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClientContexts.Count); foreach (var client in UiClientContexts) { var key = client.Key; var path = _wsPath + key; var roomKey = client.Value.Token.RoomKey; _server.AddWebSocketService(path, () => { this.LogInformation("Building a UiClient with ID {id}", client.Value.Token.Id); return BuildUiClient(roomKey, client.Value.Token, key); }); } } } else { this.LogWarning("No secret found"); } this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClientContexts.Count); } catch (Exception ex) { this.LogError("Exception retrieving secret: {message}", ex.Message); this.LogDebug(ex, "Stack Trace"); } } /// /// UpdateSecret method /// public void UpdateSecret() { try { if (_secret == null) { this.LogError("Secret is null"); _secret = new ServerTokenSecrets(string.Empty); } _secret.Tokens.Clear(); foreach (var uiClientContext in UiClientContexts) { _secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token); } var serializedSecret = JsonConvert.SerializeObject(_secret); _secretProvider.SetSecret(SecretProviderKey, serializedSecret); } catch (Exception ex) { this.LogError("Exception updating secret: {message}", ex.Message); this.LogDebug(ex, "Stack Trace"); } } /// /// 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}"); } /// /// Validates the grant code against the room key /// 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); } /// /// Validates the grant code against the room key /// 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); } } /// /// Generates a new client token for the specified bridge /// 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 }; UiClientContexts.Add(key, new UiClientContext(token)); var path = _wsPath + key; _server.AddWebSocketService(path, () => { this.LogInformation("Building a UiClient with ID {id}", token.Id); return BuildUiClient(bridge.RoomKey, token, key); }); this.LogInformation("Added new WebSocket UiClient for path: {path}", path); this.LogInformation("Token: {@token}", token); this.LogVerbose("{serviceCount} websocket services present", _server.WebSocketServices.Count); UpdateSecret(); return (key, path); } private UiClient BuildUiClient(string roomKey, JoinToken token, string key) { var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token); this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id); c.Controller = _parent; c.RoomKey = roomKey; if (uiClients.ContainsKey(token.Id)) { this.LogWarning("removing client with duplicate id {id}", token.Id); uiClients.Remove(token.Id); } uiClients.Add(token.Id, c); // UiClients[key].SetClient(c); c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId); token.Id = null; return c; } /// /// Prints out the session data for each path /// public void PrintSessionData() { foreach (var path in _server.WebSocketServices.Paths) { this.LogInformation("Path: {path}", path); this.LogInformation(" Session Count: {sessionCount}", _server.WebSocketServices[path].Sessions.Count); this.LogInformation(" Active Session Count: {activeSessionCount}", _server.WebSocketServices[path].Sessions.ActiveIDs.Count()); this.LogInformation(" Inactive Session Count: {inactiveSessionCount}", _server.WebSocketServices[path].Sessions.InactiveIDs.Count()); this.LogInformation(" Active Clients:"); foreach (var session in _server.WebSocketServices[path].Sessions.IDs) { this.LogInformation(" Client ID: {id}", (_server.WebSocketServices[path].Sessions[session] as UiClient)?.Id); } } } /// /// Removes all clients from the server /// private void RemoveAllTokens(string s) { if (s == "?" || string.IsNullOrEmpty(s)) { CrestronConsole.ConsoleCommandResponse(@"Remove 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 UiClientContexts) { 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)); } } UiClientContexts.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 (UiClientContexts.ContainsKey(key)) { var uiClientContext = UiClientContexts[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)) { UiClientContexts.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", UiClientContexts.Count)); foreach (var client in UiClientContexts) { 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 != null && client.Context.WebSocket.IsAlive) { 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) { this.LogError("Exception in OnGet handler: {message}", ex.Message); this.LogDebug(ex, "Stack Trace"); } } 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); byte[] body; if (!UiClientContexts.TryGetValue(token, out UiClientContext clientContext)) { var message = "Token invalid or has expired"; res.StatusCode = 401; res.ContentType = "application/json"; this.LogVerbose("{message}", message); body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; res.Close(body, true); return; } var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey); if (bridge == null) { 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); body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; res.Close(body, true); return; } res.StatusCode = 200; res.ContentType = "application/json"; var devices = DeviceManager.GetDevices(); Dictionary deviceInterfaces = new Dictionary(); foreach (var device in devices) { var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo { Key = device.Key, Name = (device as IKeyName)?.Name ?? "", Interfaces = interfaces }); } var clientId = $"{Utilities.GetNextClientId()}"; clientContext.Token.Id = clientId; this.LogVerbose("Assigning ClientId: {clientId}", clientId); // Construct the response object JoinResponse jRes = new JoinResponse { ClientId = clientId, 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, DeviceInterfaceSupport = deviceInterfaces }; // Serialize to JSON and convert to Byte[] var json = JsonConvert.SerializeObject(jRes); body = Encoding.UTF8.GetBytes(json); res.ContentLength64 = body.LongLength; // Send the response 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.LogWarning("File not found: {filePath}", filePath); res.StatusCode = (int)HttpStatusCode.NotFound; res.Close(); return; } res.ContentLength64 = contents.LongLength; res.Close(contents, true); } /// /// StopServer method /// public void StopServer() { this.LogVerbose("Stopping WebSocket Server"); _server.Stop(CloseStatusCode.Normal, "Server Shutting Down"); } /// /// Sends a message to all connectd clients /// /// /// /// SendMessageToAllClients method /// public void SendMessageToAllClients(string message) { foreach (var client in uiClients.Values) { if (!client.Context.WebSocket.IsAlive) { continue; } client.Context.WebSocket.Send(message); } } /// /// Sends a message to a specific client /// /// /// /// /// SendMessageToClient method /// public void SendMessageToClient(object clientId, string message) { if (clientId == null) { return; } if (uiClients.TryGetValue((string)clientId, out var client)) { var socket = client.Context.WebSocket; if (!socket.IsAlive) { this.LogError("Unable to send message to client {id}. Client is disconnected: {message}", clientId, message); return; } socket.Send(message); } else { this.LogWarning("Unable to find client with ID: {clientId}", clientId); } } } }