mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-06 18:56:39 +00:00
add more resiliency to the license check
This commit is contained in:
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user