From b3203cf96122a2887401abcecadee36c6b006b29 Mon Sep 17 00:00:00 2001 From: Joshua Gutenplan Date: Mon, 18 Feb 2019 15:49:45 -0800 Subject: [PATCH] updated the secure and non secure servers and added clients that are meant to be used with our server classes. The clients add the shared key and other stability features when we manage the server. --- .../Pepperdash Core/Comm/EventArgs.cs | 74 +- .../Comm/GenericSecureTcpIpClient.cs | 371 -------- .../GenericSecureTcpIpClient_ForServer.cs | 739 ++++++++++++++++ .../Comm/GenericSecureTcpIpServer.cs | 823 +++++++++++++---- .../Comm/GenericTcpIpClient_ForServer.cs | 739 ++++++++++++++++ .../Comm/GenericTcpIpServer.cs | 827 ++++++++++++++---- .../Comm/TcpServerConfigObject.cs | 22 + .../Pepperdash Core/PepperDash_Core.csproj | 4 +- .../Properties/AssemblyInfo.cs | 2 +- 9 files changed, 2858 insertions(+), 743 deletions(-) delete mode 100644 Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs create mode 100644 Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient_ForServer.cs create mode 100644 Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient_ForServer.cs create mode 100644 Pepperdash Core/Pepperdash Core/Comm/TcpServerConfigObject.cs diff --git a/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs b/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs index 3732e0d..ed43c62 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs @@ -18,9 +18,7 @@ using Crestron.SimplSharp.CrestronSockets; namespace PepperDash.Core { - #region GenericSocketStatusChangeEventArgs public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client); - public class GenericSocketStatusChageEventArgs : EventArgs { public ISocketStatus Client { get; private set; } @@ -32,44 +30,68 @@ namespace PepperDash.Core Client = client; } } - #endregion - #region DynamicTCPServerStateChangedEventArgs - public delegate void DynamicTCPServerStateChangedEventDelegate(object server); - - public class DynamicTCPServerStateChangedEventArgs : EventArgs + public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state); + public class GenericTcpServerStateChangedEventArgs : EventArgs { - public bool Secure { get; private set; } - public object Server { get; private set; } + public ServerState State { get; private set; } - public DynamicTCPServerStateChangedEventArgs() { } + public GenericTcpServerStateChangedEventArgs() { } - public DynamicTCPServerStateChangedEventArgs(object server, bool secure) + public GenericTcpServerStateChangedEventArgs(ServerState state) { - Secure = secure; - Server = server; + State = state; } } - #endregion - #region DynamicTCPSocketStatusChangeEventDelegate - public delegate void DynamicTCPSocketStatusChangeEventDelegate(object server); - - public class DynamicTCPSocketStatusChangeEventArgs : EventArgs + public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus); + public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs { - public bool Secure { get; private set; } - public object Server { get; private set; } + public object Socket { get; private set; } + public uint ReceivedFromClientIndex { get; private set; } + public SocketStatus ClientStatus { get; set; } + + public GenericTcpServerSocketStatusChangeEventArgs() { } - public DynamicTCPSocketStatusChangeEventArgs() { } - - public DynamicTCPSocketStatusChangeEventArgs(object server, bool secure) + public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus) { - Secure = secure; - Server = server; + Socket = socket; + ClientStatus = clientStatus; + } + + public GenericTcpServerSocketStatusChangeEventArgs(object socket, uint clientIndex, SocketStatus clientStatus) + { + Socket = socket; + ReceivedFromClientIndex = clientIndex; + ClientStatus = clientStatus; } } - #endregion + public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs + { + public uint ReceivedFromClientIndex { get; private set; } + public string Text { get; private set; } + + public GenericTcpServerCommMethodReceiveTextArgs(string text) + { + Text = text; + } + + public GenericTcpServerCommMethodReceiveTextArgs(string text, uint clientIndex) + { + Text = text; + ReceivedFromClientIndex = clientIndex; + } + } + + public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs + { + public bool IsReady; + public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady) + { + IsReady = isReady; + } + } diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs deleted file mode 100644 index 82305ad..0000000 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs +++ /dev/null @@ -1,371 +0,0 @@ -/*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/GenericSecureTcpIpClient_ForServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient_ForServer.cs new file mode 100644 index 0000000..13e1c66 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient_ForServer.cs @@ -0,0 +1,739 @@ +/*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_ForServer : Device, IAutoReconnect + { + /// + /// Band aid delegate for choked server + /// + internal delegate void ConnectionHasHungCallbackDelegate(); + + #region Events + + //public event EventHandler BytesReceived; + + public event EventHandler TextReceived; + + public event EventHandler ConnectionChange; + + + /// + /// This is something of a band-aid callback. If the client times out during the connection process, because the server + /// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help + /// keep a watch on the server and reset it if necessary. + /// + internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback; + + /// + /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require + /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. + /// + public event EventHandler ClientReadyForCommunications; + + #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; } + + /// + /// Semaphore on connect method + /// + bool IsTryingToConnect; + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get + { + if (Client != null) + return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; + else + return false; + } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// Bool showing if socket is ready for communication after shared key exchange + /// + public bool IsReadyForCommunication { get; set; } + + /// + /// S+ helper for IsReadyForCommunication + /// + public ushort UIsReadyForCommunication + { + get { return (ushort)(IsReadyForCommunication ? 1 : 0); } + } + + /// + /// Client socket status Read only + /// + public SocketStatus ClientStatus + { + get + { + if (Client != null) + return Client.ClientStatus; + else + return SocketStatus.SOCKET_STATUS_NO_CONNECT; + } + } + + /// + /// 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; + + /// + /// private Timer for auto reconnect + /// + CTimer RetryTimer; + + + public bool HeartbeatEnabled { get; set; } + public ushort UHeartbeatEnabled + { + get { return (ushort)(HeartbeatEnabled ? 1 : 0); } + set { HeartbeatEnabled = value == 1; } + } + public string HeartbeatString = "heartbeat"; + public int HeartbeatInterval = 50000; + CTimer HeartbeatSendTimer; + CTimer HeartbeatAckTimer; + /// + /// Used to force disconnection on a dead connect attempt + /// + CTimer ConnectFailTimer; + CTimer WaitForSharedKey; + private int ConnectionCount; + /// + /// Internal secure client + /// + SecureTCPClient Client; + + bool ProgramIsStopping; + + #endregion + + #region Constructors + + //Base class constructor + public GenericSecureTcpIpClient_ForServer(string key, string address, int port, int bufferSize) + : base(key) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Hostname = address; + Port = port; + BufferSize = bufferSize; + AutoReconnectIntervalMs = 5000; + + } + + //base class constructor + public GenericSecureTcpIpClient_ForServer() + : 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 || programEventType == eProgramStatusEventType.Paused) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection"); + ProgramIsStopping = true; + Disconnect(); + } + + } + + /// + /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name. + /// + public void Connect() + { + ConnectionCount++; + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Attempting connect Count:{0}", ConnectionCount); + + + if (IsConnected) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); + return; + } + if (IsTryingToConnect) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); + return; + } + try + { + IsTryingToConnect = true; + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + if (string.IsNullOrEmpty(Hostname)) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); + return; + } + if (Port < 1 || Port > 65535) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); + return; + } + + // clean up previous client + if (Client != null) + { + Cleanup(); + } + DisconnectCalledByUser = false; + + Client = new SecureTCPClient(Hostname, Port, BufferSize); + Client.SocketStatusChange += Client_SocketStatusChange; + Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); + Client.AddressClientConnectedTo = Hostname; + Client.PortNumber = Port; + // SecureClient = c; + + //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); + + ConnectFailTimer = new CTimer(o => + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); + if (IsTryingToConnect) + { + IsTryingToConnect = false; + + //if (ConnectionHasHungCallback != null) + //{ + // ConnectionHasHungCallback(); + //} + //SecureClient.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + } + }, 30000); + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Making Connection Count:{0}", ConnectionCount); + Client.ConnectToServerAsync(o => + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); + + if (ConnectFailTimer != null) + { + ConnectFailTimer.Stop(); + } + IsTryingToConnect = false; + + if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); + o.ReceiveDataAsync(Receive); + + if (SharedKeyRequired) + { + WaitingForSharedKeyResponse = true; + WaitForSharedKey = new CTimer(timer => + { + + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); + // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); + // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup + o.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + //OnClientReadyForcommunications(false); // Should send false event + }, 15000); + } + } + else + { + Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); + CheckClosedAndTryReconnect(); + } + }); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message); + IsTryingToConnect = false; + CheckClosedAndTryReconnect(); + } + } + + /// + /// + /// + public void Disconnect() + { + Debug.Console(2, "Disconnect Called"); + + DisconnectCalledByUser = true; + if (IsConnected) + { + Client.DisconnectFromServer(); + + } + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + Cleanup(); + } + + /// + /// Internal call to close up client. ALWAYS use this when disconnecting. + /// + void Cleanup() + { + IsTryingToConnect = false; + + if (Client != null) + { + //SecureClient.DisconnectFromServer(); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); + Client.SocketStatusChange -= Client_SocketStatusChange; + Client.Dispose(); + Client = null; + } + if (ConnectFailTimer != null) + { + ConnectFailTimer.Stop(); + ConnectFailTimer.Dispose(); + ConnectFailTimer = null; + } + } + + + /// ff + /// Called from Connect failure or Socket Status change if + /// auto reconnect and socket disconnected (Not disconnected by user) + /// + void CheckClosedAndTryReconnect() + { + if (Client != null) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Cleaning up remotely closed/failed connection."); + Cleanup(); + } + if (!DisconnectCalledByUser && AutoReconnect) + { + var halfInterval = AutoReconnectIntervalMs / 2; + var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Attempting reconnect in {0} ms, randomized", rndTime); + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + RetryTimer = new CTimer(o => Connect(), rndTime); + } + } + + /// + /// Receive callback + /// + /// + /// + void Receive(SecureTCPClient client, int numBytes) + { + if (numBytes > 0) + { + string str = string.Empty; + + try + { + var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); + str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); + if (!string.IsNullOrEmpty(checkHeartbeat(str))) + { + + if (SharedKeyRequired && str == "SharedKey:") + { + Debug.Console(1, this, "Server asking for shared key, sending"); + SendText(SharedKey + "\n"); + } + else if (SharedKeyRequired && str == "Shared Key Match") + { + StopWaitForSharedKeyTimer(); + + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Shared key confirmed. Ready for communication"); + OnClientReadyForcommunications(true); // Successful key exchange + } + else if (SharedKeyRequired == false && IsReadyForCommunication == false) + { + OnClientReadyForcommunications(true); // Key not required + } + + else + { + + //var bytesHandler = BytesReceived; + //if (bytesHandler != null) + // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var textHandler = TextReceived; + if (textHandler != null) + textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str)); + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); + } + } + if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + client.ReceiveDataAsync(Receive); + } + + void HeartbeatStart() + { + if (HeartbeatEnabled) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Starting Heartbeat"); + if (HeartbeatSendTimer == null) + { + + HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + } + if (HeartbeatAckTimer == null) + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + } + + } + void HeartbeatStop() + { + + if (HeartbeatSendTimer != null) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stoping Heartbeat Send"); + HeartbeatSendTimer.Stop(); + HeartbeatSendTimer = null; + } + if (HeartbeatAckTimer != null) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stoping Heartbeat Ack"); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer = null; + } + + } + void SendHeartbeat(object notused) + { + this.SendText(HeartbeatString); + Debug.Console(2, this, "Sending Heartbeat"); + + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(string received) + { + try + { + if (HeartbeatEnabled) + { + if (!string.IsNullOrEmpty(HeartbeatString)) + { + var remainingText = received.Replace(HeartbeatString, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatString)) + { + if (HeartbeatAckTimer != null) + { + HeartbeatAckTimer.Reset(HeartbeatInterval * 2); + } + else + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + Debug.Console(1, this, "Heartbeat Received: {0}, from Server", HeartbeatString); + return remainingText; + } + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + } + return received; + } + + + + void HeartbeatAckTimerFail(object o) + { + try + { + + if (IsConnected) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); + SendText("Heartbeat not received by server, closing connection"); + CheckClosedAndTryReconnect(); + } + + } + catch (Exception ex) + { + ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); + } + } + + /// + /// + /// + void StopWaitForSharedKeyTimer() + { + if (WaitForSharedKey != null) + { + WaitForSharedKey.Stop(); + WaitForSharedKey = null; + } + } + + /// + /// General send method + /// + public void SendText(string text) + { + if (!string.IsNullOrEmpty(text)) + { + try + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + Client.SendDataAsync(bytes, bytes.Length, (c, n) => + { + // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? + if (n <= 0) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); + } + }); + } + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); + } + } + } + + /// + /// + /// + public void SendBytes(byte[] bytes) + { + if (bytes.Length > 0) + { + try + { + if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + Client.SendData(bytes, bytes.Length); + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); + } + } + } + + /// + /// SocketStatusChange Callback + /// + /// + /// + void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus) + { + if (ProgramIsStopping) + { + ProgramIsStopping = false; + return; + } + try + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); + + OnConnectionChange(); + // The client could be null or disposed by this time... + if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + HeartbeatStop(); + OnClientReadyForcommunications(false); // socket has gone low + CheckClosedAndTryReconnect(); + } + } + catch (Exception ex) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); + } + } + + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + var handler = ConnectionChange; + if (handler != null) + ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus)); + } + + /// + /// Helper to fire ClientReadyForCommunications event + /// + void OnClientReadyForcommunications(bool isReady) + { + IsReadyForCommunication = isReady; + if (this.IsReadyForCommunication) { HeartbeatStart(); } + var handler = ClientReadyForCommunications; + if (handler != null) + handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); + } + #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 index 365cbcb..58f46c8 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs @@ -15,6 +15,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace PepperDash.Core { @@ -24,20 +26,61 @@ namespace PepperDash.Core /// /// Event for Receiving text /// - public event EventHandler TextReceived; + public event EventHandler TextReceived; /// /// Event for client connection socket status change /// - public event EventHandler ClientConnectionChange; + public event EventHandler ClientConnectionChange; /// /// Event for Server State Change /// - public event EventHandler ServerStateChange; + public event EventHandler ServerStateChange; + + /// + /// For a server with a pre shared key, this will fire after the communication is established and the key exchange is complete. If no shared key, this will fire + /// after connection is successful. Use this event to know when the client is ready for communication to avoid stepping on shared key. + /// + public event EventHandler ServerClientReadyForCommunications; + + /// + /// A band aid event to notify user that the server has choked. + /// + public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; } + + public delegate void ServerHasChokedCallbackDelegate(); + #endregion #region Properties/Variables + + /// + /// + /// + CCriticalSection ServerCCSection = new CCriticalSection(); + + + /// + /// A bandaid client that monitors whether the server is reachable + /// + GenericSecureTcpIpClient_ForServer MonitorClient; + + /// + /// Timer to operate the bandaid monitor client in a loop. + /// + CTimer MonitorClientTimer; + + /// + /// + /// + int MonitorClientFailureCount; + + /// + /// 3 by default + /// + public int MonitorClientMaxFailureCount { get; set; } + /// /// Text representation of the Socket Status enum values for the server /// @@ -45,10 +88,10 @@ namespace PepperDash.Core { get { - if (Server != null) - return Server.State.ToString(); - else - return ServerState.SERVER_NOT_LISTENING.ToString(); + if (SecureServer != null) + return SecureServer.State.ToString(); + return ServerState.SERVER_NOT_LISTENING.ToString(); + } } @@ -58,7 +101,16 @@ namespace PepperDash.Core /// public bool IsConnected { - get { return (Server != null) && (Server.State == ServerState.SERVER_CONNECTED); } + get + { + if (SecureServer != null) + return (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED; + return false; + + //return (Secure ? SecureServer != null : UnsecureServer != null) && + //(Secure ? (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED : + // (UnsecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED); + } } /// @@ -74,7 +126,16 @@ namespace PepperDash.Core /// public bool IsListening { - get { return (Server != null) && (Server.State == ServerState.SERVER_LISTENING); } + get + { + if (SecureServer != null) + return (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING; + else + return false; + //return (Secure ? SecureServer != null : UnsecureServer != null) && + //(Secure ? (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING : + // (UnsecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING); + } } /// @@ -93,8 +154,8 @@ namespace PepperDash.Core { get { - if (Server != null) - return (ushort)Server.NumberOfClientsConnected; + if (SecureServer != null) + return (ushort)SecureServer.NumberOfClientsConnected; return 0; } } @@ -175,11 +236,13 @@ namespace PepperDash.Core //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 + //flags to show the secure server is waiting for client at index to send the shared key List WaitingForSharedKey = new List(); + List ClientReadyAfterKeyExchange = new List(); + //Store the connected client indexes - List ConnectedClientsIndexes = new List(); + public List ConnectedClientsIndexes = new List(); /// /// Defaults to 2000 @@ -192,23 +255,72 @@ namespace PepperDash.Core private bool ServerStopped { get; set; } //Servers - SecureTCPServer Server; + SecureTCPServer SecureServer; + + /// + /// + /// + bool ProgramIsStopping; #endregion #region Constructors /// - /// constructor + /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually. /// public GenericSecureTcpIpServer() : base("Uninitialized Dynamic TCP Server") { + HeartbeatRequiredIntervalInSeconds = 15; CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + } + + /// + /// constructor with debug key set at instantiation. Make sure to set all properties before listening. + /// + /// + public GenericSecureTcpIpServer(string key) + : base("Uninitialized Dynamic TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + Key = key; + } + + /// + /// Contstructor that sets all properties by calling the initialize method with a config object. + /// + /// + public GenericSecureTcpIpServer(TcpServerConfigObject serverConfigObject) + : base("Uninitialized Dynamic TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + Initialize(serverConfigObject); } #endregion #region Methods - Server Actions + /// + /// Disconnects all clients and stops the server + /// + public void KillServer() + { + ServerStopped = true; + if (MonitorClient != null) + { + MonitorClient.Disconnect(); + } + DisconnectAllClientsForShutdown(); + StopListening(); + } + /// /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ /// @@ -218,37 +330,83 @@ namespace PepperDash.Core Key = key; } + public void Initialize(TcpServerConfigObject serverConfigObject) + { + try + { + if (serverConfigObject != null || string.IsNullOrEmpty(serverConfigObject.Key)) + { + Key = serverConfigObject.Key; + MaxClients = serverConfigObject.MaxClients; + Port = serverConfigObject.Port; + SharedKeyRequired = serverConfigObject.SharedKeyRequired; + SharedKey = serverConfigObject.SharedKey; + HeartbeatRequired = serverConfigObject.HeartbeatRequired; + HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds; + HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch; + BufferSize = serverConfigObject.BufferSize; + + } + else + { + ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); + } + } + catch + { + ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); + } + } + /// /// Start listening on the specified port /// public void Listen() { + ServerCCSection.Enter(); try { if (Port < 1 || Port > 65535) { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "Server '{0}': Invalid port", Key); + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "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); + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "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); + + if (SecureServer == null) + { + SecureServer = new SecureTCPServer(Port, MaxClients); + SecureServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5); + SecureServer.HandshakeTimeout = 30; + SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange); + } + else + { + KillServer(); + SecureServer.PortNumber = Port; + } ServerStopped = false; - Server.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); - onServerStateChange(); - Debug.Console(2, "Server Status: {0}, Socket Status: {1}\r\n", Server.State.ToString(), Server.ServerSocketStatus); + SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); + OnServerStateChange(SecureServer.State); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus); + + // StartMonitorClient(); + + + ServerCCSection.Leave(); } catch (Exception ex) { - ErrorLog.Error("Error with Dynamic Server: {0}", ex.ToString()); + ServerCCSection.Leave(); + ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key); } } @@ -257,23 +415,80 @@ namespace PepperDash.Core /// public void StopListening() { - Debug.Console(2, "Stopping Listener"); - if (Server != null) - Server.Stop(); - ServerStopped = true; - onServerStateChange(); + try + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); + if (SecureServer != null) + { + SecureServer.Stop(); + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State); + //SecureServer = null; + } + + ServerStopped = true; + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server Stopped"); + + OnServerStateChange(SecureServer.State); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); + } } + /// + /// Disconnects Client + /// + /// + public void DisconnectClient(uint client) + { + try + { + SecureServer.Disconnect(client); + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); + } + } /// /// Disconnect All Clients /// - public void DisconnectAllClients() + public void DisconnectAllClientsForShutdown() { - Debug.Console(2, "Disconnecting All Clients"); - if (Server != null) - Server.DisconnectAll(); - onConnectionChange(); - onServerStateChange(); //State shows both listening and connected + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); + if (SecureServer != null) + { + SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange; + foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly + { + var i = index; + if (!SecureServer.ClientConnected(index)) + continue; + try + { + SecureServer.Disconnect(i); + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); + } + } + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); + } + + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); + ConnectedClientsIndexes.Clear(); + + if (!ProgramIsStopping) + { + OnConnectionChange(); + OnServerStateChange(SecureServer.State); //State shows both listening and connected + } + + // var o = new { }; } /// @@ -282,11 +497,29 @@ namespace PepperDash.Core /// public void BroadcastText(string text) { - if (ConnectedClientsIndexes.Count > 0) + CCriticalSection CCBroadcast = new CCriticalSection(); + CCBroadcast.Enter(); + try { - byte[] b = Encoding.GetEncoding(28591).GetBytes(text); - foreach (uint i in ConnectedClientsIndexes) - Server.SendDataAsync(i, b, b.Length, SendDataAsyncCallback); + if (ConnectedClientsIndexes.Count > 0) + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(text); + foreach (uint i in ConnectedClientsIndexes) + { + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i))) + { + SocketErrorCodes error = SecureServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); + if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) + Debug.Console(0, error.ToString()); + } + } + } + CCBroadcast.Leave(); + } + catch (Exception ex) + { + CCBroadcast.Leave(); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message); } } @@ -297,18 +530,48 @@ namespace PepperDash.Core /// public void SendTextToClient(string text, uint clientIndex) { - byte[] b = Encoding.GetEncoding(28591).GetBytes(text); - Server.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); + try + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(text); + if (SecureServer != null && SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + { + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) + SecureServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); + } + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + } } //private method to check heartbeat requirements and start or reset timer - void checkHeartbeat(uint clientIndex, string received) + string checkHeartbeat(uint clientIndex, string received) { - if (HeartbeatRequired) + try { - if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) + if (HeartbeatRequired) { - if (received == HeartbeatStringToMatch) + if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) + { + var remainingText = received.Replace(HeartbeatStringToMatch, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatStringToMatch)) + { + if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) + HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); + else + { + CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); + HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + } + Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); + // Return Heartbeat + SendTextToClient(HeartbeatStringToMatch, clientIndex); + return remainingText; + } + } + else { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); @@ -317,187 +580,405 @@ namespace PepperDash.Core CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } + Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + } + return received; + } + + public string GetClientIPAddress(uint clientIndex) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) + { + var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); + return ipa; + + } + else + { + return ""; + } + } + + #endregion + + #region Methods - HeartbeatTimer Callback + + void HeartbeatTimer_CallbackFunction(object o) + { + uint clientIndex = 99999; + string address = string.Empty; + try + { + clientIndex = (uint)o; + address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", + address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); + + if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); + + var discoResult = SecureServer.Disconnect(clientIndex); + //Debug.Console(1, this, "{0}", discoResult); + + if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) + { + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Dispose(); + HeartbeatTimerDictionary.Remove(clientIndex); + } + } + catch (Exception ex) + { + ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key); + } + } + + #endregion + + #region Methods - Socket Status Changed Callbacks + /// + /// Secure Server Socket Status Changed Callback + /// + /// + /// + /// + void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus) + { + try + { + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + if (ConnectedClientsIndexes.Contains(clientIndex)) + ConnectedClientsIndexes.Remove(clientIndex); + if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) + { + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Dispose(); + HeartbeatTimerDictionary.Remove(clientIndex); + } + if (ClientReadyAfterKeyExchange.Contains(clientIndex)) + ClientReadyAfterKeyExchange.Remove(clientIndex); + } + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); + } + onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); + } + + #endregion + + #region Methods Connected Callbacks + /// + /// Secure TCP Client Connected to Secure Server Callback + /// + /// + /// + void SecureConnectCallback(SecureTCPServer server, uint clientIndex) + { + try + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", + server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), + clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); + if (clientIndex != 0) + { + if (server.ClientConnected(clientIndex)) + { + + if (!ConnectedClientsIndexes.Contains(clientIndex)) + { + ConnectedClientsIndexes.Add(clientIndex); + } + if (SharedKeyRequired) + { + if (!WaitingForSharedKey.Contains(clientIndex)) + { + WaitingForSharedKey.Add(clientIndex); + } + byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); + server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + } + else + { + OnServerClientReadyForCommunications(clientIndex); + } + if (HeartbeatRequired) + { + if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) + { + HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); + } + } + + server.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); } } else { - if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) - HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); - else + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); + if (!ServerStopped) { - CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); - HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); + return; } } } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + } + //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", + // server.State, + // MaxClients, + // ServerStopped); + if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection"); + server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); + + } } + #endregion - #region Methods - Callbacks + #region Methods - Send/Receive 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 + /// Secure Received Data Async Callback /// /// /// /// - void ReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived) + 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)) + try { - received = received.Replace("\r", ""); - received = received.Replace("\n", ""); - if (received != SharedKey) + byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); + received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived); + if (WaitingForSharedKey.Contains(clientIndex)) { - 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); + 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(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); + mySecureTCPServer.SendData(clientIndex, b, b.Length); + mySecureTCPServer.Disconnect(clientIndex); + WaitingForSharedKey.Remove(clientIndex); + return; + } + if (mySecureTCPServer.NumberOfClientsConnected > 0) + mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); + WaitingForSharedKey.Remove(clientIndex); + byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); + mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); + OnServerClientReadyForCommunications(clientIndex); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); + return; } - 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); + //var address = mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + //Debug.Console(1, this, "Secure Server Listening on Port: {0}, client IP: {1}, Client Index: {4}, NumberOfBytesReceived: {2}, Received: {3}\r\n", + // mySecureTCPServer.PortNumber.ToString(), address , numberOfBytesReceived.ToString(), received, clientIndex.ToString()); + if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) + onTextReceived(received, clientIndex); } - else + catch (Exception ex) { - 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); + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); } - checkHeartbeat(clientIndex, received); } if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) - mySecureTCPServer.ReceiveDataAsync(clientIndex, ReceivedDataAsyncCallback); + mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); } + #endregion #region Methods - EventHelpers/Callbacks + //Private Helper method to call the Connection Change Event - void onConnectionChange() + void onConnectionChange(uint clientIndex, SocketStatus clientStatus) { + if (clientIndex != 0) //0 is error not valid client change + { + var handler = ClientConnectionChange; + if (handler != null) + { + handler(this, new GenericTcpServerSocketStatusChangeEventArgs(SecureServer, clientIndex, clientStatus)); + } + } + } + + //Private Helper method to call the Connection Change Event + void OnConnectionChange() + { + if (ProgramIsStopping) + { + return; + } var handler = ClientConnectionChange; if (handler != null) - handler(this, new DynamicTCPSocketStatusChangeEventArgs(Server, false)); + { + handler(this, new GenericTcpServerSocketStatusChangeEventArgs()); + } } //Private Helper Method to call the Text Received Event - void onTextReceived(string text) + void onTextReceived(string text, uint clientIndex) { var handler = TextReceived; if (handler != null) - handler(this, new GenericCommMethodReceiveTextArgs(text)); + handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex)); } //Private Helper Method to call the Server State Change Event - void onServerStateChange() + void OnServerStateChange(ServerState state) { + if (ProgramIsStopping) + { + return; + } var handler = ServerStateChange; if (handler != null) - handler(this, new DynamicTCPServerStateChangedEventArgs(Server, false)); + { + handler(this, new GenericTcpServerStateChangedEventArgs(state)); + } } - //Private Event Handler method to handle the closing of connections when the program stops + /// + /// 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(); + ProgramIsStopping = true; + // kill bandaid things + if (MonitorClientTimer != null) + MonitorClientTimer.Stop(); + if (MonitorClient != null) + MonitorClient.Disconnect(); + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); + KillServer(); + } + } + + //Private event handler method to raise the event that the server is ready to send data after a successful client shared key negotiation + void OnServerClientReadyForCommunications(uint clientIndex) + { + ClientReadyAfterKeyExchange.Add(clientIndex); + var handler = ServerClientReadyForCommunications; + if (handler != null) + handler(this, new GenericTcpServerSocketStatusChangeEventArgs( + this, clientIndex, SecureServer.GetServerSocketStatusForSpecificClient(clientIndex))); + } + #endregion + + #region Monitor Client + /// + /// Starts the monitor client cycle. Timed wait, then call RunMonitorClient + /// + void StartMonitorClient() + { + if (MonitorClientTimer != null) + { + return; + } + MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); + } + + /// + /// + /// + void RunMonitorClient() + { + MonitorClient = new GenericSecureTcpIpClient_ForServer(Key + "-MONITOR", "127.0.0.1", Port, 2000); + MonitorClient.SharedKeyRequired = this.SharedKeyRequired; + MonitorClient.SharedKey = this.SharedKey; + MonitorClient.ConnectionHasHungCallback = MonitorClientHasHungCallback; + //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; + MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); + + MonitorClient.Connect(); + // From here MonitorCLient either connects or hangs, MonitorClient will call back + + } + + /// + /// + /// + void StopMonitorClient() + { + if (MonitorClient == null) + return; + + MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm; + MonitorClient.Disconnect(); + MonitorClient = null; + } + + /// + /// On monitor connect, restart the operation + /// + void MonitorClient_IsReadyForComm(object sender, GenericTcpServerClientReadyForcommunicationsEventArgs args) + { + if (args.IsReady) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); + MonitorClientTimer.Stop(); + MonitorClientTimer = null; + MonitorClientFailureCount = 0; + CrestronEnvironment.Sleep(2000); + StopMonitorClient(); + StartMonitorClient(); + } + } + + /// + /// If the client hangs, add to counter and maybe fire the choke event + /// + void MonitorClientHasHungCallback() + { + MonitorClientFailureCount++; + MonitorClientTimer.Stop(); + MonitorClientTimer = null; + StopMonitorClient(); + if (MonitorClientFailureCount < MonitorClientMaxFailureCount) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", + MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); + StartMonitorClient(); + } + else + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, + "\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************", + MonitorClientMaxFailureCount); + + var handler = ServerHasChoked; + if (handler != null) + handler(); + // Some external thing is in charge here. Expected reset of program } } #endregion diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient_ForServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient_ForServer.cs new file mode 100644 index 0000000..820f5bb --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient_ForServer.cs @@ -0,0 +1,739 @@ +/*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 GenericTcpIpClient_ForServer : Device, IAutoReconnect + { + /// + /// Band aid delegate for choked server + /// + internal delegate void ConnectionHasHungCallbackDelegate(); + + #region Events + + //public event EventHandler BytesReceived; + + public event EventHandler TextReceived; + + public event EventHandler ConnectionChange; + + + /// + /// This is something of a band-aid callback. If the client times out during the connection process, because the server + /// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help + /// keep a watch on the server and reset it if necessary. + /// + internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback; + + /// + /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require + /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. + /// + public event EventHandler ClientReadyForCommunications; + + #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; } + + /// + /// Semaphore on connect method + /// + bool IsTryingToConnect; + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get + { + if (Client != null) + return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; + else + return false; + } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// Bool showing if socket is ready for communication after shared key exchange + /// + public bool IsReadyForCommunication { get; set; } + + /// + /// S+ helper for IsReadyForCommunication + /// + public ushort UIsReadyForCommunication + { + get { return (ushort)(IsReadyForCommunication ? 1 : 0); } + } + + /// + /// Client socket status Read only + /// + public SocketStatus ClientStatus + { + get + { + if (Client != null) + return Client.ClientStatus; + else + return SocketStatus.SOCKET_STATUS_NO_CONNECT; + } + } + + /// + /// 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; + + /// + /// private Timer for auto reconnect + /// + CTimer RetryTimer; + + + public bool HeartbeatEnabled { get; set; } + public ushort UHeartbeatEnabled + { + get { return (ushort)(HeartbeatEnabled ? 1 : 0); } + set { HeartbeatEnabled = value == 1; } + } + public string HeartbeatString = "heartbeat"; + public int HeartbeatInterval = 50000; + CTimer HeartbeatSendTimer; + CTimer HeartbeatAckTimer; + /// + /// Used to force disconnection on a dead connect attempt + /// + CTimer ConnectFailTimer; + CTimer WaitForSharedKey; + private int ConnectionCount; + /// + /// Internal secure client + /// + TCPClient Client; + + bool ProgramIsStopping; + + #endregion + + #region Constructors + + //Base class constructor + public GenericTcpIpClient_ForServer(string key, string address, int port, int bufferSize) + : base(key) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Hostname = address; + Port = port; + BufferSize = bufferSize; + AutoReconnectIntervalMs = 5000; + + } + + //base class constructor + public GenericTcpIpClient_ForServer() + : 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 || programEventType == eProgramStatusEventType.Paused) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection"); + ProgramIsStopping = true; + Disconnect(); + } + + } + + /// + /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name. + /// + public void Connect() + { + ConnectionCount++; + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Attempting connect Count:{0}", ConnectionCount); + + + if (IsConnected) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); + return; + } + if (IsTryingToConnect) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); + return; + } + try + { + IsTryingToConnect = true; + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + if (string.IsNullOrEmpty(Hostname)) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); + return; + } + if (Port < 1 || Port > 65535) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); + return; + } + + // clean up previous client + if (Client != null) + { + Cleanup(); + } + DisconnectCalledByUser = false; + + Client = new TCPClient(Hostname, Port, BufferSize); + Client.SocketStatusChange += Client_SocketStatusChange; + Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); + Client.AddressClientConnectedTo = Hostname; + Client.PortNumber = Port; + // SecureClient = c; + + //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); + + ConnectFailTimer = new CTimer(o => + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); + if (IsTryingToConnect) + { + IsTryingToConnect = false; + + //if (ConnectionHasHungCallback != null) + //{ + // ConnectionHasHungCallback(); + //} + //SecureClient.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + } + }, 30000); + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Making Connection Count:{0}", ConnectionCount); + Client.ConnectToServerAsync(o => + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); + + if (ConnectFailTimer != null) + { + ConnectFailTimer.Stop(); + } + IsTryingToConnect = false; + + if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); + o.ReceiveDataAsync(Receive); + + if (SharedKeyRequired) + { + WaitingForSharedKeyResponse = true; + WaitForSharedKey = new CTimer(timer => + { + + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); + // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); + // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup + o.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + //OnClientReadyForcommunications(false); // Should send false event + }, 15000); + } + } + else + { + Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); + CheckClosedAndTryReconnect(); + } + }); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message); + IsTryingToConnect = false; + CheckClosedAndTryReconnect(); + } + } + + /// + /// + /// + public void Disconnect() + { + Debug.Console(2, "Disconnect Called"); + + DisconnectCalledByUser = true; + if (IsConnected) + { + Client.DisconnectFromServer(); + + } + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + Cleanup(); + } + + /// + /// Internal call to close up client. ALWAYS use this when disconnecting. + /// + void Cleanup() + { + IsTryingToConnect = false; + + if (Client != null) + { + //SecureClient.DisconnectFromServer(); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); + Client.SocketStatusChange -= Client_SocketStatusChange; + Client.Dispose(); + Client = null; + } + if (ConnectFailTimer != null) + { + ConnectFailTimer.Stop(); + ConnectFailTimer.Dispose(); + ConnectFailTimer = null; + } + } + + + /// ff + /// Called from Connect failure or Socket Status change if + /// auto reconnect and socket disconnected (Not disconnected by user) + /// + void CheckClosedAndTryReconnect() + { + if (Client != null) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Cleaning up remotely closed/failed connection."); + Cleanup(); + } + if (!DisconnectCalledByUser && AutoReconnect) + { + var halfInterval = AutoReconnectIntervalMs / 2; + var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Attempting reconnect in {0} ms, randomized", rndTime); + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + RetryTimer = new CTimer(o => Connect(), rndTime); + } + } + + /// + /// Receive callback + /// + /// + /// + void Receive(TCPClient client, int numBytes) + { + if (numBytes > 0) + { + string str = string.Empty; + + try + { + var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); + str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); + if (!string.IsNullOrEmpty(checkHeartbeat(str))) + { + + if (SharedKeyRequired && str == "SharedKey:") + { + Debug.Console(1, this, "Server asking for shared key, sending"); + SendText(SharedKey + "\n"); + } + else if (SharedKeyRequired && str == "Shared Key Match") + { + StopWaitForSharedKeyTimer(); + + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Shared key confirmed. Ready for communication"); + OnClientReadyForcommunications(true); // Successful key exchange + } + else if (SharedKeyRequired == false && IsReadyForCommunication == false) + { + OnClientReadyForcommunications(true); // Key not required + } + + else + { + + //var bytesHandler = BytesReceived; + //if (bytesHandler != null) + // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var textHandler = TextReceived; + if (textHandler != null) + textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str)); + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); + } + } + if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + client.ReceiveDataAsync(Receive); + } + + void HeartbeatStart() + { + if (HeartbeatEnabled) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Starting Heartbeat"); + if (HeartbeatSendTimer == null) + { + + HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + } + if (HeartbeatAckTimer == null) + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + } + + } + void HeartbeatStop() + { + + if (HeartbeatSendTimer != null) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stoping Heartbeat Send"); + HeartbeatSendTimer.Stop(); + HeartbeatSendTimer = null; + } + if (HeartbeatAckTimer != null) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stoping Heartbeat Ack"); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer = null; + } + + } + void SendHeartbeat(object notused) + { + this.SendText(HeartbeatString); + Debug.Console(2, this, "Sending Heartbeat"); + + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(string received) + { + try + { + if (HeartbeatEnabled) + { + if (!string.IsNullOrEmpty(HeartbeatString)) + { + var remainingText = received.Replace(HeartbeatString, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatString)) + { + if (HeartbeatAckTimer != null) + { + HeartbeatAckTimer.Reset(HeartbeatInterval * 2); + } + else + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + Debug.Console(1, this, "Heartbeat Received: {0}, from Server", HeartbeatString); + return remainingText; + } + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + } + return received; + } + + + + void HeartbeatAckTimerFail(object o) + { + try + { + + if (IsConnected) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); + SendText("Heartbeat not received by server, closing connection"); + CheckClosedAndTryReconnect(); + } + + } + catch (Exception ex) + { + ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); + } + } + + /// + /// + /// + void StopWaitForSharedKeyTimer() + { + if (WaitForSharedKey != null) + { + WaitForSharedKey.Stop(); + WaitForSharedKey = null; + } + } + + /// + /// General send method + /// + public void SendText(string text) + { + if (!string.IsNullOrEmpty(text)) + { + try + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + Client.SendDataAsync(bytes, bytes.Length, (c, n) => + { + // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? + if (n <= 0) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); + } + }); + } + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); + } + } + } + + /// + /// + /// + public void SendBytes(byte[] bytes) + { + if (bytes.Length > 0) + { + try + { + if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + Client.SendData(bytes, bytes.Length); + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); + } + } + } + + /// + /// SocketStatusChange Callback + /// + /// + /// + void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) + { + if (ProgramIsStopping) + { + ProgramIsStopping = false; + return; + } + try + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); + + OnConnectionChange(); + // The client could be null or disposed by this time... + if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + HeartbeatStop(); + OnClientReadyForcommunications(false); // socket has gone low + CheckClosedAndTryReconnect(); + } + } + catch (Exception ex) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); + } + } + + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + var handler = ConnectionChange; + if (handler != null) + ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus)); + } + + /// + /// Helper to fire ClientReadyForCommunications event + /// + void OnClientReadyForcommunications(bool isReady) + { + IsReadyForCommunication = isReady; + if (this.IsReadyForCommunication) { HeartbeatStart(); } + var handler = ClientReadyForCommunications; + if (handler != null) + handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); + } + #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 index 5b96684..83add98 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs @@ -15,6 +15,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace PepperDash.Core { @@ -24,20 +26,61 @@ namespace PepperDash.Core /// /// Event for Receiving text /// - public event EventHandler TextReceived; + public event EventHandler TextReceived; /// /// Event for client connection socket status change /// - public event EventHandler ClientConnectionChange; + public event EventHandler ClientConnectionChange; /// /// Event for Server State Change /// - public event EventHandler ServerStateChange; + public event EventHandler ServerStateChange; + + /// + /// For a server with a pre shared key, this will fire after the communication is established and the key exchange is complete. If no shared key, this will fire + /// after connection is successful. Use this event to know when the client is ready for communication to avoid stepping on shared key. + /// + public event EventHandler ServerClientReadyForCommunications; + + /// + /// A band aid event to notify user that the server has choked. + /// + public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; } + + public delegate void ServerHasChokedCallbackDelegate(); + #endregion #region Properties/Variables + + /// + /// + /// + CCriticalSection ServerCCSection = new CCriticalSection(); + + + /// + /// A bandaid client that monitors whether the server is reachable + /// + GenericTcpIpClient_ForServer MonitorClient; + + /// + /// Timer to operate the bandaid monitor client in a loop. + /// + CTimer MonitorClientTimer; + + /// + /// + /// + int MonitorClientFailureCount; + + /// + /// 3 by default + /// + public int MonitorClientMaxFailureCount { get; set; } + /// /// Text representation of the Socket Status enum values for the server /// @@ -45,10 +88,10 @@ namespace PepperDash.Core { get { - if (Server != null) - return Server.State.ToString(); - else - return ServerState.SERVER_NOT_LISTENING.ToString(); + if (SecureServer != null) + return SecureServer.State.ToString(); + return ServerState.SERVER_NOT_LISTENING.ToString(); + } } @@ -58,7 +101,16 @@ namespace PepperDash.Core /// public bool IsConnected { - get { return (Server != null) && (Server.State == ServerState.SERVER_CONNECTED); } + get + { + if (SecureServer != null) + return (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED; + return false; + + //return (Secure ? SecureServer != null : UnsecureServer != null) && + //(Secure ? (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED : + // (UnsecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED); + } } /// @@ -74,7 +126,16 @@ namespace PepperDash.Core /// public bool IsListening { - get { return (Server != null) && (Server.State == ServerState.SERVER_LISTENING); } + get + { + if (SecureServer != null) + return (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING; + else + return false; + //return (Secure ? SecureServer != null : UnsecureServer != null) && + //(Secure ? (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING : + // (UnsecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING); + } } /// @@ -93,8 +154,8 @@ namespace PepperDash.Core { get { - if (Server != null) - return (ushort)Server.NumberOfClientsConnected; + if (SecureServer != null) + return (ushort)SecureServer.NumberOfClientsConnected; return 0; } } @@ -175,11 +236,13 @@ namespace PepperDash.Core //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 + //flags to show the secure server is waiting for client at index to send the shared key List WaitingForSharedKey = new List(); + List ClientReadyAfterKeyExchange = new List(); + //Store the connected client indexes - List ConnectedClientsIndexes = new List(); + public List ConnectedClientsIndexes = new List(); /// /// Defaults to 2000 @@ -192,23 +255,72 @@ namespace PepperDash.Core private bool ServerStopped { get; set; } //Servers - TCPServer Server; + SecureTCPServer SecureServer; + + /// + /// + /// + bool ProgramIsStopping; #endregion #region Constructors /// - /// constructor + /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually. /// public GenericTcpIpServer() : base("Uninitialized Dynamic TCP Server") { + HeartbeatRequiredIntervalInSeconds = 15; CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + } + + /// + /// constructor with debug key set at instantiation. Make sure to set all properties before listening. + /// + /// + public GenericTcpIpServer(string key) + : base("Uninitialized Dynamic TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + Key = key; + } + + /// + /// Contstructor that sets all properties by calling the initialize method with a config object. + /// + /// + public GenericTcpIpServer(TcpServerConfigObject serverConfigObject) + : base("Uninitialized Dynamic TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + Initialize(serverConfigObject); } #endregion #region Methods - Server Actions + /// + /// Disconnects all clients and stops the server + /// + public void KillServer() + { + ServerStopped = true; + if (MonitorClient != null) + { + MonitorClient.Disconnect(); + } + DisconnectAllClientsForShutdown(); + StopListening(); + } + /// /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ /// @@ -218,37 +330,83 @@ namespace PepperDash.Core Key = key; } + public void Initialize(TcpServerConfigObject serverConfigObject) + { + try + { + if (serverConfigObject != null || string.IsNullOrEmpty(serverConfigObject.Key)) + { + Key = serverConfigObject.Key; + MaxClients = serverConfigObject.MaxClients; + Port = serverConfigObject.Port; + SharedKeyRequired = serverConfigObject.SharedKeyRequired; + SharedKey = serverConfigObject.SharedKey; + HeartbeatRequired = serverConfigObject.HeartbeatRequired; + HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds; + HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch; + BufferSize = serverConfigObject.BufferSize; + + } + else + { + ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); + } + } + catch + { + ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); + } + } + /// /// Start listening on the specified port /// public void Listen() { + ServerCCSection.Enter(); try { if (Port < 1 || Port > 65535) { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "Server '{0}': Invalid port", Key); + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "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); + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "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); + + if (SecureServer == null) + { + SecureServer = new SecureTCPServer(Port, MaxClients); + SecureServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5); + SecureServer.HandshakeTimeout = 30; + SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange); + } + else + { + KillServer(); + SecureServer.PortNumber = Port; + } ServerStopped = false; - Server.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); - onServerStateChange(); - Debug.Console(2, "Server Status: {0}, Socket Status: {1}\r\n", Server.State.ToString(), Server.ServerSocketStatus); + SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); + OnServerStateChange(SecureServer.State); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus); + + // StartMonitorClient(); + + + ServerCCSection.Leave(); } catch (Exception ex) { - ErrorLog.Error("Error with Dynamic Server: {0}", ex.ToString()); + ServerCCSection.Leave(); + ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key); } } @@ -257,23 +415,80 @@ namespace PepperDash.Core /// public void StopListening() { - Debug.Console(2, "Stopping Listener"); - if (Server != null) - Server.Stop(); - ServerStopped = true; - onServerStateChange(); + try + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); + if (SecureServer != null) + { + SecureServer.Stop(); + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State); + //SecureServer = null; + } + + ServerStopped = true; + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server Stopped"); + + OnServerStateChange(SecureServer.State); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); + } } + /// + /// Disconnects Client + /// + /// + public void DisconnectClient(uint client) + { + try + { + SecureServer.Disconnect(client); + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); + } + } /// /// Disconnect All Clients /// - public void DisconnectAllClients() + public void DisconnectAllClientsForShutdown() { - Debug.Console(2, "Disconnecting All Clients"); - if (Server != null) - Server.DisconnectAll(); - onConnectionChange(); - onServerStateChange(); //State shows both listening and connected + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); + if (SecureServer != null) + { + SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange; + foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly + { + var i = index; + if (!SecureServer.ClientConnected(index)) + continue; + try + { + SecureServer.Disconnect(i); + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); + } + } + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); + } + + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); + ConnectedClientsIndexes.Clear(); + + if (!ProgramIsStopping) + { + OnConnectionChange(); + OnServerStateChange(SecureServer.State); //State shows both listening and connected + } + + // var o = new { }; } /// @@ -282,11 +497,29 @@ namespace PepperDash.Core /// public void BroadcastText(string text) { - if (ConnectedClientsIndexes.Count > 0) + CCriticalSection CCBroadcast = new CCriticalSection(); + CCBroadcast.Enter(); + try { - byte[] b = Encoding.GetEncoding(28591).GetBytes(text); - foreach (uint i in ConnectedClientsIndexes) - Server.SendDataAsync(i, b, b.Length, SendDataAsyncCallback); + if (ConnectedClientsIndexes.Count > 0) + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(text); + foreach (uint i in ConnectedClientsIndexes) + { + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i))) + { + SocketErrorCodes error = SecureServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); + if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) + Debug.Console(0, error.ToString()); + } + } + } + CCBroadcast.Leave(); + } + catch (Exception ex) + { + CCBroadcast.Leave(); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message); } } @@ -297,18 +530,48 @@ namespace PepperDash.Core /// public void SendTextToClient(string text, uint clientIndex) { - byte[] b = Encoding.GetEncoding(28591).GetBytes(text); - Server.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); + try + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(text); + if (SecureServer != null && SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + { + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) + SecureServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); + } + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + } } //private method to check heartbeat requirements and start or reset timer - void checkHeartbeat(uint clientIndex, string received) + string checkHeartbeat(uint clientIndex, string received) { - if (HeartbeatRequired) + try { - if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) + if (HeartbeatRequired) { - if (received == HeartbeatStringToMatch) + if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) + { + var remainingText = received.Replace(HeartbeatStringToMatch, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatStringToMatch)) + { + if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) + HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); + else + { + CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); + HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + } + Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); + // Return Heartbeat + SendTextToClient(HeartbeatStringToMatch, clientIndex); + return remainingText; + } + } + else { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); @@ -317,187 +580,405 @@ namespace PepperDash.Core CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } + Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + } + return received; + } + + public string GetClientIPAddress(uint clientIndex) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) + { + var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); + return ipa; + + } + else + { + return ""; + } + } + + #endregion + + #region Methods - HeartbeatTimer Callback + + void HeartbeatTimer_CallbackFunction(object o) + { + uint clientIndex = 99999; + string address = string.Empty; + try + { + clientIndex = (uint)o; + address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", + address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); + + if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); + + var discoResult = SecureServer.Disconnect(clientIndex); + //Debug.Console(1, this, "{0}", discoResult); + + if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) + { + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Dispose(); + HeartbeatTimerDictionary.Remove(clientIndex); + } + } + catch (Exception ex) + { + ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key); + } + } + + #endregion + + #region Methods - Socket Status Changed Callbacks + /// + /// Secure Server Socket Status Changed Callback + /// + /// + /// + /// + void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus) + { + try + { + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + if (ConnectedClientsIndexes.Contains(clientIndex)) + ConnectedClientsIndexes.Remove(clientIndex); + if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) + { + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Dispose(); + HeartbeatTimerDictionary.Remove(clientIndex); + } + if (ClientReadyAfterKeyExchange.Contains(clientIndex)) + ClientReadyAfterKeyExchange.Remove(clientIndex); + } + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); + } + onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); + } + + #endregion + + #region Methods Connected Callbacks + /// + /// Secure TCP Client Connected to Secure Server Callback + /// + /// + /// + void SecureConnectCallback(SecureTCPServer server, uint clientIndex) + { + try + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", + server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), + clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); + if (clientIndex != 0) + { + if (server.ClientConnected(clientIndex)) + { + + if (!ConnectedClientsIndexes.Contains(clientIndex)) + { + ConnectedClientsIndexes.Add(clientIndex); + } + if (SharedKeyRequired) + { + if (!WaitingForSharedKey.Contains(clientIndex)) + { + WaitingForSharedKey.Add(clientIndex); + } + byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); + server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + } + else + { + OnServerClientReadyForCommunications(clientIndex); + } + if (HeartbeatRequired) + { + if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) + { + HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); + } + } + + server.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); } } else { - if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) - HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); - else + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); + if (!ServerStopped) { - CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); - HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); + return; } } } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + } + //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", + // server.State, + // MaxClients, + // ServerStopped); + if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection"); + server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); + + } } + #endregion - #region Methods - Callbacks + #region Methods - Send/Receive Callbacks /// - /// Callback to disconnect if heartbeat timer finishes without being reset + /// Secure Received Data Async Callback /// - /// - 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) + void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, 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)) + try { - received = received.Replace("\r", ""); - received = received.Replace("\n", ""); - if (received != SharedKey) + byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); + received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived); + if (WaitingForSharedKey.Contains(clientIndex)) { - 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); + 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(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); + mySecureTCPServer.SendData(clientIndex, b, b.Length); + mySecureTCPServer.Disconnect(clientIndex); + WaitingForSharedKey.Remove(clientIndex); + return; + } + if (mySecureTCPServer.NumberOfClientsConnected > 0) + mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); + WaitingForSharedKey.Remove(clientIndex); + byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); + mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); + OnServerClientReadyForCommunications(clientIndex); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); + return; } - 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); + //var address = mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + //Debug.Console(1, this, "Secure Server Listening on Port: {0}, client IP: {1}, Client Index: {4}, NumberOfBytesReceived: {2}, Received: {3}\r\n", + // mySecureTCPServer.PortNumber.ToString(), address , numberOfBytesReceived.ToString(), received, clientIndex.ToString()); + if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) + onTextReceived(received, clientIndex); } - else + catch (Exception ex) { - 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); + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); } - checkHeartbeat(clientIndex, received); } - if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) - myTCPServer.ReceiveDataAsync(clientIndex, ReceivedDataAsyncCallback); + if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); } + #endregion #region Methods - EventHelpers/Callbacks + //Private Helper method to call the Connection Change Event - void onConnectionChange() + void onConnectionChange(uint clientIndex, SocketStatus clientStatus) { + if (clientIndex != 0) //0 is error not valid client change + { + var handler = ClientConnectionChange; + if (handler != null) + { + handler(this, new GenericTcpServerSocketStatusChangeEventArgs(SecureServer, clientIndex, clientStatus)); + } + } + } + + //Private Helper method to call the Connection Change Event + void OnConnectionChange() + { + if (ProgramIsStopping) + { + return; + } var handler = ClientConnectionChange; if (handler != null) - handler(this, new DynamicTCPSocketStatusChangeEventArgs(Server, false)); + { + handler(this, new GenericTcpServerSocketStatusChangeEventArgs()); + } } //Private Helper Method to call the Text Received Event - void onTextReceived(string text) + void onTextReceived(string text, uint clientIndex) { var handler = TextReceived; if (handler != null) - handler(this, new GenericCommMethodReceiveTextArgs(text)); + handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex)); } //Private Helper Method to call the Server State Change Event - void onServerStateChange() + void OnServerStateChange(ServerState state) { + if (ProgramIsStopping) + { + return; + } var handler = ServerStateChange; if (handler != null) - handler(this, new DynamicTCPServerStateChangedEventArgs(Server, false)); + { + handler(this, new GenericTcpServerStateChangedEventArgs(state)); + } } - //Private Event Handler method to handle the closing of connections when the program stops + /// + /// 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(); + ProgramIsStopping = true; + // kill bandaid things + if (MonitorClientTimer != null) + MonitorClientTimer.Stop(); + if (MonitorClient != null) + MonitorClient.Disconnect(); + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); + KillServer(); + } + } + + //Private event handler method to raise the event that the server is ready to send data after a successful client shared key negotiation + void OnServerClientReadyForCommunications(uint clientIndex) + { + ClientReadyAfterKeyExchange.Add(clientIndex); + var handler = ServerClientReadyForCommunications; + if (handler != null) + handler(this, new GenericTcpServerSocketStatusChangeEventArgs( + this, clientIndex, SecureServer.GetServerSocketStatusForSpecificClient(clientIndex))); + } + #endregion + + #region Monitor Client + /// + /// Starts the monitor client cycle. Timed wait, then call RunMonitorClient + /// + void StartMonitorClient() + { + if (MonitorClientTimer != null) + { + return; + } + MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); + } + + /// + /// + /// + void RunMonitorClient() + { + MonitorClient = new GenericTcpIpClient_ForServer(Key + "-MONITOR", "127.0.0.1", Port, 2000); + MonitorClient.SharedKeyRequired = this.SharedKeyRequired; + MonitorClient.SharedKey = this.SharedKey; + MonitorClient.ConnectionHasHungCallback = MonitorClientHasHungCallback; + //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; + MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); + + MonitorClient.Connect(); + // From here MonitorCLient either connects or hangs, MonitorClient will call back + + } + + /// + /// + /// + void StopMonitorClient() + { + if (MonitorClient == null) + return; + + MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm; + MonitorClient.Disconnect(); + MonitorClient = null; + } + + /// + /// On monitor connect, restart the operation + /// + void MonitorClient_IsReadyForComm(object sender, GenericTcpServerClientReadyForcommunicationsEventArgs args) + { + if (args.IsReady) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); + MonitorClientTimer.Stop(); + MonitorClientTimer = null; + MonitorClientFailureCount = 0; + CrestronEnvironment.Sleep(2000); + StopMonitorClient(); + StartMonitorClient(); + } + } + + /// + /// If the client hangs, add to counter and maybe fire the choke event + /// + void MonitorClientHasHungCallback() + { + MonitorClientFailureCount++; + MonitorClientTimer.Stop(); + MonitorClientTimer = null; + StopMonitorClient(); + if (MonitorClientFailureCount < MonitorClientMaxFailureCount) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", + MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); + StartMonitorClient(); + } + else + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, + "\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************", + MonitorClientMaxFailureCount); + + var handler = ServerHasChoked; + if (handler != null) + handler(); + // Some external thing is in charge here. Expected reset of program } } #endregion diff --git a/Pepperdash Core/Pepperdash Core/Comm/TcpServerConfigObject.cs b/Pepperdash Core/Pepperdash Core/Comm/TcpServerConfigObject.cs new file mode 100644 index 0000000..c7c1998 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/TcpServerConfigObject.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Core +{ + public class TcpServerConfigObject + { + public string Key { get; set; } + public bool Secure { get; set; } + public ushort MaxClients { get; set; } + public int Port { get; set; } + public bool SharedKeyRequired { get; set; } + public string SharedKey { get; set; } + public bool HeartbeatRequired { get; set; } + public ushort HeartbeatRequiredIntervalInSeconds { get; set; } + public string HeartbeatStringToMatch { get; set; } + public int BufferSize { get; set; } + } +} \ 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 5e509c5..13a79a5 100644 --- a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj +++ b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj @@ -64,9 +64,10 @@ + - + Code @@ -75,6 +76,7 @@ + diff --git a/Pepperdash Core/Pepperdash Core/Properties/AssemblyInfo.cs b/Pepperdash Core/Pepperdash Core/Properties/AssemblyInfo.cs index 61cd67e..de52999 100644 --- a/Pepperdash Core/Pepperdash Core/Properties/AssemblyInfo.cs +++ b/Pepperdash Core/Pepperdash Core/Properties/AssemblyInfo.cs @@ -4,4 +4,4 @@ [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Pepperdash_Core")] [assembly: AssemblyCopyright("Copyright © PepperDash 2019")] -[assembly: AssemblyVersion("1.0.13.*")] +[assembly: AssemblyVersion("1.0.14.*")]