ironlicensing-dotnet/LicenseManager.cs

449 lines
15 KiB
C#

using System.Net.Http.Json;
using System.Text.Json;
using IronLicensing.Client.Exceptions;
using IronLicensing.Client.Models;
using Microsoft.Extensions.Options;
namespace IronLicensing.Client;
public class LicenseManager : ILicenseManager
{
private readonly HttpClient _httpClient;
private readonly LicensingOptions _options;
private readonly IMachineIdentifier _machineIdentifier;
private readonly ILicenseCache _cache;
private readonly ISignatureVerifier _signatureVerifier;
private LicenseInfo? _currentLicense;
private readonly SemaphoreSlim _syncLock = new(1, 1);
public LicenseInfo? CurrentLicense => _currentLicense;
public LicenseStatus Status => _currentLicense?.Status ?? LicenseStatus.Unknown;
public bool IsLicensed => _currentLicense != null &&
(_currentLicense.Status == LicenseStatus.Active || _currentLicense.Status == LicenseStatus.Trial);
public bool IsTrial => _currentLicense?.Status == LicenseStatus.Trial;
public int TrialDaysRemaining
{
get
{
if (_currentLicense?.Status != LicenseStatus.Trial || !_currentLicense.ExpiresAt.HasValue)
return 0;
var remaining = (_currentLicense.ExpiresAt.Value - DateTime.UtcNow).Days;
return Math.Max(0, remaining);
}
}
public event EventHandler<LicenseChangedEventArgs>? LicenseChanged;
public event EventHandler<LicenseValidationEventArgs>? ValidationCompleted;
public LicenseManager(
HttpClient httpClient,
IOptions<LicensingOptions> options,
IMachineIdentifier machineIdentifier,
ILicenseCache cache,
ISignatureVerifier signatureVerifier)
{
_httpClient = httpClient;
_options = options.Value;
_machineIdentifier = machineIdentifier;
_cache = cache;
_signatureVerifier = signatureVerifier;
ConfigureHttpClient();
}
private void ConfigureHttpClient()
{
_httpClient.BaseAddress = new Uri(_options.ApiBaseUrl.TrimEnd('/') + "/");
_httpClient.DefaultRequestHeaders.Add("X-Public-Key", _options.PublicKey);
_httpClient.DefaultRequestHeaders.Add("X-Product-Slug", _options.ProductSlug);
_httpClient.Timeout = _options.HttpTimeout;
}
public async Task<LicenseResult> ValidateAsync(string? licenseKey = null)
{
await _syncLock.WaitAsync();
try
{
var key = licenseKey ?? _currentLicense?.LicenseKey;
if (string.IsNullOrEmpty(key))
{
// Try to load from cache
var cached = await _cache.GetCachedLicenseAsync();
if (cached != null)
{
key = cached.License.LicenseKey;
}
}
if (string.IsNullOrEmpty(key))
{
return LicenseResult.Fail("NO_LICENSE", "No license key provided");
}
try
{
var request = new
{
licenseKey = key,
machineId = _machineIdentifier.GetMachineId(),
machineName = _machineIdentifier.GetMachineName(),
platform = _machineIdentifier.GetPlatform(),
appVersion = GetAppVersion()
};
var response = await _httpClient.PostAsJsonAsync("licenses/validate", request);
var result = await response.Content.ReadFromJsonAsync<ValidationResponse>();
if (result == null)
{
return await HandleOfflineValidation(key);
}
if (result.Valid && result.License != null)
{
var licenseInfo = MapToLicenseInfo(key, result.License);
await UpdateLicense(licenseInfo);
// Cache with signature for offline validation
var rawJson = JsonSerializer.Serialize(result.License);
await _cache.SaveLicenseAsync(licenseInfo, result.Signature, result.SigningPublicKey);
// Save signing public key if provided
if (!string.IsNullOrEmpty(result.SigningPublicKey))
{
await _cache.SaveSigningPublicKeyAsync(result.SigningPublicKey);
}
RaiseValidationCompleted(true, licenseInfo, null, null);
return LicenseResult.Ok(licenseInfo);
}
else
{
RaiseValidationCompleted(false, null, result.Error?.Code, result.Error?.Message);
return LicenseResult.Fail(result.Error?.Code ?? "UNKNOWN", result.Error?.Message ?? "Validation failed");
}
}
catch (HttpRequestException)
{
return await HandleOfflineValidation(key);
}
catch (TaskCanceledException)
{
return await HandleOfflineValidation(key);
}
}
finally
{
_syncLock.Release();
}
}
private async Task<LicenseResult> HandleOfflineValidation(string licenseKey)
{
var cached = await _cache.GetCachedLicenseAsync();
if (cached == null || cached.License.LicenseKey != licenseKey)
{
RaiseValidationCompleted(false, null, "OFFLINE", "Cannot validate license while offline");
return LicenseResult.Fail("OFFLINE", "Cannot validate license while offline");
}
// Check offline grace period
var daysSinceValidation = (DateTime.UtcNow - cached.License.LastValidated).TotalDays;
if (daysSinceValidation > _options.OfflineGraceDays)
{
RaiseValidationCompleted(false, null, "OFFLINE_EXPIRED", "Offline grace period expired. Please connect to the internet to validate your license.");
return LicenseResult.Fail("OFFLINE_EXPIRED", "Offline grace period expired. Please connect to the internet to validate your license.");
}
// Verify signature if required
if (_options.RequireSignatureForOffline)
{
var publicKey = _options.SigningPublicKey ?? cached.SigningPublicKey ?? await _cache.GetSigningPublicKeyAsync();
if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(cached.Signature) || string.IsNullOrEmpty(cached.RawLicenseJson))
{
// No signature available - check if we allow this
if (_options.RequireSignatureForOffline)
{
RaiseValidationCompleted(false, null, "NO_SIGNATURE", "Offline validation requires a signed license");
return LicenseResult.Fail("NO_SIGNATURE", "Offline validation requires a signed license");
}
}
else
{
// Verify signature
var isValid = _signatureVerifier.Verify(publicKey, cached.RawLicenseJson, cached.Signature);
if (!isValid)
{
RaiseValidationCompleted(false, null, "INVALID_SIGNATURE", "License signature verification failed");
return LicenseResult.Fail("INVALID_SIGNATURE", "License signature verification failed");
}
}
}
// Valid offline license
await UpdateLicense(cached.License);
RaiseValidationCompleted(true, cached.License, null, null);
return LicenseResult.Ok(cached.License);
}
public async Task<LicenseResult> ActivateAsync(string licenseKey)
{
await _syncLock.WaitAsync();
try
{
var request = new
{
licenseKey,
machineId = _machineIdentifier.GetMachineId(),
machineName = _machineIdentifier.GetMachineName(),
platform = _machineIdentifier.GetPlatform(),
appVersion = GetAppVersion()
};
var response = await _httpClient.PostAsJsonAsync("licenses/activate", request);
var result = await response.Content.ReadFromJsonAsync<ActivationResponse>();
if (result?.Success == true)
{
// Release lock before calling ValidateAsync (it acquires its own lock)
_syncLock.Release();
try
{
return await ValidateAsync(licenseKey);
}
finally
{
await _syncLock.WaitAsync();
}
}
return LicenseResult.Fail(result?.Error?.Code ?? "UNKNOWN", result?.Error?.Message ?? "Activation failed");
}
catch (Exception ex)
{
return LicenseResult.Fail("ERROR", ex.Message);
}
finally
{
_syncLock.Release();
}
}
public async Task<bool> DeactivateAsync()
{
if (_currentLicense == null)
return false;
await _syncLock.WaitAsync();
try
{
var request = new
{
licenseKey = _currentLicense.LicenseKey,
machineId = _machineIdentifier.GetMachineId()
};
var response = await _httpClient.PostAsJsonAsync("licenses/deactivate", request);
var result = await response.Content.ReadFromJsonAsync<ActivationResponse>();
if (result?.Success == true)
{
await ClearCacheAsync();
await UpdateLicense(null);
return true;
}
return false;
}
catch
{
return false;
}
finally
{
_syncLock.Release();
}
}
public async Task<CheckoutResult> StartPurchaseAsync(string tierId, string customerEmail)
{
try
{
var request = new
{
tierId,
customerEmail,
successUrl = _options.SuccessUrl,
cancelUrl = _options.CancelUrl,
metadata = new Dictionary<string, string>
{
["machineId"] = _machineIdentifier.GetMachineId()
}
};
var response = await _httpClient.PostAsJsonAsync("checkout/create", request);
var result = await response.Content.ReadFromJsonAsync<CheckoutApiResponse>();
if (result != null)
{
return new CheckoutResult
{
Success = true,
SessionId = result.SessionId,
CheckoutUrl = result.CheckoutUrl,
ExpiresAt = result.ExpiresAt
};
}
return new CheckoutResult { Success = false, ErrorMessage = "Failed to create checkout session" };
}
catch (Exception ex)
{
return new CheckoutResult { Success = false, ErrorMessage = ex.Message };
}
}
public bool HasFeature(string featureKey)
{
if (_currentLicense == null)
return false;
return _currentLicense.Features.Contains(featureKey) || _currentLicense.Features.Contains("*");
}
public async Task<bool> HasFeatureAsync(string featureKey)
{
if (_currentLicense == null)
return false;
// Check local first
if (HasFeature(featureKey))
return true;
// Check with server
try
{
var response = await _httpClient.GetAsync($"licenses/{_currentLicense.LicenseKey}/features/{featureKey}");
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
return result.TryGetProperty("enabled", out var enabled) && enabled.GetBoolean();
}
}
catch
{
// Fall back to local check
}
return false;
}
public void RequireFeature(string featureKey)
{
if (!HasFeature(featureKey))
{
throw new LicenseRequiredException(featureKey);
}
}
public async Task<bool> StartTrialAsync(string email)
{
try
{
var request = new
{
email,
machineId = _machineIdentifier.GetMachineId(),
machineName = _machineIdentifier.GetMachineName(),
platform = _machineIdentifier.GetPlatform()
};
var response = await _httpClient.PostAsJsonAsync("trial/start", request);
var result = await response.Content.ReadFromJsonAsync<TrialResponse>();
if (result?.Success == true && !string.IsNullOrEmpty(result.LicenseKey))
{
// Activate the trial license
var activationResult = await ActivateAsync(result.LicenseKey);
return activationResult.Success;
}
return false;
}
catch
{
return false;
}
}
public async Task ClearCacheAsync()
{
await _cache.ClearAsync();
}
private async Task UpdateLicense(LicenseInfo? license)
{
var oldLicense = _currentLicense;
_currentLicense = license;
LicenseChanged?.Invoke(this, new LicenseChangedEventArgs
{
OldLicense = oldLicense,
NewLicense = license
});
await Task.CompletedTask;
}
private void RaiseValidationCompleted(bool success, LicenseInfo? license, string? errorCode, string? errorMessage)
{
ValidationCompleted?.Invoke(this, new LicenseValidationEventArgs
{
Success = success,
License = license,
ErrorCode = errorCode,
ErrorMessage = errorMessage
});
}
private static LicenseInfo MapToLicenseInfo(string licenseKey, ValidationLicenseInfo validation)
{
return new LicenseInfo
{
LicenseKey = licenseKey,
Tier = validation.Tier,
Status = ParseStatus(validation.Status),
ExpiresAt = validation.ExpiresAt,
Features = validation.Features,
ActivationsUsed = validation.ActivationsUsed,
ActivationsMax = validation.ActivationsMax,
LastValidated = DateTime.UtcNow
};
}
private static LicenseStatus ParseStatus(string status)
{
return status.ToLowerInvariant() switch
{
"trial" => LicenseStatus.Trial,
"active" => LicenseStatus.Active,
"expired" => LicenseStatus.Expired,
"suspended" => LicenseStatus.Suspended,
"revoked" => LicenseStatus.Revoked,
_ => LicenseStatus.Unknown
};
}
private static string GetAppVersion()
{
var assembly = System.Reflection.Assembly.GetEntryAssembly();
return assembly?.GetName().Version?.ToString() ?? "1.0.0";
}
}