Files
raven/server/AyaNova/util/License.cs
2022-08-26 20:40:44 +00:00

1134 lines
44 KiB
C#

#define DEVELOPMENT_TEST_ROCKFISH
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/";
#if (DEVELOPMENT_TEST_ROCKFISH)
private const string LICENSE_SERVER_URL_ROCKFISH = "http://localhost:3001/";
#warning FYI DEVELOPMENT_TEST_ROCKFISH is defined
#else
private const string LICENSE_SERVER_URL_ROCKFISH = "https://rockfish.ayanova.com/";
#endif
//CRITICAL PROBLEM IF THIS IS TRUE
#if (DEVELOPMENT_TEST_ROCKFISH && !DEBUG)
#error ### HOLDUP: DEVELOPMENT_TEST_ROCKFISH is defined in a RELEASE BUILD!!!!
#endif
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/";
internal const string LICENSE_MISMATCH_TO_BUILD_ERROR = "E1020 - Not licensed for this version of AyaNova. Fix: revert to previous version used or contact technical support for options";
//Warning: magic string, do not change this, triggers special login procedures to fix license issue
internal const string SERVER_STATE_LOCKOUT_DUE_TO_LICENSE_EXCEEDED_CAPACITY_ERROR = "E1020 - Active count exceeded capacity";
//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";
//ActiveInternalUsers subscription license
private const string ACTIVE_INTERNAL_USERS_FEATURE_NAME = "ActiveInternalUsers";
//ActiveCustomerUsers subscription license
private const string ACTIVE_CUSTOMER_USERS_FEATURE_NAME = "ActiveCustomerUsers";
//This feature name means it's a trial key
private const string TRIAL_FEATURE_NAME = "TrialMode";
//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 matching license feature or license is missing</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 ActiveTechsCount
{
get
{
return GetLicenseFeature(SERVICE_TECHS_FEATURE_NAME)?.Count ?? 0;
}
}
public long ActiveInternalUsersCount
{
get
{
return GetLicenseFeature(ACTIVE_INTERNAL_USERS_FEATURE_NAME)?.Count ?? 0;
}
}
public long ActiveCustomerContactUsersCount
{
get
{
return GetLicenseFeature(ACTIVE_CUSTOMER_USERS_FEATURE_NAME)?.Count ?? 0;
}
}
/// <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 string LicenseFormat { get; set; }
public string Id { get; set; }
public string RegisteredTo { get; set; }
public string DbId { get; set; }
public bool Perpetual { 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.Perpetual ? "Perpetual" : "Subscription")}");
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)
{
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.Perpetual ? "perpetual" : "subscription")}, ");
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)
{
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,
perpetual = ActiveKey.Perpetual,
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
},
serverVersion = AyaNovaVersion.FullNameAndVersion,
#if (SUBSCRIPTION_BUILD)
build = "Subscription",
#else
build = "Perpetual",
#endif
activeTechUserCount = AyaNova.Biz.UserBiz.ActiveTechUserCountAsync().Result,
activeCustomerContactUserCount = AyaNova.Biz.UserBiz.ActiveCustomerContactUserCountAsync().Result,
activeInternalUserCount = AyaNova.Biz.UserBiz.ActiveInternalUserCountAsync().Result
}
});
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;
#if (SUBSCRIPTION_BUILD)
trialRequest.Perpetual=false;
#else
trialRequest.Perpetual = true;
#endif
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)
{
#if (SUBSCRIPTION_BUILD)
LicenseUrlParameter += "?dtt=true&pp=false";//signal to rockfish to provide a key immediately for dev testing
#else
LicenseUrlParameter += "?dtt=true&pp=true";//signal to rockfish to provide a key immediately for dev testing
#endif
}
#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>
/// Minimal no side effects check if ok to proceed with boot
/// and do schema updates etc, this is like a pre license initialize
/// called from startup.cs to ensure the safety of the db in case
/// the user has installed a version they are not entitled to
/// which would permanently change their db
/// </summary>
internal static void BootSafetyCheck(AyContext ct, ILogger log)
{
//this is necessary to accomodate checking build date against subscription or perpetual / subscription vs build type and preventing schema update if they upgrade but are not entitled to
//so they don't damage their database saving us having to walk them through a potentially flawed restore
//if there is a build date issue or a license type mismatch issue it will fail with an exception, log to log file, log to console and not go beyond the license check preserving the db
//Note: case 4160 is to build an external license fetcher utility to allow a user to upgrade without uninstalling the newer version by purchasing a new sub and installing the key out of AyaNova
//If they don't want to purchase then they must downgrade HOWEVER they can do the case 4170 thing with the flag AYANOVA_REMOVE_LICENSE_FROM_DB
//verify the build date and version match this build
log.LogDebug("Boot database safety check");
//NOTE: a completely missing db will trigger an exception on this line, we expect that and will be fine as the caller in startup will understand this scenario
//and proceed with the first boot schema update
var schema = ct.SchemaVersion.AsNoTracking().SingleOrDefault();
var ldb = ct.License.AsNoTracking().SingleOrDefault();
if (schema == null || ldb == null || string.IsNullOrWhiteSpace(schema.Id))
return;//no key record at all or no schema, no need to prevent the normal boot up
//is there an actual license in the key?
if (ldb.Key == "none")
return;//nope, let it do it's thing and schema update if necessary
//we have a schema and a license, check it now for build date vs maintenance expiry check
//parse will try to parse the key and will check the build type and maint date so if it bombs here it will throw and startup.cs will properly understand that
ServerDbId = schema.Id;
Parse(ldb.Key, log);
// AyaNova.Core.License.InitializeAsync(apiServerState, dbContext, _newLog).Wait();
}
/// <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 || 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 (SUBSCRIPTION_BUILD)
if (await AyaNova.Biz.UserBiz.ActiveInternalUserCountAsync() > _ActiveLicense.ActiveInternalUsersCount)
{
var msg = $"{SERVER_STATE_LOCKOUT_DUE_TO_LICENSE_EXCEEDED_CAPACITY_ERROR} (internal staff User count)";
apiServerState.SetSystemLock(msg);
log.LogCritical(msg);
return;
}
if (await AyaNova.Biz.UserBiz.ActiveCustomerContactUserCountAsync() > _ActiveLicense.ActiveCustomerContactUsersCount)
{
var msg = $"{SERVER_STATE_LOCKOUT_DUE_TO_LICENSE_EXCEEDED_CAPACITY_ERROR} (Customer Contact User count)";
apiServerState.SetSystemLock(msg);
log.LogCritical(msg);
return;
}
#else
if (await AyaNova.Biz.UserBiz.ActiveTechUserCountAsync() > _ActiveLicense.ActiveTechsCount)
{
apiServerState.SetSystemLock(SERVER_STATE_LOCKOUT_DUE_TO_LICENSE_EXCEEDED_CAPACITY_ERROR);
log.LogCritical(SERVER_STATE_LOCKOUT_DUE_TO_LICENSE_EXCEEDED_CAPACITY_ERROR);
return;
}
#endif
//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.");
}
#if (SUBSCRIPTION_BUILD)
//SUBSCRIPTION USER COUNTS - new license causes exceeding counts?
long NewInsideUserLicensedCount = ParsedNewKey.ActiveInternalUsersCount;
long ExistingActiveInsideUserCount = await AyaNova.Biz.UserBiz.ActiveInternalUserCountAsync();
long NewCustomerLicensedCount = ParsedNewKey.ActiveCustomerContactUsersCount;
long ExistingCustomerUserCount = await AyaNova.Biz.UserBiz.ActiveCustomerContactUserCountAsync();
string err = "E1020 - Can't install license: ";
bool throwit = false;
if (ExistingActiveInsideUserCount > NewInsideUserLicensedCount)
{
throwit = true;
err += $"{ExistingActiveInsideUserCount} active internal users of {NewInsideUserLicensedCount} permitted";
}
if (ExistingCustomerUserCount > NewCustomerLicensedCount)
{
throwit = true;
err += $"{ExistingCustomerUserCount} active Customer login users of {NewCustomerLicensedCount} permitted";
}
if (throwit)
throw new ApplicationException(err);
#else
//PERPETUAL, vet the TECHCOUNT - new license causes exceeding count?
long NewTechCount = ParsedNewKey.ActiveTechsCount;
if (await AyaNova.Biz.UserBiz.ActiveTechUserCountAsync() > 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.ActiveTechUserCountAsync() > NewTechCount)
throw new ApplicationException("E1020 - Can't install key, too many active techs and / or subcontractors in database. Deactivate enough to install key.");
}
#endif
//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("Parsing and 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 (token.SelectToken("Key.Perpetual") != null)
key.Perpetual = (bool)token.SelectToken("Key.Perpetual");
else
key.Perpetual = true;
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(LICENSE_MISMATCH_TO_BUILD_ERROR);
//NOTE: LICENSE_MISMATCH_TO_BUILD_ERROR matched for in startup.cs DO NOT change this without fixing the side effects
throw new ApplicationException(LICENSE_MISMATCH_TO_BUILD_ERROR);
}
//Subscription licenses only for subscription builds and Perpetual licenses only for Perpetual builds
#if (SUBSCRIPTION_BUILD)
if(key.Perpetual){
Console.WriteLine(LICENSE_MISMATCH_TO_BUILD_ERROR);
//NOTE: LICENSE_MISMATCH_TO_BUILD_ERROR bit below is checked for in startup.cs DO NOT remove this without fixing the side effects
throw new ApplicationException(LICENSE_MISMATCH_TO_BUILD_ERROR);
}
#else
if (!key.Perpetual)
{
Console.WriteLine(LICENSE_MISMATCH_TO_BUILD_ERROR);
//NOTE: LICENSE_MISMATCH_TO_BUILD_ERROR bit below is checked for in startup.cs DO NOT remove this without fixing the side effects
throw new ApplicationException(LICENSE_MISMATCH_TO_BUILD_ERROR);
}
#endif
//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 subscription licenses, only we can upgrade them adn they are always valid to be upgraded
if (!k.Perpetual) return false;
//Ok, it's a perpetual license and not a trial so check away
return k.MaintenanceExpiration < dtB;
}
#endregion
}//eoc
}//eons