Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify registration, don't require callback to server #293

Merged
merged 2 commits into from
Jan 21, 2024
Merged
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
233 changes: 38 additions & 195 deletions src/dymaptic.GeoBlazor.Core/RegistrationValidator.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Configuration;
using Microsoft.JSInterop;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Security;
using System.Text;
using System.Text.Json;
using System.Web;


#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

namespace dymaptic.GeoBlazor.Core;
Expand All @@ -18,157 +17,59 @@ public interface IAppValidator

internal class RegistrationValidator: IAppValidator
{
public RegistrationValidator(GeoBlazorSettings settings, IJSRuntime jsRuntime, HttpClient httpClient,
NavigationManager navigationManager, IConfiguration configuration)
public RegistrationValidator(GeoBlazorSettings settings, IJSRuntime jsRuntime, NavigationManager navigationManager)
{
_settings = settings;
_jsRuntime = jsRuntime;
_httpClient = httpClient;
_navigationManager = navigationManager;
_machineName = Environment.MachineName;
#if DEBUG
_licenseServerUrl = configuration["LicenseServerUrl"] ?? "https://licensing-dev.dymaptic.com/";
#endif
}

public async Task ValidateLicense()
{
// if we've already shown the message, there's no need to check again
if (_messageShown)
// if we've already shown the message or validated, there's no need to check again
if (_messageShown || _isValidated)
{
return;
}

// if we've already found a valid license while the software is running, there's no need to check
if ( _inMemoryValidationResult is not null && _inMemoryValidationResult.IsValid)
{
if (_inMemoryValidationResult.BaseUri != _navigationManager.BaseUri)
{
_inMemoryValidationResult = null;
}
else
{
return;
}
}
if (_validating) return;

string? storedValidation;
BlazorMode blazorMode;
_validating = true;

if (_jsRuntime.GetType().Name.Contains("Remote")) // Server
{
blazorMode = BlazorMode.Server;
storedValidation = await GetServerFileValidationResult();
}
else
{
blazorMode = OperatingSystem.IsBrowser() ? BlazorMode.WebAssembly : BlazorMode.Maui;
storedValidation = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "validationResult");
}
BlazorMode blazorMode = _jsRuntime.GetType().Name.Contains("Remote") ? BlazorMode.Server :
OperatingSystem.IsBrowser() ? BlazorMode.WebAssembly : BlazorMode.Maui;

ValidationResult? validationResult = null;
ValidationResult? storedValidationResult = null;
string? registration = _settings.RegistrationKey;
bool valid = registration is not null;

if (storedValidation is not null)
if (valid)
{
try
{
storedValidationResult = JsonSerializer.Deserialize<ValidationResult>(storedValidation);
string registrationText = Encoding.UTF8.GetString(Convert.FromBase64String(registration!));

// don't use stored results with a different base uri or machine name
if (storedValidationResult?.BaseUri != _navigationManager.BaseUri ||
!storedValidationResult.MachineName.Equals(_machineName, StringComparison.OrdinalIgnoreCase))
RegistrationObject registrationObject =
JsonSerializer.Deserialize<RegistrationObject>(registrationText)!;
if (valid && registrationObject!.LicenseVersion != 1)
{
storedValidationResult = null;
valid = false;
}

if (storedValidationResult?.AttemptedConnect is not null &&
storedValidationResult.AttemptedConnect.Value.AddMinutes(5) > DateTime.UtcNow)
{
// too soon to check again, the connection was down
return;
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}

// if there is no valid stored license, call the server to get a new license
if ((storedValidationResult is null || !storedValidationResult.IsValid) &&
_settings.RegistrationKey is not null)
{
try
{
var queryString = new Dictionary<string, string>
if (valid && registrationObject.LicenseType != "Free")
{
{ "licenseKey", HttpUtility.UrlEncode(_settings.RegistrationKey) },
{ "licenseTypeName", "Free" },
{ "softwareName", "GeoBlazorCore" }
};

if (!string.IsNullOrWhiteSpace(_settings.MauiAppName))
{
queryString["mauiAppName"] = HttpUtility.UrlEncode(_settings.MauiAppName);
valid = false;
}

// build query string without QueryHelpers, which is not available in WebAssembly
var queryStringString = string.Join("&", queryString.Select(kvp => $"{kvp.Key}={kvp.Value}"));
#if DEBUG
var url = $"{_licenseServerUrl}/api/validate?{queryStringString}";
#else
var url = $"https://licensing.dymaptic.com/api/validate?{queryStringString}";
#endif
_httpClient.DefaultRequestHeaders.Referrer = new Uri(_navigationManager.BaseUri);
HttpResponseMessage response = await _httpClient.GetAsync(url);

validationResult = await response.Content.ReadFromJsonAsync<ValidationResult>();

if (validationResult is not null)
if (valid && registrationObject.Software != "GeoBlazorCore")
{
validationResult.MachineName = _machineName;
validationResult.BaseUri = _navigationManager.BaseUri;
string jsonResult = JsonSerializer.Serialize(validationResult);

await SaveFile(blazorMode, jsonResult);
valid = false;
}
}
catch (HttpRequestException)
catch
{
if (storedValidationResult?.AttemptedConnect is not null)
{
validationResult = storedValidationResult;
validationResult.AttemptedConnect = DateTime.UtcNow;
}
else
{
validationResult =
new ValidationResult(false, DateTime.MaxValue, "Unable to reach license server.")
{
AttemptedConnect = DateTime.UtcNow
};
}

string jsonResult = JsonSerializer.Serialize(validationResult);
await SaveFile(blazorMode, jsonResult);

// the server appears to be down, try again in 5 minutes
return;
}
catch (Exception)
{
// don't throw anything here, we will deal with failure after checking stored result
valid = false;
}
}

// if we failed to reach the server, or the server returned an error, use the stored result
if (validationResult is null && storedValidationResult is not null)
{
validationResult = storedValidationResult;
}

if (validationResult is null || !validationResult.IsValid)

if (!valid)
{
if (!_messageShown)
{
Expand All @@ -180,86 +81,22 @@ public async Task ValidateLicense()
}

_messageShown = true;
return;
}
return;
}

_inMemoryValidationResult = validationResult;
_isValidated = true;
_validating = false;
}

private async Task<string?> GetServerFileValidationResult()
{
string directoryPath = _settings.ValidationServerStoragePath ?? Path.GetTempPath();
string filePath = Path.Combine(directoryPath, ServerFileName);

if (!File.Exists(filePath))
{
return null;
}

return await File.ReadAllTextAsync(filePath);
}

private async Task SaveFile(BlazorMode blazorMode, string encodedResult)
{
if (blazorMode == BlazorMode.Server) // Server
{
await SaveServerFileValidationResult(encodedResult);
}
else
{
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "validationResult", encodedResult);
}
}

private async Task SaveServerFileValidationResult(string result)
{
string directoryPath = _settings.ValidationServerStoragePath ?? Path.GetTempPath();
try
{
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}

string filePath = Path.Combine(directoryPath, ServerFileName);
await File.WriteAllTextAsync(filePath, result);
}
catch (SecurityException)
{
string failMessage =
$"Unable to save registration validation file. Please verify that the application has write access to the directory {directoryPath}.";
await _jsRuntime.InvokeVoidAsync(
$"console.log('{failMessage}'");
Console.WriteLine(failMessage);
}
}

private static ValidationResult? _inMemoryValidationResult;
private static bool _isValidated;
private readonly GeoBlazorSettings _settings;
private readonly IJSRuntime _jsRuntime;
private readonly HttpClient _httpClient;
private readonly NavigationManager _navigationManager;
private readonly string _machineName;
private const string ServerFileName = "geoblazor-registration-validation";
private readonly string _registrationMessage =
"Thank you for using GeoBlazor! Please visit https://licensing.dymaptic.com/geoblazor-core to register.";
private static bool _messageShown;
#if DEBUG
private readonly string _licenseServerUrl;
#endif
}

/// <summary>
/// For internal use only
/// </summary>
public record ValidationResult(bool IsValid, DateTime ExpirationDate, string? Message = null)
{
public string MachineName { get; set; } = string.Empty;
public Version? Version { get; set; }

public DateTime? AttemptedConnect { get; set; }
public string? BaseUri { get; set; }
private bool _validating;
}

internal enum BlazorMode
Expand All @@ -269,4 +106,10 @@ internal enum BlazorMode
WebAssembly,
Maui
#pragma warning restore CS1591
}
}

internal record RegistrationObject(
string Email,
string LicenseType,
string Software,
int LicenseVersion);