using System;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace NetCoreServer
{
///
/// SSL session is used to read and write data from the connected SSL client
///
/// Thread-safe
public class SslSession : IDisposable
{
///
/// Initialize the session with a given server
///
/// SSL server
public SslSession(SslServer server)
{
Id = Guid.NewGuid();
Server = server;
OptionReceiveBufferSize = server.OptionReceiveBufferSize;
OptionSendBufferSize = server.OptionSendBufferSize;
}
///
/// Session Id
///
public Guid Id { get; }
///
/// Server
///
public SslServer Server { get; }
///
/// Socket
///
public Socket Socket { get; private set; }
///
/// Number of bytes pending sent by the session
///
public long BytesPending { get; private set; }
///
/// Number of bytes sending by the session
///
public long BytesSending { get; private set; }
///
/// Number of bytes sent by the session
///
public long BytesSent { get; private set; }
///
/// Number of bytes received by the session
///
public long BytesReceived { get; private set; }
///
/// Option: receive buffer limit
///
public int OptionReceiveBufferLimit { get; set; } = 0;
///
/// Option: receive buffer size
///
public int OptionReceiveBufferSize { get; set; } = 8192;
///
/// Option: send buffer limit
///
public int OptionSendBufferLimit { get; set; } = 0;
///
/// Option: send buffer size
///
public int OptionSendBufferSize { get; set; } = 8192;
#region Connect/Disconnect session
private bool _disconnecting;
private SslStream _sslStream;
private Guid? _sslStreamId;
///
/// Is the session connected?
///
public bool IsConnected { get; private set; }
///
/// Is the session handshaked?
///
public bool IsHandshaked { get; private set; }
///
/// Connect the session
///
/// Session socket
internal void Connect(Socket socket)
{
Socket = socket;
// Update the session socket disposed flag
IsSocketDisposed = false;
// Setup buffers
_receiveBuffer = new Buffer();
_sendBufferMain = new Buffer();
_sendBufferFlush = new Buffer();
// Apply the option: keep alive
if (Server.OptionKeepAlive)
Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
if (Server.OptionTcpKeepAliveTime >= 0)
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, Server.OptionTcpKeepAliveTime);
if (Server.OptionTcpKeepAliveInterval >= 0)
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, Server.OptionTcpKeepAliveInterval);
if (Server.OptionTcpKeepAliveRetryCount >= 0)
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, Server.OptionTcpKeepAliveRetryCount);
// Apply the option: no delay
if (Server.OptionNoDelay)
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
// Prepare receive & send buffers
_receiveBuffer.Reserve(OptionReceiveBufferSize);
_sendBufferMain.Reserve(OptionSendBufferSize);
_sendBufferFlush.Reserve(OptionSendBufferSize);
// Reset statistic
BytesPending = 0;
BytesSending = 0;
BytesSent = 0;
BytesReceived = 0;
// Call the session connecting handler
OnConnecting();
// Call the session connecting handler in the server
Server.OnConnectingInternal(this);
// Update the connected flag
IsConnected = true;
// Call the session connected handler
OnConnected();
// Call the session connected handler in the server
Server.OnConnectedInternal(this);
try
{
// Create SSL stream
_sslStreamId = Guid.NewGuid();
_sslStream = (Server.Context.CertificateValidationCallback != null) ? new SslStream(new NetworkStream(Socket, false), false, Server.Context.CertificateValidationCallback) : new SslStream(new NetworkStream(Socket, false), false);
// Call the session handshaking handler
OnHandshaking();
// Call the session handshaking handler in the server
Server.OnHandshakingInternal(this);
// Begin the SSL handshake
_sslStream.BeginAuthenticateAsServer(Server.Context.Certificate, Server.Context.ClientCertificateRequired, Server.Context.Protocols, false, ProcessHandshake, _sslStreamId);
}
catch (Exception)
{
SendError(SocketError.NotConnected);
Disconnect();
}
}
///
/// Disconnect the session
///
/// 'true' if the section was successfully disconnected, 'false' if the section is already disconnected
public virtual bool Disconnect()
{
if (!IsConnected)
return false;
if (_disconnecting)
return false;
// Update the disconnecting flag
_disconnecting = true;
// Call the session disconnecting handler
OnDisconnecting();
// Call the session disconnecting handler in the server
Server.OnDisconnectingInternal(this);
try
{
try
{
// Shutdown the SSL stream
_sslStream.ShutdownAsync().Wait();
}
catch (Exception) {}
// Dispose the SSL stream & buffer
_sslStream.Dispose();
_sslStreamId = null;
try
{
// Shutdown the socket associated with the client
Socket.Shutdown(SocketShutdown.Both);
}
catch (SocketException) {}
// Close the session socket
Socket.Close();
// Dispose the session socket
Socket.Dispose();
// Update the session socket disposed flag
IsSocketDisposed = true;
}
catch (ObjectDisposedException) {}
// Update the handshaked flag
IsHandshaked = false;
// Update the connected flag
IsConnected = false;
// Update sending/receiving flags
_receiving = false;
_sending = false;
// Clear send/receive buffers
ClearBuffers();
// Call the session disconnected handler
OnDisconnected();
// Call the session disconnected handler in the server
Server.OnDisconnectedInternal(this);
// Unregister session
Server.UnregisterSession(Id);
// Reset the disconnecting flag
_disconnecting = false;
return true;
}
#endregion
#region Send/Receive data
// Receive buffer
private bool _receiving;
private Buffer _receiveBuffer;
// Send buffer
private readonly object _sendLock = new object();
private bool _sending;
private Buffer _sendBufferMain;
private Buffer _sendBufferFlush;
private long _sendBufferFlushOffset;
///
/// Send data to the client (synchronous)
///
/// Buffer to send
/// Size of sent data
public virtual long Send(byte[] buffer) => Send(buffer.AsSpan());
///
/// Send data to the client (synchronous)
///
/// Buffer to send
/// Buffer offset
/// Buffer size
/// Size of sent data
public virtual long Send(byte[] buffer, long offset, long size) => Send(buffer.AsSpan((int)offset, (int)size));
///
/// Send data to the client (synchronous)
///
/// Buffer to send as a span of bytes
/// Size of sent data
public virtual long Send(ReadOnlySpan buffer)
{
if (!IsHandshaked)
return 0;
if (buffer.IsEmpty)
return 0;
try
{
// Sent data to the server
_sslStream.Write(buffer);
long sent = buffer.Length;
// Update statistic
BytesSent += sent;
Interlocked.Add(ref Server._bytesSent, sent);
// Call the buffer sent handler
OnSent(sent, BytesPending + BytesSending);
return sent;
}
catch (Exception)
{
SendError(SocketError.OperationAborted);
Disconnect();
return 0;
}
}
///
/// Send text to the client (synchronous)
///
/// Text string to send
/// Size of sent text
public virtual long Send(string text) => Send(Encoding.UTF8.GetBytes(text));
///
/// Send text to the client (synchronous)
///
/// Text to send as a span of characters
/// Size of sent text
public virtual long Send(ReadOnlySpan text) => Send(Encoding.UTF8.GetBytes(text.ToArray()));
///
/// Send data to the client (asynchronous)
///
/// Buffer to send
/// 'true' if the data was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(byte[] buffer) => SendAsync(buffer.AsSpan());
///
/// Send data to the client (asynchronous)
///
/// Buffer to send
/// Buffer offset
/// Buffer size
/// 'true' if the data was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(byte[] buffer, long offset, long size) => SendAsync(buffer.AsSpan((int)offset, (int)size));
///
/// Send data to the client (asynchronous)
///
/// Buffer to send as a span of bytes
/// 'true' if the data was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(ReadOnlySpan buffer)
{
if (!IsHandshaked)
return false;
if (buffer.IsEmpty)
return true;
lock (_sendLock)
{
// Check the send buffer limit
if (((_sendBufferMain.Size + buffer.Length) > OptionSendBufferLimit) && (OptionSendBufferLimit > 0))
{
SendError(SocketError.NoBufferSpaceAvailable);
return false;
}
// Fill the main send buffer
_sendBufferMain.Append(buffer);
// Update statistic
BytesPending = _sendBufferMain.Size;
// Avoid multiple send handlers
if (_sending)
return true;
else
_sending = true;
// Try to send the main buffer
TrySend();
}
return true;
}
///
/// Send text to the client (asynchronous)
///
/// Text string to send
/// 'true' if the text was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(string text) => SendAsync(Encoding.UTF8.GetBytes(text));
///
/// Send text to the client (asynchronous)
///
/// Text to send as a span of characters
/// 'true' if the text was successfully sent, 'false' if the session is not connected
public virtual bool SendAsync(ReadOnlySpan text) => SendAsync(Encoding.UTF8.GetBytes(text.ToArray()));
///
/// Receive data from the client (synchronous)
///
/// Buffer to receive
/// Size of received data
public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); }
///
/// Receive data from the client (synchronous)
///
/// Buffer to receive
/// Buffer offset
/// Buffer size
/// Size of received data
public virtual long Receive(byte[] buffer, long offset, long size)
{
if (!IsHandshaked)
return 0;
if (size == 0)
return 0;
try
{
// Receive data from the client
long received = _sslStream.Read(buffer, (int)offset, (int)size);
if (received > 0)
{
// Update statistic
BytesReceived += received;
Interlocked.Add(ref Server._bytesReceived, received);
// Call the buffer received handler
OnReceived(buffer, 0, received);
}
return received;
}
catch (Exception)
{
SendError(SocketError.OperationAborted);
Disconnect();
return 0;
}
}
///
/// Receive text from the client (synchronous)
///
/// Text size to receive
/// Received text
public virtual string Receive(long size)
{
var buffer = new byte[size];
var length = Receive(buffer);
return Encoding.UTF8.GetString(buffer, 0, (int)length);
}
///
/// Receive data from the client (asynchronous)
///
public virtual void ReceiveAsync()
{
// Try to receive data from the client
TryReceive();
}
///
/// Try to receive new data
///
private void TryReceive()
{
if (_receiving)
return;
if (!IsHandshaked)
return;
try
{
// Async receive with the receive handler
IAsyncResult result;
do
{
if (!IsHandshaked)
return;
_receiving = true;
result = _sslStream.BeginRead(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity, ProcessReceive, _sslStreamId);
} while (result.CompletedSynchronously);
}
catch (ObjectDisposedException) {}
}
///
/// Try to send pending data
///
private void TrySend()
{
if (!IsHandshaked)
return;
bool empty = false;
lock (_sendLock)
{
// Is previous socket send in progress?
if (_sendBufferFlush.IsEmpty)
{
// Swap flush and main buffers
_sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush);
_sendBufferFlushOffset = 0;
// Update statistic
BytesPending = 0;
BytesSending += _sendBufferFlush.Size;
// Check if the flush buffer is empty
if (_sendBufferFlush.IsEmpty)
{
// Need to call empty send buffer handler
empty = true;
// End sending process
_sending = false;
}
}
else
return;
}
// Call the empty send buffer handler
if (empty)
{
OnEmpty();
return;
}
try
{
// Async write with the write handler
_sslStream.BeginWrite(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset), ProcessSend, _sslStreamId);
}
catch (ObjectDisposedException) {}
}
///
/// Clear send/receive buffers
///
private void ClearBuffers()
{
lock (_sendLock)
{
// Clear send buffers
_sendBufferMain.Clear();
_sendBufferFlush.Clear();
_sendBufferFlushOffset= 0;
// Update statistic
BytesPending = 0;
BytesSending = 0;
}
}
#endregion
#region IO processing
///
/// This method is invoked when an asynchronous handshake operation completes
///
private void ProcessHandshake(IAsyncResult result)
{
try
{
if (IsHandshaked)
return;
// Validate SSL stream Id
var sslStreamId = result.AsyncState as Guid?;
if (_sslStreamId != sslStreamId)
return;
// End the SSL handshake
_sslStream.EndAuthenticateAsServer(result);
// Update the handshaked flag
IsHandshaked = true;
// Try to receive something from the client
TryReceive();
// Check the socket disposed state: in some rare cases it might be disconnected while receiving!
if (IsSocketDisposed)
return;
// Call the session handshaked handler
OnHandshaked();
// Call the session handshaked handler in the server
Server.OnHandshakedInternal(this);
// Call the empty send buffer handler
if (_sendBufferMain.IsEmpty)
OnEmpty();
}
catch (Exception)
{
SendError(SocketError.NotConnected);
Disconnect();
}
}
///
/// This method is invoked when an asynchronous receive operation completes
///
private void ProcessReceive(IAsyncResult result)
{
try
{
if (!IsHandshaked)
return;
// Validate SSL stream Id
var sslStreamId = result.AsyncState as Guid?;
if (_sslStreamId != sslStreamId)
return;
// End the SSL read
long size = _sslStream.EndRead(result);
// Received some data from the client
if (size > 0)
{
// Update statistic
BytesReceived += size;
Interlocked.Add(ref Server._bytesReceived, size);
// Call the buffer received handler
OnReceived(_receiveBuffer.Data, 0, size);
// If the receive buffer is full increase its size
if (_receiveBuffer.Capacity == size)
{
// Check the receive buffer limit
if (((2 * size) > OptionReceiveBufferLimit) && (OptionReceiveBufferLimit > 0))
{
SendError(SocketError.NoBufferSpaceAvailable);
Disconnect();
return;
}
_receiveBuffer.Reserve(2 * size);
}
}
_receiving = false;
// If zero is returned from a read operation, the remote end has closed the connection
if (size > 0)
{
if (!result.CompletedSynchronously)
TryReceive();
}
else
Disconnect();
}
catch (Exception)
{
SendError(SocketError.OperationAborted);
Disconnect();
}
}
///
/// This method is invoked when an asynchronous send operation completes
///
private void ProcessSend(IAsyncResult result)
{
try
{
// Validate SSL stream Id
var sslStreamId = result.AsyncState as Guid?;
if (_sslStreamId != sslStreamId)
return;
if (!IsHandshaked)
return;
// End the SSL write
_sslStream.EndWrite(result);
long size = _sendBufferFlush.Size;
// Send some data to the client
if (size > 0)
{
// Update statistic
BytesSending -= size;
BytesSent += size;
Interlocked.Add(ref Server._bytesSent, size);
// Increase the flush buffer offset
_sendBufferFlushOffset += size;
// Successfully send the whole flush buffer
if (_sendBufferFlushOffset == _sendBufferFlush.Size)
{
// Clear the flush buffer
_sendBufferFlush.Clear();
_sendBufferFlushOffset = 0;
}
// Call the buffer sent handler
OnSent(size, BytesPending + BytesSending);
}
// Try to send again if the session is valid
TrySend();
}
catch (Exception)
{
SendError(SocketError.OperationAborted);
Disconnect();
}
}
#endregion
#region Session handlers
///
/// Handle client connecting notification
///
protected virtual void OnConnecting() {}
///
/// Handle client connected notification
///
protected virtual void OnConnected() {}
///
/// Handle client handshaking notification
///
protected virtual void OnHandshaking() {}
///
/// Handle client handshaked notification
///
protected virtual void OnHandshaked() {}
///
/// Handle client disconnecting notification
///
protected virtual void OnDisconnecting() {}
///
/// Handle client disconnected notification
///
protected virtual void OnDisconnected() {}
///
/// Handle buffer received notification
///
/// Received buffer
/// Received buffer offset
/// Received buffer size
///
/// Notification is called when another part of buffer was received from the client
///
protected virtual void OnReceived(byte[] buffer, long offset, long size) {}
///
/// Handle buffer sent notification
///
/// Size of sent buffer
/// Size of pending buffer
///
/// Notification is called when another part of buffer was sent to the client.
/// This handler could be used to send another buffer to the client for instance when the pending size is zero.
///
protected virtual void OnSent(long sent, long pending) {}
///
/// Handle empty send buffer notification
///
///
/// Notification is called when the send buffer is empty and ready for a new data to send.
/// This handler could be used to send another buffer to the client.
///
protected virtual void OnEmpty() {}
///
/// Handle error notification
///
/// Socket error code
protected virtual void OnError(SocketError error) {}
#endregion
#region Error handling
///
/// Send error notification
///
/// Socket error code
private void SendError(SocketError error)
{
// Skip disconnect errors
if ((error == SocketError.ConnectionAborted) ||
(error == SocketError.ConnectionRefused) ||
(error == SocketError.ConnectionReset) ||
(error == SocketError.OperationAborted) ||
(error == SocketError.Shutdown))
return;
OnError(error);
}
#endregion
#region IDisposable implementation
///
/// Disposed flag
///
public bool IsDisposed { get; private set; }
///
/// Session socket disposed flag
///
public bool IsSocketDisposed { get; private set; } = true;
// Implement IDisposable.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposingManagedResources)
{
// The idea here is that Dispose(Boolean) knows whether it is
// being called to do explicit cleanup (the Boolean is true)
// versus being called due to a garbage collection (the Boolean
// is false). This distinction is useful because, when being
// disposed explicitly, the Dispose(Boolean) method can safely
// execute code using reference type fields that refer to other
// objects knowing for sure that these other objects have not been
// finalized or disposed of yet. When the Boolean is false,
// the Dispose(Boolean) method should not execute code that
// refer to reference type fields because those objects may
// have already been finalized."
if (!IsDisposed)
{
if (disposingManagedResources)
{
// Dispose managed resources here...
Disconnect();
}
// Dispose unmanaged resources here...
// Set large fields to null here...
// Mark as disposed.
IsDisposed = true;
}
}
#endregion
}
}