using Pulsar.Common.Extensions; using Pulsar.Common.Messages; using Pulsar.Common.Messages.Other; using Pulsar.Common.Networking; using Pulsar.Server.Forms; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Windows.Forms; namespace Pulsar.Server.Networking { public class Server { /// /// Occurs when the state of the server changes. /// public event ServerStateEventHandler ServerState; /// /// Represents a method that will handle a change in the server's state. /// /// The server which changed its state. /// The new listening state of the server. /// The port the server is listening on, if listening is True. public delegate void ServerStateEventHandler(Server s, bool listening, ushort port); /// /// Fires an event that informs subscribers that the server has changed it's state. /// /// The new listening state of the server. private void OnServerState(bool listening) { if (Listening == listening) return; Listening = listening; var handler = ServerState; handler?.Invoke(this, listening, Port); } /// /// Occurs when the state of a client changes. /// public event ClientStateEventHandler ClientState; /// /// Represents a method that will handle a change in a client's state. /// /// The server, the client is connected to. /// The client which changed its state. /// The new connection state of the client. public delegate void ClientStateEventHandler(Server s, Client c, bool connected); /// /// Fires an event that informs subscribers that a client has changed its state. /// /// The client which changed its state. /// The new connection state of the client. private void OnClientState(Client c, bool connected) { if (!connected) RemoveClient(c); var handler = ClientState; handler?.Invoke(this, c, connected); } /// /// Occurs when a message is received by a client. /// public event ClientReadEventHandler ClientRead; /// /// Represents a method that will handle a message received from a client. /// /// The server, the client is connected to. /// The client that has received the message. /// The message that received by the client. public delegate void ClientReadEventHandler(Server s, Client c, IMessage message); /// /// Fires an event that informs subscribers that a message has been /// received from the client. /// /// The client that has received the message. /// The message that received by the client. /// The length of the message. private void OnClientRead(Client c, IMessage message, int messageLength) { BytesReceived += messageLength; var handler = ClientRead; handler?.Invoke(this, c, message); } /// /// Occurs when a message is sent by a client. /// public event ClientWriteEventHandler ClientWrite; /// /// Represents the method that will handle the sent message by a client. /// /// The server, the client is connected to. /// The client that has sent the message. /// The message that has been sent by the client. public delegate void ClientWriteEventHandler(Server s, Client c, IMessage message); /// /// Fires an event that informs subscribers that the client has sent a message. /// /// The client that has sent the message. /// The message that has been sent by the client. /// The length of the message. private void OnClientWrite(Client c, IMessage message, int messageLength) { BytesSent += messageLength; var handler = ClientWrite; handler?.Invoke(this, c, message); } /// /// The port on which the server is listening. /// For multi-port scenarios, this is the last port that was started. /// public ushort Port { get; private set; } /// /// The total amount of received bytes. /// public long BytesReceived { get; set; } /// /// The total amount of sent bytes. /// public long BytesSent { get; set; } /// /// The keep-alive time in ms. /// private const uint KeepAliveTime = 25000; // 25 s /// /// The keep-alive interval in ms. /// private const uint KeepAliveInterval = 25000; // 25 s /// /// The listening state of the server. True if listening, else False. /// public bool Listening { get; private set; } /// /// Gets the clients currently connected to the server. /// protected Client[] Clients { get { lock (_clientsLock) { return _clients.ToArray(); } } } /// /// Gets the number of clients currently connected to the server without array allocation. /// public int ClientCount { get { lock (_clientsLock) { return _clients.Count; } } } /// /// Handle(s) of the Server Socket(s). /// private readonly List _handles = new List(); private readonly object _handlesLock = new object(); /// /// Accept event args, one per listening socket. /// private readonly Dictionary _acceptArgs = new Dictionary(); /// /// The server certificate. /// protected readonly X509Certificate2 ServerCertificate; /// /// List of the clients connected to the server. /// private readonly List _clients = new List(); /// /// The UPnP service used to create port mappings per port. /// private readonly Dictionary _upnpByPort = new Dictionary(); /// /// Lock object for the list of clients. /// private readonly object _clientsLock = new object(); /// /// Determines if the server is currently processing Disconnect method. /// protected bool ProcessingDisconnect { get; set; } /// /// Constructor of the server. /// /// The server certificate. protected Server(X509Certificate2 serverCertificate) { ServerCertificate = serverCertificate; } /// /// Updates the status strip icon for the server listening state. /// /// True if server is listening, false otherwise. private void UpdateServerStatusIcon(bool isListening) { var mainForm = GetMainFormSafe(); if (mainForm == null) return; var iconResource = isListening ? Properties.Resources.bullet_green : Properties.Resources.bullet_red; try { if (mainForm.InvokeRequired) { mainForm.BeginInvoke(new Action(() => SetStatusStripIcon(mainForm, iconResource))); } else { SetStatusStripIcon(mainForm, iconResource); } } catch (Exception) { } } /// /// Safely gets the main form instance if it exists and is valid. /// /// The main form instance or null if not available. private static FrmMain GetMainFormSafe() { var mainForm = Application.OpenForms.OfType().FirstOrDefault(); return (mainForm != null && !mainForm.IsDisposed && !mainForm.Disposing) ? mainForm : null; } /// /// Sets the status strip icon if the control is valid. /// /// The main form instance. /// The icon to set. private static void SetStatusStripIcon(FrmMain mainForm, System.Drawing.Image icon) { if (mainForm.statusStrip?.IsDisposed == false && mainForm.statusStrip.Items.ContainsKey("listenToolStripStatusLabel")) { mainForm.statusStrip.Items["listenToolStripStatusLabel"].Image = icon; } } /// /// Begins listening for clients on a single port. /// public void Listen(ushort port, bool ipv6, bool enableUPnP) { ListenMany(new[] { port }, ipv6, enableUPnP); } /// /// Begins listening for clients on multiple ports. /// /// Ports to listen on. /// If set to true, use a dual-stack socket to allow IPv4/6 connections. Otherwise use IPv4-only socket. /// Enables the automatic UPnP port forwarding for each port. public void ListenMany(IEnumerable ports, bool ipv6, bool enableUPnP) { var startNow = !Listening; foreach (var port in ports.Distinct()) { lock (_handlesLock) { if (_handles.Any(h => (h.LocalEndPoint as IPEndPoint)?.Port == port)) continue; } Socket handle; if (Socket.OSSupportsIPv6 && ipv6) { handle = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); handle.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0); handle.Bind(new IPEndPoint(IPAddress.IPv6Any, port)); } else { handle = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); handle.Bind(new IPEndPoint(IPAddress.Any, port)); } handle.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); handle.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); handle.Listen(1000); lock (_handlesLock) { _handles.Add(handle); } if (enableUPnP) { var upnp = new UPnPService(); _upnpByPort[port] = upnp; upnp.CreatePortMapAsync(port); } var item = new SocketAsyncEventArgs(); item.Completed += AcceptClient; _acceptArgs[handle] = item; Port = port; // keep last started port for compatibility if (!handle.AcceptAsync(item)) AcceptClient(handle, item); var mainForm = GetMainFormSafe(); if (mainForm != null) { try { if (mainForm.InvokeRequired) { mainForm.BeginInvoke(new Action(() => { try { mainForm.EventLog($"Started listening for connections on port: {port}", "info"); UpdateServerStatusIcon(true); } catch (Exception) { } })); } else { mainForm.EventLog($"Started listening for connections on port: {port}", "info"); UpdateServerStatusIcon(true); } } catch (Exception) { } } } if (startNow && _handles.Count > 0) { OnServerState(true); } } /// /// Accepts and begins authenticating an incoming client. /// /// The listening socket. /// Asynchronous socket event. private void AcceptClient(object s, SocketAsyncEventArgs e) { var listenSocket = s as Socket; if (listenSocket == null) { // Try to recover the listen socket from our map listenSocket = _acceptArgs.Keys.FirstOrDefault(); } try { do { switch (e.SocketError) { case SocketError.Success: try { Socket clientSocket = e.AcceptSocket; clientSocket.SetKeepAliveEx(KeepAliveInterval, KeepAliveTime); clientSocket.NoDelay = true; var networkStream = new NetworkStream(clientSocket, true); var client = new Client(networkStream, (IPEndPoint)clientSocket.RemoteEndPoint, ServerCertificate); AddClient(client); OnClientState(client, true); } catch (Exception) { try { e.AcceptSocket?.Close(); } catch { } } break; case SocketError.ConnectionReset: break; default: throw new SocketException((int)e.SocketError); } e.AcceptSocket = null; // enable reuse } while (listenSocket != null && !listenSocket.AcceptAsync(e)); } catch (ObjectDisposedException) { } catch (Exception) { Disconnect(); } } /// /// Adds a connected client to the list of clients, /// subscribes to the client's events. /// /// The client to add. private void AddClient(Client client) { lock (_clientsLock) { client.ClientState += OnClientState; client.ClientRead += OnClientRead; _clients.Add(client); } } /// /// Removes a disconnected client from the list of clients, /// unsubscribes from the client's events. /// /// The client to remove. private void RemoveClient(Client client) { if (ProcessingDisconnect) return; lock (_clientsLock) { client.ClientState -= OnClientState; client.ClientRead -= OnClientRead; _clients.Remove(client); } } /// /// Disconnect the server from all of the clients and discontinue /// listening (placing the server in an "off" state). /// public void Disconnect() { if (ProcessingDisconnect) return; ProcessingDisconnect = true; List toClose; lock (_handlesLock) { toClose = _handles.ToList(); _handles.Clear(); } foreach (var handle in toClose) { try { handle.Close(); } catch { } } foreach (var kvp in _acceptArgs.ToList()) { try { kvp.Value.Dispose(); } catch { } } _acceptArgs.Clear(); foreach (var upnpKvp in _upnpByPort.ToList()) { try { upnpKvp.Value.DeletePortMapAsync(upnpKvp.Key); } catch { } } _upnpByPort.Clear(); lock (_clientsLock) { var clientsToDisconnect = _clients.ToList(); _clients.Clear(); foreach (var client in clientsToDisconnect) { try { client.Disconnect(); client.ClientState -= OnClientState; client.ClientRead -= OnClientRead; } catch { } } } ProcessingDisconnect = false; OnServerState(false); UpdateServerStatusIcon(false); } /// /// Gets the ports the server is currently listening on. /// public ushort[] GetListeningPorts() { lock (_handlesLock) { return _handles.Select(h => (ushort)((IPEndPoint)h.LocalEndPoint).Port).ToArray(); } } } }