feat(license): implement Hardware-ID v2, multi-platform Linux support, StateStore LLS2 hardening, and AI Agent docs
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Deploymentcenter.Client;
|
||||
|
||||
public class LicenseValidationResult
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public string Status { get; set; } = "unknown";
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string HardwareId { get; set; } = string.Empty;
|
||||
public bool IsCached { get; set; }
|
||||
public long? ExpiresAt { get; set; }
|
||||
}
|
||||
|
||||
public class LicenseClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILicensePrompt _prompt;
|
||||
|
||||
public LicenseClient(HttpClient? httpClient = null, ILicensePrompt? prompt = null)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_prompt = prompt ?? new ConsoleLicensePrompt();
|
||||
}
|
||||
|
||||
public async Task<LicenseValidationResult> ValidateAsync(string productSlug, string licenseKey, string serverBaseUrl)
|
||||
{
|
||||
var hwInfo = HardwareId.GetHardwareId(productSlug);
|
||||
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
try
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
product = productSlug,
|
||||
license_key = licenseKey,
|
||||
hardware_id = hwInfo.HardwareId,
|
||||
legacy_hardware_id = hwInfo.LegacyHardwareId,
|
||||
hwid_version = hwInfo.HwidVersion,
|
||||
hwid_source = hwInfo.HwidSource,
|
||||
platform = hwInfo.Platform,
|
||||
hostname = Environment.MachineName,
|
||||
app_version = "1.0.0",
|
||||
nonce = Guid.NewGuid().ToString("N")
|
||||
};
|
||||
|
||||
string jsonStr = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(jsonStr, Encoding.UTF8, "application/json");
|
||||
string endpoint = $"{serverBaseUrl.TrimEnd('/')}/api/license/v1/validate";
|
||||
|
||||
HttpResponseMessage response = await _httpClient.PostAsync(endpoint, content);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string resBody = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(resBody);
|
||||
var root = doc.RootElement;
|
||||
|
||||
string status = root.TryGetProperty("status", out var sProp) ? sProp.GetString() ?? "unknown" : "unknown";
|
||||
string message = root.TryGetProperty("message", out var mProp) ? mProp.GetString() ?? "" : "";
|
||||
long? expiresAt = root.TryGetProperty("expires_at", out var eProp) && eProp.ValueKind == JsonValueKind.Number ? eProp.GetInt64() : null;
|
||||
|
||||
if (status.Equals("valid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Save encrypted local cache
|
||||
var cache = new LocalCacheData
|
||||
{
|
||||
SchemaVersion = 2,
|
||||
ProductSlug = productSlug,
|
||||
LicenseKey = licenseKey,
|
||||
HardwareId = hwInfo.HardwareId,
|
||||
Status = "valid",
|
||||
IssuedAt = now,
|
||||
ExpiresAt = expiresAt ?? (now + 7 * 86400),
|
||||
MaxSeenTime = now,
|
||||
Checksum = hwInfo.HardwareId
|
||||
};
|
||||
|
||||
StateStore.Save(productSlug, hwInfo.HardwareId, cache);
|
||||
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
Status = status,
|
||||
Message = message,
|
||||
HardwareId = hwInfo.HardwareId,
|
||||
IsCached = false,
|
||||
ExpiresAt = expiresAt
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Status = status,
|
||||
Message = message,
|
||||
HardwareId = hwInfo.HardwareId,
|
||||
IsCached = false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Server request failed -> Fall back to encrypted offline cache
|
||||
var cache = StateStore.Load(productSlug, hwInfo.HardwareId);
|
||||
if (cache != null && cache.Status == "valid")
|
||||
{
|
||||
// Check time-rollback protection
|
||||
if (now < cache.MaxSeenTime)
|
||||
{
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Status = "clock_rollback",
|
||||
Message = "System clock rollback detected! Online verification required.",
|
||||
HardwareId = hwInfo.HardwareId
|
||||
};
|
||||
}
|
||||
|
||||
// Check cache expiry
|
||||
if (cache.ExpiresAt > 0 && now > cache.ExpiresAt)
|
||||
{
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Status = "cache_expired",
|
||||
Message = "Cached license has expired.",
|
||||
HardwareId = hwInfo.HardwareId
|
||||
};
|
||||
}
|
||||
|
||||
// Update max_seen_time
|
||||
cache.MaxSeenTime = now;
|
||||
StateStore.Save(productSlug, hwInfo.HardwareId, cache);
|
||||
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
Status = "valid_offline",
|
||||
Message = "License validated via secure offline cache",
|
||||
HardwareId = hwInfo.HardwareId,
|
||||
IsCached = true,
|
||||
ExpiresAt = cache.ExpiresAt
|
||||
};
|
||||
}
|
||||
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Status = "network_error",
|
||||
Message = $"Server communication error and no valid cache available: {ex.Message}",
|
||||
HardwareId = hwInfo.HardwareId
|
||||
};
|
||||
}
|
||||
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Status = "unknown_error",
|
||||
Message = "Validation failed.",
|
||||
HardwareId = hwInfo.HardwareId
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> DeactivateAsync(string productSlug, string licenseKey, string serverBaseUrl, string authToken = "")
|
||||
{
|
||||
var hwInfo = HardwareId.GetHardwareId(productSlug);
|
||||
var payload = new
|
||||
{
|
||||
product = productSlug,
|
||||
license_key = licenseKey,
|
||||
hardware_id = hwInfo.HardwareId,
|
||||
nonce = Guid.NewGuid().ToString("N")
|
||||
};
|
||||
|
||||
string jsonStr = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(jsonStr, Encoding.UTF8, "application/json");
|
||||
string endpoint = $"{serverBaseUrl.TrimEnd('/')}/api/license/v1/deactivate";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authToken))
|
||||
{
|
||||
request.Headers.Add("X-Watchdog-Key", authToken);
|
||||
request.Headers.Add("Authorization", $"Bearer {authToken}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user