//******************************************************************************************************
// ClientConnection.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 06/24/2011 - Ritchie
// Generated original version of source code.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using GSF.Communication;
using GSF.Diagnostics;
using GSF.IO;
using GSF.Security.Cryptography;
using GSF.Threading;
namespace GSF.TimeSeries.Transport
{
///
/// Represents a client connection to the .
///
public class ClientConnection : IProvideStatus, IDisposable
{
#region [ Members ]
// Constants
private const int EvenKey = 0; // Even key/IV index
private const int OddKey = 1; // Odd key/IV index
private const int KeyIndex = 0; // Index of cipher key component in keyIV array
private const int IVIndex = 1; // Index of initialization vector component in keyIV array
// Fields
private DataPublisher m_parent;
private readonly Guid m_clientID;
private Guid m_subscriberID;
private readonly string m_connectionID;
private readonly string m_hostName;
private string m_subscriberAcronym;
private string m_subscriberName;
private string m_sharedSecret;
private string m_subscriberInfo;
private IClientSubscription m_subscription;
private volatile bool m_authenticated;
private volatile byte[][][] m_keyIVs;
private volatile int m_cipherIndex;
private List m_validIPAddresses;
private IPAddress m_ipAddress;
private IServer m_commandChannel;
private UdpServer m_dataChannel;
private string m_configurationString;
private bool m_connectionEstablished;
private bool m_isSubscribed;
private Ticks m_lastCipherKeyUpdateTime;
private SharedTimer m_pingTimer;
private SharedTimer m_reconnectTimer;
private OperationalModes m_operationalModes;
private Encoding m_encoding;
private bool m_disposed;
private bool m_clientNotFoundExceptionExceptionOccurred;
#endregion
#region [ Constructors ]
///
/// Creates a new instance.
///
/// Parent data publisher.
/// Client ID of associated connection.
/// command channel used to lookup connection information.
public ClientConnection(DataPublisher parent, Guid clientID, IServer commandChannel)
{
m_parent = parent;
m_clientID = clientID;
m_commandChannel = commandChannel;
m_subscriberID = clientID;
m_keyIVs = null;
m_cipherIndex = 0;
// Setup ping timer
m_pingTimer = Common.TimerScheduler.CreateTimer(5000);
m_pingTimer.AutoReset = true;
m_pingTimer.Elapsed += m_pingTimer_Elapsed;
m_pingTimer.Start();
// Setup reconnect timer
m_reconnectTimer = Common.TimerScheduler.CreateTimer(1000);
m_reconnectTimer.AutoReset = false;
m_reconnectTimer.Elapsed += m_reconnectTimer_Elapsed;
// Attempt to lookup remote connection identification for logging purposes
try
{
Socket commandChannelSocket = GetCommandChannelSocket();
IPEndPoint remoteEndPoint = null;
if ((object)commandChannel != null)
remoteEndPoint = commandChannelSocket.RemoteEndPoint as IPEndPoint;
if ((object)remoteEndPoint != null)
{
m_ipAddress = remoteEndPoint.Address;
if (remoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6)
m_connectionID = "[" + m_ipAddress + "]:" + remoteEndPoint.Port;
else
m_connectionID = m_ipAddress + ":" + remoteEndPoint.Port;
try
{
IPHostEntry ipHost = Dns.GetHostEntry(remoteEndPoint.Address);
if (!string.IsNullOrWhiteSpace(ipHost.HostName))
{
m_hostName = ipHost.HostName;
m_connectionID = m_hostName + " (" + m_connectionID + ")";
}
}
// Just ignoring possible DNS lookup failures...
catch (ArgumentNullException)
{
// The hostNameOrAddress parameter is null.
}
catch (ArgumentOutOfRangeException)
{
// The length of hostNameOrAddress parameter is greater than 255 characters.
}
catch (ArgumentException)
{
// The hostNameOrAddress parameter is an invalid IP address.
}
catch (SocketException)
{
// An error was encountered when resolving the hostNameOrAddress parameter.
}
}
}
catch
{
// At worst we'll just use the client GUID for identification
m_connectionID = m_subscriberID == Guid.Empty ? clientID.ToString() : m_subscriberID.ToString();
}
if (string.IsNullOrWhiteSpace(m_connectionID))
m_connectionID = "unavailable";
if (string.IsNullOrWhiteSpace(m_hostName))
{
if ((object)m_ipAddress != null)
m_hostName = m_ipAddress.ToString();
else
m_hostName = m_connectionID;
}
if ((object)m_ipAddress == null)
m_ipAddress = IPAddress.None;
}
///
/// Releases the unmanaged resources before the object is reclaimed by .
///
~ClientConnection()
{
Dispose(false);
}
#endregion
#region [ Properties ]
///
/// Gets client ID of this .
///
public Guid ClientID => m_clientID;
///
/// Gets or sets reference to data channel, attaching to or detaching from events as needed, associated with this .
///
public UdpServer DataChannel
{
get
{
return m_dataChannel;
}
set
{
m_connectionEstablished = (object)value != null;
if ((object)m_dataChannel != null)
{
// Detach from events on existing data channel reference
m_dataChannel.ClientConnectingException -= m_dataChannel_ClientConnectingException;
m_dataChannel.SendClientDataException -= m_dataChannel_SendClientDataException;
m_dataChannel.ServerStarted -= m_dataChannel_ServerStarted;
m_dataChannel.ServerStopped -= m_dataChannel_ServerStopped;
if (m_dataChannel != value)
m_dataChannel.Dispose();
}
// Assign new data channel reference
m_dataChannel = value;
if ((object)m_dataChannel != null)
{
// Save UDP settings so channel can be reestablished if needed
m_configurationString = m_dataChannel.ConfigurationString;
// Attach to events on new data channel reference
m_dataChannel.ClientConnectingException += m_dataChannel_ClientConnectingException;
m_dataChannel.SendClientDataException += m_dataChannel_SendClientDataException;
m_dataChannel.ServerStarted += m_dataChannel_ServerStarted;
m_dataChannel.ServerStopped += m_dataChannel_ServerStopped;
}
}
}
///
/// Gets command channel.
///
public IServer CommandChannel => m_commandChannel;
///
/// Gets publication channel - that is, data channel if defined otherwise command channel.
///
public IServer PublishChannel => (object)m_dataChannel == null ? m_commandChannel : m_dataChannel;
///
/// Gets connected state of the associated client socket.
///
public bool IsConnected
{
get
{
Socket commandChannelSocket;
bool isConnected = false;
try
{
commandChannelSocket = GetCommandChannelSocket();
if ((object)commandChannelSocket != null)
isConnected = commandChannelSocket.Connected;
}
catch
{
isConnected = false;
}
return isConnected;
}
}
///
/// Gets or sets IsSubscribed state.
///
public bool IsSubscribed
{
get
{
return m_isSubscribed;
}
set
{
m_isSubscribed = value;
}
}
///
/// Gets or sets flag that indicates if the socket exception for "No client found for ID [Guid]" has been thrown.
///
///
/// Since this message might be thrown many times before the communications channel has had a chance to disconnect
/// the socket, it is best to stop attempting to send data when this error has been encountered.
///
public bool ClientNotFoundExceptionOccurred
{
// Users have encountered issues when a client disconnects where many thousands of exceptions get thrown, every 3ms.
// This can cause the entire system to become unresponsive and causes all devices to reset (no data).
// System only recovers when the client disconnect process finally executes as this can take some time to occur.
get
{
return m_clientNotFoundExceptionExceptionOccurred;
}
set
{
m_clientNotFoundExceptionExceptionOccurred = value;
}
}
///
/// Gets or sets the based subscriber ID of this .
///
public Guid SubscriberID
{
get
{
return m_subscriberID;
}
set
{
m_subscriberID = value;
}
}
///
/// Gets or sets the subscriber acronym of this .
///
public string SubscriberAcronym
{
get
{
return m_subscriberAcronym;
}
set
{
m_subscriberAcronym = value;
}
}
///
/// Gets or sets the subscriber name of this .
///
public string SubscriberName
{
get
{
return m_subscriberName;
}
set
{
m_subscriberName = value;
}
}
///
/// Gets or sets subscriber info for this .
///
public string SubscriberInfo
{
get
{
if (string.IsNullOrWhiteSpace(m_subscriberInfo))
return m_subscriberName;
return m_subscriberInfo;
}
set
{
if (string.IsNullOrWhiteSpace(value))
{
m_subscriberInfo = null;
}
else
{
Dictionary settings = value.ParseKeyValuePairs();
string source, version, buildDate;
settings.TryGetValue("source", out source);
settings.TryGetValue("version", out version);
settings.TryGetValue("buildDate", out buildDate);
m_subscriberInfo = $"{source.ToNonNullNorWhiteSpace("unknown source")} version {version.ToNonNullNorWhiteSpace("?.?.?.?")} built on {buildDate.ToNonNullNorWhiteSpace("undefined date")}";
}
}
}
///
/// Gets the connection identification of this .
///
public string ConnectionID => m_connectionID;
///
/// Gets or sets authenticated state of this .
///
public bool Authenticated
{
get
{
return m_authenticated;
}
set
{
m_authenticated = value;
}
}
///
/// Gets or sets shared secret used to lookup cipher keys only known to client and server.
///
public string SharedSecret
{
get
{
return m_sharedSecret;
}
set
{
m_sharedSecret = value;
}
}
///
/// Gets active and standby keys and initialization vectors.
///
public byte[][][] KeyIVs => m_keyIVs;
///
/// Gets current cipher index.
///
public int CipherIndex => m_cipherIndex;
///
/// Gets time of last cipher key update.
///
public Ticks LastCipherKeyUpdateTime => m_lastCipherKeyUpdateTime;
///
/// Gets or sets the list of valid IP addresses that this client can connect from.
///
public List ValidIPAddresses
{
get
{
return m_validIPAddresses;
}
set
{
m_validIPAddresses = value;
}
}
///
/// Gets the IP address of the remote client connection.
///
public IPAddress IPAddress => m_ipAddress;
///
/// Gets or sets subscription associated with this .
///
public IClientSubscription Subscription
{
get
{
return m_subscription;
}
set
{
m_subscription = value;
if ((object)m_subscription != null)
m_subscription.Name = m_hostName;
}
}
///
/// Gets the subscriber name of this .
///
public string Name => SubscriberName;
///
/// Gets or sets a set of flags that define ways in
/// which the subscriber and publisher communicate.
///
public OperationalModes OperationalModes
{
get
{
return m_operationalModes;
}
set
{
m_operationalModes = value;
switch ((OperationalEncoding)(value & OperationalModes.EncodingMask))
{
case OperationalEncoding.Unicode:
m_encoding = Encoding.Unicode;
break;
case OperationalEncoding.BigEndianUnicode:
m_encoding = Encoding.BigEndianUnicode;
break;
case OperationalEncoding.UTF8:
m_encoding = Encoding.UTF8;
break;
case OperationalEncoding.ANSI:
m_encoding = Encoding.Default;
break;
default:
throw new InvalidOperationException($"Unsupported encoding detected: {value}");
}
}
}
///
/// Character encoding used to send messages to subscriber.
///
public Encoding Encoding => m_encoding ?? Encoding.Unicode;
///
/// Gets a formatted message describing the status of this .
///
public string Status
{
get
{
StringBuilder status = new StringBuilder();
const string formatString = "{0,26}: {1}";
status.AppendLine();
status.AppendFormat(formatString, "Subscriber ID", m_connectionID);
status.AppendLine();
status.AppendFormat(formatString, "Subscriber name", SubscriberName);
status.AppendLine();
status.AppendFormat(formatString, "Subscriber acronym", SubscriberAcronym);
status.AppendLine();
status.AppendFormat(formatString, "Publish channel protocol", PublishChannel.TransportProtocol);
status.AppendLine();
status.AppendFormat(formatString, "Data packet security", (object)m_keyIVs == null ? "unencrypted" : "encrypted");
status.AppendLine();
if ((object)m_dataChannel != null)
{
status.AppendLine();
status.Append(m_dataChannel.Status);
}
return status.ToString();
}
}
#endregion
#region [ Methods ]
///
/// Releases all the resources used by the object.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
{
if ((object)m_pingTimer != null)
{
m_pingTimer.Elapsed -= m_pingTimer_Elapsed;
m_pingTimer.Dispose();
m_pingTimer = null;
}
if ((object)m_reconnectTimer != null)
{
m_reconnectTimer.Elapsed -= m_reconnectTimer_Elapsed;
m_reconnectTimer.Dispose();
m_reconnectTimer = null;
}
DataChannel = null;
m_commandChannel = null;
m_ipAddress = null;
m_subscription = null;
m_parent = null;
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
}
}
}
///
/// Creates or updates cipher keys.
///
internal void UpdateKeyIVs()
{
using (AesManaged symmetricAlgorithm = new AesManaged())
{
symmetricAlgorithm.KeySize = 256;
symmetricAlgorithm.GenerateKey();
symmetricAlgorithm.GenerateIV();
if ((object)m_keyIVs == null)
{
// Initialize new key set
m_keyIVs = new byte[2][][];
m_keyIVs[EvenKey] = new byte[2][];
m_keyIVs[OddKey] = new byte[2][];
m_keyIVs[EvenKey][KeyIndex] = symmetricAlgorithm.Key;
m_keyIVs[EvenKey][IVIndex] = symmetricAlgorithm.IV;
symmetricAlgorithm.GenerateKey();
symmetricAlgorithm.GenerateIV();
m_keyIVs[OddKey][KeyIndex] = symmetricAlgorithm.Key;
m_keyIVs[OddKey][IVIndex] = symmetricAlgorithm.IV;
m_cipherIndex = EvenKey;
}
else
{
// Generate a new key set for current cipher index
m_keyIVs[m_cipherIndex][KeyIndex] = symmetricAlgorithm.Key;
m_keyIVs[m_cipherIndex][IVIndex] = symmetricAlgorithm.IV;
// Set run-time to the other key set
m_cipherIndex ^= 1;
}
}
m_lastCipherKeyUpdateTime = DateTime.UtcNow.Ticks;
}
///
/// Rotates or initializes the crypto keys for this .
///
public bool RotateCipherKeys()
{
// Make sure at least a second has passed before next key rotation
if ((DateTime.UtcNow.Ticks - m_lastCipherKeyUpdateTime).ToMilliseconds() >= 1000.0D)
{
try
{
// Since this function cannot be not called more than once per second there
// is no real benefit to maintaining these memory streams at a member level
using (BlockAllocatedMemoryStream response = new BlockAllocatedMemoryStream())
{
byte[] bytes, bufferLen;
// Create or update cipher keys and initialization vectors
UpdateKeyIVs();
// Add current cipher index to response
response.WriteByte((byte)m_cipherIndex);
// Serialize new keys
using (BlockAllocatedMemoryStream buffer = new BlockAllocatedMemoryStream())
{
// Write even key
bufferLen = BigEndian.GetBytes(m_keyIVs[EvenKey][KeyIndex].Length);
buffer.Write(bufferLen, 0, bufferLen.Length);
buffer.Write(m_keyIVs[EvenKey][KeyIndex], 0, m_keyIVs[EvenKey][KeyIndex].Length);
// Write even initialization vector
bufferLen = BigEndian.GetBytes(m_keyIVs[EvenKey][IVIndex].Length);
buffer.Write(bufferLen, 0, bufferLen.Length);
buffer.Write(m_keyIVs[EvenKey][IVIndex], 0, m_keyIVs[EvenKey][IVIndex].Length);
// Write odd key
bufferLen = BigEndian.GetBytes(m_keyIVs[OddKey][KeyIndex].Length);
buffer.Write(bufferLen, 0, bufferLen.Length);
buffer.Write(m_keyIVs[OddKey][KeyIndex], 0, m_keyIVs[OddKey][KeyIndex].Length);
// Write odd initialization vector
bufferLen = BigEndian.GetBytes(m_keyIVs[OddKey][IVIndex].Length);
buffer.Write(bufferLen, 0, bufferLen.Length);
buffer.Write(m_keyIVs[OddKey][IVIndex], 0, m_keyIVs[OddKey][IVIndex].Length);
// Get bytes from serialized buffer
bytes = buffer.ToArray();
}
// Encrypt keys using private keys known only to current client and server
if (m_authenticated && !string.IsNullOrWhiteSpace(m_sharedSecret))
bytes = bytes.Encrypt(m_sharedSecret, CipherStrength.Aes256);
// Add serialized key response
response.Write(bytes, 0, bytes.Length);
// Send cipher key updates
m_parent.SendClientResponse(m_clientID, ServerResponse.UpdateCipherKeys, ServerCommand.Subscribe, response.ToArray());
}
// Send success message
m_parent.SendClientResponse(m_clientID, ServerResponse.Succeeded, ServerCommand.RotateCipherKeys, "New cipher keys established.");
m_parent.OnStatusMessage(MessageLevel.Info, $"{ConnectionID} cipher keys rotated.");
return true;
}
catch (Exception ex)
{
// Send failure message
m_parent.SendClientResponse(m_clientID, ServerResponse.Failed, ServerCommand.RotateCipherKeys, "Failed to establish new cipher keys: " + ex.Message);
m_parent.OnStatusMessage(MessageLevel.Warning, $"Failed to establish new cipher keys for {ConnectionID}: {ex.Message}");
return false;
}
}
m_parent.SendClientResponse(m_clientID, ServerResponse.Failed, ServerCommand.RotateCipherKeys, "Cipher key rotation skipped, keys were already rotated within last second.");
m_parent.OnStatusMessage(MessageLevel.Warning, $"Cipher key rotation skipped for {ConnectionID}, keys were already rotated within last second.");
return false;
}
///
/// Gets the instance used by this client
/// connection to send and receive data over the command channel.
///
/// The socket instance used by the client to send and receive data over the command channel.
public Socket GetCommandChannelSocket()
{
TcpServer tcpCommandChannel = m_commandChannel as TcpServer;
TlsServer tlsCommandChannel = m_commandChannel as TlsServer;
TransportProvider tcpProvider;
TransportProvider tlsProvider;
if ((object)tcpCommandChannel != null && tcpCommandChannel.TryGetClient(m_clientID, out tcpProvider))
return tcpProvider.Provider;
if ((object)tlsCommandChannel != null && tlsCommandChannel.TryGetClient(m_clientID, out tlsProvider))
return tlsProvider.Provider.Socket;
return null;
}
private void m_pingTimer_Elapsed(object sender, EventArgs e)
{
// Send a no-op keep-alive ping to make sure the client is still connected
m_parent.SendClientResponse(m_clientID, ServerResponse.NoOP, ServerCommand.Subscribe);
}
private void m_dataChannel_ClientConnectingException(object sender, EventArgs e)
{
Exception ex = e.Argument;
m_parent.OnProcessException(MessageLevel.Info, new ConnectionException($"Data channel client connection exception occurred while sending client data to \"{m_connectionID}\": {ex.Message}", ex));
}
private void m_dataChannel_SendClientDataException(object sender, EventArgs e)
{
Exception ex = e.Argument2;
m_parent.OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data channel exception occurred while sending client data to \"{m_connectionID}\": {ex.Message}", ex));
}
private void m_dataChannel_ServerStarted(object sender, EventArgs e)
{
m_parent.OnStatusMessage(MessageLevel.Info, "Data channel started.");
}
private void m_dataChannel_ServerStopped(object sender, EventArgs e)
{
if (m_connectionEstablished)
{
m_parent.OnStatusMessage(MessageLevel.Info, "Data channel stopped unexpectedly, restarting data channel...");
if ((object)m_reconnectTimer != null)
m_reconnectTimer.Start();
}
else
{
m_parent.OnStatusMessage(MessageLevel.Info, "Data channel stopped.");
}
}
private void m_reconnectTimer_Elapsed(object sender, EventArgs e)
{
try
{
m_parent.OnStatusMessage(MessageLevel.Info, "Attempting to restart data channel...");
DataChannel = null;
UdpServer dataChannel = new UdpServer(m_configurationString);
dataChannel.Start();
DataChannel = dataChannel;
m_parent.OnStatusMessage(MessageLevel.Info, "Data channel successfully restarted.");
}
catch (Exception ex)
{
m_parent.OnStatusMessage(MessageLevel.Warning, $"Failed to restart data channel due to exception: {ex.Message}");
m_reconnectTimer.Start();
}
}
#endregion
}
}