devextreme
Version:
JavaScript/TypeScript Component Suite for Responsive Web Development
301 lines (256 loc) • 9.43 kB
JavaScript
const { MESSAGES } = require('./messages');
const LCX_SIGNATURE = 'LCXv1';
const LCP_SIGNATURE = 'LCPv1';
const SIGN_LENGTH = 68 * 2; // 136 chars
const ENCODE_MAP_STR =
'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F' +
'\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F' +
'\x20x\x220]qA\'u`U?.wOCLyJnz@$*DmsMhlW/T)dKHQ+jNEa6G:VZk9!p>%e7i3S5\\^=P&(Ic,2#rtgY<R_bX-;BfFv[841o{|}~\x7F';
const DECODE_MAP_STR =
'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F' +
'\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F' +
'\x20R\x22f6U`\'aA7Fdp,?#yeYx[KWwQMqk^T+5&r/8ItLDb2C0;H._ElZ@*N>ojOv\\$]m)JncBVsi<XGP=93zS%g:h(u-!14{|}~\x7F';
const ENCODE_MAP = Array.from(ENCODE_MAP_STR);
const DECODE_MAP = Array.from(DECODE_MAP_STR);
function mapString(text, mapArr) {
if(!text) return text;
let out = '';
for(let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
out += code < mapArr.length ? mapArr[code] : text[i];
}
return out;
}
function encode(text) {
return mapString(text, ENCODE_MAP);
}
function decode(text) {
return mapString(text, DECODE_MAP);
}
function assertNonEmptyString(value, name) {
if(typeof value !== 'string' || value.trim().length === 0) {
throw new Error(`${name} must be a non-empty string`);
}
}
function readLine(state) {
const s = state.s;
const start = state.pos;
const idx = s.indexOf('\n', start);
if(idx === -1) {
state.pos = s.length;
const line = s.slice(start);
return line.endsWith('\r') ? line.slice(0, -1) : line;
}
state.pos = idx + 1;
const line = s.slice(start, idx);
return line.endsWith('\r') ? line.slice(0, -1) : line;
}
function readInt(state) {
const line = readLine(state);
const n = parseInt(line, 10);
if(!Number.isFinite(n) || n < 0) {
throw new Error('Invalid license data');
}
return n;
}
function readString(state, fixedLength) {
const len = typeof fixedLength === 'number' ? fixedLength : readInt(state);
const start = state.pos;
const end = start + len;
if(end > state.s.length) {
throw new Error('Invalid license data');
}
state.pos = end;
return state.s.slice(start, end);
}
function safeBase64ToUtf8(b64) {
try {
return Buffer.from(b64, 'base64').toString('utf8');
} catch{
throw new Error('Invalid license data');
}
}
function convertLCXtoLCP(licenseString) {
assertNonEmptyString(licenseString, 'licenseString');
const input = licenseString.trim();
if(!input.startsWith(LCX_SIGNATURE)) {
throw new Error('Unsupported license format');
}
const base64Part = input.slice(LCX_SIGNATURE.length);
const lcx = safeBase64ToUtf8(base64Part);
if(lcx.length < SIGN_LENGTH) {
throw new Error('Invalid license data');
}
const lcxData = decode(lcx.slice(SIGN_LENGTH));
const state = { s: lcxData, pos: 0 };
const signProducts = readString(state, SIGN_LENGTH);
void readString(state);
const productsString = readString(state);
const payloadText = signProducts + productsString;
const payloadB64 = Buffer.from(payloadText, 'utf8').toString('base64');
const encoded = encode(payloadB64);
return LCP_SIGNATURE + encoded;
}
function tryConvertLCXtoLCP(licenseString) {
try {
return convertLCXtoLCP(licenseString);
} catch{
return null;
}
}
const DEVEXTREME_HTMLJS_BIT = 1n << 54n; // ProductKind.DevExtremeHtmlJs from types.ts
const DOTNET_TICKS_EPOCH_OFFSET = 621355968000000000n;
const DOTNET_TICKS_PER_MS = 10000n;
const DOTNET_MAX_VALUE_TICKS = 3155378975999999999n;
function dotnetTicksToMs(ticksStr) {
const ticks = BigInt(ticksStr);
if(ticks >= DOTNET_MAX_VALUE_TICKS) return Infinity;
return Number((ticks - DOTNET_TICKS_EPOCH_OFFSET) / DOTNET_TICKS_PER_MS);
}
const TokenKind = Object.freeze({
corrupted: 'corrupted',
verified: 'verified',
internal: 'internal',
});
const GENERAL_ERROR = { kind: TokenKind.corrupted, error: 'general' };
const DESERIALIZATION_ERROR = { kind: TokenKind.corrupted, error: 'deserialization' };
const PRODUCT_KIND_ERROR = { kind: TokenKind.corrupted, error: 'product-kind' };
const TRIAL_EXPIRED_ERROR = { kind: TokenKind.corrupted, error: 'trial-expired' };
function readDevExtremeVersion() {
try {
const pkgPath = require('path').join(__dirname, '..', 'package.json');
const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8'));
const parts = String(pkg.version || '').split('.');
const major = parseInt(parts[0], 10);
const minor = parseInt(parts[1], 10);
if(!isNaN(major) && !isNaN(minor)) {
return { major, minor, code: major * 10 + minor };
}
} catch{}
return null;
}
function buildVersionString(devExtremeVersion){
const { major, minor, code: currentCode } = devExtremeVersion;
return `${major}.${minor}`;
}
function productsFromString(encodedString) {
if(!encodedString) {
return { products: [], errorToken: GENERAL_ERROR };
}
try {
const splitInfo = encodedString.split(';');
const licenseId = splitInfo[0];
const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0);
const products = productTuples.map(tuple => {
const parts = tuple.split(',');
const version = Number.parseInt(parts[0], 10);
const products = BigInt(parts[1]);
const expiration = parts.length > 3 ? dotnetTicksToMs(parts[3]) : Infinity;
return { version, products, expiration };
});
return { products, licenseId };
} catch{
return { products: [], errorToken: DESERIALIZATION_ERROR };
}
}
function getMaxExpiration(products) {
const expirations = products
.map(p => p.expiration)
.filter(e => e > 0 && e !== Infinity);
if(expirations.length === 0) return Infinity;
return Math.max(...expirations);
}
function findLatestDevExtremeVersion(products) {
if(!Array.isArray(products) || products.length === 0) return undefined;
const sorted = [...products].sort((a, b) => b.version - a.version);
const match = sorted.find(p => (p.products & DEVEXTREME_HTMLJS_BIT) === DEVEXTREME_HTMLJS_BIT);
return match?.version;
}
function parseLCP(lcpString) {
if(typeof lcpString !== 'string' || !lcpString.startsWith(LCP_SIGNATURE)) {
return GENERAL_ERROR;
}
try {
const b64 = decode(lcpString.slice(LCP_SIGNATURE.length));
const decoded = Buffer.from(b64, 'base64').toString('binary');
if(decoded.length < SIGN_LENGTH) {
return GENERAL_ERROR;
}
const productsPayload = decoded.slice(SIGN_LENGTH);
const decodedPayload = mapString(productsPayload, DECODE_MAP);
const { products, errorToken, licenseId } = productsFromString(decodedPayload);
if(errorToken) {
return { ...errorToken, licenseId };
}
const maxVersionAllowed = findLatestDevExtremeVersion(products);
if(!maxVersionAllowed) {
const maxExpiration = getMaxExpiration(products);
if(maxExpiration !== Infinity && maxExpiration < Date.now()) {
return { ...TRIAL_EXPIRED_ERROR, licenseId };
}
}
if(!maxVersionAllowed) {
return { ...PRODUCT_KIND_ERROR, licenseId };
}
return {
kind: TokenKind.verified,
payload: { customerId: '', maxVersionAllowed, licenseId },
};
} catch{
return GENERAL_ERROR;
}
}
function formatVersionCode(versionCode) {
return `v${Math.floor(versionCode / 10)}.${versionCode % 10}`;
}
function getLCPInfo(lcpString) {
const token = parseLCP(lcpString);
let warning = null;
let licenseId = null;
let currentVersion = '';
if(token.kind === TokenKind.corrupted) {
licenseId = token.licenseId || null;
switch(token.error) {
case 'general':
warning = { type: 'general' };
break;
case 'deserialization':
warning = { type: 'corrupted' };
break;
case 'product-kind':
warning = { type: 'trial' };
break;
case 'trial-expired':
warning = { type: 'trialExpired' };
break;
}
} else {
licenseId = token.payload.licenseId || null;
const devExtremeVersion = readDevExtremeVersion();
if(devExtremeVersion) {
currentVersion = buildVersionString(devExtremeVersion);
const { maxVersionAllowed } = token.payload;
if(maxVersionAllowed < devExtremeVersion.code) {
warning = {
type:'incompatibleVersion',
keyVersion: formatVersionCode(maxVersionAllowed),
currentVersion
};
}
}
}
return { warning, licenseId, currentVersion };
}
function getLCPWarning(lcpString) {
return getLCPInfo(lcpString).warning;
}
module.exports = {
convertLCXtoLCP,
tryConvertLCXtoLCP,
parseLCP,
getLCPInfo,
getLCPWarning,
TokenKind,
LCX_SIGNATURE,
LCP_SIGNATURE,
};