diff --git a/src/c#/GeneralUpdate.ClientCore/GeneralUpdate.ClientCore.csproj b/src/c#/GeneralUpdate.ClientCore/GeneralUpdate.ClientCore.csproj
index 0242d33d..5ac09967 100644
--- a/src/c#/GeneralUpdate.ClientCore/GeneralUpdate.ClientCore.csproj
+++ b/src/c#/GeneralUpdate.ClientCore/GeneralUpdate.ClientCore.csproj
@@ -76,6 +76,8 @@
+
+
diff --git a/src/c#/GeneralUpdate.Common/Shared/Security/IHttpAuthProvider.cs b/src/c#/GeneralUpdate.Common/Shared/Security/IHttpAuthProvider.cs
new file mode 100644
index 00000000..34184d70
--- /dev/null
+++ b/src/c#/GeneralUpdate.Common/Shared/Security/IHttpAuthProvider.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GeneralUpdate.Common.Shared.Security;
+
+///
+/// HTTP request authentication provider.
+/// Implement this interface to attach authentication credentials to outgoing HTTP requests.
+/// The provider is invoked before every HTTP request made by the update framework
+/// (version validation, download, status reporting).
+///
+public interface IHttpAuthProvider
+{
+ ///
+ /// Apply authentication to the given HTTP request message.
+ /// Called immediately before the request is sent.
+ ///
+ /// The outgoing HTTP request to attach auth to.
+ /// Cancellation token.
+ Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default);
+}
+
+// ════════════════════════════════════════════════════════════════
+// Built-in implementations
+// ════════════════════════════════════════════════════════════════
+
+///
+/// No-op auth provider. Used when no authentication is configured (the default).
+///
+public sealed class NoOpAuthProvider : IHttpAuthProvider
+{
+ public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default)
+ => Task.CompletedTask;
+}
+
+///
+/// Bearer token authentication (JWT / OAuth2).
+/// Adds Authorization: Bearer {token} header.
+///
+public sealed class BearerTokenAuthProvider : IHttpAuthProvider
+{
+ private readonly string _token;
+
+ public BearerTokenAuthProvider(string token)
+ {
+ _token = token ?? throw new ArgumentNullException(nameof(token));
+ }
+
+ public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default)
+ {
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
+ return Task.CompletedTask;
+ }
+}
+
+///
+/// API key authentication.
+/// Adds a custom header (default X-Api-Key) with the provided API key.
+///
+public sealed class ApiKeyAuthProvider : IHttpAuthProvider
+{
+ private readonly string _headerName;
+ private readonly string _apiKey;
+
+ public ApiKeyAuthProvider(string apiKey, string headerName = "X-Api-Key")
+ {
+ _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
+ _headerName = headerName ?? throw new ArgumentNullException(nameof(headerName));
+ }
+
+ public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default)
+ {
+ request.Headers.Add(_headerName, _apiKey);
+ return Task.CompletedTask;
+ }
+}
+
+///
+/// HMAC-SHA256 signature authentication.
+/// Adds X-Update-Timestamp and X-Update-Signature headers.
+/// The signature is computed over {request_body}|{unix_timestamp}.
+///
+public sealed class HmacAuthProvider : IHttpAuthProvider
+{
+ private readonly string _secretKey;
+
+ public HmacAuthProvider(string secretKey)
+ {
+ _secretKey = secretKey ?? throw new ArgumentNullException(nameof(secretKey));
+ }
+
+ public async Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default)
+ {
+ var body = request.Content != null
+ ? await request.Content.ReadAsStringAsync().ConfigureAwait(false)
+ : string.Empty;
+
+ var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
+ var payload = $"{body}|{timestamp}";
+ var signature = ComputeHmacSha256(payload, _secretKey);
+
+ request.Headers.Add("X-Update-Timestamp", timestamp);
+ request.Headers.Add("X-Update-Signature", signature);
+ }
+
+ private static string ComputeHmacSha256(string data, string key)
+ {
+ var keyBytes = Encoding.UTF8.GetBytes(key);
+ var dataBytes = Encoding.UTF8.GetBytes(data);
+
+ using var hmac = new HMACSHA256(keyBytes);
+ var hash = hmac.ComputeHash(dataBytes);
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+}
+
+///
+/// Factory methods for creating auth providers from UpdateOptions-style parameters.
+/// Used internally by VersionService to auto-select the correct provider.
+///
+public static class HttpAuthProviderFactory
+{
+ ///
+ /// Auto-select an auth provider based on the provided parameters.
+ ///
+ /// Auth scheme: "Bearer", "ApiKey", or null.
+ /// Auth token / API key.
+ /// HMAC secret key (takes priority over Token/Scheme).
+ /// The appropriate IHttpAuthProvider.
+ public static IHttpAuthProvider Create(string? scheme, string? token, string? appSecretKey)
+ {
+ // HMAC takes priority (used for signed requests)
+ if (!string.IsNullOrEmpty(appSecretKey))
+ return new HmacAuthProvider(appSecretKey);
+
+ // Bearer / API Key based on scheme
+ if (!string.IsNullOrEmpty(token))
+ {
+ return (scheme ?? string.Empty).ToLowerInvariant() switch
+ {
+ "apikey" => new ApiKeyAuthProvider(token),
+ _ => new BearerTokenAuthProvider(token) // Bearer is the default
+ };
+ }
+
+ return new NoOpAuthProvider();
+ }
+}
diff --git a/src/c#/GeneralUpdate.Common/Shared/Security/ISslValidationPolicy.cs b/src/c#/GeneralUpdate.Common/Shared/Security/ISslValidationPolicy.cs
new file mode 100644
index 00000000..a28026ba
--- /dev/null
+++ b/src/c#/GeneralUpdate.Common/Shared/Security/ISslValidationPolicy.cs
@@ -0,0 +1,40 @@
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+
+namespace GeneralUpdate.Common.Shared.Security;
+
+///
+/// SSL/TLS certificate validation policy.
+/// Implement this interface to customize certificate validation behavior
+/// (e.g., certificate pinning, custom CA trust, or bypass for testing environments).
+///
+public interface ISslValidationPolicy
+{
+ ///
+ /// Validate the server certificate presented during TLS handshake.
+ ///
+ /// The server certificate, or null if not presented.
+ /// The certificate chain built by the system.
+ /// Errors detected by the system's default validation.
+ /// true to accept the certificate; false to reject and abort the connection.
+ bool ValidateCertificate(
+ X509Certificate2? certificate,
+ X509Chain? chain,
+ SslPolicyErrors sslPolicyErrors);
+}
+
+///
+/// Default strict SSL validation policy.
+/// Accepts only certificates that pass all standard validation checks
+/// (trusted root CA, correct hostname, not expired, not revoked).
+/// This is the safe default and should be used in production.
+///
+public sealed class StrictSslValidationPolicy : ISslValidationPolicy
+{
+ ///
+ public bool ValidateCertificate(
+ X509Certificate2? certificate,
+ X509Chain? chain,
+ SslPolicyErrors sslPolicyErrors)
+ => sslPolicyErrors == SslPolicyErrors.None;
+}
diff --git a/src/c#/GeneralUpdate.Common/Shared/Service/VersionService.cs b/src/c#/GeneralUpdate.Common/Shared/Service/VersionService.cs
index 3bcfd10b..2eee7dea 100644
--- a/src/c#/GeneralUpdate.Common/Shared/Service/VersionService.cs
+++ b/src/c#/GeneralUpdate.Common/Shared/Service/VersionService.cs
@@ -1,58 +1,102 @@
-using System;
+using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
+using System.Threading;
using System.Threading.Tasks;
using GeneralUpdate.Common.Internal;
using GeneralUpdate.Common.Internal.JsonContext;
using GeneralUpdate.Common.Shared.Object;
+using GeneralUpdate.Common.Shared.Security;
namespace GeneralUpdate.Common.Shared.Service
{
public class VersionService
{
+ private static readonly HttpClientHandler _sharedHandler;
+ private static readonly HttpClient _sharedClient;
+
+ private readonly IHttpAuthProvider _authProvider;
+ private readonly ISslValidationPolicy _sslPolicy;
+ private readonly TimeSpan _timeout;
+
+ ///
+ /// Default SSL validation policy used across all HTTP requests.
+ /// Can be overridden globally via .
+ ///
+ private static ISslValidationPolicy _globalSslPolicy = new StrictSslValidationPolicy();
+
+ private static volatile bool _handlerInitialized;
+
+ static VersionService()
+ {
+ _sharedHandler = new HttpClientHandler();
+ _sharedHandler.ServerCertificateCustomValidationCallback = SharedCertificateValidation;
+ _sharedClient = new HttpClient(_sharedHandler, disposeHandler: false);
+ }
+
+ ///
+ /// Set a global SSL validation policy for all VersionService HTTP requests.
+ /// Must be called before the first HTTP request is made.
+ ///
+ public static void SetSslValidationPolicy(ISslValidationPolicy policy)
+ {
+ _globalSslPolicy = policy ?? throw new ArgumentNullException(nameof(policy));
+ _handlerInitialized = true;
+ }
+
+ private static bool SharedCertificateValidation(
+ HttpRequestMessage message,
+ X509Certificate2? certificate,
+ X509Chain? chain,
+ SslPolicyErrors sslPolicyErrors)
+ {
+ return _globalSslPolicy.ValidateCertificate(certificate, chain, sslPolicyErrors);
+ }
+
+ // ════════════════════════════════════════════════════════════
+ // Instance (with custom auth/timeout)
+ // ════════════════════════════════════════════════════════════
+ public VersionService(
+ IHttpAuthProvider? authProvider = null,
+ ISslValidationPolicy? sslPolicy = null,
+ TimeSpan? timeout = null)
+ {
+ _authProvider = authProvider ?? new NoOpAuthProvider();
+ _sslPolicy = sslPolicy ?? _globalSslPolicy;
+ _timeout = timeout ?? TimeSpan.FromSeconds(30);
+ }
+
private VersionService() { }
-
+
+ // ════════════════════════════════════════════════════════════
+ // Static convenience methods (backward-compatible API surface)
+ // ════════════════════════════════════════════════════════════
+
///
/// Report the result of this update: whether it was successful.
///
- ///
- ///
- ///
- ///
- ///
- public static async Task Report(string httpUrl
+ public static Task Report(string httpUrl
, int recordId
, int status
, int? type
, string scheme = null
, string token = null)
{
- var parameters = new Dictionary
- {
- { "RecordId", recordId },
- { "Status", status },
- { "Type", type }
- };
- await PostTaskAsync>(httpUrl, parameters, ReportRespJsonContext.Default.BaseResponseDTOBoolean, scheme, token);
+ var auth = HttpAuthProviderFactory.Create(scheme, token, null);
+ var svc = new VersionService(auth);
+ return svc.ReportAsync(httpUrl, recordId, status, type);
}
///
/// Verify whether the current version needs an update.
///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public static async Task Validate(string httpUrl
+ public static Task Validate(string httpUrl
, string version
, int appType
, string appKey
@@ -60,57 +104,150 @@ public static async Task Validate(string httpUrl
, string productId
, string scheme = null
, string token = null)
+ {
+ var auth = HttpAuthProviderFactory.Create(scheme, token, appKey);
+ var svc = new VersionService(auth);
+ return svc.ValidateAsync(httpUrl, version, appType, platform, productId);
+ }
+
+ // ════════════════════════════════════════════════════════════
+ // Instance methods (with retry support)
+ // ════════════════════════════════════════════════════════════
+
+ private async Task ReportAsync(string httpUrl
+ , int recordId
+ , int status
+ , int? type
+ , CancellationToken token = default)
+ {
+ var parameters = new Dictionary
+ {
+ { "RecordId", recordId },
+ { "Status", status },
+ { "Type", type }
+ };
+ await PostTaskAsync>(
+ httpUrl, parameters, ReportRespJsonContext.Default.BaseResponseDTOBoolean, token)
+ .ConfigureAwait(false);
+ }
+
+ private async Task ValidateAsync(string httpUrl
+ , string version
+ , int appType
+ , int platform
+ , string productId
+ , CancellationToken token = default)
{
var parameters = new Dictionary
{
{ "Version", version },
{ "AppType", appType },
- { "AppKey", appKey },
{ "Platform", platform },
{ "ProductId", productId }
};
- return await PostTaskAsync(httpUrl, parameters, VersionRespJsonContext.Default.VersionRespDTO, scheme, token);
+ return await PostTaskAsync(
+ httpUrl, parameters, VersionRespJsonContext.Default.VersionRespDTO, token)
+ .ConfigureAwait(false);
}
- private static async Task PostTaskAsync(string httpUrl, Dictionary parameters, JsonTypeInfo? typeInfo = null, string scheme = null, string token = null)
+ private async Task PostTaskAsync(
+ string httpUrl,
+ Dictionary parameters,
+ JsonTypeInfo? typeInfo = null,
+ CancellationToken token = default,
+ int maxRetries = 3)
{
- try
+ for (int attempt = 0; ; attempt++)
{
- var uri = new Uri(httpUrl);
- using var httpClient = new HttpClient(new HttpClientHandler
+ try
{
- ServerCertificateCustomValidationCallback = CheckValidationResult
- });
- httpClient.Timeout = TimeSpan.FromSeconds(15);
- httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/html, application/xhtml+xml, */*");
-
- if (!string.IsNullOrEmpty(scheme) && !string.IsNullOrEmpty(token))
+ return await SendRequestAsync(httpUrl, parameters, typeInfo, token)
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex) when (attempt < maxRetries - 1 && IsRetryable(ex))
{
- httpClient.DefaultRequestHeaders.Authorization =
- new System.Net.Http.Headers.AuthenticationHeaderValue(scheme, token);
+ GeneralTracer.Warn(
+ $"HTTP request attempt {attempt + 1}/{maxRetries} failed, retrying... Details: {ex.Message}");
+
+ var delay = TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 1000);
+ await Task.Delay(delay, token).ConfigureAwait(false);
}
-
- var parametersJson =
- JsonSerializer.Serialize(parameters, HttpParameterJsonContext.Default.DictionaryStringObject);
- var stringContent = new StringContent(parametersJson, Encoding.UTF8, "application/json");
- var result = await httpClient.PostAsync(uri, stringContent);
- var reseponseJson = await result.Content.ReadAsStringAsync();
- return typeInfo == null
- ? JsonSerializer.Deserialize(reseponseJson)
- : JsonSerializer.Deserialize(reseponseJson, typeInfo);
}
- catch (Exception e)
+ }
+
+ private async Task SendRequestAsync(
+ string httpUrl,
+ Dictionary parameters,
+ JsonTypeInfo? typeInfo,
+ CancellationToken token)
+ {
+ var uri = new Uri(httpUrl);
+
+ using var request = new HttpRequestMessage(HttpMethod.Post, uri);
+
+ // Apply auth
+ await _authProvider.ApplyAuthAsync(request, token).ConfigureAwait(false);
+
+ // Build body
+ var parametersJson = JsonSerializer.Serialize(
+ parameters, HttpParameterJsonContext.Default.DictionaryStringObject);
+ request.Content = new StringContent(parametersJson, Encoding.UTF8, "application/json");
+ request.Headers.Accept.ParseAdd("application/json");
+
+ // Re-apply auth after setting content (HMAC needs the body)
+ await _authProvider.ApplyAuthAsync(request, token).ConfigureAwait(false);
+
+ // Send via shared HttpClient with custom timeout
+ using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token);
+ timeoutCts.CancelAfter(_timeout);
+
+ var response = await _sharedClient.SendAsync(request, timeoutCts.Token)
+ .ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+
+ var responseJson = await response.Content.ReadAsStringAsync()
+ .ConfigureAwait(false);
+
+ return typeInfo == null
+ ? JsonSerializer.Deserialize(responseJson)
+ : JsonSerializer.Deserialize(responseJson, typeInfo);
+ }
+
+ ///
+ /// Determine if an exception is worth retrying.
+ /// Only retry on transient failures (network, timeout, server errors).
+ /// Do NOT retry on SSL/authentication failures — those are permanent.
+ ///
+ private static bool IsRetryable(Exception ex)
+ {
+ if (ex is OperationCanceledException)
+ return false;
+
+ if (ex is HttpRequestException hre)
{
- GeneralTracer.Error("The PostTaskAsync method in the VersionService class throws an exception.", e);
- throw e;
+ // Retry on timeout and server errors, not on client/SSL errors
+ var msg = hre.Message ?? string.Empty;
+ return msg.Contains("timeout", StringComparison.OrdinalIgnoreCase)
+ || msg.Contains("timed out", StringComparison.OrdinalIgnoreCase)
+ || msg.Contains("server", StringComparison.OrdinalIgnoreCase)
+ || msg.Contains("500")
+ || msg.Contains("502")
+ || msg.Contains("503")
+ || msg.Contains("504");
}
- }
- private static bool CheckValidationResult(
- HttpRequestMessage message,
- X509Certificate2 certificate,
- X509Chain chain,
- SslPolicyErrors sslPolicyErrors
- ) => true;
+ if (ex is TaskCanceledException)
+ return true; // Timeout
+
+ if (ex is TimeoutException)
+ return true;
+
+ // IOException = network interruption
+ if (ex is System.IO.IOException)
+ return true;
+
+ return false;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj
index 4df180fb..bbed19c1 100644
--- a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj
+++ b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj
@@ -84,6 +84,8 @@
+
+