123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- using System;
- using System.Collections.Concurrent;
- using System.Diagnostics;
- using System.Net;
- using System.Net.Sockets;
- using System.Text;
- using System.Threading;
- namespace NetCoreServer
- {
- /// <summary>
- /// SSL server is used to connect, disconnect and manage SSL sessions
- /// </summary>
- /// <remarks>Thread-safe</remarks>
- public class SslServer : IDisposable
- {
- /// <summary>
- /// Initialize SSL server with a given IP address and port number
- /// </summary>
- /// <param name="context">SSL context</param>
- /// <param name="address">IP address</param>
- /// <param name="port">Port number</param>
- public SslServer(SslContext context, IPAddress address, int port) : this(context, new IPEndPoint(address, port)) {}
- /// <summary>
- /// Initialize SSL server with a given IP address and port number
- /// </summary>
- /// <param name="context">SSL context</param>
- /// <param name="address">IP address</param>
- /// <param name="port">Port number</param>
- public SslServer(SslContext context, string address, int port) : this(context, new IPEndPoint(IPAddress.Parse(address), port)) {}
- /// <summary>
- /// Initialize SSL server with a given DNS endpoint
- /// </summary>
- /// <param name="context">SSL context</param>
- /// <param name="endpoint">DNS endpoint</param>
- public SslServer(SslContext context, DnsEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Host, endpoint.Port) {}
- /// <summary>
- /// Initialize SSL server with a given IP endpoint
- /// </summary>
- /// <param name="context">SSL context</param>
- /// <param name="endpoint">IP endpoint</param>
- public SslServer(SslContext context, IPEndPoint endpoint) : this(context, endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {}
- /// <summary>
- /// Initialize SSL server with a given SSL context, endpoint, address and port
- /// </summary>
- /// <param name="context">SSL context</param>
- /// <param name="endpoint">Endpoint</param>
- /// <param name="address">Server address</param>
- /// <param name="port">Server port</param>
- private SslServer(SslContext context, EndPoint endpoint, string address, int port)
- {
- Id = Guid.NewGuid();
- Address = address;
- Port = port;
- Context = context;
- Endpoint = endpoint;
- }
- /// <summary>
- /// Server Id
- /// </summary>
- public Guid Id { get; }
- /// <summary>
- /// SSL server address
- /// </summary>
- public string Address { get; }
- /// <summary>
- /// SSL server port
- /// </summary>
- public int Port { get; }
- /// <summary>
- /// SSL context
- /// </summary>
- public SslContext Context { get; }
- /// <summary>
- /// Endpoint
- /// </summary>
- public EndPoint Endpoint { get; private set; }
- /// <summary>
- /// Number of sessions connected to the server
- /// </summary>
- public long ConnectedSessions { get { return Sessions.Count; } }
- /// <summary>
- /// Number of bytes pending sent by the server
- /// </summary>
- public long BytesPending { get { return _bytesPending; } }
- /// <summary>
- /// Number of bytes sent by the server
- /// </summary>
- public long BytesSent { get { return _bytesSent; } }
- /// <summary>
- /// Number of bytes received by the server
- /// </summary>
- public long BytesReceived { get { return _bytesReceived; } }
- /// <summary>
- /// Option: acceptor backlog size
- /// </summary>
- /// <remarks>
- /// This option will set the listening socket's backlog size
- /// </remarks>
- public int OptionAcceptorBacklog { get; set; } = 1024;
- /// <summary>
- /// Option: dual mode socket
- /// </summary>
- /// <remarks>
- /// 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.
- /// </remarks>
- public bool OptionDualMode { get; set; }
- /// <summary>
- /// Option: keep alive
- /// </summary>
- /// <remarks>
- /// This option will setup SO_KEEPALIVE if the OS support this feature
- /// </remarks>
- public bool OptionKeepAlive { get; set; }
- /// <summary>
- /// Option: TCP keep alive time
- /// </summary>
- /// <remarks>
- /// The number of seconds a TCP connection will remain alive/idle before keepalive probes are sent to the remote
- /// </remarks>
- public int OptionTcpKeepAliveTime { get; set; } = -1;
- /// <summary>
- /// Option: TCP keep alive interval
- /// </summary>
- /// <remarks>
- /// The number of seconds a TCP connection will wait for a keepalive response before sending another keepalive probe
- /// </remarks>
- public int OptionTcpKeepAliveInterval { get; set; } = -1;
- /// <summary>
- /// Option: TCP keep alive retry count
- /// </summary>
- /// <remarks>
- /// The number of TCP keep alive probes that will be sent before the connection is terminated
- /// </remarks>
- public int OptionTcpKeepAliveRetryCount { get; set; } = -1;
- /// <summary>
- /// Option: no delay
- /// </summary>
- /// <remarks>
- /// This option will enable/disable Nagle's algorithm for SSL protocol
- /// </remarks>
- public bool OptionNoDelay { get; set; }
- /// <summary>
- /// Option: reuse address
- /// </summary>
- /// <remarks>
- /// This option will enable/disable SO_REUSEADDR if the OS support this feature
- /// </remarks>
- public bool OptionReuseAddress { get; set; }
- /// <summary>
- /// Option: enables a socket to be bound for exclusive access
- /// </summary>
- /// <remarks>
- /// This option will enable/disable SO_EXCLUSIVEADDRUSE if the OS support this feature
- /// </remarks>
- public bool OptionExclusiveAddressUse { get; set; }
- /// <summary>
- /// Option: receive buffer size
- /// </summary>
- public int OptionReceiveBufferSize { get; set; } = 8192;
- /// <summary>
- /// Option: send buffer size
- /// </summary>
- 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;
- /// <summary>
- /// Is the server started?
- /// </summary>
- public bool IsStarted { get; private set; }
- /// <summary>
- /// Is the server accepting new clients?
- /// </summary>
- public bool IsAccepting { get; private set; }
- /// <summary>
- /// Create a new socket object
- /// </summary>
- /// <remarks>
- /// Method may be override if you need to prepare some specific socket object in your implementation.
- /// </remarks>
- /// <returns>Socket object</returns>
- protected virtual Socket CreateSocket()
- {
- return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
- }
- /// <summary>
- /// Start the server
- /// </summary>
- /// <returns>'true' if the server was successfully started, 'false' if the server failed to start</returns>
- 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;
- }
- /// <summary>
- /// Stop the server
- /// </summary>
- /// <returns>'true' if the server was successfully stopped, 'false' if the server is already stopped</returns>
- 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;
- }
- /// <summary>
- /// Restart the server
- /// </summary>
- /// <returns>'true' if the server was successfully restarted, 'false' if the server failed to restart</returns>
- public virtual bool Restart()
- {
- if (!Stop())
- return false;
- while (IsStarted)
- Thread.Yield();
- return Start();
- }
- #endregion
- #region Accepting clients
- /// <summary>
- /// Start accept a new client connection
- /// </summary>
- 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);
- }
- /// <summary>
- /// Process accepted client connection
- /// </summary>
- 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);
- }
- /// <summary>
- /// This method is the callback method associated with Socket.AcceptAsync()
- /// operations and is invoked when an accept operation is complete
- /// </summary>
- private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e)
- {
- if (IsSocketDisposed)
- return;
- ProcessAccept(e);
- }
- #endregion
- #region Session factory
- /// <summary>
- /// Create SSL session factory method
- /// </summary>
- /// <returns>SSL session</returns>
- protected virtual SslSession CreateSession() { return new SslSession(this); }
- #endregion
- #region Session management
- /// <summary>
- /// Server sessions
- /// </summary>
- protected readonly ConcurrentDictionary<Guid, SslSession> Sessions = new ConcurrentDictionary<Guid, SslSession>();
- /// <summary>
- /// Disconnect all connected sessions
- /// </summary>
- /// <returns>'true' if all sessions were successfully disconnected, 'false' if the server is not started</returns>
- public virtual bool DisconnectAll()
- {
- if (!IsStarted)
- return false;
- // Disconnect all sessions
- foreach (var session in Sessions.Values)
- session.Disconnect();
- return true;
- }
- /// <summary>
- /// Find a session with a given Id
- /// </summary>
- /// <param name="id">Session Id</param>
- /// <returns>Session with a given Id or null if the session it not connected</returns>
- public SslSession FindSession(Guid id)
- {
- // Try to find the required session
- return Sessions.TryGetValue(id, out SslSession result) ? result : null;
- }
- /// <summary>
- /// Register a new session
- /// </summary>
- /// <param name="session">Session to register</param>
- internal void RegisterSession(SslSession session)
- {
- // Register a new session
- Sessions.TryAdd(session.Id, session);
- }
- /// <summary>
- /// Unregister session by Id
- /// </summary>
- /// <param name="id">Session Id</param>
- internal void UnregisterSession(Guid id)
- {
- // Unregister session by Id
- Sessions.TryRemove(id, out SslSession _);
- }
- #endregion
- #region Multicasting
- /// <summary>
- /// Multicast data to all connected sessions
- /// </summary>
- /// <param name="buffer">Buffer to multicast</param>
- /// <returns>'true' if the data was successfully multicasted, 'false' if the data was not multicasted</returns>
- public virtual bool Multicast(byte[] buffer) => Multicast(buffer.AsSpan());
- /// <summary>
- /// Multicast data to all connected clients
- /// </summary>
- /// <param name="buffer">Buffer to multicast</param>
- /// <param name="offset">Buffer offset</param>
- /// <param name="size">Buffer size</param>
- /// <returns>'true' if the data was successfully multicasted, 'false' if the data was not multicasted</returns>
- public virtual bool Multicast(byte[] buffer, long offset, long size) => Multicast(buffer.AsSpan((int)offset, (int)size));
- /// <summary>
- /// Multicast data to all connected clients
- /// </summary>
- /// <param name="buffer">Buffer to send as a span of bytes</param>
- /// <returns>'true' if the data was successfully multicasted, 'false' if the data was not multicasted</returns>
- public virtual bool Multicast(ReadOnlySpan<byte> 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;
- }
- /// <summary>
- /// Multicast text to all connected clients
- /// </summary>
- /// <param name="text">Text string to multicast</param>
- /// <returns>'true' if the text was successfully multicasted, 'false' if the text was not multicasted</returns>
- public virtual bool Multicast(string text) => Multicast(Encoding.UTF8.GetBytes(text));
- /// <summary>
- /// Multicast text to all connected clients
- /// </summary>
- /// <param name="text">Text to multicast as a span of characters</param>
- /// <returns>'true' if the text was successfully multicasted, 'false' if the text was not multicasted</returns>
- public virtual bool Multicast(ReadOnlySpan<char> text) => Multicast(Encoding.UTF8.GetBytes(text.ToArray()));
- #endregion
- #region Server handlers
- /// <summary>
- /// Handle server starting notification
- /// </summary>
- protected virtual void OnStarting() {}
- /// <summary>
- /// Handle server started notification
- /// </summary>
- protected virtual void OnStarted() {}
- /// <summary>
- /// Handle server stopping notification
- /// </summary>
- protected virtual void OnStopping() {}
- /// <summary>
- /// Handle server stopped notification
- /// </summary>
- protected virtual void OnStopped() {}
- /// <summary>
- /// Handle session connecting notification
- /// </summary>
- /// <param name="session">Connecting session</param>
- protected virtual void OnConnecting(SslSession session) {}
- /// <summary>
- /// Handle session connected notification
- /// </summary>
- /// <param name="session">Connected session</param>
- protected virtual void OnConnected(SslSession session) {}
- /// <summary>
- /// Handle session handshaking notification
- /// </summary>
- /// <param name="session">Handshaking session</param>
- protected virtual void OnHandshaking(SslSession session) {}
- /// <summary>
- /// Handle session handshaked notification
- /// </summary>
- /// <param name="session">Handshaked session</param>
- protected virtual void OnHandshaked(SslSession session) {}
- /// <summary>
- /// Handle session disconnecting notification
- /// </summary>
- /// <param name="session">Disconnecting session</param>
- protected virtual void OnDisconnecting(SslSession session) {}
- /// <summary>
- /// Handle session disconnected notification
- /// </summary>
- /// <param name="session">Disconnected session</param>
- protected virtual void OnDisconnected(SslSession session) {}
- /// <summary>
- /// Handle error notification
- /// </summary>
- /// <param name="error">Socket error code</param>
- 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
- /// <summary>
- /// Send error notification
- /// </summary>
- /// <param name="error">Socket error code</param>
- 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
- /// <summary>
- /// Disposed flag
- /// </summary>
- public bool IsDisposed { get; private set; }
- /// <summary>
- /// Acceptor socket disposed flag
- /// </summary>
- 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
- }
- }
|