Skip to content

Commit bcf4daa

Browse files
committed
feat: add HTTP/HTTPS authentication and SSL validation support
Adds HTTP transport configuration (SSL validation, proxy, timeouts, retry) and multiple authentication methods (Bearer, Basic, ApiKey, HMAC) to GeneralUpdate.Maui.Android. New files: - IHttpAuthProvider + ISslValidationPolicy interfaces - AuthScheme enum (Hmac/Bearer/ApiKey/Basic) - HttpDownloadOptions configuration record - 5 auth providers (NoOp, BearerToken, ApiKey, Basic, Hmac) + Factory - StrictSslValidationPolicy / AllowAllSslValidationPolicy - publish-nuget.yml CI workflow (mirrors GeneralUpdate.Avalonia pattern) Modified files: - UpdatePackageInfo: 5 auth fields for per-package auth - HttpRangeDownloader: auth injection, IDisposable - GeneralUpdateBootstrap.CreateDefault: optional HttpDownloadOptions param - AndroidBootstrap: disposes downloader if IDisposable Design: - Fully backward-compatible (all new params optional, default behavior identical) - Auth priority: per-package > global > none - Instance-based (no static global state) - No dependency on GeneralUpdate.Core Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: JusterChu <juster.chu@foxmail.com>
1 parent 88180f1 commit bcf4daa

11 files changed

Lines changed: 633 additions & 19 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Publish NuGet
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'NuGet package version (SemVer 2.0, e.g. 1.0.0, 1.0.0-beta.1)'
8+
required: true
9+
type: string
10+
push-to-nuget:
11+
description: 'Push package to NuGet.org'
12+
required: false
13+
type: boolean
14+
default: true
15+
16+
permissions:
17+
contents: write
18+
id-token: write
19+
20+
jobs:
21+
publish:
22+
name: Build and Publish
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v4
28+
with:
29+
fetch-depth: 0
30+
31+
- name: Validate SemVer 2.0
32+
run: |
33+
if ! echo "${{ inputs.version }}" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' > /dev/null; then
34+
echo "::error::Invalid SemVer 2.0 version: '${{ inputs.version }}'"
35+
exit 1
36+
fi
37+
echo "Version ${{ inputs.version }} is valid SemVer 2.0"
38+
39+
- name: Setup .NET
40+
uses: actions/setup-dotnet@v4
41+
with:
42+
dotnet-version: |
43+
8.0.x
44+
10.0.x
45+
46+
- name: Install Android workload
47+
run: dotnet workload install android
48+
49+
- name: Restore
50+
run: dotnet restore src/GeneralUpdate.Maui.sln
51+
52+
- name: Build
53+
run: dotnet build src/GeneralUpdate.Maui.sln --configuration Release --no-restore /p:Version=${{ inputs.version }}
54+
55+
- name: Run tests
56+
run: dotnet test src/GeneralUpdate.Maui.sln --configuration Release --no-restore --no-build /p:Version=${{ inputs.version }}
57+
58+
- name: Pack NuGet
59+
run: dotnet pack src/GeneralUpdate.Maui.sln --configuration Release --no-restore --no-build -o artifacts /p:Version=${{ inputs.version }}
60+
61+
- name: List artifacts
62+
run: ls -la artifacts/
63+
64+
- name: Create git tag
65+
run: |
66+
git config user.name "github-actions"
67+
git config user.email "github-actions@github.com"
68+
TAG="v${{ inputs.version }}"
69+
git tag --force "$TAG"
70+
git push --force-with-lease origin "$TAG"
71+
72+
- name: Create GitHub Release
73+
env:
74+
GH_TOKEN: ${{ github.token }}
75+
run: |
76+
TAG="v${{ inputs.version }}"
77+
if gh release view "$TAG" --json tagName &>/dev/null; then
78+
gh release delete "$TAG" --yes
79+
fi
80+
gh release create "$TAG" \
81+
artifacts/*.nupkg \
82+
--title "$TAG" \
83+
--generate-notes \
84+
--verify-tag
85+
86+
- name: NuGet login via Trusted Publishing (OIDC → temp API key)
87+
if: ${{ inputs.push-to-nuget == true }}
88+
id: nuget-login
89+
uses: NuGet/login@v1
90+
with:
91+
user: juster.chu
92+
93+
- name: Push NuGet package to NuGet.org
94+
if: ${{ inputs.push-to-nuget == true }}
95+
run: |
96+
dotnet nuget push artifacts/*.nupkg \
97+
--source https://api.nuget.org/v3/index.json \
98+
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
99+
--skip-duplicate
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace GeneralUpdate.Maui.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.Maui.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.Maui.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+
}
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.Maui.Android.Abstractions;
3+
4+
namespace GeneralUpdate.Maui.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="Services.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.Maui.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.Maui.Android.Enums;
2+
13
namespace GeneralUpdate.Maui.Android.Models;
24

35
/// <summary>
@@ -22,4 +24,32 @@ public sealed class UpdatePackageInfo
2224
public bool ForceUpdate { get; init; }
2325

2426
public string? ApkFileName { get; init; }
27+
28+
/// <summary>
29+
/// Per-package authentication scheme.
30+
/// When set, takes precedence over the global <see cref="HttpDownloadOptions.AuthProvider"/>.
31+
/// </summary>
32+
public AuthScheme? AuthScheme { get; init; }
33+
34+
/// <summary>
35+
/// Token value used by Bearer or ApiKey authentication.
36+
/// For Bearer: the Bearer token string.
37+
/// For ApiKey: the API key value.
38+
/// </summary>
39+
public string? AuthToken { get; init; }
40+
41+
/// <summary>
42+
/// Secret key used by HMAC-SHA256 signature authentication.
43+
/// </summary>
44+
public string? AuthSecretKey { get; init; }
45+
46+
/// <summary>
47+
/// Username used by Basic authentication.
48+
/// </summary>
49+
public string? BasicUsername { get; init; }
50+
51+
/// <summary>
52+
/// Password used by Basic authentication.
53+
/// </summary>
54+
public string? BasicPassword { get; init; }
2555
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace GeneralUpdate.Maui.Android.Services;
88
/// <summary>
99
/// Orchestrates update discovery, package download, integrity validation, and APK installation triggering.
1010
/// </summary>
11-
public sealed class AndroidBootstrap : IAndroidBootstrap
11+
public sealed class AndroidBootstrap : IAndroidBootstrap, IDisposable
1212
{
1313
private const string UpdateInProgressMessage = "An update execution is already in progress.";
1414
private readonly IUpdateDownloader _downloader;
@@ -222,4 +222,18 @@ private static UpdateFailureReason MapFailureReason(Exception ex)
222222
_ => UpdateFailureReason.Unknown
223223
};
224224
}
225+
226+
private bool _disposed;
227+
228+
public void Dispose()
229+
{
230+
if (_disposed) return;
231+
232+
if (_downloader is IDisposable disposableDownloader)
233+
{
234+
disposableDownloader.Dispose();
235+
}
236+
237+
_disposed = true;
238+
}
225239
}

0 commit comments

Comments
 (0)