[backend/core] Add HTTP proxy support

This commit is contained in:
Laura Hausmann 2025-01-12 08:26:17 +01:00
parent 2811d3bded
commit ea7bcfa652
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
4 changed files with 60 additions and 38 deletions

View file

@ -17,6 +17,7 @@ public sealed class Config
public required InstanceSection Instance { get; init; } = new(); public required InstanceSection Instance { get; init; } = new();
public required DatabaseSection Database { get; init; } = new(); public required DatabaseSection Database { get; init; } = new();
public required SecuritySection Security { get; init; } = new(); public required SecuritySection Security { get; init; } = new();
public required NetworkSection Network { get; init; } = new();
public required StorageSection Storage { get; init; } = new(); public required StorageSection Storage { get; init; } = new();
public required PerformanceSection Performance { get; init; } = new(); public required PerformanceSection Performance { get; init; } = new();
public required QueueSection Queue { get; init; } = new(); public required QueueSection Queue { get; init; } = new();
@ -67,6 +68,11 @@ public sealed class Config
public Enums.PublicPreview PublicPreview { get; init; } = Enums.PublicPreview.Public; public Enums.PublicPreview PublicPreview { get; init; } = Enums.PublicPreview.Public;
} }
public sealed class NetworkSection
{
public string? HttpProxy { get; init; } = null;
}
public sealed class DatabaseSection public sealed class DatabaseSection
{ {
[Required] public string Host { get; init; } = "localhost"; [Required] public string Host { get; init; } = "localhost";

View file

@ -88,6 +88,7 @@ public static class ServiceExtensions
services.ConfigureWithValidation<Config>(configuration) services.ConfigureWithValidation<Config>(configuration)
.ConfigureWithValidation<Config.InstanceSection>(configuration, "Instance") .ConfigureWithValidation<Config.InstanceSection>(configuration, "Instance")
.ConfigureWithValidation<Config.SecuritySection>(configuration, "Security") .ConfigureWithValidation<Config.SecuritySection>(configuration, "Security")
.ConfigureWithValidation<Config.NetworkSection>(configuration, "Network")
.ConfigureWithValidation<Config.PerformanceSection>(configuration, "Performance") .ConfigureWithValidation<Config.PerformanceSection>(configuration, "Performance")
.ConfigureWithValidation<Config.QueueConcurrencySection>(configuration, "Performance:QueueConcurrency") .ConfigureWithValidation<Config.QueueConcurrencySection>(configuration, "Performance:QueueConcurrency")
.ConfigureWithValidation<Config.BackfillSection>(configuration, "Backfill") .ConfigureWithValidation<Config.BackfillSection>(configuration, "Backfill")

View file

@ -3,32 +3,23 @@ using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using JetBrains.Annotations;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Services;
[UsedImplicitly]
public class CustomHttpClient : HttpClient, IService<HttpClient>, ISingletonService public class CustomHttpClient : HttpClient, IService<HttpClient>, ISingletonService
{ {
private static readonly FastFallback FastFallbackHandler = new();
private static readonly HttpMessageHandler InnerHandler = new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
ConnectCallback = FastFallbackHandler.ConnectCallbackAsync,
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
PooledConnectionLifetime = TimeSpan.FromMinutes(60)
};
private static readonly HttpMessageHandler Handler = new RedirectHandler(InnerHandler);
public CustomHttpClient( public CustomHttpClient(
IOptions<Config.InstanceSection> options, IOptions<Config.InstanceSection> instance,
IOptions<Config.NetworkSection> network,
IOptionsMonitor<Config.SecuritySection> security, IOptionsMonitor<Config.SecuritySection> security,
ILoggerFactory loggerFactory ILoggerFactory loggerFactory
) : base(Handler) ) : base(BuildHandler(network, security, loggerFactory))
{ {
// Configure HTTP client options // Configure HTTP client options
DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", options.Value.UserAgent); DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", instance.Value.UserAgent);
Timeout = TimeSpan.FromSeconds(30); Timeout = TimeSpan.FromSeconds(30);
// Default to HTTP/2, but allow for down-negotiation to HTTP/1.1 or HTTP/1.0 // Default to HTTP/2, but allow for down-negotiation to HTTP/1.1 or HTTP/1.0
@ -37,21 +28,41 @@ public class CustomHttpClient : HttpClient, IService<HttpClient>, ISingletonServ
// Protect against DoS attacks // Protect against DoS attacks
MaxResponseContentBufferSize = 1024 * 1024; // 1MiB MaxResponseContentBufferSize = 1024 * 1024; // 1MiB
}
// Configure FastFallback // This needs to be a static method so we can call the base constructor with varying input based on the configuration
FastFallbackHandler.Logger = loggerFactory.CreateLogger<FastFallback>(); private static RedirectHandler BuildHandler(
FastFallbackHandler.Security = security; IOptions<Config.NetworkSection> network,
IOptionsMonitor<Config.SecuritySection> security,
ILoggerFactory loggerFactory
)
{
var proxy = network.Value.HttpProxy != null ? new WebProxy(network.Value.HttpProxy) : null;
var fastFallback = new FastFallback(loggerFactory.CreateLogger<FastFallback>(), security, proxy != null);
var innerHandler = new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
ConnectCallback = fastFallback.ConnectCallbackAsync,
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
PooledConnectionLifetime = TimeSpan.FromMinutes(60),
UseProxy = proxy != null,
Proxy = proxy,
};
return new RedirectHandler(innerHandler);
} }
// Adapted from https://github.com/KazWolfe/Dalamud/blob/767cc49ecb80e29dbdda2fa8329d3c3341c964fe/Dalamud/Networking/Http/HappyEyeballsCallback.cs // Adapted from https://github.com/KazWolfe/Dalamud/blob/767cc49ecb80e29dbdda2fa8329d3c3341c964fe/Dalamud/Networking/Http/HappyEyeballsCallback.cs
private class FastFallback(int connectionBackoff = 75) private class FastFallback(
ILogger<FastFallback> logger,
IOptionsMonitor<Config.SecuritySection> security,
bool proxy,
int connectionBackoff = 75
)
{ {
public ILogger<FastFallback>? Logger { private get; set; } private bool AllowLoopback => security.CurrentValue.AllowLoopback || proxy;
public IOptionsMonitor<Config.SecuritySection>? Security { private get; set; } private bool AllowLocalIPv4 => security.CurrentValue.AllowLocalIPv4 || proxy;
private bool AllowLocalIPv6 => security.CurrentValue.AllowLocalIPv6 || proxy;
private bool AllowLoopback => Security?.CurrentValue.AllowLoopback ?? false;
private bool AllowLocalIPv4 => Security?.CurrentValue.AllowLocalIPv4 ?? false;
private bool AllowLocalIPv6 => Security?.CurrentValue.AllowLocalIPv6 ?? false;
public async ValueTask<Stream> ConnectCallbackAsync( public async ValueTask<Stream> ConnectCallbackAsync(
SocketsHttpConnectionContext context, CancellationToken token SocketsHttpConnectionContext context, CancellationToken token
@ -72,22 +83,22 @@ public class CustomHttpClient : HttpClient, IService<HttpClient>, ISingletonServ
if (!AllowLoopback && record.IsLoopback()) if (!AllowLoopback && record.IsLoopback())
{ {
Logger?.LogWarning("Refusing to connect to loopback address {address} due to possible SSRF", logger.LogWarning("Refusing to connect to loopback address {address} due to possible SSRF",
record.ToString()); record.ToString());
continue; continue;
} }
if (!AllowLocalIPv6 && record.IsLocalIPv6()) if (!AllowLocalIPv6 && record.IsLocalIPv6())
{ {
Logger?.LogWarning("Refusing to connect to local IPv6 address {address} due to possible SSRF", logger.LogWarning("Refusing to connect to local IPv6 address {address} due to possible SSRF",
record.ToString()); record.ToString());
continue; continue;
} }
if (!AllowLocalIPv4 && record.IsLocalIPv4()) if (!AllowLocalIPv4 && record.IsLocalIPv4())
{ {
Logger?.LogWarning("Refusing to connect to local IPv4 address {address} due to possible SSRF", logger.LogWarning("Refusing to connect to local IPv4 address {address} due to possible SSRF",
record.ToString()); record.ToString());
continue; continue;
} }
@ -118,8 +129,8 @@ public class CustomHttpClient : HttpClient, IService<HttpClient>, ISingletonServ
if (stream == null) if (stream == null)
{ {
throw lastException ?? throw lastException
new Exception("An unknown exception occured during fast fallback connection attempt"); ?? new Exception("An unknown exception occured during fast fallback connection attempt");
} }
await linkedToken.CancelAsync(); await linkedToken.CancelAsync();
@ -279,9 +290,9 @@ public class CustomHttpClient : HttpClient, IService<HttpClient>, ISingletonServ
//Manual Redirect //Manual Redirect
//https://github.com/dotnet/runtime/blob/ccfe21882e4a2206ce49cd5b32d3eb3cab3e530f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs //https://github.com/dotnet/runtime/blob/ccfe21882e4a2206ce49cd5b32d3eb3cab3e530f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
Uri? redirectUri; Uri? redirectUri;
while (IsRedirect(response) && while (IsRedirect(response)
IsRedirectAllowed(request) && && IsRedirectAllowed(request)
(redirectUri = GetUriForRedirect(request.RequestUri!, response)) != null) && (redirectUri = GetUriForRedirect(request.RequestUri!, response)) != null)
{ {
redirectCount++; redirectCount++;
if (redirectCount > MaxAutomaticRedirections) if (redirectCount > MaxAutomaticRedirections)

View file

@ -77,6 +77,10 @@ ExposeBlockReasons = Registered
;; Options: [Public, Restricted, RestrictedNoMedia, Lockdown] ;; Options: [Public, Restricted, RestrictedNoMedia, Lockdown]
PublicPreview = Public PublicPreview = Public
[Network]
;; Uncomment to use the specified HTTP proxy for all outgoing requests. Backend restart is required to apply changes.
;HttpProxy = 127.0.0.1:8080
[Performance] [Performance]
;; Maximum number of incoming federation requests to handle concurrently. ;; Maximum number of incoming federation requests to handle concurrently.
;; When exceeded, incoming requests are buffered in memory until they can be executed. ;; When exceeded, incoming requests are buffered in memory until they can be executed.