/*PepperDash Technology Corp. JAG Copyright: 2017 ------------------------------------ ***Notice of Ownership and Copyright*** The material in which this notice appears is the property of PepperDash Technology Corporation, which claims copyright under the laws of the United States of America in the entire body of material and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part, of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited. PepperDash Technology Corporation reserves all rights under applicable laws. ------------------------------------ */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core; namespace DynamicTCP { public class DynamicTCPServer : Device { #region Events /// /// Event for Receiving text /// public event EventHandler TextReceived; /// /// Event for client connection socket status change /// public event EventHandler ClientConnectionChange; /// /// Event for Server State Change /// public event EventHandler ServerStateChange; #endregion #region Properties/Variables /// /// Secure or unsecure TCP server. Defaults to Unsecure or standard TCP server without SSL /// public bool Secure { get; set; } /// /// S+ Helper for Secure bool. Parameter in SIMPL+ so there is no get, one way set from simpl+ Param to property in func main of SIMPL+ /// public ushort USecure { set { if (value == 1) Secure = true; else if (value == 0) Secure = false; } } /// /// Text representation of the Socket Status enum values for the server /// public string Status { get { if (Secure ? SecureServer != null : UnsecureServer != null) return Secure ? SecureServer.State.ToString() : UnsecureServer.State.ToString(); else return ""; } } /// /// Bool showing if socket is connected /// public bool IsConnected { get { return (Secure ? SecureServer != null : UnsecureServer != null) && (Secure ? SecureServer.State == ServerState.SERVER_CONNECTED : UnsecureServer.State == ServerState.SERVER_CONNECTED); } } /// /// S+ helper for IsConnected /// public ushort UIsConnected { get { return (ushort)(IsConnected ? 1 : 0); } } /// /// Bool showing if socket is connected /// public bool IsListening { get { return (Secure ? SecureServer != null : UnsecureServer != null) && (Secure ? SecureServer.State == ServerState.SERVER_LISTENING : UnsecureServer.State == ServerState.SERVER_LISTENING); } } /// /// S+ helper for IsConnected /// public ushort UIsListening { get { return (ushort)(IsListening ? 1 : 0); } } public ushort MaxClients { get; set; } // should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable /// /// Number of clients currently connected. /// public ushort NumberOfClientsConnected { get { if (Secure ? SecureServer != null : UnsecureServer != null) return Secure ? (ushort)SecureServer.NumberOfClientsConnected : (ushort)UnsecureServer.NumberOfClientsConnected; return 0; } } /// /// Port Server should listen on /// public int Port { get; set; } /// /// S+ helper for Port /// public ushort UPort { get { return Convert.ToUInt16(Port); } set { Port = Convert.ToInt32(value); } } /// /// Bool to show whether the server requires a preshared key. Must be set the same in the client, and if true shared keys must be identical on server/client /// public bool SharedKeyRequired { get; set; } /// /// S+ helper for requires shared key bool /// public ushort USharedKeyRequired { set { if (value == 1) SharedKeyRequired = true; else SharedKeyRequired = false; } } /// /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module. /// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called /// public string SharedKey { get; set; } /// /// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received /// public bool HeartbeatRequired { get; set; } /// /// S+ Helper for Heartbeat Required /// public ushort UHeartbeatRequired { set { if (value == 1) HeartbeatRequired = true; else HeartbeatRequired = false; } } /// /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ /// public int HeartbeatRequiredIntervalMs { get; set; } /// /// Simpl+ Heartbeat Analog value in seconds /// public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } } /// /// String to Match for heartbeat. If null or empty any string will reset heartbeat timer /// public string HeartbeatStringToMatch { get; set; } //private timers for Heartbeats per client Dictionary HeartbeatTimerDictionary = new Dictionary(); //flags to show the secure server is waiting for client at index to send the shared key List WaitingForSharedKey = new List(); //Store the connected client indexes List ConnectedClientsIndexes = new List(); /// /// Defaults to 2000 /// public int BufferSize { get; set; } /// /// Private flag to note that the server has stopped intentionally /// private bool ServerStopped { get; set; } //Servers SecureTCPServer SecureServer; TCPServer UnsecureServer; #endregion #region Constructors /// /// constructor /// public DynamicTCPServer() : base("Uninitialized Dynamic TCP Server") { CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); BufferSize = 2000; Secure = false; } #endregion #region Methods - Server Actions /// /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ /// /// public void Initialize(string key) { Key = key; } /// /// Start listening on the specified port /// public void Listen() { try { if (Port < 1 || Port > 65535) { Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericSecureTcpClient '{0}': Invalid port", Key); ErrorLog.Warn(string.Format("GenericSecureTcpClient '{0}': Invalid port", Key)); return; } if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) { Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericSecureTcpClient '{0}': No Shared Key set", Key); ErrorLog.Warn(string.Format("GenericSecureTcpClient '{0}': No Shared Key set", Key)); return; } if (IsListening) return; if (Secure) { SecureServer = new SecureTCPServer(Port, MaxClients); SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange); ServerStopped = false; SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); onServerStateChange(); Debug.Console(2, "Secure Server Status: {0}, Socket Status: {1}\r\n", SecureServer.State.ToString(), SecureServer.ServerSocketStatus); } else { UnsecureServer = new TCPServer(Port, MaxClients); UnsecureServer.SocketStatusChange += new TCPServerSocketStatusChangeEventHandler(UnsecureServer_SocketStatusChange); ServerStopped = false; UnsecureServer.WaitForConnectionAsync(IPAddress.Any, UnsecureConnectCallback); onServerStateChange(); Debug.Console(2, "Unsecure Server Status: {0}, Socket Status: {1}\r\n", UnsecureServer.State.ToString(), UnsecureServer.ServerSocketStatus); } } catch (Exception ex) { ErrorLog.Error("Error with Dynamic Server: {0}", ex.ToString()); } } /// /// Stop Listeneing /// public void StopListening() { Debug.Console(2, "Stopping Listener"); if (SecureServer != null) SecureServer.Stop(); if (UnsecureServer != null) UnsecureServer.Stop(); ServerStopped = true; onServerStateChange(); } /// /// Disconnect All Clients /// public void DisconnectAllClients() { Debug.Console(2, "Disconnecting All Clients"); if (SecureServer != null) SecureServer.DisconnectAll(); if (UnsecureServer != null) UnsecureServer.DisconnectAll(); onConnectionChange(); onServerStateChange(); //State shows both listening and connected } /// /// Broadcast text from server to all connected clients /// /// public void BroadcastText(string text) { if (ConnectedClientsIndexes.Count > 0) { byte[] b = Encoding.GetEncoding(28591).GetBytes(text); if (Secure) foreach (uint i in ConnectedClientsIndexes) SecureServer.SendDataAsync(i, b, b.Length, SecureSendDataAsyncCallback); else foreach (uint i in ConnectedClientsIndexes) UnsecureServer.SendDataAsync(i, b, b.Length, UnsecureSendDataAsyncCallback); } } /// /// Not sure this is useful in library, maybe Pro?? /// /// /// public void SendTextToClient(string text, uint clientIndex) { byte[] b = Encoding.GetEncoding(28591).GetBytes(text); if (Secure) SecureServer.SendDataAsync(clientIndex, b, b.Length, SecureSendDataAsyncCallback); else UnsecureServer.SendDataAsync(clientIndex, b, b.Length, UnsecureSendDataAsyncCallback); } //private method to check heartbeat requirements and start or reset timer void checkHeartbeat(uint clientIndex, string received) { if (HeartbeatRequired) { if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) { if (received == HeartbeatStringToMatch) { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); else { CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } } } else { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); else { CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } } } } #endregion #region Methods - HeartbeatTimer Callback void HeartbeatTimer_CallbackFunction(object o) { uint clientIndex = (uint)o; string address = Secure ? SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex) : UnsecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); ErrorLog.Error("Heartbeat not received for Client at IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE", address); Debug.Console(2, "Heartbeat not received for Client at IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE", address); SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); if (Secure) SecureServer.Disconnect(clientIndex); else UnsecureServer.Disconnect(clientIndex); HeartbeatTimerDictionary.Remove(clientIndex); } #endregion #region Methods - Socket Status Changed Callbacks /// /// Secure Server Socket Status Changed Callback /// /// /// /// void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus) { Debug.Console(2, "Client at {0} ServerSocketStatus {1}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), serverSocketStatus.ToString()); if (server.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) { if (SharedKeyRequired && !WaitingForSharedKey.Contains(clientIndex)) WaitingForSharedKey.Add(clientIndex); if (!ConnectedClientsIndexes.Contains(clientIndex)) ConnectedClientsIndexes.Add(clientIndex); } else { if (ConnectedClientsIndexes.Contains(clientIndex)) ConnectedClientsIndexes.Remove(clientIndex); if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) HeartbeatTimerDictionary.Remove(clientIndex); } if(SecureServer.ServerSocketStatus.ToString() != Status) onConnectionChange(); } /// /// TCP Server (Unsecure) Socket Status Change Callback /// /// /// /// void UnsecureServer_SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus serverSocketStatus) { Debug.Console(2, "Client at {0} ServerSocketStatus {1}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), serverSocketStatus.ToString()); if (server.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) { if (SharedKeyRequired && !WaitingForSharedKey.Contains(clientIndex)) WaitingForSharedKey.Add(clientIndex); if (!ConnectedClientsIndexes.Contains(clientIndex)) ConnectedClientsIndexes.Add(clientIndex); } else { if (ConnectedClientsIndexes.Contains(clientIndex)) ConnectedClientsIndexes.Remove(clientIndex); if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) HeartbeatTimerDictionary.Remove(clientIndex); } if (UnsecureServer.ServerSocketStatus.ToString() != Status) onConnectionChange(); } #endregion #region Methods Connected Callbacks /// /// Secure TCP Client Connected to Secure Server Callback /// /// /// void SecureConnectCallback(SecureTCPServer mySecureTCPServer, uint clientIndex) { if (mySecureTCPServer.ClientConnected(clientIndex)) { if (SharedKeyRequired) { byte[] b = Encoding.GetEncoding(28591).GetBytes(SharedKey + "\n"); mySecureTCPServer.SendDataAsync(clientIndex, b, b.Length, SecureSendDataAsyncCallback); Debug.Console(2, "Sent Shared Key to client at {0}", mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); } if (HeartbeatRequired) { CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); if (mySecureTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) mySecureTCPServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); } } /// /// Unsecure TCP Client Connected to Unsecure Server Callback /// /// /// void UnsecureConnectCallback(TCPServer myTCPServer, uint clientIndex) { if (myTCPServer.ClientConnected(clientIndex)) { if (SharedKeyRequired) { byte[] b = Encoding.GetEncoding(28591).GetBytes(SharedKey + "\n"); myTCPServer.SendDataAsync(clientIndex, b, b.Length, UnsecureSendDataAsyncCallback); Debug.Console(2, "Sent Shared Key to client at {0}", myTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); } if (HeartbeatRequired) { CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } myTCPServer.ReceiveDataAsync(clientIndex, UnsecureReceivedDataAsyncCallback); if (myTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) myTCPServer.WaitForConnectionAsync(IPAddress.Any, UnsecureConnectCallback); } if (myTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) myTCPServer.WaitForConnectionAsync(IPAddress.Any, UnsecureConnectCallback); } #endregion #region Methods - Send/Receive Callbacks /// /// Secure Send Data Async Callback /// /// /// /// void SecureSendDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesSent) { //Seems there is nothing to do here } /// /// Unsecure Send Data Asyc Callback /// /// /// /// void UnsecureSendDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesSent) { //Seems there is nothing to do here } /// /// Secure Received Data Async Callback /// /// /// /// void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived) { if (numberOfBytesReceived > 0) { string received = "Nothing"; byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived); if (WaitingForSharedKey.Contains(clientIndex)) { received = received.Replace("\r", ""); received = received.Replace("\n", ""); if (received != SharedKey) { byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); Debug.Console(2, "Client at index {0} Shared key did not match the server, disconnecting client", clientIndex); ErrorLog.Error("Client at index {0} Shared key did not match the server, disconnecting client", clientIndex); mySecureTCPServer.SendDataAsync(clientIndex, b, b.Length, null); mySecureTCPServer.Disconnect(clientIndex); } if (mySecureTCPServer.NumberOfClientsConnected > 0) mySecureTCPServer.ReceiveDataAsync(SecureReceivedDataAsyncCallback); WaitingForSharedKey.Remove(clientIndex); byte[] skResponse = Encoding.GetEncoding(28591).GetBytes("Shared Key Match, Connected and ready for communication"); mySecureTCPServer.SendDataAsync(clientIndex, skResponse, skResponse.Length, null); mySecureTCPServer.ReceiveDataAsync(SecureReceivedDataAsyncCallback); } else { mySecureTCPServer.ReceiveDataAsync(SecureReceivedDataAsyncCallback); Debug.Console(2, "Secure Server Listening on Port: {0}, client IP: {1}, NumberOfBytesReceived: {2}, Received: {3}\r\n", mySecureTCPServer.PortNumber, mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), numberOfBytesReceived, received); onTextReceived(received); } checkHeartbeat(clientIndex, received); } if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); } /// /// Unsecure Received Data Async Callback /// /// /// /// void UnsecureReceivedDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesReceived) { if (numberOfBytesReceived > 0) { string received = "Nothing"; byte[] bytes = myTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived); if (WaitingForSharedKey.Contains(clientIndex)) { received = received.Replace("\r", ""); received = received.Replace("\n", ""); if (received != SharedKey) { byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); Debug.Console(2, "Client at index {0} Shared key did not match the server, disconnecting client", clientIndex); ErrorLog.Error("Client at index {0} Shared key did not match the server, disconnecting client", clientIndex); myTCPServer.SendDataAsync(clientIndex, b, b.Length, null); myTCPServer.Disconnect(clientIndex); } if (myTCPServer.NumberOfClientsConnected > 0) myTCPServer.ReceiveDataAsync(UnsecureReceivedDataAsyncCallback); WaitingForSharedKey.Remove(clientIndex); byte[] skResponse = Encoding.GetEncoding(28591).GetBytes("Shared Key Match, Connected and ready for communication"); myTCPServer.SendDataAsync(clientIndex, skResponse, skResponse.Length, null); myTCPServer.ReceiveDataAsync(UnsecureReceivedDataAsyncCallback); } else { myTCPServer.ReceiveDataAsync(UnsecureReceivedDataAsyncCallback); Debug.Console(2, "Secure Server Listening on Port: {0}, client IP: {1}, NumberOfBytesReceived: {2}, Received: {3}\r\n", myTCPServer.PortNumber, myTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), numberOfBytesReceived, received); onTextReceived(received); } checkHeartbeat(clientIndex, received); } if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) myTCPServer.ReceiveDataAsync(clientIndex, UnsecureReceivedDataAsyncCallback); } #endregion #region Methods - EventHelpers/Callbacks //Private Helper method to call the Connection Change Event void onConnectionChange() { var handler = ClientConnectionChange; if (handler != null) { if (Secure) handler(this, new DynamicTCPSocketStatusChangeEventArgs(SecureServer, Secure)); else handler(this, new DynamicTCPSocketStatusChangeEventArgs(UnsecureServer, Secure)); } } //Private Helper Method to call the Text Received Event void onTextReceived(string text) { var handler = TextReceived; if (handler != null) handler(this, new CopyCoreForSimplpGenericCommMethodReceiveTextArgs(text)); } //Private Helper Method to call the Server State Change Event void onServerStateChange() { var handler = ServerStateChange; if(handler != null) { if(Secure) handler(this, new DynamicTCPServerStateChangedEventArgs(SecureServer, Secure)); else handler(this, new DynamicTCPServerStateChangedEventArgs(UnsecureServer, Secure)); } } //Private Event Handler method to handle the closing of connections when the program stops void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) { if (programEventType == eProgramStatusEventType.Stopping) { Debug.Console(1, this, "Program stopping. Closing server"); DisconnectAllClients(); StopListening(); } } #endregion } }