diff --git a/Pepperdash Core/Pepperdash Core.suo b/Pepperdash Core/Pepperdash Core.suo index df08759..fb8dbb6 100644 Binary files a/Pepperdash Core/Pepperdash Core.suo and b/Pepperdash Core/Pepperdash Core.suo differ diff --git a/Pepperdash Core/Pepperdash Core/Comm/DynamicTCPServer.cs b/Pepperdash Core/Pepperdash Core/Comm/DynamicTCPServer.cs new file mode 100644 index 0000000..bad57f4 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/DynamicTCPServer.cs @@ -0,0 +1,684 @@ +/*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 + } +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs b/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs index cc7afe4..3732e0d 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs @@ -1,4 +1,14 @@ -using System; +/*PepperDash Technology Corp. +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; @@ -24,6 +34,43 @@ namespace PepperDash.Core } #endregion + #region DynamicTCPServerStateChangedEventArgs + public delegate void DynamicTCPServerStateChangedEventDelegate(object server); + + public class DynamicTCPServerStateChangedEventArgs : EventArgs + { + public bool Secure { get; private set; } + public object Server { get; private set; } + + public DynamicTCPServerStateChangedEventArgs() { } + + public DynamicTCPServerStateChangedEventArgs(object server, bool secure) + { + Secure = secure; + Server = server; + } + } + #endregion + + #region DynamicTCPSocketStatusChangeEventDelegate + public delegate void DynamicTCPSocketStatusChangeEventDelegate(object server); + + public class DynamicTCPSocketStatusChangeEventArgs : EventArgs + { + public bool Secure { get; private set; } + public object Server { get; private set; } + + public DynamicTCPSocketStatusChangeEventArgs() { } + + public DynamicTCPSocketStatusChangeEventArgs(object server, bool secure) + { + Secure = secure; + Server = server; + } + } + #endregion + + } \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs new file mode 100644 index 0000000..82305ad --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs @@ -0,0 +1,371 @@ +/*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 System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronSockets; + +namespace PepperDash.Core +{ + public class GenericSecureTcpIpClient : Device, ISocketStatus, IAutoReconnect + { + #region Events + + public event EventHandler BytesReceived; + + public event EventHandler TextReceived; + + public event EventHandler ConnectionChange; + + #endregion + + #region Properties & Variables + /// + /// Address of server + /// + public string Hostname { get; set; } + + /// + /// Port on server + /// + public int Port { get; set; } + + /// + /// S+ helper + /// + public ushort UPort + { + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } + + /// + /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class + /// + 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 + /// + public string SharedKey { get; set; } + + /// + /// flag to show the client is waiting for the server to send the shared key + /// + private bool WaitingForSharedKeyResponse { get; set; } + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get + { + return (Client != null) && (Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED); + } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// Client socket status Read only + /// + public SocketStatus ClientStatus + { + get + { + if (Client == null) + return SocketStatus.SOCKET_STATUS_NO_CONNECT; + return Client.ClientStatus; + } + } + + /// + /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event + /// and IsConnected would be true when this == 2. + /// + public ushort UStatus + { + get { return (ushort)ClientStatus; } + } + + /// + /// Status text shows the message associated with socket status + /// + public string ClientStatusText { get { return ClientStatus.ToString(); } } + + /// + /// bool to track if auto reconnect should be set on the socket + /// + public bool AutoReconnect { get; set; } + + /// + /// S+ helper for AutoReconnect + /// + public ushort UAutoReconnect + { + get { return (ushort)(AutoReconnect ? 1 : 0); } + set { AutoReconnect = value == 1; } + } + /// + /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 + /// + public int AutoReconnectIntervalMs { get; set; } + + /// + /// Flag Set only when the disconnect method is called. + /// + bool DisconnectCalledByUser; + + /// + /// Connected bool + /// + public bool Connected + { + get { return (Client != null) && (Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED); } + } + + CTimer RetryTimer; //private Timer for auto reconnect + + SecureTCPClient Client; //Secure Client Class + + #endregion + + #region Constructors + + //Base class constructor + public GenericSecureTcpIpClient(string key, string address, int port, int bufferSize) + : base(key) + { + Hostname = address; + Port = port; + BufferSize = bufferSize; + AutoReconnectIntervalMs = 5000; + } + + //base class constructor + public GenericSecureTcpIpClient() + : base("Uninitialized DynamicTcpClient") + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + } + #endregion + + #region Methods + /// + /// Just to help S+ set the key + /// + public void Initialize(string key) + { + Key = key; + } + + /// + /// Handles closing this up when the program shuts down + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + { + if (Client != null) + { + Debug.Console(1, this, "Program stopping. Closing connection"); + Client.DisconnectFromServer(); + Client.Dispose(); + } + } + } + + /// + /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name. + /// + public void Connect() + { + if (IsConnected) + return; + + if (string.IsNullOrEmpty(Hostname)) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "DynamicTcpClient '{0}': No address set", Key); + ErrorLog.Warn(string.Format("DynamicTcpClient '{0}': No address set", Key)); + return; + } + if (Port < 1 || Port > 65535) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "DynamicTcpClient '{0}': Invalid port", Key); + ErrorLog.Warn(string.Format("DynamicTcpClient '{0}': Invalid port", Key)); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "DynamicTcpClient '{0}': No Shared Key set", Key); + ErrorLog.Warn(string.Format("DynamicTcpClient '{0}': No Shared Key set", Key)); + return; + } + Client = new SecureTCPClient(Hostname, Port, BufferSize); + Client.SocketStatusChange += SecureClient_SocketStatusChange; + DisconnectCalledByUser = false; + if (SharedKeyRequired) + WaitingForSharedKeyResponse = true; + Client.ConnectToServer(); + } + + /// + /// Disconnect client. Does not dispose. + /// + public void Disconnect() + { + DisconnectCalledByUser = true; + if(Client != null) + Client.DisconnectFromServer(); + } + + /// + /// callback after connection made + /// + /// + void ConnectToServerCallback(object o) + { + Client.ConnectToServer(); + if (Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + WaitAndTryReconnect(); + } + + /// + /// Called from Socket Status change if auto reconnect and socket disconnected (Not disconnected by user) + /// + void WaitAndTryReconnect() + { + Client.DisconnectFromServer(); + Debug.Console(2, "Attempting reconnect, status={0}", Client.ClientStatus); + + if (!DisconnectCalledByUser) + RetryTimer = new CTimer(ConnectToServerCallback, AutoReconnectIntervalMs); + } + + /// + /// Receive callback + /// + /// + /// + void SecureReceive(SecureTCPClient client, int numBytes) + { + if (numBytes > 0) + { + var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); + var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + if (WaitingForSharedKeyResponse && SharedKeyRequired) + { + if (str != (SharedKey + "\n")) + { + WaitingForSharedKeyResponse = false; + client.DisconnectFromServer(); + CrestronConsole.PrintLine("Client {0} was disconnected from server because the server did not respond with a matching shared key after connection", Key); + ErrorLog.Error("Client {0} was disconnected from server because the server did not respond with a matching shared key after connection", Key); + return; + } + else + { + WaitingForSharedKeyResponse = false; + CrestronConsole.PrintLine("Client {0} successfully connected to the server and received the Shared Key. Ready for communication", Key); + } + } + else + { + var bytesHandler = BytesReceived; + if (bytesHandler != null) + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var textHandler = TextReceived; + if (textHandler != null) + { + + textHandler(this, new GenericCommMethodReceiveTextArgs(str)); + } + } + } + client.ReceiveDataAsync(SecureReceive); + } + + /// + /// General send method + /// + public void SendText(string text) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + Client.SendData(bytes, bytes.Length); + } + + public void SendBytes(byte[] bytes) + { + Client.SendData(bytes, bytes.Length); + } + + /// + /// SocketStatusChange Callback + /// + /// + /// + void SecureClient_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus) + { + Debug.Console(2, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); + if (client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED && !DisconnectCalledByUser && AutoReconnect) + WaitAndTryReconnect(); + + // Probably doesn't need to be a switch since all other cases were eliminated + switch (clientSocketStatus) + { + case SocketStatus.SOCKET_STATUS_CONNECTED: + client.ReceiveDataAsync(SecureReceive); + if(SharedKeyRequired) + SendText(SharedKey + "\n"); + DisconnectCalledByUser = false; + break; + } + + var handler = ConnectionChange; + if (handler != null) + ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); + } + #endregion + } +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs new file mode 100644 index 0000000..365cbcb --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs @@ -0,0 +1,505 @@ +/*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; + +namespace PepperDash.Core +{ + public class GenericSecureTcpIpServer : 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 + /// + /// Text representation of the Socket Status enum values for the server + /// + public string Status + { + get + { + if (Server != null) + return Server.State.ToString(); + else + return ServerState.SERVER_NOT_LISTENING.ToString(); + } + + } + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { return (Server != null) && (Server.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 (Server != null) && (Server.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 (Server != null) + return (ushort)Server.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 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 Server; + + #endregion + + #region Constructors + /// + /// constructor + /// + public GenericSecureTcpIpServer() + : base("Uninitialized Dynamic TCP Server") + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + } + #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, "Server '{0}': Invalid port", Key); + ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "Server '{0}': No Shared Key set", Key); + ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); + return; + } + if (IsListening) + return; + Server = new SecureTCPServer(Port, MaxClients); + Server.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SocketStatusChange); + ServerStopped = false; + Server.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); + onServerStateChange(); + Debug.Console(2, "Server Status: {0}, Socket Status: {1}\r\n", Server.State.ToString(), Server.ServerSocketStatus); + } + catch (Exception ex) + { + ErrorLog.Error("Error with Dynamic Server: {0}", ex.ToString()); + } + } + + /// + /// Stop Listeneing + /// + public void StopListening() + { + Debug.Console(2, "Stopping Listener"); + if (Server != null) + Server.Stop(); + ServerStopped = true; + onServerStateChange(); + } + + /// + /// Disconnect All Clients + /// + public void DisconnectAllClients() + { + Debug.Console(2, "Disconnecting All Clients"); + if (Server != null) + Server.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); + foreach (uint i in ConnectedClientsIndexes) + Server.SendDataAsync(i, b, b.Length, SendDataAsyncCallback); + } + } + + /// + /// Not sure this is useful in library, maybe Pro?? + /// + /// + /// + public void SendTextToClient(string text, uint clientIndex) + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(text); + Server.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); + } + + //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 - Callbacks + /// + /// Callback to disconnect if heartbeat timer finishes without being reset + /// + /// + void HeartbeatTimer_CallbackFunction(object o) + { + uint clientIndex = (uint)o; + + string address = Server.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); + Server.Disconnect(clientIndex); + HeartbeatTimerDictionary.Remove(clientIndex); + } + + /// + /// TCP Server Socket Status Change Callback + /// + /// + /// + /// + void 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 (Server.ServerSocketStatus.ToString() != Status) + onConnectionChange(); + } + + /// + /// TCP Client Connected to Server Callback + /// + /// + /// + void ConnectCallback(SecureTCPServer mySecureTCPServer, uint clientIndex) + { + if (mySecureTCPServer.ClientConnected(clientIndex)) + { + if (SharedKeyRequired) + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(SharedKey + "\n"); + mySecureTCPServer.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); + 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, ReceivedDataAsyncCallback); + if (mySecureTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) + mySecureTCPServer.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); + } + if (mySecureTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) + mySecureTCPServer.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); + } + + /// + /// Send Data Asyc Callback + /// + /// + /// + /// + void SendDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesSent) + { + //Seems there is nothing to do here + } + + /// + /// Received Data Async Callback + /// + /// + /// + /// + void ReceivedDataAsyncCallback(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(ReceivedDataAsyncCallback); + 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(ReceivedDataAsyncCallback); + } + else + { + mySecureTCPServer.ReceiveDataAsync(ReceivedDataAsyncCallback); + Debug.Console(2, "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, ReceivedDataAsyncCallback); + } + #endregion + + #region Methods - EventHelpers/Callbacks + //Private Helper method to call the Connection Change Event + void onConnectionChange() + { + var handler = ClientConnectionChange; + if (handler != null) + handler(this, new DynamicTCPSocketStatusChangeEventArgs(Server, false)); + } + + //Private Helper Method to call the Text Received Event + void onTextReceived(string text) + { + var handler = TextReceived; + if (handler != null) + handler(this, new GenericCommMethodReceiveTextArgs(text)); + } + + //Private Helper Method to call the Server State Change Event + void onServerStateChange() + { + var handler = ServerStateChange; + if (handler != null) + handler(this, new DynamicTCPServerStateChangedEventArgs(Server, false)); + } + + //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 + } +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs new file mode 100644 index 0000000..5b96684 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs @@ -0,0 +1,505 @@ +/*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; + +namespace PepperDash.Core +{ + public class GenericTcpIpServer : 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 + /// + /// Text representation of the Socket Status enum values for the server + /// + public string Status + { + get + { + if (Server != null) + return Server.State.ToString(); + else + return ServerState.SERVER_NOT_LISTENING.ToString(); + } + + } + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { return (Server != null) && (Server.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 (Server != null) && (Server.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 (Server != null) + return (ushort)Server.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 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 + TCPServer Server; + + #endregion + + #region Constructors + /// + /// constructor + /// + public GenericTcpIpServer() + : base("Uninitialized Dynamic TCP Server") + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + } + #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, "Server '{0}': Invalid port", Key); + ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "Server '{0}': No Shared Key set", Key); + ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); + return; + } + if (IsListening) + return; + Server = new TCPServer(Port, MaxClients); + Server.SocketStatusChange += new TCPServerSocketStatusChangeEventHandler(SocketStatusChange); + ServerStopped = false; + Server.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); + onServerStateChange(); + Debug.Console(2, "Server Status: {0}, Socket Status: {1}\r\n", Server.State.ToString(), Server.ServerSocketStatus); + } + catch (Exception ex) + { + ErrorLog.Error("Error with Dynamic Server: {0}", ex.ToString()); + } + } + + /// + /// Stop Listeneing + /// + public void StopListening() + { + Debug.Console(2, "Stopping Listener"); + if (Server != null) + Server.Stop(); + ServerStopped = true; + onServerStateChange(); + } + + /// + /// Disconnect All Clients + /// + public void DisconnectAllClients() + { + Debug.Console(2, "Disconnecting All Clients"); + if (Server != null) + Server.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); + foreach (uint i in ConnectedClientsIndexes) + Server.SendDataAsync(i, b, b.Length, SendDataAsyncCallback); + } + } + + /// + /// Not sure this is useful in library, maybe Pro?? + /// + /// + /// + public void SendTextToClient(string text, uint clientIndex) + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(text); + Server.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); + } + + //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 - Callbacks + /// + /// Callback to disconnect if heartbeat timer finishes without being reset + /// + /// + void HeartbeatTimer_CallbackFunction(object o) + { + uint clientIndex = (uint)o; + + string address = Server.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); + Server.Disconnect(clientIndex); + HeartbeatTimerDictionary.Remove(clientIndex); + } + + /// + /// TCP Server Socket Status Change Callback + /// + /// + /// + /// + void 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 (Server.ServerSocketStatus.ToString() != Status) + onConnectionChange(); + } + + /// + /// TCP Client Connected to Server Callback + /// + /// + /// + void ConnectCallback(TCPServer myTCPServer, uint clientIndex) + { + if (myTCPServer.ClientConnected(clientIndex)) + { + if (SharedKeyRequired) + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(SharedKey + "\n"); + myTCPServer.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); + 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, ReceivedDataAsyncCallback); + if (myTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) + myTCPServer.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); + } + if (myTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) + myTCPServer.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); + } + + /// + /// Send Data Asyc Callback + /// + /// + /// + /// + void SendDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesSent) + { + //Seems there is nothing to do here + } + + /// + /// Received Data Async Callback + /// + /// + /// + /// + void ReceivedDataAsyncCallback(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(ReceivedDataAsyncCallback); + 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(ReceivedDataAsyncCallback); + } + else + { + myTCPServer.ReceiveDataAsync(ReceivedDataAsyncCallback); + Debug.Console(2, "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, ReceivedDataAsyncCallback); + } + #endregion + + #region Methods - EventHelpers/Callbacks + //Private Helper method to call the Connection Change Event + void onConnectionChange() + { + var handler = ClientConnectionChange; + if (handler != null) + handler(this, new DynamicTCPSocketStatusChangeEventArgs(Server, false)); + } + + //Private Helper Method to call the Text Received Event + void onTextReceived(string text) + { + var handler = TextReceived; + if (handler != null) + handler(this, new GenericCommMethodReceiveTextArgs(text)); + } + + //Private Helper Method to call the Server State Change Event + void onServerStateChange() + { + var handler = ServerStateChange; + if (handler != null) + handler(this, new DynamicTCPServerStateChangedEventArgs(Server, false)); + } + + //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 + } +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj index 8d0a646..4506190 100644 --- a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj +++ b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj @@ -64,10 +64,13 @@ + + + Code + - @@ -76,6 +79,7 @@ + diff --git a/Pepperdash Core/Pepperdash Core/PepperDash_Core.projectinfo b/Pepperdash Core/Pepperdash Core/PepperDash_Core.projectinfo index 700ed77..2060a7f 100644 Binary files a/Pepperdash Core/Pepperdash Core/PepperDash_Core.projectinfo and b/Pepperdash Core/Pepperdash Core/PepperDash_Core.projectinfo differ diff --git a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.clz b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.clz index a4084b6..85471a9 100644 Binary files a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.clz and b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.clz differ diff --git a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.config b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.config index 2913c74..b28fb6d 100644 --- a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.config +++ b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.config @@ -10,8 +10,8 @@ - 3/17/2017 10:28:42 AM - 1.0.6285.17060 + 3/18/2017 5:00:59 PM + 1.0.6286.28828 Crestron.SIMPLSharp, Version=2.0.52.0, Culture=neutral, PublicKeyToken=812d080f93e2de10 diff --git a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.dll b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.dll index 71a2449..acb55a8 100644 Binary files a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.dll and b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.dll differ diff --git a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.pdb b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.pdb index e89f325..f639405 100644 Binary files a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.pdb and b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.pdb differ diff --git a/Pepperdash Core/Pepperdash Core/bin/manifest.info b/Pepperdash Core/Pepperdash Core/bin/manifest.info index c4b0216..dde690b 100644 --- a/Pepperdash Core/Pepperdash Core/bin/manifest.info +++ b/Pepperdash Core/Pepperdash Core/bin/manifest.info @@ -1,4 +1,4 @@ -MainAssembly=PepperDash_Core.dll:1cb57e7d4dc5c0e9b66ba4c496fb831f +MainAssembly=PepperDash_Core.dll:9f208dcbcc49e496f38e91289aee13c4 MainAssemblyMinFirmwareVersion=1.007.0017 MainAssemblyResource=SimplSharpData.dat:315526abf906cded47fb0c7510266a7e ü diff --git a/Pepperdash Core/Pepperdash Core/bin/manifest.ser b/Pepperdash Core/Pepperdash Core/bin/manifest.ser index 3079acb..ecf2682 100644 Binary files a/Pepperdash Core/Pepperdash Core/bin/manifest.ser and b/Pepperdash Core/Pepperdash Core/bin/manifest.ser differ diff --git a/Pepperdash Core/Pepperdash Core/obj/Debug/PepperDash_Core.dll b/Pepperdash Core/Pepperdash Core/obj/Debug/PepperDash_Core.dll index 3c782b2..bc85983 100644 Binary files a/Pepperdash Core/Pepperdash Core/obj/Debug/PepperDash_Core.dll and b/Pepperdash Core/Pepperdash Core/obj/Debug/PepperDash_Core.dll differ diff --git a/Pepperdash Core/Pepperdash Core/obj/Debug/PepperDash_Core.pdb b/Pepperdash Core/Pepperdash Core/obj/Debug/PepperDash_Core.pdb index 8b58d9d..c0dc85d 100644 Binary files a/Pepperdash Core/Pepperdash Core/obj/Debug/PepperDash_Core.pdb and b/Pepperdash Core/Pepperdash Core/obj/Debug/PepperDash_Core.pdb differ