added http digest auth to download clients

This commit is contained in:
Flaminel committed 2026-10-15 19:06:26 +03:00
1 parent 9f44e0e33f
commit c16a6753f4
37 files changed
+5376 -66

No files matched your search

@@ -0,0 +1,27 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
namespace Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Sends Basic auth up front for qBittorrent, Deluge and SABnzbd behind a reverse proxy.
/// Skips requests that already carry an Authorization header.
/// </summary>
public sealed class BasicAuthHttpMessageHandler : DelegatingHandler
{
private readonly string _headerValue;
public BasicAuthHttpMessageHandler(NetworkCredential credential)
{
string raw = $"{credential.UserName}:{credential.Password}";
_headerValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(raw));
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization ??= new AuthenticationHeaderValue("Basic", _headerValue);
return base.SendAsync(request, cancellationToken);
}
}
@@ -1,4 +1,7 @@
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Services;
using Cleanuparr.Shared.Helpers;
using Microsoft.Extensions.DependencyInjection;
@@ -67,51 +70,62 @@ public class DynamicHttpClientConfiguration : IConfigureNamedOptions<HttpClientF
{
using var scope = _scopeFactory.CreateScope();
var certValidationService = scope.ServiceProvider.GetRequiredService<CertificateValidationService>();
switch (config.Type)
bool ByPassValidation(HttpRequestMessage sender, X509Certificate2? certificate, X509Chain? chain, SslPolicyErrors policy) =>
certValidationService.ShouldByPassValidationError(config.CertificateValidationType, sender, certificate, chain, policy);
HttpClientHandler handler = config.Type switch
{
case HttpClientType.WithRetry:
builder.PrimaryHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, certificate, chain, policy) =>
certValidationService.ShouldByPassValidationError(config.CertificateValidationType, sender, certificate, chain, policy)
};
break;
HttpClientType.Deluge => new HttpClientHandler
{
AllowAutoRedirect = config.AllowAutoRedirect,
UseCookies = true,
CookieContainer = new CookieContainer(),
AutomaticDecompression = config.AutomaticDecompression,
ServerCertificateCustomValidationCallback = ByPassValidation,
},
case HttpClientType.Deluge:
builder.PrimaryHandler = new HttpClientHandler
{
AllowAutoRedirect = config.AllowAutoRedirect,
UseCookies = true,
CookieContainer = new CookieContainer(),
AutomaticDecompression = config.AutomaticDecompression,
ServerCertificateCustomValidationCallback = (sender, certificate, chain, policy) =>
certValidationService.ShouldByPassValidationError(config.CertificateValidationType, sender, certificate, chain, policy),
};
break;
// uTorrent's WebUI requires the GUID cookie+token pair set manually by UTorrentHttpService
// UseCookies=false prevents .NET's CookieContainer from injecting a competing Cookie header.
HttpClientType.UTorrent => new HttpClientHandler
{
UseCookies = false,
ServerCertificateCustomValidationCallback = ByPassValidation,
},
case HttpClientType.UTorrent:
// uTorrent's WebUI requires the GUID cookie+token pair set manually by UTorrentHttpService
// UseCookies=false prevents .NET's CookieContainer from injecting a competing Cookie header.
builder.PrimaryHandler = new HttpClientHandler
{
UseCookies = false,
ServerCertificateCustomValidationCallback = (sender, certificate, chain, policy) =>
certValidationService.ShouldByPassValidationError(config.CertificateValidationType, sender, certificate, chain, policy),
};
break;
// WithRetry and Default use the same handler shape.
_ => new HttpClientHandler
{
ServerCertificateCustomValidationCallback = ByPassValidation,
},
};
case HttpClientType.Default:
default:
// Use default handler with certificate validation
var defaultHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, certificate, chain, policy) =>
certValidationService.ShouldByPassValidationError(config.CertificateValidationType, sender, certificate, chain, policy)
};
builder.PrimaryHandler = defaultHandler;
break;
ApplyHttpAuth(builder.AdditionalHandlers, handler, config);
builder.PrimaryHandler = handler;
}
/// <summary>
/// Digest goes on <see cref="HttpClientHandler.Credentials"/>, Basic on a delegating handler.
/// </summary>
internal static void ApplyHttpAuth(ICollection<DelegatingHandler> additionalHandlers, HttpClientHandler handler, HttpClientConfig config)
{
if (config.HttpAuthCredential is null)
{
return;
}
if (config.HttpAuthType == HttpAuthType.Digest)
{
Uri baseUri = config.BaseUri ?? throw new InvalidOperationException("Digest authentication requires a base URI");
// The "Digest" key blocks a fallback to NTLM/Negotiate for this authority.
handler.Credentials = new CredentialCache { { baseUri, "Digest", config.HttpAuthCredential } };
handler.PreAuthenticate = true;
return;
}
additionalHandlers.Add(new BasicAuthHttpMessageHandler(config.HttpAuthCredential));
}
private void AddRetryPolicy(HttpMessageHandlerBuilder builder, RetryConfig retryConfig)
@@ -82,7 +82,16 @@ public class DynamicHttpClientFactory : IDynamicHttpClientFactory
RegisterConfiguration(clientName, config);
}
public void RegisterDownloadClient(string clientName, int timeout, HttpClientType clientType, RetryConfig retryConfig, CertificateValidationType certificateType, bool sendUserAgent)
public void RegisterDownloadClient(
string clientName,
int timeout,
HttpClientType clientType,
RetryConfig retryConfig,
CertificateValidationType certificateType,
bool sendUserAgent,
HttpAuthType httpAuthType = HttpAuthType.Basic,
NetworkCredential? httpAuthCredential = null,
Uri? baseUri = null)
{
HttpClientConfig config = new()
{
@@ -91,7 +100,10 @@ public class DynamicHttpClientFactory : IDynamicHttpClientFactory
Type = clientType,
RetryConfig = retryConfig,
CertificateValidationType = certificateType,
SendUserAgent = sendUserAgent
SendUserAgent = sendUserAgent,
HttpAuthType = httpAuthType,
HttpAuthCredential = httpAuthCredential,
BaseUri = baseUri,
};
// Configure Deluge-specific settings if needed
@@ -23,4 +23,19 @@ public class HttpClientConfig
/// Whether requests from this client carry a User-Agent header.
/// </summary>
public bool SendUserAgent { get; set; }
/// <summary>
/// HTTP authentication scheme to answer a reverse proxy's challenge with.
/// </summary>
public HttpAuthType HttpAuthType { get; set; } = HttpAuthType.Basic;
/// <summary>
/// Credential to send as HTTP auth, or null when this client has none configured.
/// </summary>
public NetworkCredential? HttpAuthCredential { get; set; }
/// <summary>
/// Scheme, host and port of the client's URL, used as the Digest <see cref="CredentialCache"/> key.
/// </summary>
public Uri? BaseUri { get; set; }
}
@@ -1,3 +1,4 @@
using System.Net;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Persistence.Models.Configuration.General;
@@ -36,7 +37,16 @@ public interface IDynamicHttpClientFactory
/// <summary>
/// Registers a configuration for a download client
/// </summary>
void RegisterDownloadClient(string clientName, int timeout, HttpClientType clientType, RetryConfig retryConfig, CertificateValidationType certificateType, bool sendUserAgent);
void RegisterDownloadClient(
string clientName,
int timeout,
HttpClientType clientType,
RetryConfig retryConfig,
CertificateValidationType certificateType,
bool sendUserAgent,
HttpAuthType httpAuthType = HttpAuthType.Basic,
NetworkCredential? httpAuthCredential = null,
Uri? baseUri = null);
/// <summary>
/// Registers a client that opts out of every handler setting and only carries the User-Agent.