- IConnectedAddresses remembers which address each host's last connection reached, set in SafeHttpHandlerFactory's connect callback. That is once per pooled connection, with no second DNS lookup, and the address is never stored. - IGeoLocator / DbIpLocator reads the offline DB-IP Lite city and ASN databases (MaxMind .mmdb, via MaxMind.Db). It maps memory, swaps to new files within ten minutes, rounds coordinates to one decimal, never looks up a private address, and answers nothing when the files are missing. CdnNetworks names the CDNs whose edge addresses say nothing about where a server is. - deploy/max/geo-update.sh fetches this or last month's databases, checks them and swaps them in atomically. The privapub-geo timer runs it monthly as www-data, and setup.sh installs the directory, the script, the units and a first download. Describing servers will use these in M8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
65 lines
2.1 KiB
C#
65 lines
2.1 KiB
C#
using System.Net;
|
|
using System.Net.Security;
|
|
using System.Net.Sockets;
|
|
|
|
namespace PrivaPub.Infrastructure.Http
|
|
{
|
|
public sealed class BlockedDestinationException : Exception
|
|
{
|
|
public BlockedDestinationException(string host) : base($"'{host}' does not resolve to a public address") { }
|
|
}
|
|
|
|
public static class SafeHttpHandlerFactory
|
|
{
|
|
public static SocketsHttpHandler Create(FederationOptions options, IConnectedAddresses connected = default) => new()
|
|
{
|
|
SslOptions = options.AcceptAnyCertificate
|
|
? new SslClientAuthenticationOptions { RemoteCertificateValidationCallback = (_, _, _, _) => true }
|
|
: new SslClientAuthenticationOptions(),
|
|
AllowAutoRedirect = false,
|
|
UseProxy = false,
|
|
UseCookies = false,
|
|
AutomaticDecompression = DecompressionMethods.All,
|
|
ConnectTimeout = TimeSpan.FromSeconds(10),
|
|
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
|
|
MaxResponseHeadersLength = 64,
|
|
ConnectCallback = (context, token) => Connect(context.DnsEndPoint, options.AllowPrivateNetworks, token, connected)
|
|
};
|
|
|
|
public static async ValueTask<Stream> Connect(DnsEndPoint endPoint, bool allowPrivateNetworks, CancellationToken token,
|
|
IConnectedAddresses connected = default)
|
|
{
|
|
var addresses = await Resolve(endPoint.Host, token);
|
|
if (addresses.Length == 0 || !allowPrivateNetworks && !addresses.All(IpRangeGuard.IsPublic))
|
|
throw new BlockedDestinationException(endPoint.Host);
|
|
|
|
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
|
|
try
|
|
{
|
|
await socket.ConnectAsync(addresses, endPoint.Port, token);
|
|
connected?.Remember(endPoint.Host, (socket.RemoteEndPoint as IPEndPoint)?.Address);
|
|
return new NetworkStream(socket, ownsSocket: true);
|
|
}
|
|
catch
|
|
{
|
|
socket.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
static async Task<IPAddress[]> Resolve(string host, CancellationToken token)
|
|
{
|
|
if (IPAddress.TryParse(host.Trim('[', ']'), out var literal))
|
|
return new[] { literal };
|
|
try
|
|
{
|
|
return await Dns.GetHostAddressesAsync(host, token);
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
return Array.Empty<IPAddress>();
|
|
}
|
|
}
|
|
}
|
|
}
|