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.

This commit is contained in:
Joshua Gutenplan
2019-02-18 15:49:45 -08:00
parent 79b9059f1b
commit b3203cf961
9 changed files with 2858 additions and 743 deletions

View File

@@ -18,9 +18,7 @@ using Crestron.SimplSharp.CrestronSockets;
namespace PepperDash.Core namespace PepperDash.Core
{ {
#region GenericSocketStatusChangeEventArgs
public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client); public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
public class GenericSocketStatusChageEventArgs : EventArgs public class GenericSocketStatusChageEventArgs : EventArgs
{ {
public ISocketStatus Client { get; private set; } public ISocketStatus Client { get; private set; }
@@ -32,44 +30,68 @@ namespace PepperDash.Core
Client = client; Client = client;
} }
} }
#endregion
#region DynamicTCPServerStateChangedEventArgs public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state);
public delegate void DynamicTCPServerStateChangedEventDelegate(object server); public class GenericTcpServerStateChangedEventArgs : EventArgs
public class DynamicTCPServerStateChangedEventArgs : EventArgs
{ {
public bool Secure { get; private set; } public ServerState State { get; private set; }
public object Server { get; private set; }
public DynamicTCPServerStateChangedEventArgs() { } public GenericTcpServerStateChangedEventArgs() { }
public DynamicTCPServerStateChangedEventArgs(object server, bool secure) public GenericTcpServerStateChangedEventArgs(ServerState state)
{ {
Secure = secure; State = state;
Server = server;
} }
} }
#endregion
#region DynamicTCPSocketStatusChangeEventDelegate public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus);
public delegate void DynamicTCPSocketStatusChangeEventDelegate(object server); public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
public class DynamicTCPSocketStatusChangeEventArgs : EventArgs
{ {
public bool Secure { get; private set; } public object Socket { get; private set; }
public object Server { get; private set; } public uint ReceivedFromClientIndex { get; private set; }
public SocketStatus ClientStatus { get; set; }
public DynamicTCPSocketStatusChangeEventArgs() { } public GenericTcpServerSocketStatusChangeEventArgs() { }
public DynamicTCPSocketStatusChangeEventArgs(object server, bool secure) public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus)
{ {
Secure = secure; Socket = socket;
Server = server; 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;
}
}

View File

@@ -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<GenericCommMethodReceiveBytesArgs> BytesReceived;
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// 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
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
return (Client != null) && (Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED);
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client == null)
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
return Client.ClientStatus;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// Connected bool
/// </summary>
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
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
if (Client != null)
{
Debug.Console(1, this, "Program stopping. Closing connection");
Client.DisconnectFromServer();
Client.Dispose();
}
}
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
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();
}
/// <summary>
/// Disconnect client. Does not dispose.
/// </summary>
public void Disconnect()
{
DisconnectCalledByUser = true;
if(Client != null)
Client.DisconnectFromServer();
}
/// <summary>
/// callback after connection made
/// </summary>
/// <param name="o"></param>
void ConnectToServerCallback(object o)
{
Client.ConnectToServer();
if (Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
WaitAndTryReconnect();
}
/// <summary>
/// Called from Socket Status change if auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void WaitAndTryReconnect()
{
Client.DisconnectFromServer();
Debug.Console(2, "Attempting reconnect, status={0}", Client.ClientStatus);
if (!DisconnectCalledByUser)
RetryTimer = new CTimer(ConnectToServerCallback, AutoReconnectIntervalMs);
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
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);
}
/// <summary>
/// General send method
/// </summary>
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);
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
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
}
}

View File

@@ -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
{
/// <summary>
/// Band aid delegate for choked server
/// </summary>
internal delegate void ConnectionHasHungCallbackDelegate();
#region Events
//public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ConnectionChange;
/// <summary>
/// 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.
/// </summary>
internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// 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
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
if (Client != null)
return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
else
return false;
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client != null)
return Client.ClientStatus;
else
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// private Timer for auto reconnect
/// </summary>
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;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
/// </summary>
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
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
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();
}
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
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();
}
}
/// <summary>
///
/// </summary>
public void Disconnect()
{
Debug.Console(2, "Disconnect Called");
DisconnectCalledByUser = true;
if (IsConnected)
{
Client.DisconnectFromServer();
}
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
Cleanup();
}
/// <summary>
/// Internal call to close up client. ALWAYS use this when disconnecting.
/// </summary>
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;
}
}
/// <summary>ff
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
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);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
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);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// General send method
/// </summary>
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);
}
}
}
/// <summary>
///
/// </summary>
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);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
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);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (this.IsReadyForCommunication) { HeartbeatStart(); }
var handler = ClientReadyForCommunications;
if (handler != null)
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

View File

@@ -15,6 +15,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core namespace PepperDash.Core
{ {
@@ -24,20 +26,61 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Event for Receiving text /// Event for Receiving text
/// </summary> /// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived; public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary> /// <summary>
/// Event for client connection socket status change /// Event for client connection socket status change
/// </summary> /// </summary>
public event EventHandler<DynamicTCPSocketStatusChangeEventArgs> ClientConnectionChange; public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ClientConnectionChange;
/// <summary> /// <summary>
/// Event for Server State Change /// Event for Server State Change
/// </summary> /// </summary>
public event EventHandler<DynamicTCPServerStateChangedEventArgs> ServerStateChange; public event EventHandler<GenericTcpServerStateChangedEventArgs> ServerStateChange;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ServerClientReadyForCommunications;
/// <summary>
/// A band aid event to notify user that the server has choked.
/// </summary>
public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; }
public delegate void ServerHasChokedCallbackDelegate();
#endregion #endregion
#region Properties/Variables #region Properties/Variables
/// <summary>
///
/// </summary>
CCriticalSection ServerCCSection = new CCriticalSection();
/// <summary>
/// A bandaid client that monitors whether the server is reachable
/// </summary>
GenericSecureTcpIpClient_ForServer MonitorClient;
/// <summary>
/// Timer to operate the bandaid monitor client in a loop.
/// </summary>
CTimer MonitorClientTimer;
/// <summary>
///
/// </summary>
int MonitorClientFailureCount;
/// <summary>
/// 3 by default
/// </summary>
public int MonitorClientMaxFailureCount { get; set; }
/// <summary> /// <summary>
/// Text representation of the Socket Status enum values for the server /// Text representation of the Socket Status enum values for the server
/// </summary> /// </summary>
@@ -45,10 +88,10 @@ namespace PepperDash.Core
{ {
get get
{ {
if (Server != null) if (SecureServer != null)
return Server.State.ToString(); return SecureServer.State.ToString();
else
return ServerState.SERVER_NOT_LISTENING.ToString(); return ServerState.SERVER_NOT_LISTENING.ToString();
} }
} }
@@ -58,7 +101,16 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public bool IsConnected 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);
}
} }
/// <summary> /// <summary>
@@ -74,7 +126,16 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public bool IsListening 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);
}
} }
/// <summary> /// <summary>
@@ -93,8 +154,8 @@ namespace PepperDash.Core
{ {
get get
{ {
if (Server != null) if (SecureServer != null)
return (ushort)Server.NumberOfClientsConnected; return (ushort)SecureServer.NumberOfClientsConnected;
return 0; return 0;
} }
} }
@@ -175,11 +236,13 @@ namespace PepperDash.Core
//private timers for Heartbeats per client //private timers for Heartbeats per client
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>(); Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
//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<uint> WaitingForSharedKey = new List<uint>(); List<uint> WaitingForSharedKey = new List<uint>();
List<uint> ClientReadyAfterKeyExchange = new List<uint>();
//Store the connected client indexes //Store the connected client indexes
List<uint> ConnectedClientsIndexes = new List<uint>(); public List<uint> ConnectedClientsIndexes = new List<uint>();
/// <summary> /// <summary>
/// Defaults to 2000 /// Defaults to 2000
@@ -192,23 +255,72 @@ namespace PepperDash.Core
private bool ServerStopped { get; set; } private bool ServerStopped { get; set; }
//Servers //Servers
SecureTCPServer Server; SecureTCPServer SecureServer;
/// <summary>
///
/// </summary>
bool ProgramIsStopping;
#endregion #endregion
#region Constructors #region Constructors
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public GenericSecureTcpIpServer() public GenericSecureTcpIpServer()
: base("Uninitialized Dynamic TCP Server") : base("Uninitialized Dynamic TCP Server")
{ {
HeartbeatRequiredIntervalInSeconds = 15;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
BufferSize = 2000; BufferSize = 2000;
MonitorClientMaxFailureCount = 3;
}
/// <summary>
/// constructor with debug key set at instantiation. Make sure to set all properties before listening.
/// </summary>
/// <param name="key"></param>
public GenericSecureTcpIpServer(string key)
: base("Uninitialized Dynamic TCP Server")
{
HeartbeatRequiredIntervalInSeconds = 15;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
BufferSize = 2000;
MonitorClientMaxFailureCount = 3;
Key = key;
}
/// <summary>
/// Contstructor that sets all properties by calling the initialize method with a config object.
/// </summary>
/// <param name="serverConfigObject"></param>
public GenericSecureTcpIpServer(TcpServerConfigObject serverConfigObject)
: base("Uninitialized Dynamic TCP Server")
{
HeartbeatRequiredIntervalInSeconds = 15;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
BufferSize = 2000;
MonitorClientMaxFailureCount = 3;
Initialize(serverConfigObject);
} }
#endregion #endregion
#region Methods - Server Actions #region Methods - Server Actions
/// <summary>
/// Disconnects all clients and stops the server
/// </summary>
public void KillServer()
{
ServerStopped = true;
if (MonitorClient != null)
{
MonitorClient.Disconnect();
}
DisconnectAllClientsForShutdown();
StopListening();
}
/// <summary> /// <summary>
/// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
/// </summary> /// </summary>
@@ -218,37 +330,83 @@ namespace PepperDash.Core
Key = key; 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);
}
}
/// <summary> /// <summary>
/// Start listening on the specified port /// Start listening on the specified port
/// </summary> /// </summary>
public void Listen() public void Listen()
{ {
ServerCCSection.Enter();
try try
{ {
if (Port < 1 || Port > 65535) 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)); ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) 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)); ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
return; return;
} }
if (IsListening) if (IsListening)
return; 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; ServerStopped = false;
Server.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
onServerStateChange(); OnServerStateChange(SecureServer.State);
Debug.Console(2, "Server Status: {0}, Socket Status: {1}\r\n", Server.State.ToString(), Server.ServerSocketStatus); Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
// StartMonitorClient();
ServerCCSection.Leave();
} }
catch (Exception ex) 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
/// </summary> /// </summary>
public void StopListening() public void StopListening()
{ {
Debug.Console(2, "Stopping Listener"); try
if (Server != null) {
Server.Stop(); 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; ServerStopped = true;
onServerStateChange(); 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);
}
} }
/// <summary>
/// Disconnects Client
/// </summary>
/// <param name="client"></param>
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);
}
}
/// <summary> /// <summary>
/// Disconnect All Clients /// Disconnect All Clients
/// </summary> /// </summary>
public void DisconnectAllClients() public void DisconnectAllClientsForShutdown()
{ {
Debug.Console(2, "Disconnecting All Clients"); Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
if (Server != null) if (SecureServer != null)
Server.DisconnectAll(); {
onConnectionChange(); SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
onServerStateChange(); //State shows both listening and connected 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 { };
} }
/// <summary> /// <summary>
@@ -281,12 +496,30 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
public void BroadcastText(string text) public void BroadcastText(string text)
{
CCriticalSection CCBroadcast = new CCriticalSection();
CCBroadcast.Enter();
try
{ {
if (ConnectedClientsIndexes.Count > 0) if (ConnectedClientsIndexes.Count > 0)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes(text); byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
foreach (uint i in ConnectedClientsIndexes) foreach (uint i in ConnectedClientsIndexes)
Server.SendDataAsync(i, b, b.Length, SendDataAsyncCallback); {
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);
} }
} }
@@ -296,19 +529,34 @@ namespace PepperDash.Core
/// <param name="text"></param> /// <param name="text"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
public void SendTextToClient(string text, uint clientIndex) public void SendTextToClient(string text, uint clientIndex)
{
try
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes(text); byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
Server.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); 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 //private method to check heartbeat requirements and start or reset timer
void checkHeartbeat(uint clientIndex, string received) string checkHeartbeat(uint clientIndex, string received)
{
try
{ {
if (HeartbeatRequired) if (HeartbeatRequired)
{ {
if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) if (!string.IsNullOrEmpty(HeartbeatStringToMatch))
{ {
if (received == HeartbeatStringToMatch) var remainingText = received.Replace(HeartbeatStringToMatch, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatStringToMatch))
{ {
if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
@@ -317,6 +565,10 @@ namespace PepperDash.Core
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); 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 else
@@ -328,108 +580,195 @@ namespace PepperDash.Core
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); 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 #endregion
#region Methods - Callbacks #region Methods - HeartbeatTimer Callback
/// <summary>
/// Callback to disconnect if heartbeat timer finishes without being reset
/// </summary>
/// <param name="o"></param>
void HeartbeatTimer_CallbackFunction(object o) void HeartbeatTimer_CallbackFunction(object o)
{ {
uint clientIndex = (uint)o; uint clientIndex = 99999;
string address = string.Empty;
try
{
clientIndex = (uint)o;
address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
string address = Server.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);
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);
if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
Server.Disconnect(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); 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
/// <summary> /// <summary>
/// TCP Server Socket Status Change Callback /// Secure Server Socket Status Changed Callback
/// </summary> /// </summary>
/// <param name="server"></param> /// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
/// <param name="serverSocketStatus"></param> /// <param name="serverSocketStatus"></param>
void SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus) void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
{ {
Debug.Console(2, "Client at {0} ServerSocketStatus {1}", try
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), serverSocketStatus.ToString());
if (server.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
if (SharedKeyRequired && !WaitingForSharedKey.Contains(clientIndex))
WaitingForSharedKey.Add(clientIndex); 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 (!ConnectedClientsIndexes.Contains(clientIndex)) if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
ConnectedClientsIndexes.Add(clientIndex);
}
else
{ {
if (ConnectedClientsIndexes.Contains(clientIndex)) if (ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Remove(clientIndex); ConnectedClientsIndexes.Remove(clientIndex);
if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Dispose();
HeartbeatTimerDictionary.Remove(clientIndex); HeartbeatTimerDictionary.Remove(clientIndex);
} }
if (Server.ServerSocketStatus.ToString() != Status) if (ClientReadyAfterKeyExchange.Contains(clientIndex))
onConnectionChange(); 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
/// <summary> /// <summary>
/// TCP Client Connected to Server Callback /// Secure TCP Client Connected to Secure Server Callback
/// </summary> /// </summary>
/// <param name="mySecureTCPServer"></param> /// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
void ConnectCallback(SecureTCPServer mySecureTCPServer, uint clientIndex) void SecureConnectCallback(SecureTCPServer server, uint clientIndex)
{ {
if (mySecureTCPServer.ClientConnected(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 (SharedKeyRequired)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes(SharedKey + "\n"); if (!WaitingForSharedKey.Contains(clientIndex))
mySecureTCPServer.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); {
Debug.Console(2, "Sent Shared Key to client at {0}", mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(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 (HeartbeatRequired)
{ {
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
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);
}
/// <summary>
/// Send Data Asyc Callback
/// </summary>
/// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="numberOfBytesSent"></param>
void SendDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesSent)
{ {
//Seems there is nothing to do here HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
}
} }
server.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
}
}
else
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
if (!ServerStopped)
{
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 - Send/Receive Callbacks
/// <summary> /// <summary>
/// Received Data Async Callback /// Secure Received Data Async Callback
/// </summary> /// </summary>
/// <param name="mySecureTCPServer"></param> /// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
/// <param name="numberOfBytesReceived"></param> /// <param name="numberOfBytesReceived"></param>
void ReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived) void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived)
{ {
if (numberOfBytesReceived > 0) if (numberOfBytesReceived > 0)
{ {
string received = "Nothing"; string received = "Nothing";
try
{
byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived); received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
if (WaitingForSharedKey.Contains(clientIndex)) if (WaitingForSharedKey.Contains(clientIndex))
@@ -439,65 +778,207 @@ namespace PepperDash.Core
if (received != SharedKey) if (received != SharedKey)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); 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); Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
ErrorLog.Error("Client at index {0} Shared key did not match the server, disconnecting client", clientIndex); mySecureTCPServer.SendData(clientIndex, b, b.Length);
mySecureTCPServer.SendDataAsync(clientIndex, b, b.Length, null);
mySecureTCPServer.Disconnect(clientIndex); mySecureTCPServer.Disconnect(clientIndex);
WaitingForSharedKey.Remove(clientIndex);
return;
} }
if (mySecureTCPServer.NumberOfClientsConnected > 0) if (mySecureTCPServer.NumberOfClientsConnected > 0)
mySecureTCPServer.ReceiveDataAsync(ReceivedDataAsyncCallback); mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
WaitingForSharedKey.Remove(clientIndex); WaitingForSharedKey.Remove(clientIndex);
byte[] skResponse = Encoding.GetEncoding(28591).GetBytes("Shared Key Match, Connected and ready for communication"); byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
mySecureTCPServer.SendDataAsync(clientIndex, skResponse, skResponse.Length, null); mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
mySecureTCPServer.ReceiveDataAsync(ReceivedDataAsyncCallback); 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;
} }
else //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);
}
catch (Exception ex)
{ {
mySecureTCPServer.ReceiveDataAsync(ReceivedDataAsyncCallback); Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex);
Debug.Console(2, "Server Listening on Port: {0}, client IP: {1}, NumberOfBytesReceived: {2}, Received: {3}\r\n",
mySecureTCPServer.PortNumber, mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), numberOfBytesReceived, received);
onTextReceived(received);
} }
checkHeartbeat(clientIndex, received);
} }
if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
mySecureTCPServer.ReceiveDataAsync(clientIndex, ReceivedDataAsyncCallback); mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
} }
#endregion #endregion
#region Methods - EventHelpers/Callbacks #region Methods - EventHelpers/Callbacks
//Private Helper method to call the Connection Change Event //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; var handler = ClientConnectionChange;
if (handler != null) if (handler != null)
handler(this, new DynamicTCPSocketStatusChangeEventArgs(Server, false)); {
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 GenericTcpServerSocketStatusChangeEventArgs());
}
} }
//Private Helper Method to call the Text Received Event //Private Helper Method to call the Text Received Event
void onTextReceived(string text) void onTextReceived(string text, uint clientIndex)
{ {
var handler = TextReceived; var handler = TextReceived;
if (handler != null) if (handler != null)
handler(this, new GenericCommMethodReceiveTextArgs(text)); handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex));
} }
//Private Helper Method to call the Server State Change Event //Private Helper Method to call the Server State Change Event
void onServerStateChange() void OnServerStateChange(ServerState state)
{ {
if (ProgramIsStopping)
{
return;
}
var handler = ServerStateChange; var handler = ServerStateChange;
if (handler != null) 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 /// <summary>
/// Private Event Handler method to handle the closing of connections when the program stops
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
Debug.Console(1, this, "Program stopping. Closing server"); ProgramIsStopping = true;
DisconnectAllClients(); // kill bandaid things
StopListening(); 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
/// <summary>
/// Starts the monitor client cycle. Timed wait, then call RunMonitorClient
/// </summary>
void StartMonitorClient()
{
if (MonitorClientTimer != null)
{
return;
}
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
}
/// <summary>
///
/// </summary>
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
}
/// <summary>
///
/// </summary>
void StopMonitorClient()
{
if (MonitorClient == null)
return;
MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm;
MonitorClient.Disconnect();
MonitorClient = null;
}
/// <summary>
/// On monitor connect, restart the operation
/// </summary>
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();
}
}
/// <summary>
/// If the client hangs, add to counter and maybe fire the choke event
/// </summary>
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 #endregion

View File

@@ -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
{
/// <summary>
/// Band aid delegate for choked server
/// </summary>
internal delegate void ConnectionHasHungCallbackDelegate();
#region Events
//public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ConnectionChange;
/// <summary>
/// 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.
/// </summary>
internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// 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
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
if (Client != null)
return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
else
return false;
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client != null)
return Client.ClientStatus;
else
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// private Timer for auto reconnect
/// </summary>
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;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
/// </summary>
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
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
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();
}
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
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();
}
}
/// <summary>
///
/// </summary>
public void Disconnect()
{
Debug.Console(2, "Disconnect Called");
DisconnectCalledByUser = true;
if (IsConnected)
{
Client.DisconnectFromServer();
}
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
Cleanup();
}
/// <summary>
/// Internal call to close up client. ALWAYS use this when disconnecting.
/// </summary>
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;
}
}
/// <summary>ff
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
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);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
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);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// General send method
/// </summary>
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);
}
}
}
/// <summary>
///
/// </summary>
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);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
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);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (this.IsReadyForCommunication) { HeartbeatStart(); }
var handler = ClientReadyForCommunications;
if (handler != null)
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

View File

@@ -15,6 +15,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core namespace PepperDash.Core
{ {
@@ -24,20 +26,61 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Event for Receiving text /// Event for Receiving text
/// </summary> /// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived; public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary> /// <summary>
/// Event for client connection socket status change /// Event for client connection socket status change
/// </summary> /// </summary>
public event EventHandler<DynamicTCPSocketStatusChangeEventArgs> ClientConnectionChange; public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ClientConnectionChange;
/// <summary> /// <summary>
/// Event for Server State Change /// Event for Server State Change
/// </summary> /// </summary>
public event EventHandler<DynamicTCPServerStateChangedEventArgs> ServerStateChange; public event EventHandler<GenericTcpServerStateChangedEventArgs> ServerStateChange;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ServerClientReadyForCommunications;
/// <summary>
/// A band aid event to notify user that the server has choked.
/// </summary>
public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; }
public delegate void ServerHasChokedCallbackDelegate();
#endregion #endregion
#region Properties/Variables #region Properties/Variables
/// <summary>
///
/// </summary>
CCriticalSection ServerCCSection = new CCriticalSection();
/// <summary>
/// A bandaid client that monitors whether the server is reachable
/// </summary>
GenericTcpIpClient_ForServer MonitorClient;
/// <summary>
/// Timer to operate the bandaid monitor client in a loop.
/// </summary>
CTimer MonitorClientTimer;
/// <summary>
///
/// </summary>
int MonitorClientFailureCount;
/// <summary>
/// 3 by default
/// </summary>
public int MonitorClientMaxFailureCount { get; set; }
/// <summary> /// <summary>
/// Text representation of the Socket Status enum values for the server /// Text representation of the Socket Status enum values for the server
/// </summary> /// </summary>
@@ -45,10 +88,10 @@ namespace PepperDash.Core
{ {
get get
{ {
if (Server != null) if (SecureServer != null)
return Server.State.ToString(); return SecureServer.State.ToString();
else
return ServerState.SERVER_NOT_LISTENING.ToString(); return ServerState.SERVER_NOT_LISTENING.ToString();
} }
} }
@@ -58,7 +101,16 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public bool IsConnected 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);
}
} }
/// <summary> /// <summary>
@@ -74,7 +126,16 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public bool IsListening 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);
}
} }
/// <summary> /// <summary>
@@ -93,8 +154,8 @@ namespace PepperDash.Core
{ {
get get
{ {
if (Server != null) if (SecureServer != null)
return (ushort)Server.NumberOfClientsConnected; return (ushort)SecureServer.NumberOfClientsConnected;
return 0; return 0;
} }
} }
@@ -175,11 +236,13 @@ namespace PepperDash.Core
//private timers for Heartbeats per client //private timers for Heartbeats per client
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>(); Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
//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<uint> WaitingForSharedKey = new List<uint>(); List<uint> WaitingForSharedKey = new List<uint>();
List<uint> ClientReadyAfterKeyExchange = new List<uint>();
//Store the connected client indexes //Store the connected client indexes
List<uint> ConnectedClientsIndexes = new List<uint>(); public List<uint> ConnectedClientsIndexes = new List<uint>();
/// <summary> /// <summary>
/// Defaults to 2000 /// Defaults to 2000
@@ -192,23 +255,72 @@ namespace PepperDash.Core
private bool ServerStopped { get; set; } private bool ServerStopped { get; set; }
//Servers //Servers
TCPServer Server; SecureTCPServer SecureServer;
/// <summary>
///
/// </summary>
bool ProgramIsStopping;
#endregion #endregion
#region Constructors #region Constructors
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public GenericTcpIpServer() public GenericTcpIpServer()
: base("Uninitialized Dynamic TCP Server") : base("Uninitialized Dynamic TCP Server")
{ {
HeartbeatRequiredIntervalInSeconds = 15;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
BufferSize = 2000; BufferSize = 2000;
MonitorClientMaxFailureCount = 3;
}
/// <summary>
/// constructor with debug key set at instantiation. Make sure to set all properties before listening.
/// </summary>
/// <param name="key"></param>
public GenericTcpIpServer(string key)
: base("Uninitialized Dynamic TCP Server")
{
HeartbeatRequiredIntervalInSeconds = 15;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
BufferSize = 2000;
MonitorClientMaxFailureCount = 3;
Key = key;
}
/// <summary>
/// Contstructor that sets all properties by calling the initialize method with a config object.
/// </summary>
/// <param name="serverConfigObject"></param>
public GenericTcpIpServer(TcpServerConfigObject serverConfigObject)
: base("Uninitialized Dynamic TCP Server")
{
HeartbeatRequiredIntervalInSeconds = 15;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
BufferSize = 2000;
MonitorClientMaxFailureCount = 3;
Initialize(serverConfigObject);
} }
#endregion #endregion
#region Methods - Server Actions #region Methods - Server Actions
/// <summary>
/// Disconnects all clients and stops the server
/// </summary>
public void KillServer()
{
ServerStopped = true;
if (MonitorClient != null)
{
MonitorClient.Disconnect();
}
DisconnectAllClientsForShutdown();
StopListening();
}
/// <summary> /// <summary>
/// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
/// </summary> /// </summary>
@@ -218,37 +330,83 @@ namespace PepperDash.Core
Key = key; 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);
}
}
/// <summary> /// <summary>
/// Start listening on the specified port /// Start listening on the specified port
/// </summary> /// </summary>
public void Listen() public void Listen()
{ {
ServerCCSection.Enter();
try try
{ {
if (Port < 1 || Port > 65535) 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)); ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) 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)); ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
return; return;
} }
if (IsListening) if (IsListening)
return; 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; ServerStopped = false;
Server.WaitForConnectionAsync(IPAddress.Any, ConnectCallback); SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
onServerStateChange(); OnServerStateChange(SecureServer.State);
Debug.Console(2, "Server Status: {0}, Socket Status: {1}\r\n", Server.State.ToString(), Server.ServerSocketStatus); Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
// StartMonitorClient();
ServerCCSection.Leave();
} }
catch (Exception ex) 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
/// </summary> /// </summary>
public void StopListening() public void StopListening()
{ {
Debug.Console(2, "Stopping Listener"); try
if (Server != null) {
Server.Stop(); 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; ServerStopped = true;
onServerStateChange(); 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);
}
} }
/// <summary>
/// Disconnects Client
/// </summary>
/// <param name="client"></param>
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);
}
}
/// <summary> /// <summary>
/// Disconnect All Clients /// Disconnect All Clients
/// </summary> /// </summary>
public void DisconnectAllClients() public void DisconnectAllClientsForShutdown()
{ {
Debug.Console(2, "Disconnecting All Clients"); Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
if (Server != null) if (SecureServer != null)
Server.DisconnectAll(); {
onConnectionChange(); SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
onServerStateChange(); //State shows both listening and connected 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 { };
} }
/// <summary> /// <summary>
@@ -281,12 +496,30 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
public void BroadcastText(string text) public void BroadcastText(string text)
{
CCriticalSection CCBroadcast = new CCriticalSection();
CCBroadcast.Enter();
try
{ {
if (ConnectedClientsIndexes.Count > 0) if (ConnectedClientsIndexes.Count > 0)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes(text); byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
foreach (uint i in ConnectedClientsIndexes) foreach (uint i in ConnectedClientsIndexes)
Server.SendDataAsync(i, b, b.Length, SendDataAsyncCallback); {
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);
} }
} }
@@ -296,19 +529,34 @@ namespace PepperDash.Core
/// <param name="text"></param> /// <param name="text"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
public void SendTextToClient(string text, uint clientIndex) public void SendTextToClient(string text, uint clientIndex)
{
try
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes(text); byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
Server.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); 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 //private method to check heartbeat requirements and start or reset timer
void checkHeartbeat(uint clientIndex, string received) string checkHeartbeat(uint clientIndex, string received)
{
try
{ {
if (HeartbeatRequired) if (HeartbeatRequired)
{ {
if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) if (!string.IsNullOrEmpty(HeartbeatStringToMatch))
{ {
if (received == HeartbeatStringToMatch) var remainingText = received.Replace(HeartbeatStringToMatch, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatStringToMatch))
{ {
if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
@@ -317,6 +565,10 @@ namespace PepperDash.Core
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); 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 else
@@ -328,109 +580,196 @@ namespace PepperDash.Core
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); 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 #endregion
#region Methods - Callbacks #region Methods - HeartbeatTimer Callback
/// <summary>
/// Callback to disconnect if heartbeat timer finishes without being reset
/// </summary>
/// <param name="o"></param>
void HeartbeatTimer_CallbackFunction(object o) void HeartbeatTimer_CallbackFunction(object o)
{ {
uint clientIndex = (uint)o; uint clientIndex = 99999;
string address = string.Empty;
try
{
clientIndex = (uint)o;
address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
string address = Server.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);
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);
if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
Server.Disconnect(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); 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
/// <summary> /// <summary>
/// TCP Server Socket Status Change Callback /// Secure Server Socket Status Changed Callback
/// </summary> /// </summary>
/// <param name="server"></param> /// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
/// <param name="serverSocketStatus"></param> /// <param name="serverSocketStatus"></param>
void SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus serverSocketStatus) void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
{ {
Debug.Console(2, "Client at {0} ServerSocketStatus {1}", try
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), serverSocketStatus.ToString());
if (server.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
if (SharedKeyRequired && !WaitingForSharedKey.Contains(clientIndex))
WaitingForSharedKey.Add(clientIndex); 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 (!ConnectedClientsIndexes.Contains(clientIndex)) if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
ConnectedClientsIndexes.Add(clientIndex);
}
else
{ {
if (ConnectedClientsIndexes.Contains(clientIndex)) if (ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Remove(clientIndex); ConnectedClientsIndexes.Remove(clientIndex);
if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Dispose();
HeartbeatTimerDictionary.Remove(clientIndex); HeartbeatTimerDictionary.Remove(clientIndex);
} }
if (Server.ServerSocketStatus.ToString() != Status) if (ClientReadyAfterKeyExchange.Contains(clientIndex))
onConnectionChange(); 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
/// <summary> /// <summary>
/// TCP Client Connected to Server Callback /// Secure TCP Client Connected to Secure Server Callback
/// </summary> /// </summary>
/// <param name="myTCPServer"></param> /// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
void ConnectCallback(TCPServer myTCPServer, uint clientIndex) void SecureConnectCallback(SecureTCPServer server, uint clientIndex)
{ {
if (myTCPServer.ClientConnected(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 (SharedKeyRequired)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes(SharedKey + "\n"); if (!WaitingForSharedKey.Contains(clientIndex))
myTCPServer.SendDataAsync(clientIndex, b, b.Length, SendDataAsyncCallback); {
Debug.Console(2, "Sent Shared Key to client at {0}", myTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(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 (HeartbeatRequired)
{ {
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
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);
}
/// <summary>
/// Send Data Asyc Callback
/// </summary>
/// <param name="myTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="numberOfBytesSent"></param>
void SendDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesSent)
{ {
//Seems there is nothing to do here HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
}
} }
server.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
}
}
else
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
if (!ServerStopped)
{
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 - Send/Receive Callbacks
/// <summary> /// <summary>
/// Received Data Async Callback /// Secure Received Data Async Callback
/// </summary> /// </summary>
/// <param name="myTCPServer"></param> /// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
/// <param name="numberOfBytesReceived"></param> /// <param name="numberOfBytesReceived"></param>
void ReceivedDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesReceived) void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived)
{ {
if (numberOfBytesReceived > 0) if (numberOfBytesReceived > 0)
{ {
string received = "Nothing"; string received = "Nothing";
byte[] bytes = myTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); try
{
byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived); received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
if (WaitingForSharedKey.Contains(clientIndex)) if (WaitingForSharedKey.Contains(clientIndex))
{ {
@@ -439,65 +778,207 @@ namespace PepperDash.Core
if (received != SharedKey) if (received != SharedKey)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); 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); Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
ErrorLog.Error("Client at index {0} Shared key did not match the server, disconnecting client", clientIndex); mySecureTCPServer.SendData(clientIndex, b, b.Length);
myTCPServer.SendDataAsync(clientIndex, b, b.Length, null); mySecureTCPServer.Disconnect(clientIndex);
myTCPServer.Disconnect(clientIndex);
}
if (myTCPServer.NumberOfClientsConnected > 0)
myTCPServer.ReceiveDataAsync(ReceivedDataAsyncCallback);
WaitingForSharedKey.Remove(clientIndex); WaitingForSharedKey.Remove(clientIndex);
byte[] skResponse = Encoding.GetEncoding(28591).GetBytes("Shared Key Match, Connected and ready for communication"); return;
myTCPServer.SendDataAsync(clientIndex, skResponse, skResponse.Length, null);
myTCPServer.ReceiveDataAsync(ReceivedDataAsyncCallback);
} }
else 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;
}
//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);
}
catch (Exception ex)
{ {
myTCPServer.ReceiveDataAsync(ReceivedDataAsyncCallback); Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex);
Debug.Console(2, "Server Listening on Port: {0}, client IP: {1}, NumberOfBytesReceived: {2}, Received: {3}\r\n",
myTCPServer.PortNumber, myTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), numberOfBytesReceived, received);
onTextReceived(received);
} }
checkHeartbeat(clientIndex, received);
} }
if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
myTCPServer.ReceiveDataAsync(clientIndex, ReceivedDataAsyncCallback); mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
} }
#endregion #endregion
#region Methods - EventHelpers/Callbacks #region Methods - EventHelpers/Callbacks
//Private Helper method to call the Connection Change Event //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; var handler = ClientConnectionChange;
if (handler != null) if (handler != null)
handler(this, new DynamicTCPSocketStatusChangeEventArgs(Server, false)); {
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 GenericTcpServerSocketStatusChangeEventArgs());
}
} }
//Private Helper Method to call the Text Received Event //Private Helper Method to call the Text Received Event
void onTextReceived(string text) void onTextReceived(string text, uint clientIndex)
{ {
var handler = TextReceived; var handler = TextReceived;
if (handler != null) if (handler != null)
handler(this, new GenericCommMethodReceiveTextArgs(text)); handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex));
} }
//Private Helper Method to call the Server State Change Event //Private Helper Method to call the Server State Change Event
void onServerStateChange() void OnServerStateChange(ServerState state)
{ {
if (ProgramIsStopping)
{
return;
}
var handler = ServerStateChange; var handler = ServerStateChange;
if (handler != null) 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 /// <summary>
/// Private Event Handler method to handle the closing of connections when the program stops
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
Debug.Console(1, this, "Program stopping. Closing server"); ProgramIsStopping = true;
DisconnectAllClients(); // kill bandaid things
StopListening(); 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
/// <summary>
/// Starts the monitor client cycle. Timed wait, then call RunMonitorClient
/// </summary>
void StartMonitorClient()
{
if (MonitorClientTimer != null)
{
return;
}
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
}
/// <summary>
///
/// </summary>
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
}
/// <summary>
///
/// </summary>
void StopMonitorClient()
{
if (MonitorClient == null)
return;
MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm;
MonitorClient.Disconnect();
MonitorClient = null;
}
/// <summary>
/// On monitor connect, restart the operation
/// </summary>
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();
}
}
/// <summary>
/// If the client hangs, add to counter and maybe fire the choke event
/// </summary>
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 #endregion

View File

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

View File

@@ -64,9 +64,10 @@
<ItemGroup> <ItemGroup>
<Compile Include="CommunicationExtras.cs" /> <Compile Include="CommunicationExtras.cs" />
<Compile Include="Comm\ControlPropertiesConfig.cs" /> <Compile Include="Comm\ControlPropertiesConfig.cs" />
<Compile Include="Comm\GenericTcpIpClient_ForServer.cs" />
<Compile Include="Comm\GenericHttpSseClient.cs" /> <Compile Include="Comm\GenericHttpSseClient.cs" />
<Compile Include="Comm\GenericSecureTcpIpServer.cs" /> <Compile Include="Comm\GenericSecureTcpIpServer.cs" />
<Compile Include="Comm\GenericSecureTcpIpClient.cs"> <Compile Include="Comm\GenericSecureTcpIpClient_ForServer.cs">
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="Comm\eControlMethods.cs" /> <Compile Include="Comm\eControlMethods.cs" />
@@ -75,6 +76,7 @@
<Compile Include="Comm\EventArgs.cs" /> <Compile Include="Comm\EventArgs.cs" />
<Compile Include="Comm\GenericSshClient.cs" /> <Compile Include="Comm\GenericSshClient.cs" />
<Compile Include="Comm\GenericUdpServer.cs" /> <Compile Include="Comm\GenericUdpServer.cs" />
<Compile Include="Comm\TcpServerConfigObject.cs" />
<Compile Include="Config\PortalConfigReader.cs" /> <Compile Include="Config\PortalConfigReader.cs" />
<Compile Include="CoreInterfaces.cs" /> <Compile Include="CoreInterfaces.cs" />
<Compile Include="JsonToSimpl\JsonToSimplPortalFileMaster.cs" /> <Compile Include="JsonToSimpl\JsonToSimplPortalFileMaster.cs" />

View File

@@ -4,4 +4,4 @@
[assembly: AssemblyCompany("")] [assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Pepperdash_Core")] [assembly: AssemblyProduct("Pepperdash_Core")]
[assembly: AssemblyCopyright("Copyright © PepperDash 2019")] [assembly: AssemblyCopyright("Copyright © PepperDash 2019")]
[assembly: AssemblyVersion("1.0.13.*")] [assembly: AssemblyVersion("1.0.14.*")]