Files
raven/server/AyaNova/util/License.cs
2020-01-28 19:19:50 +00:00

668 lines
24 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;
namespace AyaNova.Core
{
internal static class License
{
//License server address
private const string LICENSE_SERVER_URL = "https://rockfish.ayanova.com/";
// private const string LICENSE_SERVER_URL = "http://localhost:5000/";
//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 = "ServiceMode";
//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 one and only DBID
private static Guid DbId { 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 AyaNovaLicenseKey()
{
Features = new List<LicenseFeature>();
RegisteredTo = "UNLICENSED";
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 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 Guid 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"": ""2018"",
// ""Id"": ""34-1516288681"", <----Customer id followed by key serial id
// ""RegisteredTo"": ""Super TestCo"",
// ""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"": ""ServiceMode"" <----Means it's an SAAS/Rental key
// }
// ]
// }
// }
// }
// KEY]
// [SIGNATURE
// HEcY3JCVwK9HFXEFnldUEPXP4Q7xoZfMZfOfx1cYmfVF3MVWePyZ9dqVZcY7pk3RmR1BbhQdhpljsYLl+ZLTRhNa54M0EFa/bQnBnbwYZ70EQl8fz8WOczYTEBo7Sm5EyC6gSHtYZu7yRwBvhQzpeMGth5uWnlfPb0dMm0DQM7PaqhdWWW9GCSOdZmFcxkFQ8ERLDZhVMbd8PJKyLvZ+sGMrmYTAIoL0tqa7nrxYkM71uJRTAmQ0gEl4bJdxiV825U1J+buNQuTZdacZKEPSjQQkYou10jRbReUmP2vDpvu+nA1xdJe4b5LlyQL+jiIXH17lf93xlCUb0UkDpu8iNQ==
// SIGNATURE]\";
#endregion
#region Exposed properties
/// <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");
sb.AppendLine($"DB ID: {DbId}");
}
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($"DB ID: {DbId}");
sb.AppendLine($"Type: {(ActiveKey.RentalLicense ? "Service" : "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 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
{
licensedTo = ActiveKey.RegisteredTo,
dbId = ActiveKey.DbId,
keySerial = ActiveKey.Id,
licenseExpiration = ActiveKey.LicenseExpiration,
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 Trial license request handling
/// <summary>
/// Request a key
/// </summary>
/// <returns>Result string</returns>
internal static async Task<string> RequestTrialAsync(string email, string regto, ILogger log)
{
//BEFORE_RELEASE: DO THE FOLLOWING BEFORE RELEASE:
//TODO: TESTING REMOVE BEFORE RELEASE
//for test purposes if this route is hit and this code executed then the dbid is temporarily changed to the special trial request
//dbid so I can test remotely without hassle
//TO USE: just hit the trial key request route once then the license fetch route and it should be easy peasy
log.LogCritical("WARNING License::RequestTrial - DEVELOPMENT TEST FORCING TRIAL DB KEY ID. UPDATE BEFORE RELEASE!!");
DbId = TEST_TRIAL_KEY_DBID;
//TESTING
Microsoft.AspNetCore.Http.Extensions.QueryBuilder q = new Microsoft.AspNetCore.Http.Extensions.QueryBuilder();
q.Add("dbid", DbId.ToString());
q.Add("email", email);
q.Add("regto", regto);
log.LogDebug($"Requesting trial license for DBID {DbId.ToString()}");
string sUrl = $"{LICENSE_SERVER_URL}rvr" + q.ToQueryString();
try
{
//var res = await _Client.GetStringAsync(sUrl);
var res = await ServiceProviderProvider.HttpClientFactory.CreateClient().GetStringAsync(sUrl);
return res;
}
catch (Exception ex)
{
var msg = "E1020 - Error requesting trial license key see log for details";
log.LogError(ex, msg);
return msg;
// throw new ApplicationException(msg, ex);
}
}
#endregion trial license request handling
#region License fetching and handling
/// <summary>
/// Fetch a key, validate it and install it in the db then initialize with it
/// </summary>
/// <returns>Result string</returns>
internal static async Task FetchKeyAsync(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ct, ILogger log)
{
log.LogDebug($"Fetching license for DBID {DbId.ToString()}");
string sUrl = $"{LICENSE_SERVER_URL}rvf/{DbId.ToString()}";
#if (DEBUG)
log.LogInformation("DEBUG MODE TRIAL LICENSE KEY BEING FETCHED");
sUrl = $"{LICENSE_SERVER_URL}rvf/{TEST_TRIAL_KEY_DBID.ToString()}";
#endif
try
{
//string RawTextKeyFromRockfish = await _Client.GetStringAsync(sUrl);
string RawTextKeyFromRockfish = await ServiceProviderProvider.HttpClientFactory.CreateClient().GetStringAsync(sUrl);
//FUTURE: if there is any kind of error response or REASON or LicenseFetchStatus then here is
//where to deal with it
AyaNovaLicenseKey ParsedKey = Parse(RawTextKeyFromRockfish, log);
if (ParsedKey != null)
{
await InstallAsync(RawTextKeyFromRockfish, ParsedKey, apiServerState, ct, log);
}
}
catch (Exception ex)
{
var msg = "E1020 - Error fetching license key";
log.LogError(ex, msg);
throw new ApplicationException(msg, ex);
}
}
/// <summary>
/// Initialize the key
/// Handle if first boot scenario to tag DB ID etc
/// </summary>
internal static async Task InitializeAsync(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ct, ILogger log)
{
log.LogDebug("Initializing license");
try
{
//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 = Guid.NewGuid();
ldb.Key = "none";
ct.License.Add(ldb);
await ct.SaveChangesAsync();
}
//ensure DB ID
if (ldb.DbId == Guid.Empty)
{
ldb.DbId = Guid.NewGuid();
//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
DbId = ldb.DbId;
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;
}
//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(x => x.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.");
}
//TODO: TECHCOUNT - new license causes exceeding count?
if (await AyaNova.Biz.UserBiz.ActiveCountAsync() > ParsedNewKey.GetLicenseFeature(SERVICE_TECHS_FEATURE_NAME).Count)
{
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;
//LOOKAT: reason, resultcode etc
//There is similar block related to this in ayschema for db schema version 8
ct.SaveChanges();
}
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();
#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 != "2018")
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 = (Guid)token.SelectToken("Key.DBID");
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 x = 0; x < p.Count; x++)
{
LicenseFeature lf = new LicenseFeature();
lf.Feature = (string)p[x].SelectToken("Name");
if (p[x].SelectToken("Count") != null)
{
lf.Count = (long)p[x].SelectToken("Count");
}
else
{
lf.Count = 0;
}
key.Features.Add(lf);
}
#endregion get values
//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
}//eoc
}//eons