Essentials/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs
Neil Dorin 7076eafc21 Refator: Refactor timer implementation across multiple classes to use System.Timers.Timer instead of CTimer for improved consistency and performance.
- Updated RelayControlledShade to utilize Timer for output pulsing.
- Refactored MockVC to replace CTimer with Timer for call status simulation.
- Modified VideoCodecBase to enhance documentation and improve feedback handling.
- Removed obsolete IHasCamerasMessenger and updated related classes to use IHasCamerasWithControls.
- Adjusted PressAndHoldHandler to implement Timer for button hold actions.
- Enhanced logging throughout MobileControl and RoomBridges for better debugging and information tracking.
- Cleaned up unnecessary comments and improved exception handling in various classes.
2026-03-30 11:44:15 -06:00

1087 lines
No EOL
39 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
namespace PepperDash.Core;
/// <summary>
/// Generic secure TCP/IP server
/// </summary>
public class GenericSecureTcpIpServer : Device
{
#region Events
/// <summary>
/// Event for Receiving text
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread.
/// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event.
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceivedQueueInvoke;
/// <summary>
/// Event for client connection socket status change
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ClientConnectionChange;
/// <summary>
/// Event for Server State Change
/// </summary>
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; }
/// <summary>
///
/// </summary>
public delegate void ServerHasChokedCallbackDelegate();
#endregion
#region Properties/Variables
/// <summary>
/// Server listen lock
/// </summary>
private readonly object _serverLock = new();
/// <summary>
/// Queue lock
/// </summary>
private readonly object _dequeueLock = new();
/// <summary>
/// Broadcast lock
/// </summary>
private readonly object _broadcastLock = new();
/// <summary>
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
private CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs> MessageQueue;
/// <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>
Timer MonitorClientTimer;
/// <summary>
///
/// </summary>
int MonitorClientFailureCount;
/// <summary>
/// 3 by default
/// </summary>
public int MonitorClientMaxFailureCount { get; set; }
/// <summary>
/// Text representation of the Socket Status enum values for the server
/// </summary>
public string Status
{
get
{
if (SecureServer != null)
return SecureServer.State.ToString();
return ServerState.SERVER_NOT_LISTENING.ToString();
}
}
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
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>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsListening
{
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>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsListening
{
get { return (ushort)(IsListening ? 1 : 0); }
}
/// <summary>
/// Max number of clients this server will allow for connection. Crestron max is 64. This number should be less than 65
/// </summary>
public ushort MaxClients { get; set; } // should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable
/// <summary>
/// Number of clients currently connected.
/// </summary>
public ushort NumberOfClientsConnected
{
get
{
if (SecureServer != null)
return (ushort)SecureServer.NumberOfClientsConnected;
return 0;
}
}
/// <summary>
/// Port Server should listen on
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper for Port
/// </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. Must be set the same in the client, and if true shared keys must be identical on server/client
/// </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.
/// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received
/// </summary>
public bool HeartbeatRequired { get; set; }
/// <summary>
/// S+ Helper for Heartbeat Required
/// </summary>
public ushort UHeartbeatRequired
{
set
{
if (value == 1)
HeartbeatRequired = true;
else
HeartbeatRequired = false;
}
}
/// <summary>
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary>
public int HeartbeatRequiredIntervalMs { get; set; }
/// <summary>
/// Simpl+ Heartbeat Analog value in seconds
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } }
/// <summary>
/// String to Match for heartbeat. If null or empty any string will reset heartbeat timer
/// </summary>
public string HeartbeatStringToMatch { get; set; }
//private timers for Heartbeats per client
Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
//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> ClientReadyAfterKeyExchange = new List<uint>();
/// <summary>
/// The connected client indexes
/// </summary>
public List<uint> ConnectedClientsIndexes = new List<uint>();
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Private flag to note that the server has stopped intentionally
/// </summary>
private bool ServerStopped { get; set; }
//Servers
SecureTCPServer SecureServer;
/// <summary>
///
/// </summary>
bool ProgramIsStopping;
#endregion
#region Constructors
/// <summary>
/// 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>
public GenericSecureTcpIpServer()
: base("Uninitialized Secure TCP Server")
{
HeartbeatRequiredIntervalInSeconds = 15;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
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 Secure 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. This does set Queue size.
/// </summary>
/// <param name="serverConfigObject"></param>
public GenericSecureTcpIpServer(TcpServerConfigObject serverConfigObject)
: base("Uninitialized Secure TCP Server")
{
HeartbeatRequiredIntervalInSeconds = 15;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
BufferSize = 2000;
MonitorClientMaxFailureCount = 3;
Initialize(serverConfigObject);
}
#endregion
#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>
/// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
/// </summary>
/// <param name="key"></param>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Initialze the server
/// </summary>
/// <param name="serverConfigObject"></param>
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;
ReceiveQueueSize = serverConfigObject.ReceiveQueueSize > 20 ? serverConfigObject.ReceiveQueueSize : 20;
MessageQueue = new CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs>(ReceiveQueueSize);
}
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>
/// Start listening on the specified port
/// </summary>
public void Listen()
{
lock (_serverLock)
{
try
{
if (Port < 1 || Port > 65535)
{
this.LogError("Server '{0}': Invalid port", Key);
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
this.LogError("Server '{0}': No Shared Key set", Key);
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
return;
}
if (SecureServer == null)
{
SecureServer = new SecureTCPServer(Port, MaxClients);
if (HeartbeatRequired)
SecureServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5);
SecureServer.HandshakeTimeout = 30;
SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange);
}
else
{
SecureServer.PortNumber = Port;
}
ServerStopped = false;
// Start the listner
SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
{
this.LogError("Error starting WaitForConnectionAsync {0}", status);
}
else
{
ServerStopped = false;
}
OnServerStateChange(SecureServer.State);
this.LogInformation("Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
}
catch (Exception ex)
{
this.LogException(ex, "{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
}
} // end lock
}
/// <summary>
/// Stop Listeneing
/// </summary>
public void StopListening()
{
try
{
this.LogVerbose("Stopping Listener");
if (SecureServer != null)
{
SecureServer.Stop();
this.LogVerbose("Server State: {0}", SecureServer.State);
OnServerStateChange(SecureServer.State);
}
ServerStopped = true;
}
catch (Exception ex)
{
this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
}
}
/// <summary>
/// Disconnects Client
/// </summary>
/// <param name="client"></param>
public void DisconnectClient(uint client)
{
try
{
SecureServer.Disconnect(client);
this.LogVerbose("Disconnected client index: {0}", client);
}
catch (Exception ex)
{
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
}
}
/// <summary>
/// Disconnect All Clients
/// </summary>
public void DisconnectAllClientsForShutdown()
{
this.LogInformation("Disconnecting All Clients");
if (SecureServer != null)
{
SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly
{
var i = index;
if (!SecureServer.ClientConnected(index))
continue;
try
{
SecureServer.Disconnect(i);
this.LogInformation("Disconnected client index: {0}", i);
}
catch (Exception ex)
{
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
}
}
this.LogInformation("Server Status: {0}", SecureServer.ServerSocketStatus);
}
this.LogInformation("Disconnected All Clients");
ConnectedClientsIndexes.Clear();
if (!ProgramIsStopping)
{
OnConnectionChange();
OnServerStateChange(SecureServer.State); //State shows both listening and connected
}
// var o = new { };
}
/// <summary>
/// Broadcast text from server to all connected clients
/// </summary>
/// <param name="text"></param>
public void BroadcastText(string text)
{
lock (_broadcastLock)
{
try
{
if (ConnectedClientsIndexes.Count > 0)
{
byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
foreach (uint i in ConnectedClientsIndexes)
{
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i)))
{
SocketErrorCodes error = SecureServer.SendDataAsync(i, b, b.Length, (x, y, z) => { });
if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING)
this.LogVerbose("{error}", error);
}
}
}
}
catch (Exception ex)
{
this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
}
} // end lock
}
/// <summary>
/// Not sure this is useful in library, maybe Pro??
/// </summary>
/// <param name="text"></param>
/// <param name="clientIndex"></param>
public void SendTextToClient(string text, uint clientIndex)
{
try
{
byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
if (SecureServer != null && SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
{
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
SecureServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
}
}
catch (Exception ex)
{
this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
}
}
//private method to check heartbeat requirements and start or reset timer
string checkHeartbeat(uint clientIndex, string received)
{
try
{
if (HeartbeatRequired)
{
if (!string.IsNullOrEmpty(HeartbeatStringToMatch))
{
var remainingText = received.Replace(HeartbeatStringToMatch, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatStringToMatch))
{
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else
{
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
this.LogDebug("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
// Return Heartbeat
SendTextToClient(HeartbeatStringToMatch, clientIndex);
return remainingText;
}
}
else
{
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else
{
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
this.LogInformation("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
}
}
}
catch (Exception ex)
{
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
/// <summary>
/// Get the IP Address for the client at the specifed index
/// </summary>
/// <param name="clientIndex"></param>
/// <returns></returns>
public string GetClientIPAddress(uint clientIndex)
{
this.LogInformation("GetClientIPAddress Index: {0}", clientIndex);
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
{
var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
this.LogInformation("GetClientIPAddress IPAddreess: {0}", ipa);
return ipa;
}
else
{
return "";
}
}
#endregion
#region Methods - HeartbeatTimer Callback
void HeartbeatTimer_CallbackFunction(object o)
{
uint clientIndex = 99999;
string address = string.Empty;
try
{
clientIndex = (uint)o;
address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
this.LogInformation("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
var discoResult = SecureServer.Disconnect(clientIndex);
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Dispose();
HeartbeatTimerDictionary.Remove(clientIndex);
}
}
catch (Exception ex)
{
ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key);
}
}
#endregion
#region Methods - Socket Status Changed Callbacks
/// <summary>
/// Secure Server Socket Status Changed Callback
/// </summary>
/// <param name="server"></param>
/// <param name="clientIndex"></param>
/// <param name="serverSocketStatus"></param>
void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
{
try
{
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
this.LogInformation("SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber);
if (ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Remove(clientIndex);
if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Dispose();
HeartbeatTimerDictionary.Remove(clientIndex);
}
if (ClientReadyAfterKeyExchange.Contains(clientIndex))
ClientReadyAfterKeyExchange.Remove(clientIndex);
if (WaitingForSharedKey.Contains(clientIndex))
WaitingForSharedKey.Remove(clientIndex);
if (SecureServer.MaxNumberOfClientSupported > SecureServer.NumberOfClientsConnected)
{
Listen();
}
}
}
catch (Exception ex)
{
this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex.Message);
}
//Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state
//after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag
//is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event.
System.Threading.Tasks.Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)));
}
#endregion
#region Methods Connected Callbacks
/// <summary>
/// Secure TCP Client Connected to Secure Server Callback
/// </summary>
/// <param name="server"></param>
/// <param name="clientIndex"></param>
void SecureConnectCallback(SecureTCPServer server, uint clientIndex)
{
try
{
this.LogInformation("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
if (clientIndex != 0)
{
if (server.ClientConnected(clientIndex))
{
if (!ConnectedClientsIndexes.Contains(clientIndex))
{
ConnectedClientsIndexes.Add(clientIndex);
}
if (SharedKeyRequired)
{
if (!WaitingForSharedKey.Contains(clientIndex))
{
WaitingForSharedKey.Add(clientIndex);
}
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
this.LogInformation("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
}
else
{
OnServerClientReadyForCommunications(clientIndex);
}
if (HeartbeatRequired)
{
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
}
server.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
}
}
else
{
this.LogError("Client attempt faulty.");
}
}
catch (Exception ex)
{
this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
}
// Rearm the listner
SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
{
this.LogError("Socket status connect callback status {0}", status);
if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS)
{
// There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact.
server.Stop();
Listen();
}
}
}
#endregion
#region Methods - Send/Receive Callbacks
/// <summary>
/// Secure Received Data Async Callback
/// </summary>
/// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="numberOfBytesReceived"></param>
void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived)
{
if (numberOfBytesReceived > 0)
{
string received = "Nothing";
var handler = TextReceivedQueueInvoke;
try
{
byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
if (WaitingForSharedKey.Contains(clientIndex))
{
received = received.Replace("\r", "");
received = received.Replace("\n", "");
if (received != SharedKey)
{
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
mySecureTCPServer.SendData(clientIndex, b, b.Length);
mySecureTCPServer.Disconnect(clientIndex);
return;
}
WaitingForSharedKey.Remove(clientIndex);
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
OnServerClientReadyForCommunications(clientIndex);
this.LogInformation("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
}
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
{
onTextReceived(received, clientIndex);
if (handler != null)
{
MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(received, clientIndex));
}
}
}
catch (Exception ex)
{
this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex.Message);
}
if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null)
{
if (System.Threading.Monitor.TryEnter(_dequeueLock))
System.Threading.Tasks.Task.Run(() => DequeueEvent());
}
}
else
{
mySecureTCPServer.Disconnect(clientIndex);
}
}
/// <summary>
/// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically.
/// </summary>
void DequeueEvent()
{
try
{
while (true)
{
// Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather.
var message = MessageQueue.Dequeue();
var handler = TextReceivedQueueInvoke;
if (handler != null)
{
handler(this, message);
}
}
}
catch (Exception e)
{
this.LogError(e, "DequeueEvent error");
}
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
System.Threading.Monitor.Exit(_dequeueLock);
}
#endregion
#region Methods - EventHelpers/Callbacks
//Private Helper method to call the Connection Change Event
void onConnectionChange(uint clientIndex, SocketStatus clientStatus)
{
if (clientIndex != 0) //0 is error not valid client change
{
var handler = ClientConnectionChange;
if (handler != null)
{
handler(this, new GenericTcpServerSocketStatusChangeEventArgs(SecureServer, clientIndex, clientStatus));
}
}
}
//Private Helper method to call the Connection Change Event
void OnConnectionChange()
{
if (ProgramIsStopping)
{
return;
}
var handler = ClientConnectionChange;
if (handler != null)
{
handler(this, new GenericTcpServerSocketStatusChangeEventArgs());
}
}
//Private Helper Method to call the Text Received Event
void onTextReceived(string text, uint clientIndex)
{
var handler = TextReceived;
if (handler != null)
handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex));
}
//Private Helper Method to call the Server State Change Event
void OnServerStateChange(ServerState state)
{
if (ProgramIsStopping)
{
return;
}
var handler = ServerStateChange;
if (handler != null)
{
handler(this, new GenericTcpServerStateChangedEventArgs(state));
}
}
/// <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)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
ProgramIsStopping = true;
// kill bandaid things
if (MonitorClientTimer != null)
MonitorClientTimer.Stop();
if (MonitorClient != null)
MonitorClient.Disconnect();
this.LogInformation("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 Timer(60000) { AutoReset = false };
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
MonitorClientTimer.Start();
}
/// <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;
this.LogInformation("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)
{
this.LogInformation("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)
{
this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
StartMonitorClient();
}
else
{
this.LogError(
"\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
}