[backend/core] Add HTTP proxy support
This commit is contained in:
parent
2811d3bded
commit
ea7bcfa652
4 changed files with 60 additions and 38 deletions
|
@ -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";
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Add table
Reference in a new issue