Skip to content

Commit

Permalink
Add license checking functionality (#25510)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Bulychev <[email protected]>
Co-authored-by: Alexander Bulychev <[email protected]>
Co-authored-by: ilya.kharchenko <[email protected]>
Co-authored-by: ksercs <[email protected]>
Co-authored-by: Ilya Vinogradov <[email protected]>
  • Loading branch information
5 people authored Oct 5, 2023
1 parent 07346b8 commit b40991a
Show file tree
Hide file tree
Showing 14 changed files with 261 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ts_declarations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
if ! diff $target.current $target -U 5 > $target.diff; then
echo "FAIL: $target is outdated:"
cat $target.diff | sed "1,2d"
echo "Execute 'npm run update-ts' to update dx.all.d.ts"
echo "Execute 'npm run regenerate-all' to update dx.all.d.ts"
exit 1
else
echo "TS is up-to-date"
Expand Down
37 changes: 7 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/devextreme/artifacts/npm/devextreme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"inferno-hydrate": "^7.4.9",
"jszip": "^3.7.1",
"rrule": "^2.7.1",
"sha1": "1.1.1",
"sha-1": "1.0.0",
"turndown": "~7.1.0"
},
"jest": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { parseToken } from './license_validation';
import errors from '@js/core/errors';

import {
parseLicenseKey,
setLicenseCheckSkipCondition,
verifyLicense,
} from './license_validation';

describe('license token', () => {
it.each([
Expand All @@ -22,7 +28,7 @@ describe('license token', () => {
},
},
])('verifies and decodes payload [%#]', ({ token, payload: expected }) => {
const license = parseToken(token);
const license = parseLicenseKey(token);

expect(license.kind).toBe('verified');
if (license.kind === 'verified') {
Expand All @@ -31,7 +37,7 @@ describe('license token', () => {
});

it('verifies and decodes payload with extra fields', () => {
const license = parseToken('ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxLAogICJleHRyYUZpZWxkIjogIkE5OTk5OTkiCn0=.fqm8mVhQ9+x/R7E7MVwUP3nJaYL3KldhYffVXdDqPVyHIQi66Z2XZ2RdygH4J0jvUpjhZ6yzmGPV0J0WoPbKyhtnY4ELhove/IAwpn8WGfRw3wLSxfR+RWuaKcw2yvlUA1JqrQUrIrN23UwNQodbJ/hGm30s0h1bf8zCvQ/d31k=');
const license = parseLicenseKey('ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxLAogICJleHRyYUZpZWxkIjogIkE5OTk5OTkiCn0=.fqm8mVhQ9+x/R7E7MVwUP3nJaYL3KldhYffVXdDqPVyHIQi66Z2XZ2RdygH4J0jvUpjhZ6yzmGPV0J0WoPbKyhtnY4ELhove/IAwpn8WGfRw3wLSxfR+RWuaKcw2yvlUA1JqrQUrIrN23UwNQodbJ/hGm30s0h1bf8zCvQ/d31k=');

expect(license.kind).toBe('verified');
if (license.kind === 'verified') {
Expand All @@ -44,7 +50,7 @@ describe('license token', () => {
});

it('fails if payload is not verified', () => {
const license = parseToken('ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.NVsilC5uWlD5QGS6bocLMlsVVK0VpZXYwU2DstUiLRpEI79/onuR8dGWasCLBo4PORDHPkNA/Ej8XeCHzJ0EkXRRZ7E2LrP/xlEfHRXTruvW4IEbZt3LiwJBt6/isLz+wzXtYtjV7tpE07/Y0TFoy+mWpHoU11GVtwKh6weRxkg=');
const license = parseLicenseKey('ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.NVsilC5uWlD5QGS6bocLMlsVVK0VpZXYwU2DstUiLRpEI79/onuR8dGWasCLBo4PORDHPkNA/Ej8XeCHzJ0EkXRRZ7E2LrP/xlEfHRXTruvW4IEbZt3LiwJBt6/isLz+wzXtYtjV7tpE07/Y0TFoy+mWpHoU11GVtwKh6weRxkg=');

expect(license.kind).toBe('corrupted');

Expand All @@ -54,7 +60,7 @@ describe('license token', () => {
});

it('fails if payload is invalid JSON', () => {
const license = parseToken('YWJj.vjx6wAI9jVkHJAnKcsuYNZ5UvCq3UhypQ+0f+kZ37/Qc1uj4BM6//Kfi4SVsXGOaOTFYWgzesROnHCp3jZRqphJwal4yXHD1sGFi6FEdB4MgdgNZvsZSnxNWLs/7s07CzuHLTpJrAG7sTdHVkQWZNnSCKjzV7909c/Stl9+hkLo=');
const license = parseLicenseKey('YWJj.vjx6wAI9jVkHJAnKcsuYNZ5UvCq3UhypQ+0f+kZ37/Qc1uj4BM6//Kfi4SVsXGOaOTFYWgzesROnHCp3jZRqphJwal4yXHD1sGFi6FEdB4MgdgNZvsZSnxNWLs/7s07CzuHLTpJrAG7sTdHVkQWZNnSCKjzV7909c/Stl9+hkLo=');

expect(license.kind).toBe('corrupted');

Expand All @@ -64,7 +70,7 @@ describe('license token', () => {
});

it('fails if payload is invalid Base64', () => {
const license = parseToken('ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjM3Yjg4ZjBmLWQ0MmMtNDJiZS05YjhkLTU1ZGMwYzUzYzAxZiIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjIxCn0-.EnP/RDKg0eSyaPU1eDUFll1lqOdYbhN3u73LhN1op8vjNwA0P1vKiT1DfQRmXudlleGWgDkLA2OmJYUER8j7I3LSFf3hLkBAoWoBErgveTb2zkbz8P1i9lE+XmzIXeYHyZBYUt0IPkNfajF9zzbSDDin1CvW7pnADi0vIeZ5ICQ=');
const license = parseLicenseKey('ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjM3Yjg4ZjBmLWQ0MmMtNDJiZS05YjhkLTU1ZGMwYzUzYzAxZiIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjIxCn0-.EnP/RDKg0eSyaPU1eDUFll1lqOdYbhN3u73LhN1op8vjNwA0P1vKiT1DfQRmXudlleGWgDkLA2OmJYUER8j7I3LSFf3hLkBAoWoBErgveTb2zkbz8P1i9lE+XmzIXeYHyZBYUt0IPkNfajF9zzbSDDin1CvW7pnADi0vIeZ5ICQ=');

expect(license.kind).toBe('corrupted');

Expand All @@ -78,7 +84,7 @@ describe('license token', () => {
'ewogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.ok32DBaAgf3ijLmNQb+A0kUV2AiSivqvZJADdF607qqlAaduAVnotJtgdwm/Ib3MErfaGrDohCYoFMnKQevkRxFkA7tK3kOBnTZPUnZY0r3wyulMQmr4Qo+Sjf/fyXs4IYpGsC7/uJjgrCos8uzBegfmgfM93XSt6pKl9+c5xvc=',
'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIKfQ==.resgTqmazrorRNw7mmtV31XQnmTSw0uLEArsmpzCjWMQJLocBfAjpFvKBf+SAG9q+1iOSFySj64Uv2xBVqHnyeNVBRbouOKOnAB8RpkKvN4sc5SDc8JAG5TkwPVSzK/VLBpQxpqbxlcrRfHwz9gXqQoPt4/ZVATn285iw3DW0CU=',
])('fails if payload misses required fields [%#]', (token) => {
const license = parseToken(token);
const license = parseLicenseKey(token);

expect(license.kind).toBe('corrupted');

Expand All @@ -88,7 +94,7 @@ describe('license token', () => {
});

it('fails if payload has unsupported version', () => {
const license = parseToken('ewogICJmb3JtYXQiOiAyLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.tTBymZMROsYyMiP6ldXFqGurbzqjhSQIu/pjyEUJA3v/57VgToomYl7FVzBj1asgHpadvysyTUiX3nFvPxbp166L3+LB3Jybw9ueMnwePu5vQOO0krqKLBqRq+TqHKn7k76uYRbkCIo5UajNfzetHhlkin3dJf3x2K/fcwbPW5A=');
const license = parseLicenseKey('ewogICJmb3JtYXQiOiAyLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.tTBymZMROsYyMiP6ldXFqGurbzqjhSQIu/pjyEUJA3v/57VgToomYl7FVzBj1asgHpadvysyTUiX3nFvPxbp166L3+LB3Jybw9ueMnwePu5vQOO0krqKLBqRq+TqHKn7k76uYRbkCIo5UajNfzetHhlkin3dJf3x2K/fcwbPW5A=');

expect(license.kind).toBe('corrupted');

Expand All @@ -100,7 +106,7 @@ describe('license token', () => {
it.each([
'', '.', 'a', 'a.', '.a', '.a.', '.a.', '.a.b', 'a.b.', '.a.b.',
])('is not parsed from invalid input [%#]', (invalidInput) => {
const license = parseToken(invalidInput);
const license = parseLicenseKey(invalidInput);

expect(license.kind).toBe('corrupted');

Expand All @@ -109,3 +115,96 @@ describe('license token', () => {
}
});
});

describe('License check', () => {
const TOKEN_23_1 = 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.DiDceRbil4IzXl5av7pNkKieyqHHhRf+CM477zDu4N9fyrhkQsjRourYvgVfkbSm+EQplkXhlMBc3s8Vm9n+VtPaMbeWXis92cdW/6HiT+Dm54xw5vZ5POGunKRrNYUzd9zTbYcz0bYA/dc/mHFeUdXA0UlKcx1uMaXmtJrkK74=';
const TOKEN_23_2 = 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjYxMjFmMDIyLTFjMTItNDNjZC04YWE0LTkwNzJkNDU4YjYxNCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMyCn0=.RENyZ3Ga5rCB7/XNKYbk2Ffv1n9bUexYNhyOlqcAD02YVnPw6XyQcN+ZORScKDU9gOInJ4o7vPxkgh10KvMZNn+FuBK8UcUR7kchk7z0CHGuOcIn2jD5X2hG6SYJ0UCBG/JDG35AL09T7Uv/pGj4PolRsANxtuMpoqmvX2D2vkU=';
const TOKEN_UNVERIFIED = 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.NVsilC5uWlD5QGS6bocLMlsVVK0VpZXYwU2DstUiLRpEI79/onuR8dGWasCLBo4PORDHPkNA/Ej8XeCHzJ0EkXRRZ7E2LrP/xlEfHRXTruvW4IEbZt3LiwJBt6/isLz+wzXtYtjV7tpE07/Y0TFoy+mWpHoU11GVtwKh6weRxkg=';
const TOKEN_INVALID_JSON = 'YWJj.vjx6wAI9jVkHJAnKcsuYNZ5UvCq3UhypQ+0f+kZ37/Qc1uj4BM6//Kfi4SVsXGOaOTFYWgzesROnHCp3jZRqphJwal4yXHD1sGFi6FEdB4MgdgNZvsZSnxNWLs/7s07CzuHLTpJrAG7sTdHVkQWZNnSCKjzV7909c/Stl9+hkLo=';
const TOKEN_INVALID_BASE64 = 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjM3Yjg4ZjBmLWQ0MmMtNDJiZS05YjhkLTU1ZGMwYzUzYzAxZiIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjIxCn0-.EnP/RDKg0eSyaPU1eDUFll1lqOdYbhN3u73LhN1op8vjNwA0P1vKiT1DfQRmXudlleGWgDkLA2OmJYUER8j7I3LSFf3hLkBAoWoBErgveTb2zkbz8P1i9lE+XmzIXeYHyZBYUt0IPkNfajF9zzbSDDin1CvW7pnADi0vIeZ5ICQ=';
const TOKEN_MISSING_FIELD_1 = 'ewogICJmb3JtYXQiOiAxLAogICJtYXhWZXJzaW9uQWxsb3dlZCI6IDIzMQp9.WH30cajUFcKqw/fwt4jITM/5tzVwPpbdbezhhdBi5oeOvU06zKY0J4M8gQy8GQ++RPYVCAo2md6vI9D80FD2CC4w+hpQLJNJJgNUHYPrgG6CX1yAB3M+NKHsPP9S71bXAgwvignb5uPo0R5emQzr4RKDhWQMKtgqEcRe+yme2mU=';
const TOKEN_MISSING_FIELD_2 = 'ewogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.ok32DBaAgf3ijLmNQb+A0kUV2AiSivqvZJADdF607qqlAaduAVnotJtgdwm/Ib3MErfaGrDohCYoFMnKQevkRxFkA7tK3kOBnTZPUnZY0r3wyulMQmr4Qo+Sjf/fyXs4IYpGsC7/uJjgrCos8uzBegfmgfM93XSt6pKl9+c5xvc=';
const TOKEN_MISSING_FIELD_3 = 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIKfQ==.resgTqmazrorRNw7mmtV31XQnmTSw0uLEArsmpzCjWMQJLocBfAjpFvKBf+SAG9q+1iOSFySj64Uv2xBVqHnyeNVBRbouOKOnAB8RpkKvN4sc5SDc8JAG5TkwPVSzK/VLBpQxpqbxlcrRfHwz9gXqQoPt4/ZVATn285iw3DW0CU=';
const TOKEN_UNSUPPORTED_VERSION = 'ewogICJmb3JtYXQiOiAyLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.tTBymZMROsYyMiP6ldXFqGurbzqjhSQIu/pjyEUJA3v/57VgToomYl7FVzBj1asgHpadvysyTUiX3nFvPxbp166L3+LB3Jybw9ueMnwePu5vQOO0krqKLBqRq+TqHKn7k76uYRbkCIo5UajNfzetHhlkin3dJf3x2K/fcwbPW5A=';

beforeEach(() => {
jest.spyOn(errors, 'log').mockImplementation(() => {});
setLicenseCheckSkipCondition(false);
});

afterEach(() => {
jest.restoreAllMocks();
});

test('W0019 error should be logged if license is empty', () => {
[
['', '1.0'],
[null, '1.0'],
[undefined, '1.0'],
].forEach(([token, version], index) => {
verifyLicense(token as string, version as string);
expect(errors.log).toHaveBeenCalledTimes(index + 1);
expect(errors.log).toHaveBeenCalledWith('W0019');
setLicenseCheckSkipCondition(false);
});
});

test('No messages should be logged if license is valid', () => {
[
[TOKEN_23_1, '23.1'],
[TOKEN_23_1, '12.3'],
[TOKEN_23_2, '23.1'],
[TOKEN_23_2, '23.2'],
].forEach(([token, version]) => {
verifyLicense(token, version);
expect(errors.log).not.toHaveBeenCalled();
});
});

test('Message should be logged only once', () => {
verifyLicense('', '1.0');
verifyLicense('', '1.0');
verifyLicense('', '1.0');

expect(errors.log).toHaveBeenCalledTimes(1);
});

test('No messages should be logged if setLicenseCheckSkipCondition() used', () => {
setLicenseCheckSkipCondition();
verifyLicense('', '1.0');
expect(errors.log).not.toHaveBeenCalled();
});

test('W0020 error should be logged if license is outdated', () => {
[
[TOKEN_23_1, '23.2'],
[TOKEN_23_2, '42.4'],
].forEach(([token, version], index) => {
verifyLicense(token, version);
expect(errors.log).toHaveBeenCalledTimes(index + 1);
expect(errors.log).toHaveBeenCalledWith('W0020');
setLicenseCheckSkipCondition(false);
});
});

test('W0021 error should be logged if license is corrupted/invalid', () => {
[
[TOKEN_UNVERIFIED, '1.2.3'],
[TOKEN_INVALID_JSON, '1.2.3'],
[TOKEN_INVALID_BASE64, '1.2.3'],
[TOKEN_MISSING_FIELD_1, '1.2.3'],
[TOKEN_MISSING_FIELD_2, '1.2.3'],
[TOKEN_MISSING_FIELD_3, '1.2.3'],
[TOKEN_UNSUPPORTED_VERSION, '1.2.3'],
['Another', '1.2.3'],
['str@nge'],
['in.put'],
['3.2.1', '1.2.3'],
[TOKEN_23_1, '123'],
].forEach(([token, version]) => {
verifyLicense(token, version);
expect(errors.log).toHaveBeenCalledWith('W0021');
setLicenseCheckSkipCondition(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import errors from '@js/core/errors';
import { version as packageVersion } from '@js/core/version';

import { verify } from './rsa_pkcs1_sha1';

export interface License {
Expand All @@ -10,30 +13,38 @@ interface Payload extends Partial<License> {
readonly format?: number;
}

const enum TokenKind {
corrupted = 'corrupted',
verified = 'verified',
}

export type Token = {
readonly kind: 'verified';
readonly kind: TokenKind.verified;
readonly payload: License;
} | {
readonly kind: 'corrupted';
readonly kind: TokenKind.corrupted;
readonly error: 'general' | 'verification' | 'decoding' | 'deserialization' | 'payload' | 'version';
};
export type LicenseVerifyResult = 'W0019' | 'W0020' | 'W0021' | null;

const SPLITTER = '.';
const FORMAT = 1;

const GENERAL_ERROR: Token = { kind: 'corrupted', error: 'general' };
const VERIFICATION_ERROR: Token = { kind: 'corrupted', error: 'verification' };
const DECODING_ERROR: Token = { kind: 'corrupted', error: 'decoding' };
const DESERIALIZATION_ERROR: Token = { kind: 'corrupted', error: 'deserialization' };
const PAYLOAD_ERROR: Token = { kind: 'corrupted', error: 'payload' };
const VERSION_ERROR: Token = { kind: 'corrupted', error: 'version' };
const GENERAL_ERROR: Token = { kind: TokenKind.corrupted, error: 'general' };
const VERIFICATION_ERROR: Token = { kind: TokenKind.corrupted, error: 'verification' };
const DECODING_ERROR: Token = { kind: TokenKind.corrupted, error: 'decoding' };
const DESERIALIZATION_ERROR: Token = { kind: TokenKind.corrupted, error: 'deserialization' };
const PAYLOAD_ERROR: Token = { kind: TokenKind.corrupted, error: 'payload' };
const VERSION_ERROR: Token = { kind: TokenKind.corrupted, error: 'version' };

export function parseToken(encodedToken: string | undefined): Token {
if (encodedToken === undefined) {
let isLicenseVerified = false;

export function parseLicenseKey(encodedKey: string | undefined): Token {
if (encodedKey === undefined) {
return GENERAL_ERROR;
}

const parts = encodedToken.split(SPLITTER);
const parts = encodedKey.split(SPLITTER);

if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
return GENERAL_ERROR;
Expand Down Expand Up @@ -70,11 +81,63 @@ export function parseToken(encodedToken: string | undefined): Token {
}

return {
kind: 'verified',
kind: TokenKind.verified,
payload: {
customerId,
maxVersionAllowed,
...rest,
},
};
}

export function verifyLicense(licenseKey: string, version: string = packageVersion): void {
if (isLicenseVerified) {
return;
}
isLicenseVerified = true;

let warning: LicenseVerifyResult = null;

try {
if (!licenseKey) {
warning = 'W0019';
return;
}

const license = parseLicenseKey(licenseKey);

if (license.kind === TokenKind.corrupted) {
warning = 'W0021';
return;
}

const [major, minor] = version.split('.').map(Number);

if (!(major && minor)) {
warning = 'W0021';
return;
}

if (major * 10 + minor > license.payload.maxVersionAllowed) {
warning = 'W0020';
}
} catch (e) {
warning = 'W0021';
} finally {
if (warning) {
errors.log(warning);
}
}
}

/// #DEBUG
export function setLicenseCheckSkipCondition(value = true): void {
isLicenseVerified = value;
}
/// #ENDDEBUG

// NOTE: We need this default export
// to allow QUnit mock the verifyLicense function
export default {
verifyLicense,
};
Loading

0 comments on commit b40991a

Please sign in to comment.