using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace NetCoreServer
{
///
/// SSL server is used to connect, disconnect and manage SSL sessions
///
/// Thread-safe
public class SslServer : IDisposable
{
///
/// Initialize SSL server with a given IP address and port number
///
/// SSL context
/// IP address
/// Port number
public SslServer(SslContext context, IPAddress address, int port) : this(context, new IPEndPoint(address, port)) {}
///
/// Initialize SSL server with a given IP address and port number
///
/// SSL context
/// IP address
/// Port number
public SslServer(SslContext context, string address, int port) : this(context, new IPEndPoint(IPAddress.Parse(address), port)) {}
///
/// Initialize SSL server with a given DNS endpoint
///
/// SSL context
/// DNS endpoint
public SslServer(SslContext context, DnsEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Host, endpoint.Port) {}
///
/// Initialize SSL server with a given IP endpoint
///
/// SSL context
/// IP endpoint
public SslServer(SslContext context, IPEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {}
///
/// Initialize SSL server with a given SSL context, endpoint, address and port
///
/// SSL context
/// Endpoint
/// Server address
/// Server port
private SslServer(SslContext context, EndPoint endpoint, string address, int port)
{
Id = Guid.NewGuid();
Address = address;
Port = port;
Context = context;
Endpoint = endpoint;
}
///
/// Server Id
///
public Guid Id { get; }
///
/// SSL server address
///
public string Address { get; }
///
/// SSL server port
///
public int Port { get; }
///
/// SSL context
///
public SslContext Context { get; }
///
/// Endpoint
///
public EndPoint Endpoint { get; private set; }
///
/// Number of sessions connected to the server
///
public long ConnectedSessions { get { return Sessions.Count; } }
///
/// Number of bytes pending sent by the server
///
public long BytesPending { get { return _bytesPending; } }
///
/// Number of bytes sent by the server
///
public long BytesSent { get { return _bytesSent; } }
///
/// Number of bytes received by the server
///
public long BytesReceived { get { return _bytesReceived; } }
///
/// Option: acceptor backlog size
///
///
/// This option will set the listening socket's backlog size
///
public int OptionAcceptorBacklog { get; set; } = 1024;
///
/// Option: dual mode socket
///
///
/// Specifies whether the Socket is a dual-mode socket used for both IPv4 and IPv6.
/// Will work only if socket is bound on IPv6 address.
///
public bool OptionDualMode { get; set; }
///
/// Option: keep alive
///
///
/// This option will setup SO_KEEPALIVE if the OS support this feature
///
public bool OptionKeepAlive { get; set; }
///
/// Option: TCP keep alive time
///
///
/// The number of seconds a TCP connection will remain alive/idle before keepalive probes are sent to the remote
///
public int OptionTcpKeepAliveTime { get; set; } = -1;
///
/// Option: TCP keep alive interval
///
///
/// The number of seconds a TCP connection will wait for a keepalive response before sending another keepalive probe
///
public int OptionTcpKeepAliveInterval { get; set; } = -1;
///
/// Option: TCP keep alive retry count
///
///
/// The number of TCP keep alive probes that will be sent before the connection is terminated
///
public int OptionTcpKeepAliveRetryCount { get; set; } = -1;
///
/// Option: no delay
///
///
/// This option will enable/disable Nagle's algorithm for SSL protocol
///
public bool OptionNoDelay { get; set; }
///
/// Option: reuse address
///
///
/// This option will enable/disable SO_REUSEADDR if the OS support this feature
///
public bool OptionReuseAddress { get; set; }
///
/// Option: enables a socket to be bound for exclusive access
///
///
/// This option will enable/disable SO_EXCLUSIVEADDRUSE if the OS support this feature
///
public bool OptionExclusiveAddressUse { get; set; }
///
/// Option: receive buffer size
///
public int OptionReceiveBufferSize { get; set; } = 8192;
///
/// Option: send buffer size
///
public int OptionSendBufferSize { get; set; } = 8192;
#region Start/Stop server
// Server acceptor
private Socket _acceptorSocket;
private SocketAsyncEventArgs _acceptorEventArg;
// Server statistic
internal long _bytesPending;
internal long _bytesSent;
internal long _bytesReceived;
///
/// Is the server started?
///
public bool IsStarted { get; private set; }
///
/// Is the server accepting new clients?
///
public bool IsAccepting { get; private set; }
///
/// Create a new socket object
///
///
/// Method may be override if you need to prepare some specific socket object in your implementation.
///
/// Socket object
protected virtual Socket CreateSocket()
{
return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
}
///
/// Start the server
///
/// 'true' if the server was successfully started, 'false' if the server failed to start
public virtual bool Start()
{
Debug.Assert(!IsStarted, "SSL server is already started!");
if (IsStarted)
return false;
// Setup acceptor event arg
_acceptorEventArg = new SocketAsyncEventArgs();
_acceptorEventArg.Completed += OnAsyncCompleted;
// Create a new acceptor socket
_acceptorSocket = CreateSocket();
// Update the acceptor socket disposed flag
IsSocketDisposed = false;
// Apply the option: reuse address
_acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress);
// Apply the option: exclusive address use
_acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse);
// Apply the option: dual mode (this option must be applied before listening)
if (_acceptorSocket.AddressFamily == AddressFamily.InterNetworkV6)
_acceptorSocket.DualMode = OptionDualMode;
// Bind the acceptor socket to the endpoint
_acceptorSocket.Bind(Endpoint);
// Refresh the endpoint property based on the actual endpoint created
Endpoint = _acceptorSocket.LocalEndPoint;
// Call the server starting handler
OnStarting();
// Start listen to the acceptor socket with the given accepting backlog size
_acceptorSocket.Listen(OptionAcceptorBacklog);
// Reset statistic
_bytesPending = 0;
_bytesSent = 0;
_bytesReceived = 0;
// Update the started flag
IsStarted = true;
// Call the server started handler
OnStarted();
// Perform the first server accept
IsAccepting = true;
StartAccept(_acceptorEventArg);
return true;
}
///
/// Stop the server
///
/// 'true' if the server was successfully stopped, 'false' if the server is already stopped
public virtual bool Stop()
{
Debug.Assert(IsStarted, "SSL server is not started!");
if (!IsStarted)
return false;
// Stop accepting new clients
IsAccepting = false;
// Reset acceptor event arg
_acceptorEventArg.Completed -= OnAsyncCompleted;
// Call the server stopping handler
OnStopping();
try
{
// Close the acceptor socket
_acceptorSocket.Close();
// Dispose the acceptor socket
_acceptorSocket.Dispose();
// Dispose event arguments
_acceptorEventArg.Dispose();
// Update the acceptor socket disposed flag
IsSocketDisposed = true;
}
catch (ObjectDisposedException) {}
// Disconnect all sessions
DisconnectAll();
// Update the started flag
IsStarted = false;
// Call the server stopped handler
OnStopped();
return true;
}
///
/// Restart the server
///
/// 'true' if the server was successfully restarted, 'false' if the server failed to restart
public virtual bool Restart()
{
if (!Stop())
return false;
while (IsStarted)
Thread.Yield();
return Start();
}
#endregion
#region Accepting clients
///
/// Start accept a new client connection
///
private void StartAccept(SocketAsyncEventArgs e)
{
// Socket must be cleared since the context object is being reused
e.AcceptSocket = null;
// Async accept a new client connection
if (!_acceptorSocket.AcceptAsync(e))
ProcessAccept(e);
}
///
/// Process accepted client connection
///
private void ProcessAccept(SocketAsyncEventArgs e)
{
if (e.SocketError == SocketError.Success)
{
// Create a new session to register
var session = CreateSession();
// Register the session
RegisterSession(session);
// Connect new session
session.Connect(e.AcceptSocket);
}
else
SendError(e.SocketError);
// Accept the next client connection
if (IsAccepting)
StartAccept(e);
}
///
/// This method is the callback method associated with Socket.AcceptAsync()
/// operations and is invoked when an accept operation is complete
///
private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e)
{
if (IsSocketDisposed)
return;
ProcessAccept(e);
}
#endregion
#region Session factory
///
/// Create SSL session factory method
///
/// SSL session
protected virtual SslSession CreateSession() { return new SslSession(this); }
#endregion
#region Session management
///
/// Server sessions
///
protected readonly ConcurrentDictionary Sessions = new ConcurrentDictionary();
///
/// Disconnect all connected sessions
///
/// 'true' if all sessions were successfully disconnected, 'false' if the server is not started
public virtual bool DisconnectAll()
{
if (!IsStarted)
return false;
// Disconnect all sessions
foreach (var session in Sessions.Values)
session.Disconnect();
return true;
}
///
/// Find a session with a given Id
///
/// Session Id
/// Session with a given Id or null if the session it not connected
public SslSession FindSession(Guid id)
{
// Try to find the required session
return Sessions.TryGetValue(id, out SslSession result) ? result : null;
}
///
/// Register a new session
///
/// Session to register
internal void RegisterSession(SslSession session)
{
// Register a new session
Sessions.TryAdd(session.Id, session);
}
///
/// Unregister session by Id
///
/// Session Id
internal void UnregisterSession(Guid id)
{
// Unregister session by Id
Sessions.TryRemove(id, out SslSession _);
}
#endregion
#region Multicasting
///
/// Multicast data to all connected sessions
///
/// Buffer to multicast
/// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted
public virtual bool Multicast(byte[] buffer) => Multicast(buffer.AsSpan());
///
/// Multicast data to all connected clients
///
/// Buffer to multicast
/// Buffer offset
/// Buffer size
/// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted
public virtual bool Multicast(byte[] buffer, long offset, long size) => Multicast(buffer.AsSpan((int)offset, (int)size));
///
/// Multicast data to all connected clients
///
/// Buffer to send as a span of bytes
/// 'true' if the data was successfully multicasted, 'false' if the data was not multicasted
public virtual bool Multicast(ReadOnlySpan buffer)
{
if (!IsStarted)
return false;
if (buffer.IsEmpty)
return true;
// Multicast data to all sessions
foreach (var session in Sessions.Values)
session.SendAsync(buffer);
return true;
}
///
/// Multicast text to all connected clients
///
/// Text string to multicast
/// 'true' if the text was successfully multicasted, 'false' if the text was not multicasted
public virtual bool Multicast(string text) => Multicast(Encoding.UTF8.GetBytes(text));
///
/// Multicast text to all connected clients
///
/// Text to multicast as a span of characters
/// 'true' if the text was successfully multicasted, 'false' if the text was not multicasted
public virtual bool Multicast(ReadOnlySpan text) => Multicast(Encoding.UTF8.GetBytes(text.ToArray()));
#endregion
#region Server handlers
///
/// Handle server starting notification
///
protected virtual void OnStarting() {}
///
/// Handle server started notification
///
protected virtual void OnStarted() {}
///
/// Handle server stopping notification
///
protected virtual void OnStopping() {}
///
/// Handle server stopped notification
///
protected virtual void OnStopped() {}
///
/// Handle session connecting notification
///
/// Connecting session
protected virtual void OnConnecting(SslSession session) {}
///
/// Handle session connected notification
///
/// Connected session
protected virtual void OnConnected(SslSession session) {}
///
/// Handle session handshaking notification
///
/// Handshaking session
protected virtual void OnHandshaking(SslSession session) {}
///
/// Handle session handshaked notification
///
/// Handshaked session
protected virtual void OnHandshaked(SslSession session) {}
///
/// Handle session disconnecting notification
///
/// Disconnecting session
protected virtual void OnDisconnecting(SslSession session) {}
///
/// Handle session disconnected notification
///
/// Disconnected session
protected virtual void OnDisconnected(SslSession session) {}
///
/// Handle error notification
///
/// Socket error code
protected virtual void OnError(SocketError error) {}
internal void OnConnectingInternal(SslSession session) { OnConnecting(session); }
internal void OnConnectedInternal(SslSession session) { OnConnected(session); }
internal void OnHandshakingInternal(SslSession session) { OnHandshaking(session); }
internal void OnHandshakedInternal(SslSession session) { OnHandshaked(session); }
internal void OnDisconnectingInternal(SslSession session) { OnDisconnecting(session); }
internal void OnDisconnectedInternal(SslSession session) { OnDisconnected(session); }
#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; }
///
/// Acceptor 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...
Stop();
}
// Dispose unmanaged resources here...
// Set large fields to null here...
// Mark as disposed.
IsDisposed = true;
}
}
#endregion
}
}