974 lines
37 KiB
C#
974 lines
37 KiB
C#
using System;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
using System.Net.Http;
|
|
using AyaNova.Util;
|
|
using AyaNova.Models;
|
|
using System.Linq;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
//JSON KEY
|
|
using Org.BouncyCastle.Security;
|
|
using Org.BouncyCastle.OpenSsl;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
|
|
|
|
|
|
|
|
namespace AyaNova.Core
|
|
{
|
|
|
|
|
|
|
|
internal static class License
|
|
{
|
|
|
|
//License server addresses
|
|
private const string LICENSE_SERVER_URL_ROCKFISH = "https://rockfish.ayanova.com/";
|
|
private const string LICENSE_SERVER_URL_IO = "https://io.ayanova.com/";
|
|
private const string LICENSE_SERVER_URL_EUROPA = "https://europa.ayanova.com/";
|
|
private const string LICENSE_SERVER_URL_CALLISTO = "https://callisto.ayanova.com/";
|
|
|
|
|
|
// #if (DEBUG)
|
|
// private const string LICENSE_SERVER_URL = "http://localhost:3001/";
|
|
// #else
|
|
// private const string LICENSE_SERVER_URL = "https://rockfish.ayanova.com/";
|
|
// #endif
|
|
|
|
//Unlicensed token
|
|
private const string UNLICENSED_TOKEN = "UNLICENSED";
|
|
|
|
//REVOKED token
|
|
private const string REVOKED_TOKEN = "REVOKED";
|
|
|
|
|
|
|
|
//Scheduleable users
|
|
private const string SERVICE_TECHS_FEATURE_NAME = "ServiceTechs";
|
|
|
|
//Accounting add-on
|
|
private const string ACCOUNTING_FEATURE_NAME = "Accounting";
|
|
|
|
//This feature name means it's a trial key
|
|
private const string TRIAL_FEATURE_NAME = "TrialMode";
|
|
|
|
//This feature name means it's a SAAS or rental mode key for month to month hosted service
|
|
private const string RENTAL_FEATURE_NAME = "Subscription";
|
|
|
|
|
|
//Trial key magic number for development and testing, all other guids will be fully licensed
|
|
// private static Guid TEST_TRIAL_KEY_DBID = new Guid("{A6D18A8A-5613-4979-99DA-80D07641A2FE}");
|
|
|
|
|
|
|
|
//Current license key, can be empty
|
|
private static AyaNovaLicenseKey _ActiveLicense = new AyaNovaLicenseKey();
|
|
|
|
//The license dbid, separate from the server dbid
|
|
private static string LicenseDbId { get; set; }
|
|
|
|
|
|
#region license classes
|
|
|
|
//DTO object returned on license query
|
|
internal class LicenseFeature
|
|
{
|
|
//name of feature / product
|
|
public string Feature { get; set; }
|
|
|
|
//Optional count for items that require it
|
|
public long Count { get; set; }
|
|
|
|
}
|
|
|
|
//DTO object for parsed key
|
|
internal class AyaNovaLicenseKey
|
|
{
|
|
|
|
public enum LicenseStatus
|
|
{
|
|
NONE = 0,//fast track
|
|
ActiveTrial = 1,//slow track
|
|
ExpiredTrial = 2,//fast track
|
|
ActivePurchased = 3,//slow track
|
|
ExpiredPurchased = 4,//fast track
|
|
Revoked = 5//slow track
|
|
}
|
|
|
|
public AyaNovaLicenseKey()
|
|
{
|
|
Features = new List<LicenseFeature>();
|
|
RegisteredTo = UNLICENSED_TOKEN;
|
|
Id = RegisteredTo;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Fetch the license status of the feature in question
|
|
/// </summary>
|
|
/// <param name="Feature"></param>
|
|
/// <returns>LicenseFeature object or null if there is no license</returns>
|
|
public LicenseFeature GetLicenseFeature(string Feature)
|
|
{
|
|
if (IsEmpty)
|
|
return null;
|
|
|
|
string lFeature = Feature.ToLowerInvariant();
|
|
|
|
foreach (LicenseFeature l in Features)
|
|
{
|
|
if (l.Feature.ToLowerInvariant() == lFeature)
|
|
{
|
|
return l;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public long ActiveNumber
|
|
{
|
|
get
|
|
{
|
|
return GetLicenseFeature(SERVICE_TECHS_FEATURE_NAME).Count;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check for the existance of license feature
|
|
/// </summary>
|
|
/// <param name="Feature"></param>
|
|
/// <returns>bool</returns>
|
|
public bool HasLicenseFeature(string Feature)
|
|
{
|
|
if (IsEmpty)
|
|
return false;
|
|
|
|
string lFeature = Feature.ToLowerInvariant();
|
|
|
|
foreach (LicenseFeature l in Features)
|
|
{
|
|
if (l.Feature.ToLowerInvariant() == lFeature)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public LicenseStatus Status
|
|
{
|
|
get
|
|
{
|
|
//TEST
|
|
// return LicenseStatus.Revoked;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(RegisteredTo) || RegisteredTo == UNLICENSED_TOKEN)
|
|
return LicenseStatus.NONE;
|
|
if (RegisteredTo == REVOKED_TOKEN)
|
|
return LicenseStatus.Revoked;
|
|
if (TrialLicense && !LicenseExpired)
|
|
return LicenseStatus.ActiveTrial;
|
|
if (TrialLicense && LicenseExpired)
|
|
return LicenseStatus.ExpiredTrial;
|
|
if (!TrialLicense && !LicenseExpired)
|
|
return LicenseStatus.ActivePurchased;
|
|
if (!TrialLicense && LicenseExpired)
|
|
return LicenseStatus.ExpiredPurchased;
|
|
throw new System.Exception("License::Status - unable to determine license status");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//Has any kind of valid license that is active
|
|
//used for auth route checking to allow for fixing this issue
|
|
public bool KeyDoesNotNeedAttention
|
|
{
|
|
get
|
|
{
|
|
|
|
return (Status == LicenseStatus.ActivePurchased) || (Status == LicenseStatus.ActiveTrial);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
public bool IsEmpty
|
|
{
|
|
get
|
|
{
|
|
//Key is empty if it's not registered to anyone or there are no features in it
|
|
return string.IsNullOrWhiteSpace(RegisteredTo) || (Features == null || Features.Count == 0);
|
|
}
|
|
}
|
|
|
|
public bool WillExpire
|
|
{
|
|
get
|
|
{
|
|
return LicenseExpiration < DateUtil.EmptyDateValue;
|
|
}
|
|
}
|
|
|
|
|
|
public bool LicenseExpired
|
|
{
|
|
get
|
|
{
|
|
return LicenseExpiration < DateTime.Now;
|
|
}
|
|
}
|
|
|
|
public bool MaintenanceExpired
|
|
{
|
|
get
|
|
{
|
|
return MaintenanceExpiration < DateTime.Now;
|
|
}
|
|
}
|
|
|
|
|
|
public bool TrialLicense
|
|
{
|
|
get
|
|
{
|
|
return HasLicenseFeature(TRIAL_FEATURE_NAME);
|
|
}
|
|
}
|
|
|
|
public bool RentalLicense
|
|
{
|
|
get
|
|
{
|
|
return HasLicenseFeature(RENTAL_FEATURE_NAME);
|
|
}
|
|
}
|
|
|
|
public string LicenseFormat { get; set; }
|
|
public string Id { get; set; }
|
|
public string RegisteredTo { get; set; }
|
|
public string DbId { get; set; }
|
|
public DateTime LicenseExpiration { get; set; }
|
|
public DateTime MaintenanceExpiration { get; set; }
|
|
public List<LicenseFeature> Features { get; set; }
|
|
|
|
|
|
|
|
}
|
|
#endregion
|
|
|
|
#region sample v8 key
|
|
// private static string SAMPLE_KEY = @"[KEY
|
|
// {
|
|
// ""Key"": {
|
|
// ""LicenseFormat"": ""8"",
|
|
// ""Id"": ""34-1516288681"", <----Customer id followed by key serial id
|
|
// ""RegisteredTo"": ""Super TestCo"", or "REVOKED" if revoked
|
|
// ""DBID"": ""df558559-7f8a-4c7b-955c-959ebcdf71f3"",
|
|
// ""LicenseExpiration"": ""2019-01-18T07:18:01.2329138-08:00"", <--- UTC, DateTime if perpetual license 1/1/5555 indicates not expiring
|
|
// ""MaintenanceExpiration"": ""2019-01-18T07:18:01.2329138-08:00"", <-- UTC, DateTime support and updates subscription runs out, applies to all features
|
|
// ""Features"": {
|
|
// ""Feature"": [
|
|
// {
|
|
// ""Name"": ""Scheduleable users"",
|
|
// ""Count"":""10"",
|
|
// },
|
|
// {
|
|
// ""Name"": ""Accounting""
|
|
// },
|
|
// {
|
|
// ""Name"": ""TrialMode""<---means is a trial key
|
|
// },
|
|
// {
|
|
// ""Name"": ""Subscription"" <----Means it's an SAAS/Rental key
|
|
// }
|
|
|
|
// ]
|
|
// }
|
|
// }
|
|
// }
|
|
// KEY]
|
|
// [SIGNATURE
|
|
// HEcY3JCVwK9HFXEFnldUEPXP4Q7xoZfMZfOfx1cYmfVF3MVWePyZ9dqVZcY7pk3RmR1BbhQdhpljsYLl+ZLTRhNa54M0EFa/bQnBnbwYZ70EQl8fz8WOczYTEBo7Sm5EyC6gSHtYZu7yRwBvhQzpeMGth5uWnlfPb0dMm0DQM7PaqhdWWW9GCSOdZmFcxkFQ8ERLDZhVMbd8PJKyLvZ+sGMrmYTAIoL0tqa7nrxYkM71uJRTAmQ0gEl4bJdxiV825U1J+buNQuTZdacZKEPSjQQkYou10jRbReUmP2vDpvu+nA1xdJe4b5LlyQL+jiIXH17lf93xlCUb0UkDpu8iNQ==
|
|
// SIGNATURE]\";
|
|
|
|
#endregion
|
|
|
|
#region Exposed properties
|
|
|
|
//The database id value stored in the schema table
|
|
internal static string ServerDbId { get; private set; }
|
|
|
|
internal static bool LicenseConsentRequired { get; private set; }
|
|
|
|
|
|
/// <summary>
|
|
/// Fetch a summary of the license key for displaying to the end user
|
|
/// </summary>
|
|
/// <returns> string containing current license information</returns>
|
|
internal static string LicenseInfo
|
|
{
|
|
get
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
if (ActiveKey.IsEmpty)
|
|
{
|
|
sb.AppendLine(UNLICENSED_TOKEN);
|
|
sb.AppendLine($"Server DB ID: {ServerDbId}");
|
|
}
|
|
else
|
|
{
|
|
if (ActiveKey.TrialLicense)
|
|
sb.AppendLine("TRIAL LICENSE FOR EVALUATION PURPOSES ONLY");
|
|
|
|
sb.AppendLine($"Registered to: {ActiveKey.RegisteredTo}");
|
|
//sb.AppendLine($"Key ID: {ActiveKey.Id}");
|
|
//sb.AppendLine($"Server DB ID: {ServerDbId}");
|
|
sb.AppendLine($"Type: {(ActiveKey.RentalLicense ? "Subscription" : "Perpetual")}");
|
|
if (ActiveKey.WillExpire)
|
|
sb.AppendLine($"License expires: {DateUtil.ServerDateTimeString(ActiveKey.LicenseExpiration)}");
|
|
sb.AppendLine($"Maintenance subscription expires: {DateUtil.ServerDateTimeString(ActiveKey.MaintenanceExpiration)}");
|
|
sb.AppendLine("Features:");
|
|
foreach (LicenseFeature l in ActiveKey.Features)
|
|
{
|
|
//don't show the rental or trial features
|
|
if (l.Feature != TRIAL_FEATURE_NAME && l.Feature != RENTAL_FEATURE_NAME)
|
|
{
|
|
if (l.Count != 0)
|
|
sb.AppendLine($"{l.Feature} - {l.Count}");
|
|
else
|
|
sb.AppendLine($"{l.Feature}");
|
|
}
|
|
}
|
|
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Fetch a summary of the license key for displaying in the log
|
|
/// </summary>
|
|
/// <returns> string containing current license information</returns>
|
|
internal static string LicenseInfoLogFormat
|
|
{
|
|
get
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
if (ActiveKey.IsEmpty)
|
|
{
|
|
return $"UNLICENSED, DB ID: {LicenseDbId}";
|
|
}
|
|
else
|
|
{
|
|
|
|
if (ActiveKey.TrialLicense)
|
|
sb.Append("TRIAL, ");
|
|
|
|
sb.Append($"regto: {ActiveKey.RegisteredTo}, ");
|
|
sb.Append($"keyid: {ActiveKey.Id}, ");
|
|
sb.Append($"dbid: {LicenseDbId}, ");
|
|
// sb.Append($"serverdbid: {ServerDbId}");
|
|
sb.Append($"type: {(ActiveKey.RentalLicense ? "subscription" : "perpetual")}, ");
|
|
if (ActiveKey.WillExpire)
|
|
sb.Append($"exp: {DateUtil.ServerDateTimeString(ActiveKey.LicenseExpiration)} (utc), ");
|
|
sb.Append($"maint. sub. exps: {DateUtil.ServerDateTimeString(ActiveKey.MaintenanceExpiration)} (utc), ");
|
|
sb.Append("feat:");
|
|
foreach (LicenseFeature l in ActiveKey.Features)
|
|
{
|
|
//don't show the rental or trial features
|
|
if (l.Feature != TRIAL_FEATURE_NAME && l.Feature != RENTAL_FEATURE_NAME)
|
|
{
|
|
if (l.Count != 0)
|
|
sb.Append($"{l.Feature} - {l.Count}, ");
|
|
else
|
|
sb.Append($"{l.Feature}, ");
|
|
}
|
|
}
|
|
|
|
}
|
|
return sb.ToString().Trim().TrimEnd(',');
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Fetch a summary of the license key for displaying to the end user
|
|
/// via the API
|
|
/// </summary>
|
|
/// <returns> JSON object containing current license information</returns>
|
|
internal static object LicenseInfoAsJson
|
|
{
|
|
get
|
|
{
|
|
Newtonsoft.Json.Linq.JObject o = Newtonsoft.Json.Linq.JObject.FromObject(new
|
|
{
|
|
license = new
|
|
{
|
|
serverDbId = ServerDbId,
|
|
licensedTo = ActiveKey.RegisteredTo,
|
|
dbId = ActiveKey.DbId,
|
|
keySerial = ActiveKey.Id,
|
|
licenseExpiration = ActiveKey.LicenseExpiration,
|
|
licenseWillExpire = ActiveKey.WillExpire,
|
|
maintenanceExpired = ActiveKey.MaintenanceExpired,
|
|
maintenanceExpiration = ActiveKey.MaintenanceExpiration,
|
|
features =
|
|
from f in ActiveKey.Features
|
|
orderby f.Feature
|
|
select new
|
|
{
|
|
Feature = f.Feature,
|
|
Count = f.Count
|
|
}
|
|
}
|
|
});
|
|
return o;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Returns the active key
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
internal static AyaNovaLicenseKey ActiveKey
|
|
{
|
|
get
|
|
{
|
|
return _ActiveLicense;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region EULA consent
|
|
/// <summary>
|
|
/// Set consent flag for license agreement
|
|
/// </summary>
|
|
internal static async Task FlagEULA(AyContext ct, ILogger log)
|
|
{
|
|
try
|
|
{
|
|
var CurrentInDbKeyRecord = await ct.License.OrderBy(z => z.Id).FirstOrDefaultAsync();
|
|
if (CurrentInDbKeyRecord == null)
|
|
throw new ApplicationException("E1020 - Can't update EULA agreement, no key record found");
|
|
//Update current license
|
|
CurrentInDbKeyRecord.LicenseAgree = true;
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var msg = "E1020 - Error during EULA agreement flagging";
|
|
log.LogError(ex, msg);
|
|
throw new ApplicationException(msg, ex);
|
|
}
|
|
LicenseConsentRequired = false;
|
|
}
|
|
#endregion
|
|
|
|
#region Trial license request handling
|
|
/// <summary>
|
|
/// Request a key
|
|
/// </summary>
|
|
/// <returns>Result string</returns>
|
|
internal static async Task<string> RequestTrialAsync(RequestTrial trialRequest, ILogger log)
|
|
{
|
|
trialRequest.DbId = ServerDbId;
|
|
|
|
log.LogDebug($"Requesting trial license for DBID {LicenseDbId}");
|
|
string sUrl = $"{LICENSE_SERVER_URL_ROCKFISH}rvr";
|
|
try
|
|
{
|
|
var content = new StringContent(JsonConvert.SerializeObject(trialRequest), Encoding.UTF8, "application/json");
|
|
var client = ServiceProviderProvider.HttpClientFactory.CreateClient();
|
|
var res = await client.PostAsync(sUrl, content);
|
|
var responseText = await res.Content.ReadAsStringAsync();
|
|
if (res.IsSuccessStatusCode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(responseText))
|
|
return "ok";
|
|
else
|
|
return responseText;
|
|
}
|
|
else return $"E1020 - Error requesting trial license key: {res.ReasonPhrase}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var msg = "E1020 - Error requesting trial license key see log for details";
|
|
log.LogError(ex, msg);
|
|
return msg;
|
|
}
|
|
}
|
|
#endregion trial license request handling
|
|
|
|
|
|
#region License fetching and handling
|
|
|
|
public class dtoFetchRequest
|
|
{
|
|
public string DbId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetch a key, validate it and install it in the db then initialize with it
|
|
/// </summary>
|
|
/// <returns>Result string</returns>
|
|
#if (DEBUG)
|
|
internal static async Task<string> FetchKeyAsync(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ct, ILogger log, bool calledFromInternalJob, bool devTestTrial = false)
|
|
#else
|
|
internal static async Task<string> FetchKeyAsync(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ct, ILogger log, bool calledFromInternalJob)
|
|
#endif
|
|
{
|
|
if (calledFromInternalJob)
|
|
log.LogDebug($"Fetching license for DBID {LicenseDbId} (called by job)");
|
|
else
|
|
log.LogInformation($"Fetching license for DBID {LicenseDbId}");
|
|
|
|
var FetchRequest = new dtoFetchRequest() { DbId = LicenseDbId };
|
|
string LicenseUrlParameter = "rvf";
|
|
#if (DEBUG)
|
|
if (devTestTrial)
|
|
{
|
|
LicenseUrlParameter += "?dtt=true";//signal to rockfish to provide a key immediately for dev testing
|
|
}
|
|
#endif
|
|
|
|
try
|
|
{
|
|
var content = new StringContent(JsonConvert.SerializeObject(FetchRequest), Encoding.UTF8, "application/json");
|
|
var client = ServiceProviderProvider.HttpClientFactory.CreateClient();
|
|
HttpResponseMessage res = null;
|
|
|
|
res = await DoFetchAsync(LicenseUrlParameter, content, client);
|
|
if (res.IsSuccessStatusCode)
|
|
{
|
|
var responseText = await res.Content.ReadAsStringAsync();
|
|
var responseJson = JObject.Parse(responseText);
|
|
var keyText = responseJson["data"]["key"].Value<string>();
|
|
|
|
AyaNovaLicenseKey ParsedKey = Parse(keyText, log);
|
|
|
|
if (ParsedKey != null)
|
|
{
|
|
await InstallAsync(keyText, ParsedKey, apiServerState, ct, log);
|
|
//Log the license info so it's on the record
|
|
log.LogInformation($"License - new key installed [{AyaNova.Core.License.LicenseInfoLogFormat}]");
|
|
return "ok";
|
|
}
|
|
return $"E1020 - Error fetching license key: No key was returned";
|
|
}
|
|
else
|
|
{
|
|
//some kind of server error??
|
|
if ((int)res.StatusCode > 499)
|
|
{
|
|
return $"E1020 - License server error fetching license key, contact technical support: {res.ReasonPhrase}";
|
|
}
|
|
//If it's NOT FOUND, that's not an error, just a normal response to be expected
|
|
if (res.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
{
|
|
return "notfound";
|
|
}
|
|
|
|
return $"E1020 - Error fetching license key: {res.ReasonPhrase}";
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
|
|
|
|
if (ex is System.Net.Http.HttpRequestException)
|
|
{//communications issue or rockfish down?
|
|
if (!calledFromInternalJob)
|
|
{
|
|
//user wants to see this:
|
|
var msg = $"E1020 - COMM Error fetching / installing license key: {ex.Message}";
|
|
//we want it in the log as this is on demand so it won't fill up the log and tech support might need to see this
|
|
log.LogError(ex, msg);
|
|
return msg;
|
|
}
|
|
else
|
|
{
|
|
//happened when called from JOB, don't want to fill up the log with this error so just logdebug it and move on
|
|
var msg = $"E1020 - COMM Error fetching / installing license key (From Job)";
|
|
log.LogDebug(ex, msg);
|
|
return msg;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//some other error
|
|
if (calledFromInternalJob) throw;
|
|
var msg = "E1020 - Error fetching / installing license key";
|
|
log.LogError(ex, msg);
|
|
return msg;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task<HttpResponseMessage> DoFetchAsync(string LicenseUrlParameters, StringContent content, HttpClient client)
|
|
{
|
|
var LicenseServer = string.Empty;
|
|
for (int x = 0; x < 4; x++)
|
|
{
|
|
try
|
|
{
|
|
switch (x)
|
|
{
|
|
case 0:
|
|
LicenseServer = LICENSE_SERVER_URL_ROCKFISH;
|
|
break;
|
|
case 1:
|
|
LicenseServer = LICENSE_SERVER_URL_IO;
|
|
break;
|
|
case 2:
|
|
LicenseServer = LICENSE_SERVER_URL_EUROPA;
|
|
break;
|
|
case 3:
|
|
LicenseServer = LICENSE_SERVER_URL_CALLISTO;
|
|
break;
|
|
}
|
|
|
|
return await client.PostAsync($"{LicenseServer}{LicenseUrlParameters}", content);
|
|
|
|
}
|
|
catch (System.Net.Http.HttpRequestException)
|
|
{
|
|
if (x == 3)
|
|
throw;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Initialize the license
|
|
///
|
|
/// </summary>
|
|
internal static async Task InitializeAsync(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ct, ILogger log)
|
|
{
|
|
log.LogDebug("Initializing license");
|
|
|
|
try
|
|
{
|
|
//First fetch the schema db id for the servers database, the license must match
|
|
var schema = await ct.SchemaVersion.AsNoTracking().SingleOrDefaultAsync();
|
|
//if (schema == null || schema.Id == Guid.Empty)
|
|
if (schema == null || string.IsNullOrWhiteSpace(schema.Id))
|
|
{
|
|
//cryptic message deliberately, this is probably caused by someone trying to circumvent licensing
|
|
var msg = "E1030 - Database integrity check failed (2). Contact support.";
|
|
apiServerState.SetOpsOnly(msg);
|
|
log.LogCritical(msg);
|
|
return;
|
|
}
|
|
ServerDbId = schema.Id;
|
|
|
|
//Fetch key from db as no tracking so doesn't hang round if need to immediately clear and then re-add the key
|
|
Models.License ldb = await ct.License.AsNoTracking().SingleOrDefaultAsync();
|
|
|
|
//Non existent license should restrict server to ops routes only with closed API
|
|
if (ldb == null)
|
|
{
|
|
ldb = new Models.License();
|
|
ldb.DbId = ServerDbId;
|
|
ldb.Key = "none";
|
|
ldb.LicenseAgree = false;
|
|
ct.License.Add(ldb);
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
|
|
//ensure DB ID
|
|
// if (ldb.DbId == Guid.Empty)
|
|
if (string.IsNullOrWhiteSpace(ldb.DbId))
|
|
{
|
|
ldb.DbId = ServerDbId;
|
|
ldb.LicenseAgree = false;
|
|
//Convert the no tracking record fetched above to tracking
|
|
//this is required because a prior call to initialize before dumping the db would mean the license is still in memory in the context
|
|
ct.Entry(ldb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
|
|
//Get it early and set it here so that it can be displayed early to the user even if not licensed
|
|
LicenseDbId = ldb.DbId;
|
|
|
|
//someone must agree to the license on first login from the client, this stores that
|
|
LicenseConsentRequired = !ldb.LicenseAgree;
|
|
|
|
if (ldb.Key == "none")
|
|
{
|
|
var msg = "E1020 - License key not found in database, running in unlicensed mode";
|
|
apiServerState.SetSystemLock(msg);
|
|
log.LogWarning(msg);
|
|
return;
|
|
}
|
|
|
|
//Validate the key
|
|
AyaNovaLicenseKey k = Parse(ldb.Key, log);
|
|
if (k == null)
|
|
{
|
|
var msg = "E1020 - License key in database is not valid, running in unlicensed mode";
|
|
apiServerState.SetSystemLock(msg);
|
|
log.LogCritical(msg);
|
|
return;
|
|
}
|
|
|
|
_ActiveLicense = k;
|
|
|
|
if (_ActiveLicense.LicenseExpired)
|
|
{
|
|
var msg = $"E1020 - License key expired {DateUtil.ServerDateTimeString(_ActiveLicense.LicenseExpiration)}";
|
|
apiServerState.SetSystemLock(msg);
|
|
log.LogCritical(msg);
|
|
return;
|
|
}
|
|
|
|
if (_ActiveLicense.Status == AyaNovaLicenseKey.LicenseStatus.Revoked)
|
|
{
|
|
var msg = $"E1020 - License key revoked";
|
|
apiServerState.SetSystemLock(msg);
|
|
log.LogCritical(msg);
|
|
return;
|
|
}
|
|
|
|
//Has someone been trying funny business with the active techs in the db?
|
|
if (await AyaNova.Biz.UserBiz.ActiveCountAsync() > _ActiveLicense.ActiveNumber)
|
|
{
|
|
var msg = $"E1020 - Active count exceeded capacity";
|
|
apiServerState.SetSystemLock(msg);
|
|
log.LogCritical(msg);
|
|
return;
|
|
}
|
|
|
|
//Key is ok, might not have been on first boot so check and clear if locked
|
|
//This works for now because system lock only means license lock
|
|
//if ever changed for other purposes then need to handle that see serverstate for ideas
|
|
if (apiServerState.IsSystemLocked)
|
|
{
|
|
apiServerState.ClearSystemLock();
|
|
}
|
|
log.LogDebug("License key OK");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var msg = "E1020 - Error initializing license key";
|
|
log.LogCritical(ex, msg);
|
|
apiServerState.SetSystemLock(msg);
|
|
throw new ApplicationException(msg, ex);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Install key to db
|
|
/// </summary>
|
|
private static async Task<bool> InstallAsync(string RawTextNewKey, AyaNovaLicenseKey ParsedNewKey, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ct, ILogger log)
|
|
{
|
|
try
|
|
{
|
|
var CurrentInDbKeyRecord = await ct.License.OrderBy(z => z.Id).FirstOrDefaultAsync();
|
|
if (CurrentInDbKeyRecord == null)
|
|
throw new ApplicationException("E1020 - Can't install key, no key record found");
|
|
|
|
if (ParsedNewKey == null)
|
|
{
|
|
throw new ApplicationException("E1020 - License.Install -> key could not be parsed");
|
|
}
|
|
|
|
//Can't install a trial into a non-empty db
|
|
if (ParsedNewKey.TrialLicense && !await DbUtil.DBIsEmptyAsync(ct, log))
|
|
{
|
|
throw new ApplicationException("E1020 - Can't install a trial key into a non empty AyaNova database. Erase the database first.");
|
|
}
|
|
|
|
//TECHCOUNT - new license causes exceeding count?
|
|
long NewTechCount = ParsedNewKey.GetLicenseFeature(SERVICE_TECHS_FEATURE_NAME).Count;
|
|
if (await AyaNova.Biz.UserBiz.ActiveCountAsync() > NewTechCount)
|
|
{
|
|
//attempt to set enough of the eldest last login techs to inactive
|
|
await AyaNova.Biz.UserBiz.DeActivateExcessiveTechs(NewTechCount, log);
|
|
|
|
if (await AyaNova.Biz.UserBiz.ActiveCountAsync() > NewTechCount)
|
|
throw new ApplicationException("E1020 - Can't install key, too many active techs and / or subcontractors in database. Deactivate enough to install key.");
|
|
}
|
|
|
|
//Update current license
|
|
CurrentInDbKeyRecord.Key = RawTextNewKey;
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var msg = "E1020 - Error installing license key";
|
|
log.LogError(ex, msg);
|
|
throw new ApplicationException(msg, ex);
|
|
}
|
|
finally
|
|
{
|
|
await InitializeAsync(apiServerState, ct, log);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
#region PARSE and Validate key
|
|
/// <summary>
|
|
/// Parses and validates the integrity of a passed in textual license key
|
|
/// </summary>
|
|
/// <returns>a populated key if valid or else null</returns>
|
|
private static AyaNovaLicenseKey Parse(string k, ILogger log)
|
|
{
|
|
AyaNovaLicenseKey key = new AyaNovaLicenseKey();
|
|
|
|
log.LogDebug("Validating license");
|
|
|
|
if (string.IsNullOrWhiteSpace(k))
|
|
{
|
|
throw new ApplicationException("E1020 - License.Parse -> License key is empty and can't be validated");
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!k.Contains("[KEY") ||
|
|
!k.Contains("KEY]") ||
|
|
!k.Contains("[SIGNATURE") ||
|
|
!k.Contains("SIGNATURE]"))
|
|
{
|
|
throw new ApplicationException("E1020 - License.Parse -> License key is missing required delimiters");
|
|
|
|
}
|
|
|
|
|
|
string keyNoWS = System.Text.RegularExpressions.Regex.Replace(StringUtil.Extract(k, "[KEY", "KEY]").Trim(), "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
|
|
string keySig = StringUtil.Extract(k, "[SIGNATURE", "SIGNATURE]").Trim();
|
|
//bugbug second time around after installing key, keysig has cr/lf characters in it after this extract method runs, not sure wtf as it isnt there the first time
|
|
|
|
#region Check Signature
|
|
|
|
//***** NOTE: this is our real 2016 public key
|
|
var publicPem = @"-----BEGIN PUBLIC KEY-----
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz7wrvLDcKVMZ31HFGBnL
|
|
WL08IodYIV5VJkKy1Z0n2snprhSiu3izxTyz+SLpftvKHJpky027ii7l/pL9Bo3J
|
|
cjU5rKrxXavnE7TuYPjXn16dNLd0K/ERSU+pXLmUaVN0nUWuGuUMoGJMEXoulS6p
|
|
JiG11yu3BM9fL2Nbj0C6a+UwzEHFmns3J/daZOb4gAzMUdJfh9OJ0+wRGzR8ZxyC
|
|
99Na2gDmqYglUkSMjwLTL/HbgwF4OwmoQYJBcET0Wa6Gfb17SaF8XRBV5ZtpCsbS
|
|
tkthGeoXZkFriB9c1eFQLKpBYQo2DW3H1MPG2nAlQZLbkJj5cSh7/t1bRF08m6P+
|
|
EQIDAQAB
|
|
-----END PUBLIC KEY-----";
|
|
|
|
|
|
PemReader pr = new PemReader(new StringReader(publicPem));
|
|
var KeyParameter = (Org.BouncyCastle.Crypto.AsymmetricKeyParameter)pr.ReadObject();
|
|
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
|
|
signer.Init(false, KeyParameter);
|
|
var expectedSig = Convert.FromBase64String(keySig);
|
|
var msgBytes = Encoding.UTF8.GetBytes(keyNoWS);
|
|
signer.BlockUpdate(msgBytes, 0, msgBytes.Length);
|
|
if (!signer.VerifySignature(expectedSig))
|
|
{
|
|
throw new ApplicationException("E1020 - License.Parse -> License key failed integrity check and is not valid");
|
|
}
|
|
|
|
#endregion check signature
|
|
|
|
#region Get Values
|
|
Newtonsoft.Json.Linq.JToken token = Newtonsoft.Json.Linq.JObject.Parse(keyNoWS);
|
|
|
|
key.LicenseFormat = (string)token.SelectToken("Key.LicenseFormat");
|
|
if (key.LicenseFormat != "8")
|
|
throw new ApplicationException($"E1020 - License.Parse -> License key format {key.LicenseFormat} not recognized");
|
|
key.Id = (string)token.SelectToken("Key.Id");
|
|
key.RegisteredTo = (string)token.SelectToken("Key.RegisteredTo");
|
|
key.DbId = (string)token.SelectToken("Key.DBID");
|
|
if (key.DbId != ServerDbId)
|
|
throw new ApplicationException($"E1020 - License.Parse -> License key does not match this server");
|
|
key.LicenseExpiration = (DateTime)token.SelectToken("Key.LicenseExpiration");
|
|
key.MaintenanceExpiration = (DateTime)token.SelectToken("Key.MaintenanceExpiration");
|
|
|
|
//FEATURES
|
|
Newtonsoft.Json.Linq.JArray p = (Newtonsoft.Json.Linq.JArray)token.SelectToken("Key.Features");
|
|
for (int z = 0; z < p.Count; z++)
|
|
{
|
|
LicenseFeature lf = new LicenseFeature();
|
|
lf.Feature = (string)p[z].SelectToken("Name");
|
|
if (p[z].SelectToken("Count") != null)
|
|
{
|
|
lf.Count = (long)p[z].SelectToken("Count");
|
|
}
|
|
else
|
|
{
|
|
lf.Count = 0;
|
|
}
|
|
key.Features.Add(lf);
|
|
}
|
|
|
|
|
|
#endregion get values
|
|
|
|
//Check if attempting to use a build of AyaNova that is newer than maintenance subscription expiry
|
|
if (MExBB(Util.FileUtil.GetLinkerTimestampUtc(System.Reflection.Assembly.GetExecutingAssembly()), key))
|
|
{
|
|
Console.WriteLine("E1020 - Not licensed for this version of AyaNova. Fix: downgrade back to previous version in use or contact technical support for options.");
|
|
|
|
//NOTE: VERSION-TOO-NEW bit below is checked for in startup.cs DO NOT remove this without fixing the side effects
|
|
throw new ApplicationException("E1020 VERSION-TOO-NEW - Not licensed for this version of AyaNova. Fix: downgrade back to previous version in use or contact technical support for options.");
|
|
}
|
|
//All is well return key
|
|
return key;
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var msg = "E1020 - License key not valid";
|
|
log.LogError(ex, msg);
|
|
throw new ApplicationException(msg, ex);
|
|
}
|
|
|
|
|
|
}
|
|
#endregion
|
|
|
|
|
|
|
|
#region Validate Build date against maintenance date in license
|
|
//MaintenanceExpirationBeforeBuild?
|
|
internal static bool MExBB(DateTime dtB, AyaNovaLicenseKey k)
|
|
{
|
|
//Is maintenance expiration date an earlier date than the current build date?
|
|
//return true if a problem or false if ok
|
|
|
|
//Do not worry about evaluation / trial licenses
|
|
//users can upgrade them any time
|
|
if (k.TrialLicense) return false;
|
|
|
|
//Do not worry about rental SAAS licenses, only we can upgrade them
|
|
if(k.RentalLicense) return false;
|
|
|
|
//Ok, it's a perpetual license and not a trial so check away
|
|
return k.MaintenanceExpiration < dtB;
|
|
}
|
|
#endregion
|
|
|
|
|
|
|
|
}//eoc
|
|
|
|
}//eons
|