Files
SocialPub/PrivaPub/Infrastructure/Http/SafeHttpHandlerFactory.cs
T
thepraandClaude Opus 5.5 8f4d6cbdf9
Build / Build (push) Successful in 57s
Deploy / privapub.thepra.dev (push) Successful in 1m12s
A private fediverse on the workstation: PrivaPub against a real GoToSocial
tools/pasture/run.sh starts PrivaPub, GoToSocial and Mongo on one podman network behind Caddy's internal CA, and
interop.sh drives both through their own client APIs: follows (one to a locked account), posts, CW, replies, likes,
boosts, DMs, edits, deletes and unfollow. All 25 checks pass, three fresh runs in a row.

- Federation:AcceptAnyCertificate joins the two test-network switches; startup refuses all three in Production.
- WebFinger falls back to http only when AllowPlainHttp is on.
- A bootstrap logger, so a failure before the host is built is no longer silent.
- P4 is ticked in the roadmap, with what has not been run live (Lemmy, a Mastodon circle member).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 13:19:16 +02:00

63 lines
1.9 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) => 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)
};
public static async ValueTask<Stream> Connect(DnsEndPoint endPoint, bool allowPrivateNetworks, CancellationToken token)
{
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);
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>();
}
}
}
}