-
Notifications
You must be signed in to change notification settings - Fork 666
License: Add new licensing mechanism #33206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ajivanyandev
wants to merge
50
commits into
DevExpress:26_1
Choose a base branch
from
ajivanyandev:license/26-1-full-pipeline
base: 26_1
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
50 commits
Select commit
Hold shift + click to select a range
b136f46
add modules to retrieve lcx key and convert lcx to lcp
ajivanyandev c886711
Add CLI command to write LCP to the given output file
ajivanyandev 9d579ac
add LCP bundler-plugin
GoodDayForSurf 448a14a
redefined plugin structure
ajivanyandev b299af2
fix file structure
ajivanyandev ea372db
remove unnecessary exports
ajivanyandev 12b87ee
change file structure
ajivanyandev 642664d
small fix
ajivanyandev 281cbee
DX product key parsing for the client
VasilyStrelyaev c5f7195
validation logic fix
ajivanyandev 8cb189e
add some errors, little change in key retrieval logic
ajivanyandev e7e15b7
change cli logic: make --out param optional
ajivanyandev 0867245
small fix in warning
ajivanyandev aad224e
change trial oanel logic to match new warnings logic
ajivanyandev 649342d
log source when running plugin
ajivanyandev 5e2cdaf
Add validation on cli and plugin pipelines, minor fixes
ajivanyandev 6215d5e
fix non modular behaviour
ajivanyandev 842fd3f
cli wrapper to call from bin
ajivanyandev 0aa8494
get rid of class based implementations for cleanup
ajivanyandev 9b4fc95
add payload tests, fix key validation test logic
ajivanyandev 701dab4
fix type error
ajivanyandev 4d2d800
isolate warning logic into a helper function
ajivanyandev 237859e
remove egow check for 25_2
ajivanyandev 867043e
rename license command
ajivanyandev 3d1f90e
Add license id to the generated license file and license related warn…
ajivanyandev bdec4ec
revert license id warnings in runtime
ajivanyandev 86939cb
remove id from the generated file
ajivanyandev 5c807fe
new warnings for devextreme-license (CLI tool)
ajivanyandev 544ede9
cli fixes
ajivanyandev 0a614d4
add browser warnings for license
ajivanyandev b1e31ec
fix typo
ajivanyandev 25fe95b
fix small issues
ajivanyandev 3391274
type fix
ajivanyandev 1b0c8d0
minor fixes
ajivanyandev f6f5d27
browser warnings update
ajivanyandev d06ccd9
part of cli warning fixes
ajivanyandev d0ffcc0
browser warning messages update
ajivanyandev 3bc1292
cleanup and final fixes for the warnings. Added DX1003 handler
ajivanyandev d78da08
Fix expired trial detection and add a test
ajivanyandev fe4db44
fix warning text
ajivanyandev b299e3c
seperate warnings
ajivanyandev a79a7a8
remove unnecessary comment
ajivanyandev a776da5
update tests after splitting warning
ajivanyandev 3eeb4fb
remove old key validation code and add warnings
ajivanyandev 5d33a2b
package lock fix
ajivanyandev b028da1
Merge branch '26_1' into license/26-1-full-pipeline
GoodDayForSurf 133cf1e
Merge branch '26_1' into license/26-1-full-pipeline
GoodDayForSurf 03416e0
update tests
ajivanyandev 36706df
some cleanup
ajivanyandev 0405086
Update cwd param handling
ajivanyandev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| #!/usr/bin/env node | ||
| 'use strict'; | ||
|
|
||
| require('../license/devextreme-license'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| export const FORMAT = 1; | ||
| export const RTM_MIN_PATCH_VERSION = 3; | ||
| export const KEY_SPLITTER = '.'; | ||
|
|
||
| export const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx'; | ||
| export const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx'; | ||
|
|
||
| export const LICENSE_KEY_PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; | ||
|
|
||
| export const NBSP = '\u00A0'; | ||
| export const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export const LCP_SIGNATURE = 'LCPv1'; | ||
| export const SIGN_LENGTH = 68 * 2; // 136 characters | ||
| export const DECODE_MAP = '\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000a\u000b\u000c\u000d\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f\u0020R\u0022f6U`\'aA7Fdp,?#yeYx[KWwQMqk^T+5&r/8ItLDb2C0;H._ElZ@*N>ojOv\u005c$]m)JncBVsi<XGP=93zS%g:h(u-!14{|}~'; | ||
| export const RSA_PUBLIC_KEY_XML = '<RSAKeyValue><Modulus>94ACmndawR6kB4PEJnXBBrz5Dn8ekEf5IvL7ro5ZvOyLVDiRwZXYR2uF8tFUSYjS5v7kOg74lfpZqfPXof7kcZwV3ENuy3tB7rqPBZaAqTMp5nBsZOc2H7MgDBXzrSdd4hzASQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>'; |
99 changes: 99 additions & 0 deletions
99
packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| /* eslint-disable */ | ||
|
|
||
| import { | ||
ajivanyandev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| describe, | ||
| expect, | ||
| it, | ||
| jest, | ||
| } from '@jest/globals'; | ||
| import { version as currentVersion } from '@js/core/version'; | ||
|
|
||
| import { parseVersion } from '../../../utils/version'; | ||
| import { TokenKind } from '../types'; | ||
| import { parseDevExpressProductKey } from './lcp_key_validator'; | ||
| import { findLatestDevExtremeVersion, isLicenseValid } from './license_info'; | ||
| import { createProductInfo } from './product_info'; | ||
|
|
||
| const DOT_NET_TICKS_EPOCH_OFFSET = 621355968000000000n; | ||
| const DOT_NET_TICKS_PER_MS = 10000n; | ||
| const DEVEXTREME_HTML_JS_BIT = 1n << 54n; | ||
|
|
||
| function msToDotNetTicks(ms: number): string { | ||
| return (BigInt(ms) * DOT_NET_TICKS_PER_MS + DOT_NET_TICKS_EPOCH_OFFSET).toString(); | ||
| } | ||
|
|
||
| function createLcpSource(payload: string): string { | ||
| const signature = 'A'.repeat(136); | ||
| return `LCPv1${btoa(`${signature}${payload}`)}`; | ||
| } | ||
|
|
||
| function loadParserWithBypassedSignatureCheck() { | ||
| jest.resetModules(); | ||
| jest.doMock('./utils', () => { | ||
| const actual = jest.requireActual('./utils') as Record<string, unknown>; | ||
| return { | ||
| ...actual, | ||
| encodeString: (text: string) => text, | ||
| shiftDecodeText: (text: string) => text, | ||
| verifyHash: () => true, | ||
| }; | ||
| }); | ||
|
|
||
| // eslint-disable-next-line | ||
| const { parseDevExpressProductKey } = require('./lcp_key_validator'); | ||
| // eslint-disable-next-line | ||
| const { TokenKind } = require('../types'); | ||
| return { parseDevExpressProductKey, TokenKind }; | ||
| } | ||
|
|
||
| function getTrialLicense() { | ||
| const { major, minor } = parseVersion(currentVersion); | ||
| const products = [ | ||
| createProductInfo(parseInt(`${major}${minor}`, 10), 0n), | ||
| ]; | ||
| return { products }; | ||
| } | ||
|
|
||
| describe('LCP key validation', () => { | ||
| it('serializer returns an invalid license for malformed input', () => { | ||
| const token = parseDevExpressProductKey('not-a-real-license'); | ||
| expect(token.kind).toBe(TokenKind.corrupted); | ||
| }); | ||
|
|
||
| (process.env.DX_PRODUCT_KEY ? it : it.skip)('developer product license fixtures parse into valid LicenseInfo instances', () => { | ||
| const token = parseDevExpressProductKey(process.env.DX_PRODUCT_KEY as string); | ||
| expect(token.kind).toBe(TokenKind.verified); | ||
| }); | ||
|
|
||
| it('trial fallback does not grant product access', () => { | ||
| const trialLicense = getTrialLicense(); | ||
| expect(isLicenseValid(trialLicense)).toBe(true); | ||
|
|
||
| const version = findLatestDevExtremeVersion(trialLicense); | ||
|
|
||
| expect(version).toBe(undefined); | ||
| }); | ||
|
|
||
| it('does not classify a valid DevExtreme product key as trial-expired when expiration metadata is in the past', () => { | ||
| const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck(); | ||
| const expiredAt = msToDotNetTicks(Date.UTC(2020, 0, 1)); | ||
|
|
||
| const payload = `meta;251,${DEVEXTREME_HTML_JS_BIT},0,${expiredAt};`; | ||
| const token = parseDevExpressProductKey(createLcpSource(payload)); | ||
|
|
||
| expect(token.kind).toBe(TokenKind.verified); | ||
| }); | ||
|
|
||
| it('returns trial-expired for expired trial keys without DevExtreme product access', () => { | ||
| const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck(); | ||
| const expiredAt = msToDotNetTicks(Date.UTC(2020, 0, 1)); | ||
|
|
||
| const payload = `meta;251,0,0,${expiredAt};`; | ||
| const token = parseDevExpressProductKey(createLcpSource(payload)); | ||
|
|
||
| expect(token.kind).toBe(TokenKind.corrupted); | ||
| if (token.kind === TokenKind.corrupted) { | ||
| expect(token.error).toBe('trial-expired'); | ||
| } | ||
| }); | ||
| }); | ||
121 changes: 121 additions & 0 deletions
121
packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import { FORMAT } from '../const'; | ||
| import { | ||
| DESERIALIZATION_ERROR, | ||
| type ErrorToken, | ||
| GENERAL_ERROR, | ||
| PRODUCT_KIND_ERROR, | ||
| type Token, | ||
| TokenKind, | ||
| TRIAL_EXPIRED_ERROR, | ||
| VERIFICATION_ERROR, | ||
| } from '../types'; | ||
| import { | ||
| LCP_SIGNATURE, | ||
| RSA_PUBLIC_KEY_XML, | ||
| SIGN_LENGTH, | ||
| } from './const'; | ||
| import { findLatestDevExtremeVersion, getMaxExpiration } from './license_info'; | ||
| import { createProductInfo, type ProductInfo } from './product_info'; | ||
| import { | ||
| dotNetTicksToMs, | ||
| encodeString, | ||
| shiftDecodeText, | ||
| verifyHash, | ||
| } from './utils'; | ||
|
|
||
| interface ParsedProducts { | ||
| products: ProductInfo[]; | ||
| errorToken?: ErrorToken; | ||
| } | ||
|
|
||
| export function isProductOnlyLicense(license: string): boolean { | ||
| return typeof license === 'string' && license.startsWith(LCP_SIGNATURE); | ||
| } | ||
|
|
||
| function productsFromString(encodedString: string): ParsedProducts { | ||
| if (!encodedString) { | ||
| return { | ||
| products: [], | ||
| errorToken: GENERAL_ERROR, | ||
| }; | ||
| } | ||
|
|
||
| try { | ||
| const splitInfo = encodedString.split(';'); | ||
| 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 productsValue = BigInt(parts[1]); | ||
| const expiration = parts.length > 3 ? dotNetTicksToMs(parts[3]) : Infinity; | ||
| return createProductInfo( | ||
| version, | ||
| productsValue, | ||
| expiration, | ||
| ); | ||
| }); | ||
|
|
||
| return { | ||
| products, | ||
| }; | ||
| } catch (error) { | ||
| return { | ||
| products: [], | ||
| errorToken: DESERIALIZATION_ERROR, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| export function parseDevExpressProductKey(productsLicenseSource: string): Token { | ||
| if (!isProductOnlyLicense(productsLicenseSource)) { | ||
| return GENERAL_ERROR; | ||
| } | ||
|
|
||
| try { | ||
| const productsLicense = atob( | ||
| shiftDecodeText(productsLicenseSource.substring(LCP_SIGNATURE.length)), | ||
| ); | ||
|
|
||
| const signature = productsLicense.substring(0, SIGN_LENGTH); | ||
| const productsPayload = productsLicense.substring(SIGN_LENGTH); | ||
|
|
||
| if (!verifyHash(RSA_PUBLIC_KEY_XML, productsPayload, signature)) { | ||
| return VERIFICATION_ERROR; | ||
| } | ||
|
|
||
| const { | ||
| products, | ||
| errorToken, | ||
| } = productsFromString( | ||
| encodeString(productsPayload, shiftDecodeText), | ||
| ); | ||
|
|
||
| if (errorToken) { | ||
| return errorToken; | ||
| } | ||
|
|
||
| const maxVersionAllowed = findLatestDevExtremeVersion({ products }); | ||
|
|
||
| if (!maxVersionAllowed) { | ||
| const maxExpiration = getMaxExpiration({ products }); | ||
| if (maxExpiration !== Infinity && maxExpiration < Date.now()) { | ||
| return TRIAL_EXPIRED_ERROR; | ||
| } | ||
| } | ||
|
|
||
| if (!maxVersionAllowed) { | ||
| return PRODUCT_KIND_ERROR; | ||
| } | ||
|
|
||
| return { | ||
| kind: TokenKind.verified, | ||
| payload: { | ||
| customerId: '', | ||
| maxVersionAllowed, | ||
| format: FORMAT, | ||
| }, | ||
| }; | ||
| } catch (error) { | ||
| return GENERAL_ERROR; | ||
| } | ||
| } |
28 changes: 28 additions & 0 deletions
28
packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { isProduct, type ProductInfo } from './product_info'; | ||
| import { ProductKind } from './types'; | ||
|
|
||
| export interface LicenseInfo { | ||
| readonly products: ProductInfo[]; | ||
| } | ||
|
|
||
| export function isLicenseValid(info: LicenseInfo): boolean { | ||
| return Array.isArray(info.products) && info.products.length > 0; | ||
| } | ||
|
|
||
| export function getMaxExpiration(info: LicenseInfo): number { | ||
| const expirationDates = info.products | ||
| .map((p) => p.expiration) | ||
| .filter((e) => e > 0 && e !== Infinity); | ||
| if (expirationDates.length === 0) return Infinity; | ||
| return Math.max(...expirationDates); | ||
| } | ||
|
|
||
| export function findLatestDevExtremeVersion(info: LicenseInfo): number | undefined { | ||
| if (!isLicenseValid(info)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const sorted = [...info.products].sort((a, b) => b.version - a.version); | ||
|
|
||
| return sorted.find((p) => isProduct(p, ProductKind.DevExtremeHtmlJs))?.version; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.