Skip to content

Commit dbc7e55

Browse files
JusterZhuclaude
andauthored
feat: add HTTP/HTTPS compatibility and multiple authentication methods (#15)
* feat: add HTTP/HTTPS compatibility and multiple authentication support Add authentication providers (Bearer, Basic, ApiKey, HMAC), SSL validation policies, retry with exponential backoff, download timeout, and proxy support. All new APIs are backward-compatible with existing code. New: - IHttpAuthProvider / ISslValidationPolicy interfaces - AuthScheme enum (Hmac/Bearer/ApiKey/Basic) - 5 auth providers + HttpAuthProviderFactory - StrictSslValidationPolicy / AllowAllSslValidationPolicy - HttpDownloadOptions configuration record - Per-package auth fields on UpdatePackageInfo - Auth injection, retry, and timeout in HttpResumableApkDownloader Closes #14 Co-Authored-By: Claude <noreply@anthropic.com> * fix: address Copilot review comments - Set _ownsClient = false in public constructor (prevents disposing external client) - Set HttpClient.Timeout = InfiniteTimeSpan when created internally (timeout managed via CancellationTokenSource) - Fall back to global auth provider when per-package AuthScheme has missing credentials + log warning - Wire RequestTimeout into HEAD probe request (separate from DownloadTimeout) - Simplify Enums.AuthScheme? to AuthScheme? (using already imported) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0c0caa1 commit dbc7e55

11 files changed

Lines changed: 614 additions & 13 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace GeneralUpdate.Avalonia.Android.Abstractions;
2+
3+
/// <summary>
4+
/// Provides authentication for HTTP requests.
5+
/// Implementations can add headers, modify the request, or perform
6+
/// any other authentication flow before the request is sent.
7+
/// </summary>
8+
public interface IHttpAuthProvider
9+
{
10+
Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default);
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Net.Security;
2+
using System.Security.Cryptography.X509Certificates;
3+
4+
namespace GeneralUpdate.Avalonia.Android.Abstractions;
5+
6+
/// <summary>
7+
/// Provides custom SSL/TLS certificate validation logic.
8+
/// Used to configure <see cref="System.Net.Http.HttpClientHandler.ServerCertificateCustomValidationCallback"/>.
9+
/// </summary>
10+
public interface ISslValidationPolicy
11+
{
12+
bool ValidateCertificate(
13+
X509Certificate2? certificate,
14+
X509Chain? chain,
15+
SslPolicyErrors sslPolicyErrors);
16+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace GeneralUpdate.Avalonia.Android.Enums;
2+
3+
/// <summary>
4+
/// Defines the supported HTTP authentication schemes for update downloads.
5+
/// </summary>
6+
public enum AuthScheme
7+
{
8+
/// <summary>
9+
/// HMAC-SHA256 signature-based authentication.
10+
/// Adds X-Update-Timestamp and X-Update-Signature headers.
11+
/// </summary>
12+
Hmac = 0,
13+
14+
/// <summary>
15+
/// Bearer token authentication via Authorization header.
16+
/// </summary>
17+
Bearer = 1,
18+
19+
/// <summary>
20+
/// API key authentication via a custom header (default: X-Api-Key).
21+
/// </summary>
22+
ApiKey = 2,
23+
24+
/// <summary>
25+
/// HTTP Basic authentication via Authorization header.
26+
/// </summary>
27+
Basic = 3
28+
}

src/GeneralUpdate.Avalonia.Android/GeneralUpdateBootstrap.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public static IAndroidBootstrap CreateDefault(
1414
HttpClient? httpClient = null,
1515
IVersionComparer? versionComparer = null,
1616
IUpdateEventDispatcher? eventDispatcher = null,
17-
IUpdateLogger? logger = null)
17+
IUpdateLogger? logger = null,
18+
HttpDownloadOptions? httpOptions = null)
1819
{
1920
var usedContextProvider = contextProvider ?? new DefaultAndroidContextProvider();
2021
var context = usedContextProvider.GetContext();
@@ -32,9 +33,22 @@ public static IAndroidBootstrap CreateDefault(
3233
var effectiveOptions = options with { DownloadDirectoryPath = effectiveDownloadDirectory };
3334
var usedLogger = logger ?? new NoOpUpdateLogger();
3435
var usedStorage = new PhysicalFileStorage();
35-
var usedClient = httpClient ?? new HttpClient();
3636

37-
var downloader = new HttpResumableApkDownloader(usedClient, usedStorage, effectiveOptions, usedLogger);
37+
HttpResumableApkDownloader downloader;
38+
if (httpOptions != null)
39+
{
40+
// Use internal constructor that builds HttpClient from HttpDownloadOptions
41+
// (SSL validation, proxy, auth, timeouts)
42+
downloader = new HttpResumableApkDownloader(
43+
usedStorage, effectiveOptions, httpOptions, usedLogger);
44+
}
45+
else
46+
{
47+
// Legacy path: use injected httpClient or a bare new one
48+
var usedClient = httpClient ?? new HttpClient();
49+
downloader = new HttpResumableApkDownloader(
50+
usedClient, usedStorage, effectiveOptions, usedLogger);
51+
}
3852
var validator = new Sha256HashValidator();
3953
var installer = new AndroidApkInstaller(
4054
usedContextProvider,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System.Net;
2+
using GeneralUpdate.Avalonia.Android.Abstractions;
3+
4+
namespace GeneralUpdate.Avalonia.Android.Models;
5+
6+
/// <summary>
7+
/// Configures HTTP transport behavior for update downloads:
8+
/// SSL/TLS certificate validation, timeouts, proxy, retry, and authentication.
9+
/// <para>
10+
/// When provided to <see cref="GeneralUpdateBootstrap.CreateDefault"/>,
11+
/// the library constructs an internal <see cref="HttpClient"/> from these settings.
12+
/// When null, the existing behavior is preserved (bare HttpClient, no auth, system SSL).
13+
/// </para>
14+
/// </summary>
15+
public sealed record HttpDownloadOptions
16+
{
17+
/// <summary>
18+
/// Custom SSL/TLS certificate validation policy.
19+
/// Defaults to null, which uses the system's default certificate validation.
20+
/// Set to <see cref="Services.AllowAllSslValidationPolicy"/> for self-signed certificates
21+
/// in development environments only.
22+
/// </summary>
23+
public ISslValidationPolicy? SslValidationPolicy { get; init; }
24+
25+
/// <summary>
26+
/// Timeout for individual HTTP requests (HEAD probes, etc.).
27+
/// Default is 30 seconds.
28+
/// </summary>
29+
public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(30);
30+
31+
/// <summary>
32+
/// Overall timeout for the entire download operation.
33+
/// Default is 10 minutes.
34+
/// </summary>
35+
public TimeSpan DownloadTimeout { get; init; } = TimeSpan.FromMinutes(10);
36+
37+
/// <summary>
38+
/// Optional web proxy for HTTP requests.
39+
/// When set, <see cref="UseProxy"/> must also be true for the proxy to take effect.
40+
/// </summary>
41+
public IWebProxy? Proxy { get; init; }
42+
43+
/// <summary>
44+
/// Whether to use the configured <see cref="Proxy"/>.
45+
/// Default is false.
46+
/// </summary>
47+
public bool UseProxy { get; init; }
48+
49+
/// <summary>
50+
/// Maximum number of retry attempts for transient failures.
51+
/// Default is 3 (meaning 1 initial attempt + 2 retries).
52+
/// Set to 1 to disable retry.
53+
/// </summary>
54+
public int MaxRetryAttempts { get; init; } = 3;
55+
56+
/// <summary>
57+
/// Base delay for exponential backoff retry.
58+
/// Actual delays are: baseDelay * 2^attempt.
59+
/// Default is 1 second.
60+
/// </summary>
61+
public TimeSpan RetryBaseDelay { get; init; } = TimeSpan.FromSeconds(1);
62+
63+
/// <summary>
64+
/// Global authentication provider applied to all download requests.
65+
/// Per-package authentication on <see cref="UpdatePackageInfo"/> takes precedence.
66+
/// </summary>
67+
public IHttpAuthProvider? AuthProvider { get; init; }
68+
69+
/// <summary>
70+
/// Builds an <see cref="HttpClientHandler"/> from the configured options.
71+
/// Applies SSL validation policy and proxy settings.
72+
/// </summary>
73+
internal HttpClientHandler BuildHandler()
74+
{
75+
var handler = new HttpClientHandler();
76+
77+
if (SslValidationPolicy != null)
78+
{
79+
handler.ServerCertificateCustomValidationCallback =
80+
(_, cert, chain, errors) => SslValidationPolicy.ValidateCertificate(cert, chain, errors);
81+
}
82+
83+
if (UseProxy && Proxy != null)
84+
{
85+
handler.Proxy = Proxy;
86+
handler.UseProxy = true;
87+
}
88+
else
89+
{
90+
handler.UseProxy = false;
91+
}
92+
93+
return handler;
94+
}
95+
}

src/GeneralUpdate.Avalonia.Android/Models/UpdatePackageInfo.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using GeneralUpdate.Avalonia.Android.Enums;
2+
13
namespace GeneralUpdate.Avalonia.Android.Models;
24

35
public sealed record UpdatePackageInfo
@@ -11,4 +13,32 @@ public sealed record UpdatePackageInfo
1113
public DateTimeOffset? PublishTime { get; init; }
1214
public bool IsForced { get; init; }
1315
public string? FileName { get; init; }
16+
17+
/// <summary>
18+
/// Per-package authentication scheme.
19+
/// When set, takes precedence over the global <see cref="HttpDownloadOptions.AuthProvider"/>.
20+
/// </summary>
21+
public AuthScheme? AuthScheme { get; init; }
22+
23+
/// <summary>
24+
/// Token value used by Bearer or ApiKey authentication.
25+
/// For Bearer: the Bearer token string.
26+
/// For ApiKey: the API key value.
27+
/// </summary>
28+
public string? AuthToken { get; init; }
29+
30+
/// <summary>
31+
/// Secret key used by HMAC-SHA256 signature authentication.
32+
/// </summary>
33+
public string? AuthSecretKey { get; init; }
34+
35+
/// <summary>
36+
/// Username used by Basic authentication.
37+
/// </summary>
38+
public string? BasicUsername { get; init; }
39+
40+
/// <summary>
41+
/// Password used by Basic authentication.
42+
/// </summary>
43+
public string? BasicPassword { get; init; }
1444
}

src/GeneralUpdate.Avalonia.Android/Services/AndroidBootstrap.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ public void Dispose()
282282
}
283283

284284
_operationGate.Dispose();
285+
286+
if (_downloader is IDisposable disposableDownloader)
287+
{
288+
disposableDownloader.Dispose();
289+
}
290+
285291
_disposed = true;
286292
}
287293

0 commit comments

Comments
 (0)