add more resiliency to the license check

This commit is contained in:
miloschwartz
2025-12-09 11:25:52 -05:00
parent f9b03943c3
commit 6453b070bb

View File

@@ -64,11 +64,14 @@ export class License {
private validationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/validate`; private validationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/validate`;
private activationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/activate`; private activationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/activate`;
private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval }); private statusCache = new NodeCache();
private licenseKeyCache = new NodeCache(); private licenseKeyCache = new NodeCache();
private statusKey = "status"; private statusKey = "status";
private serverSecret!: string; private serverSecret!: string;
private phoneHomeFailureCount = 0;
private checkInProgress = false;
private doRecheck = false;
private publicKey = `-----BEGIN PUBLIC KEY----- private publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF
@@ -83,9 +86,11 @@ LQIDAQAB
constructor(private hostMeta: HostMeta) { constructor(private hostMeta: HostMeta) {
setInterval( setInterval(
async () => { async () => {
this.doRecheck = true;
await this.check(); await this.check();
this.doRecheck = false;
}, },
1000 * 60 * 60 1000 * this.phoneHomeInterval
); );
} }
@@ -103,6 +108,7 @@ LQIDAQAB
public async forceRecheck() { public async forceRecheck() {
this.statusCache.flushAll(); this.statusCache.flushAll();
this.licenseKeyCache.flushAll(); this.licenseKeyCache.flushAll();
this.phoneHomeFailureCount = 0;
return await this.check(); return await this.check();
} }
@@ -118,24 +124,49 @@ LQIDAQAB
} }
public async check(): Promise<LicenseStatus> { public async check(): Promise<LicenseStatus> {
// If a check is already in progress, return the last known status
if (this.checkInProgress) {
logger.debug(
"License check already in progress, returning last known status"
);
const lastStatus = this.statusCache.get(this.statusKey) as
| LicenseStatus
| undefined;
if (lastStatus) {
return lastStatus;
}
// If no cached status exists, return default status
return {
hostId: this.hostMeta.hostMetaId,
isHostLicensed: true,
isLicenseValid: false
};
}
const status: LicenseStatus = { const status: LicenseStatus = {
hostId: this.hostMeta.hostMetaId, hostId: this.hostMeta.hostMetaId,
isHostLicensed: true, isHostLicensed: true,
isLicenseValid: false isLicenseValid: false
}; };
this.checkInProgress = true;
try { try {
if (this.statusCache.has(this.statusKey)) { if (!this.doRecheck && this.statusCache.has(this.statusKey)) {
const res = this.statusCache.get("status") as LicenseStatus; const res = this.statusCache.get("status") as LicenseStatus;
return res; return res;
} }
// Invalidate all logger.debug("Checking license status...");
this.licenseKeyCache.flushAll(); // Build new cache in temporary Map before invalidating old cache
const newCache = new Map<string, LicenseKeyCache>();
const allKeysRes = await db.select().from(licenseKey); const allKeysRes = await db.select().from(licenseKey);
if (allKeysRes.length === 0) { if (allKeysRes.length === 0) {
status.isHostLicensed = false; status.isHostLicensed = false;
// Invalidate all and set new cache (empty)
this.licenseKeyCache.flushAll();
this.statusCache.set(this.statusKey, status);
return status; return status;
} }
@@ -158,7 +189,7 @@ LQIDAQAB
this.publicKey this.publicKey
); );
this.licenseKeyCache.set<LicenseKeyCache>(decryptedKey, { newCache.set(decryptedKey, {
licenseKey: decryptedKey, licenseKey: decryptedKey,
licenseKeyEncrypted: key.licenseKeyId, licenseKeyEncrypted: key.licenseKeyId,
valid: payload.valid, valid: payload.valid,
@@ -177,14 +208,11 @@ LQIDAQAB
); );
logger.error(e); logger.error(e);
this.licenseKeyCache.set<LicenseKeyCache>( newCache.set(key.licenseKeyId, {
key.licenseKeyId, licenseKey: key.licenseKeyId,
{ licenseKeyEncrypted: key.licenseKeyId,
licenseKey: key.licenseKeyId, valid: false
licenseKeyEncrypted: key.licenseKeyId, });
valid: false
}
);
} }
} }
@@ -206,17 +234,29 @@ LQIDAQAB
if (!apiResponse?.success) { if (!apiResponse?.success) {
throw new Error(apiResponse?.error); throw new Error(apiResponse?.error);
} }
// Reset failure count on success
this.phoneHomeFailureCount = 0;
} catch (e) { } catch (e) {
logger.error("Error communicating with license server:"); this.phoneHomeFailureCount++;
logger.error(e); if (this.phoneHomeFailureCount === 1) {
// First failure: fail silently
logger.error("Error communicating with license server:");
logger.error(e);
logger.error(`Allowing failure. Will retry one more time at next run interval.`);
// return last known good status
return this.statusCache.get(
this.statusKey
) as LicenseStatus;
} else {
// Subsequent failures: fail abruptly
throw e;
}
} }
// Check and update all license keys with server response // Check and update all license keys with server response
for (const key of keys) { for (const key of keys) {
try { try {
const cached = this.licenseKeyCache.get<LicenseKeyCache>( const cached = newCache.get(key.licenseKey)!;
key.licenseKey
)!;
const licenseKeyRes = const licenseKeyRes =
apiResponse?.data?.licenseKeys[key.licenseKey]; apiResponse?.data?.licenseKeys[key.licenseKey];
@@ -240,10 +280,7 @@ LQIDAQAB
`Can't trust license key: ${key.licenseKey}` `Can't trust license key: ${key.licenseKey}`
); );
cached.valid = false; cached.valid = false;
this.licenseKeyCache.set<LicenseKeyCache>( newCache.set(key.licenseKey, cached);
key.licenseKey,
cached
);
continue; continue;
} }
@@ -274,10 +311,7 @@ LQIDAQAB
}) })
.where(eq(licenseKey.licenseKeyId, encryptedKey)); .where(eq(licenseKey.licenseKeyId, encryptedKey));
this.licenseKeyCache.set<LicenseKeyCache>( newCache.set(key.licenseKey, cached);
key.licenseKey,
cached
);
} catch (e) { } catch (e) {
logger.error(`Error validating license key: ${key}`); logger.error(`Error validating license key: ${key}`);
logger.error(e); logger.error(e);
@@ -286,9 +320,7 @@ LQIDAQAB
// Compute host status // Compute host status
for (const key of keys) { for (const key of keys) {
const cached = this.licenseKeyCache.get<LicenseKeyCache>( const cached = newCache.get(key.licenseKey)!;
key.licenseKey
)!;
if (cached.type === "host") { if (cached.type === "host") {
status.isLicenseValid = cached.valid; status.isLicenseValid = cached.valid;
@@ -299,9 +331,17 @@ LQIDAQAB
continue; continue;
} }
} }
// Invalidate old cache and set new cache
this.licenseKeyCache.flushAll();
for (const [key, value] of newCache.entries()) {
this.licenseKeyCache.set<LicenseKeyCache>(key, value);
}
} catch (error) { } catch (error) {
logger.error("Error checking license status:"); logger.error("Error checking license status:");
logger.error(error); logger.error(error);
} finally {
this.checkInProgress = false;
} }
this.statusCache.set(this.statusKey, status); this.statusCache.set(this.statusKey, status);
@@ -430,20 +470,58 @@ LQIDAQAB
: key.instanceId : key.instanceId
})); }));
const response = await fetch(this.validationServerUrl, { const maxAttempts = 10;
method: "POST", const initialRetryDelay = 1 * 1000; // 1 seconds
headers: { const exponentialFactor = 1.2;
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseKeys: decryptedKeys,
instanceName: this.hostMeta.hostMetaId
})
});
const data = await response.json(); let lastError: Error | undefined;
return data as ValidateLicenseAPIResponse; for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await fetch(this.validationServerUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseKeys: decryptedKeys,
instanceName: this.hostMeta.hostMetaId
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data as ValidateLicenseAPIResponse;
} catch (error) {
lastError =
error instanceof Error ? error : new Error(String(error));
if (attempt < maxAttempts) {
// Calculate exponential backoff delay
const retryDelay = Math.floor(
initialRetryDelay *
Math.pow(exponentialFactor, attempt - 1)
);
logger.debug(
`License validation request failed (attempt ${attempt}/${maxAttempts}), retrying in ${retryDelay} ms...`
);
await new Promise((resolve) =>
setTimeout(resolve, retryDelay)
);
} else {
logger.error(
`License validation request failed after ${maxAttempts} attempts`
);
throw lastError;
}
}
}
throw lastError || new Error("License validation request failed");
} }
} }