Skip to content
Closed
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
Expand Up @@ -76,6 +76,8 @@
<Compile Include="..\GeneralUpdate.Common\Shared\Object\VersionInfo.cs" Link="Common\VersionInfo.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Object\VersionOSS.cs" Link="Common\VersionOSS.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Service\VersionService.cs" Link="Common\VersionService.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Security\ISslValidationPolicy.cs" Link="Common\ISslValidationPolicy.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Security\IHttpAuthProvider.cs" Link="Common\IHttpAuthProvider.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Trace\GeneralTracer.cs" Link="Common\GeneralTracer.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Trace\TextTraceListener.cs" Link="Common\TextTraceListener.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Trace\WindowsOutputDebugListener.cs" Link="Common\WindowsOutputDebugListener.cs" />
Expand Down
93 changes: 93 additions & 0 deletions src/c#/GeneralUpdate.Common/Shared/Security/IHttpAuthProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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;

public interface IHttpAuthProvider
{
Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default);
}

public sealed class NoOpAuthProvider : IHttpAuthProvider
{
public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default)
=> Task.CompletedTask;
}

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;
}
}

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;
}
}

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 ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var sig = ComputeHmacSha256($"{body}|{ts}", _secretKey);
request.Headers.Add("X-Update-Timestamp", ts);
request.Headers.Add("X-Update-Signature", sig);
}
private static string ComputeHmacSha256(string data, string key)
{
var hash = new HMACSHA256(Encoding.UTF8.GetBytes(key))
.ComputeHash(Encoding.UTF8.GetBytes(data));
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}

public static class HttpAuthProviderFactory
{
public static IHttpAuthProvider Create(string? scheme, string? token, string? appSecretKey)
{
if (!string.IsNullOrEmpty(appSecretKey))
return new HmacAuthProvider(appSecretKey);
if (!string.IsNullOrEmpty(token))
{
return (scheme ?? "").ToLowerInvariant() switch
{
"apikey" => new ApiKeyAuthProvider(token),
_ => new BearerTokenAuthProvider(token)
};
}
return new NoOpAuthProvider();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace GeneralUpdate.Common.Shared.Security;

public interface ISslValidationPolicy
{
bool ValidateCertificate(
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors);
}

public sealed class StrictSslValidationPolicy : ISslValidationPolicy
{
public bool ValidateCertificate(
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
=> sslPolicyErrors == SslPolicyErrors.None;
}
215 changes: 134 additions & 81 deletions src/c#/GeneralUpdate.Common/Shared/Service/VersionService.cs
Original file line number Diff line number Diff line change
@@ -1,116 +1,169 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
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 HttpClient _sharedClient;
private static ISslValidationPolicy _globalSslPolicy = new StrictSslValidationPolicy();

private readonly IHttpAuthProvider _authProvider;
private readonly TimeSpan _timeout;
private readonly int _maxRetries;

static VersionService()
{
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = SharedCertValidation;
_sharedClient = new HttpClient(handler, disposeHandler: false);
}

public static void SetSslValidationPolicy(ISslValidationPolicy policy)
{
_globalSslPolicy = policy ?? throw new ArgumentNullException(nameof(policy));
}

private static bool SharedCertValidation(
HttpRequestMessage message,
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
return _globalSslPolicy.ValidateCertificate(certificate, chain, sslPolicyErrors);
}

public VersionService(
IHttpAuthProvider? authProvider = null,
TimeSpan? timeout = null,
int maxRetries = 3)
{
_authProvider = authProvider ?? new NoOpAuthProvider();
_timeout = timeout ?? TimeSpan.FromSeconds(30);
_maxRetries = maxRetries;
}

private VersionService() { }

/// <summary>
/// Report the result of this update: whether it was successful.
/// </summary>
/// <param name="httpUrl"></param>
/// <param name="recordId"></param>
/// <param name="status"></param>
/// <param name="type"></param>
/// <returns></returns>
public static async Task Report(string httpUrl
, int recordId
, int status
, int? type
, string scheme = null
, string token = null)

// ═══════════ Static API (backward-compatible) ═══════════

public static Task Report(string httpUrl, int recordId, int status, int? type,
string scheme = null, string token = null)
{
var parameters = new Dictionary<string, object>
{
{ "RecordId", recordId },
{ "Status", status },
{ "Type", type }
var auth = HttpAuthProviderFactory.Create(scheme, token, null);
var svc = new VersionService(auth);
return svc.ReportAsync(httpUrl, recordId, status, type);
}

public static Task<VersionRespDTO> Validate(string httpUrl, string version,
int appType, string appKey, int platform, 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 ═══════════

private async Task ReportAsync(string httpUrl, int recordId, int status,
int? type, CancellationToken token = default)
{
var p = new Dictionary<string, object> {
{ "RecordId", recordId }, { "Status", status }, { "Type", type }
};
await PostTaskAsync<BaseResponseDTO<bool>>(httpUrl, parameters, ReportRespJsonContext.Default.BaseResponseDTOBoolean, scheme, token);
await PostAsync<BaseResponseDTO<bool>>(
httpUrl, p, ReportRespJsonContext.Default.BaseResponseDTOBoolean, token)
.ConfigureAwait(false);
}

/// <summary>
/// Verify whether the current version needs an update.
/// </summary>
/// <param name="httpUrl"></param>
/// <param name="version"></param>
/// <param name="appType"></param>
/// <param name="appKey"></param>
/// <param name="platform"></param>
/// <param name="productId"></param>
/// <returns></returns>
public static async Task<VersionRespDTO> Validate(string httpUrl
, string version
, int appType
, string appKey
, int platform
, string productId
, string scheme = null
, string token = null)
private async Task<VersionRespDTO> ValidateAsync(string httpUrl, string version,
int appType, int platform, string productId, CancellationToken token = default)
{
var parameters = new Dictionary<string, object>
{
{ "Version", version },
{ "AppType", appType },
{ "AppKey", appKey },
{ "Platform", platform },
{ "ProductId", productId }
var p = new Dictionary<string, object> {
{ "Version", version }, { "AppType", appType },
{ "Platform", platform }, { "ProductId", productId }
};
return await PostTaskAsync<VersionRespDTO>(httpUrl, parameters, VersionRespJsonContext.Default.VersionRespDTO, scheme, token);
return await PostAsync<VersionRespDTO>(
httpUrl, p, VersionRespJsonContext.Default.VersionRespDTO, token)
.ConfigureAwait(false);
}

private static async Task<T> PostTaskAsync<T>(string httpUrl, Dictionary<string, object> parameters, JsonTypeInfo<T>? typeInfo = null, string scheme = null, string token = null)
private async Task<T> PostAsync<T>(string httpUrl, Dictionary<string, object> parameters,
JsonTypeInfo<T>? typeInfo, CancellationToken token, int maxRetriesOverride = 0)
{
try
int max = maxRetriesOverride > 0 ? maxRetriesOverride : _maxRetries;
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 SendAsync<T>(httpUrl, parameters, typeInfo, token)
.ConfigureAwait(false);
}
catch (Exception ex) when (attempt < max - 1 && IsRetryable(ex))
{
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(scheme, token);
GeneralTracer.Warn(
$"HTTP attempt {attempt + 1}/{max} failed, retrying. {ex.Message}");
await Task.Delay(TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 1000), 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<T>(reseponseJson)
: JsonSerializer.Deserialize(reseponseJson, typeInfo);
}
catch (Exception e)
}

private async Task<T> SendAsync<T>(string httpUrl, Dictionary<string, object> parameters,
JsonTypeInfo<T>? typeInfo, CancellationToken token)
{
var uri = new Uri(httpUrl);
using var request = new HttpRequestMessage(HttpMethod.Post, uri);
request.Headers.Accept.ParseAdd("application/json");

var json = JsonSerializer.Serialize(
parameters, HttpParameterJsonContext.Default.DictionaryStringObject);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");

await _authProvider.ApplyAuthAsync(request, token).ConfigureAwait(false);

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<T>(responseJson)
: JsonSerializer.Deserialize(responseJson, typeInfo);
}

private static bool IsRetryable(Exception ex)
{
if (ex is OperationCanceledException) return false;
if (ex is TaskCanceledException) return true;
if (ex is TimeoutException) return true;
if (ex is System.IO.IOException) return true;
if (ex is HttpRequestException hre)
{
GeneralTracer.Error("The PostTaskAsync method in the VersionService class throws an exception.", e);
throw e;
var msg = hre.Message ?? "";
return msg.Contains("timeout", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("timed out", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("500") || msg.Contains("502")
|| msg.Contains("503") || msg.Contains("504");
}
return false;
}

private static bool CheckValidationResult(
HttpRequestMessage message,
X509Certificate2 certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors
) => true;
}
}
}
2 changes: 2 additions & 0 deletions src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
<Compile Include="..\GeneralUpdate.Common\Shared\Object\VersionInfo.cs" Link="Common\VersionInfo.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Object\VersionOSS.cs" Link="Common\VersionOSS.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Service\VersionService.cs" Link="Common\VersionService.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Security\ISslValidationPolicy.cs" Link="Common\ISslValidationPolicy.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Security\IHttpAuthProvider.cs" Link="Common\IHttpAuthProvider.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Trace\GeneralTracer.cs" Link="Common\GeneralTracer.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Trace\TextTraceListener.cs" Link="Common\TextTraceListener.cs" />
<Compile Include="..\GeneralUpdate.Common\Shared\Trace\WindowsOutputDebugListener.cs" Link="Common\WindowsOutputDebugListener.cs" />
Expand Down
Loading