Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace GeneralUpdate.Avalonia.Android.Abstractions;

/// <summary>
/// Provides authentication for HTTP requests.
/// Implementations can add headers, modify the request, or perform
/// any other authentication flow before the request is sent.
/// </summary>
public interface IHttpAuthProvider
{
Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace GeneralUpdate.Avalonia.Android.Abstractions;

/// <summary>
/// Provides custom SSL/TLS certificate validation logic.
/// Used to configure <see cref="System.Net.Http.HttpClientHandler.ServerCertificateCustomValidationCallback"/>.
/// </summary>
public interface ISslValidationPolicy
{
bool ValidateCertificate(
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors);
}
28 changes: 28 additions & 0 deletions src/GeneralUpdate.Avalonia.Android/Enums/AuthScheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace GeneralUpdate.Avalonia.Android.Enums;

/// <summary>
/// Defines the supported HTTP authentication schemes for update downloads.
/// </summary>
public enum AuthScheme
{
/// <summary>
/// HMAC-SHA256 signature-based authentication.
/// Adds X-Update-Timestamp and X-Update-Signature headers.
/// </summary>
Hmac = 0,

/// <summary>
/// Bearer token authentication via Authorization header.
/// </summary>
Bearer = 1,

/// <summary>
/// API key authentication via a custom header (default: X-Api-Key).
/// </summary>
ApiKey = 2,

/// <summary>
/// HTTP Basic authentication via Authorization header.
/// </summary>
Basic = 3
}
20 changes: 17 additions & 3 deletions src/GeneralUpdate.Avalonia.Android/GeneralUpdateBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public static IAndroidBootstrap CreateDefault(
HttpClient? httpClient = null,
IVersionComparer? versionComparer = null,
IUpdateEventDispatcher? eventDispatcher = null,
IUpdateLogger? logger = null)
IUpdateLogger? logger = null,
HttpDownloadOptions? httpOptions = null)
{
var usedContextProvider = contextProvider ?? new DefaultAndroidContextProvider();
var context = usedContextProvider.GetContext();
Expand All @@ -32,9 +33,22 @@ public static IAndroidBootstrap CreateDefault(
var effectiveOptions = options with { DownloadDirectoryPath = effectiveDownloadDirectory };
var usedLogger = logger ?? new NoOpUpdateLogger();
var usedStorage = new PhysicalFileStorage();
var usedClient = httpClient ?? new HttpClient();

var downloader = new HttpResumableApkDownloader(usedClient, usedStorage, effectiveOptions, usedLogger);
HttpResumableApkDownloader downloader;
if (httpOptions != null)
{
// Use internal constructor that builds HttpClient from HttpDownloadOptions
// (SSL validation, proxy, auth, timeouts)
downloader = new HttpResumableApkDownloader(
usedStorage, effectiveOptions, httpOptions, usedLogger);
}
else
{
// Legacy path: use injected httpClient or a bare new one
var usedClient = httpClient ?? new HttpClient();
downloader = new HttpResumableApkDownloader(
usedClient, usedStorage, effectiveOptions, usedLogger);
}
var validator = new Sha256HashValidator();
var installer = new AndroidApkInstaller(
usedContextProvider,
Expand Down
95 changes: 95 additions & 0 deletions src/GeneralUpdate.Avalonia.Android/Models/HttpDownloadOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Net;
using GeneralUpdate.Avalonia.Android.Abstractions;

namespace GeneralUpdate.Avalonia.Android.Models;

/// <summary>
/// Configures HTTP transport behavior for update downloads:
/// SSL/TLS certificate validation, timeouts, proxy, retry, and authentication.
/// <para>
/// When provided to <see cref="GeneralUpdateBootstrap.CreateDefault"/>,
/// the library constructs an internal <see cref="HttpClient"/> from these settings.
/// When null, the existing behavior is preserved (bare HttpClient, no auth, system SSL).
/// </para>
/// </summary>
public sealed record HttpDownloadOptions
{
/// <summary>
/// Custom SSL/TLS certificate validation policy.
/// Defaults to null, which uses the system's default certificate validation.
/// Set to <see cref="Services.AllowAllSslValidationPolicy"/> for self-signed certificates
/// in development environments only.
/// </summary>
public ISslValidationPolicy? SslValidationPolicy { get; init; }

/// <summary>
/// Timeout for individual HTTP requests (HEAD probes, etc.).
/// Default is 30 seconds.
/// </summary>
public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(30);
Comment on lines +25 to +29

/// <summary>
/// Overall timeout for the entire download operation.
/// Default is 10 minutes.
/// </summary>
public TimeSpan DownloadTimeout { get; init; } = TimeSpan.FromMinutes(10);

/// <summary>
/// Optional web proxy for HTTP requests.
/// When set, <see cref="UseProxy"/> must also be true for the proxy to take effect.
/// </summary>
public IWebProxy? Proxy { get; init; }

/// <summary>
/// Whether to use the configured <see cref="Proxy"/>.
/// Default is false.
/// </summary>
public bool UseProxy { get; init; }

/// <summary>
/// Maximum number of retry attempts for transient failures.
/// Default is 3 (meaning 1 initial attempt + 2 retries).
/// Set to 1 to disable retry.
/// </summary>
public int MaxRetryAttempts { get; init; } = 3;

/// <summary>
/// Base delay for exponential backoff retry.
/// Actual delays are: baseDelay * 2^attempt.
/// Default is 1 second.
/// </summary>
public TimeSpan RetryBaseDelay { get; init; } = TimeSpan.FromSeconds(1);

/// <summary>
/// Global authentication provider applied to all download requests.
/// Per-package authentication on <see cref="UpdatePackageInfo"/> takes precedence.
/// </summary>
public IHttpAuthProvider? AuthProvider { get; init; }

/// <summary>
/// Builds an <see cref="HttpClientHandler"/> from the configured options.
/// Applies SSL validation policy and proxy settings.
/// </summary>
internal HttpClientHandler BuildHandler()
{
var handler = new HttpClientHandler();

if (SslValidationPolicy != null)
{
handler.ServerCertificateCustomValidationCallback =
(_, cert, chain, errors) => SslValidationPolicy.ValidateCertificate(cert, chain, errors);
}

if (UseProxy && Proxy != null)
{
handler.Proxy = Proxy;
handler.UseProxy = true;
}
else
{
handler.UseProxy = false;
}

return handler;
}
}
30 changes: 30 additions & 0 deletions src/GeneralUpdate.Avalonia.Android/Models/UpdatePackageInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using GeneralUpdate.Avalonia.Android.Enums;

namespace GeneralUpdate.Avalonia.Android.Models;

public sealed record UpdatePackageInfo
Expand All @@ -11,4 +13,32 @@ public sealed record UpdatePackageInfo
public DateTimeOffset? PublishTime { get; init; }
public bool IsForced { get; init; }
public string? FileName { get; init; }

/// <summary>
/// Per-package authentication scheme.
/// When set, takes precedence over the global <see cref="HttpDownloadOptions.AuthProvider"/>.
/// </summary>
public AuthScheme? AuthScheme { get; init; }

/// <summary>
/// Token value used by Bearer or ApiKey authentication.
/// For Bearer: the Bearer token string.
/// For ApiKey: the API key value.
/// </summary>
public string? AuthToken { get; init; }

/// <summary>
/// Secret key used by HMAC-SHA256 signature authentication.
/// </summary>
public string? AuthSecretKey { get; init; }

/// <summary>
/// Username used by Basic authentication.
/// </summary>
public string? BasicUsername { get; init; }

/// <summary>
/// Password used by Basic authentication.
/// </summary>
public string? BasicPassword { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ public void Dispose()
}

_operationGate.Dispose();

if (_downloader is IDisposable disposableDownloader)
{
disposableDownloader.Dispose();
}

_disposed = true;
}

Expand Down
Loading
Loading