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 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 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(); } } } }