1 | import compareVersions from 'compare-versions';
|
2 | import fs from 'fs';
|
3 | import os from 'os';
|
4 | import path from 'path';
|
5 | import requestPromise from 'request-promise-native';
|
6 | import util from 'util';
|
7 |
|
8 | import getParentDirectoryContainingFileSync from './getParentDirectoryContainingFileSync.js';
|
9 |
|
10 | const readFile = util.promisify(fs.readFile);
|
11 | const writeFile = util.promisify(fs.writeFile);
|
12 |
|
13 | const DEFAULT_LICENSE_FILENAME = 'fonto.lic';
|
14 |
|
15 | const LICENSE_FILE_ERROR_SOLUTION =
|
16 | 'Please check if you have a license file installed, or contact support for obtaining a license file.';
|
17 |
|
18 | const MINIMUM_LICENSE_VERSION = 1.0;
|
19 |
|
20 | const VERSION_TYPES = {
|
21 | NIGHTLY: 'nightly',
|
22 | PRERELEASE: 'prerelease',
|
23 | STABLE: 'stable',
|
24 | };
|
25 |
|
26 | class ProductVersions {
|
27 | constructor(name, versions) {
|
28 | this._name = name;
|
29 |
|
30 | this._productVersions = versions
|
31 | .sort((a, b) => {
|
32 | if (a === VERSION_TYPES.NIGHTLY) {
|
33 | return -1;
|
34 | }
|
35 | if (b === VERSION_TYPES.NIGHTLY) {
|
36 | return 1;
|
37 | }
|
38 |
|
39 | try {
|
40 | return compareVersions(b, a);
|
41 | } catch (_error) {
|
42 | return String(b).localeCompare(a);
|
43 | }
|
44 | })
|
45 | .map((version) => {
|
46 | const type =
|
47 | version === VERSION_TYPES.NIGHTLY
|
48 | ? VERSION_TYPES.NIGHTLY
|
49 | : version.includes('-')
|
50 | ? VERSION_TYPES.PRERELEASE
|
51 | : VERSION_TYPES.STABLE;
|
52 |
|
53 | return { version, type };
|
54 | });
|
55 | }
|
56 |
|
57 | getAll() {
|
58 | return this._productVersions.map(
|
59 | (productVersion) => productVersion.version
|
60 | );
|
61 | }
|
62 |
|
63 | getAllStable() {
|
64 | return this._productVersions.reduce((acc, productVersion) => {
|
65 | if (productVersion.type === VERSION_TYPES.STABLE) {
|
66 | acc.push(productVersion.version);
|
67 | }
|
68 | return acc;
|
69 | }, []);
|
70 | }
|
71 |
|
72 | getLatestStable() {
|
73 | const latestStableproductVersion = this._productVersions.find(
|
74 | (productVersion) => productVersion.type === VERSION_TYPES.STABLE
|
75 | );
|
76 |
|
77 | return latestStableproductVersion
|
78 | ? latestStableproductVersion.version
|
79 | : null;
|
80 | }
|
81 |
|
82 | includes(version) {
|
83 | return this._productVersions.some(
|
84 | (productVersion) => productVersion.version === version
|
85 | );
|
86 | }
|
87 | }
|
88 |
|
89 | export default class FdtLicense {
|
90 | constructor(app) {
|
91 | this.app = app;
|
92 |
|
93 | this._backendBaseUrl =
|
94 | process.env.LICENSE_SERVICE_URL ||
|
95 | 'https://licenseserver.fontoxml.com';
|
96 |
|
97 | this._license = this._loadLicenseFile();
|
98 | }
|
99 |
|
100 | |
101 |
|
102 |
|
103 |
|
104 |
|
105 | _getLicenseFilename() {
|
106 | if (process.env.FDT_LICENSE_FILENAME) {
|
107 | return process.env.FDT_LICENSE_FILENAME;
|
108 | }
|
109 |
|
110 | const licenseFilenameInHomedir = path.join(
|
111 | os.homedir(),
|
112 | DEFAULT_LICENSE_FILENAME
|
113 | );
|
114 | const licenseFilenameInPathAncestry =
|
115 | getParentDirectoryContainingFileSync(
|
116 | process.cwd(),
|
117 | DEFAULT_LICENSE_FILENAME,
|
118 | true
|
119 | );
|
120 |
|
121 | if (licenseFilenameInPathAncestry) {
|
122 | return path.join(
|
123 | licenseFilenameInPathAncestry,
|
124 | DEFAULT_LICENSE_FILENAME
|
125 | );
|
126 | }
|
127 |
|
128 | try {
|
129 | fs.accessSync(licenseFilenameInHomedir, fs.constants.R_OK);
|
130 | return licenseFilenameInHomedir;
|
131 | } catch (_error) {
|
132 |
|
133 | }
|
134 |
|
135 | return null;
|
136 | }
|
137 |
|
138 | _decodeAndValidateLicenseFileData(data) {
|
139 | let decryptedData;
|
140 |
|
141 | try {
|
142 | decryptedData = JSON.parse(
|
143 | Buffer.from(data, 'base64').toString('utf8')
|
144 | );
|
145 | } catch (error) {
|
146 | return {
|
147 | error: new this.app.logger.ErrorWithSolution(
|
148 | 'License file has invalid data.',
|
149 | LICENSE_FILE_ERROR_SOLUTION,
|
150 | error
|
151 | ),
|
152 | };
|
153 | }
|
154 |
|
155 | if (typeof decryptedData !== 'object') {
|
156 | return {
|
157 | error: new this.app.logger.ErrorWithSolution(
|
158 | 'License file has invalid data.',
|
159 | LICENSE_FILE_ERROR_SOLUTION
|
160 | ),
|
161 | };
|
162 | }
|
163 |
|
164 |
|
165 | if (!decryptedData.key || typeof decryptedData.key !== 'string') {
|
166 | return {
|
167 | error: new this.app.logger.ErrorWithSolution(
|
168 | 'License file has an invalid key.',
|
169 | LICENSE_FILE_ERROR_SOLUTION
|
170 | ),
|
171 | };
|
172 | }
|
173 |
|
174 |
|
175 | if (
|
176 | !decryptedData.licensee ||
|
177 | typeof decryptedData.licensee !== 'string'
|
178 | ) {
|
179 | return {
|
180 | error: new this.app.logger.ErrorWithSolution(
|
181 | 'License file has an invalid licensee.',
|
182 | LICENSE_FILE_ERROR_SOLUTION
|
183 | ),
|
184 | };
|
185 | }
|
186 |
|
187 |
|
188 | if (!decryptedData.products || !Array.isArray(decryptedData.products)) {
|
189 | return {
|
190 | error: new this.app.logger.ErrorWithSolution(
|
191 | 'License file has invalid products.',
|
192 | LICENSE_FILE_ERROR_SOLUTION
|
193 | ),
|
194 | };
|
195 | }
|
196 |
|
197 | const hasInvalidProducts = decryptedData.products.some((product) => {
|
198 | return (
|
199 | typeof product !== 'object' ||
|
200 | !product.id ||
|
201 | typeof product.id !== 'string' ||
|
202 | !product.label ||
|
203 | typeof product.label !== 'string'
|
204 | );
|
205 | });
|
206 | if (hasInvalidProducts) {
|
207 | return {
|
208 | error: new this.app.logger.ErrorWithSolution(
|
209 | 'License file has invalid products.',
|
210 | LICENSE_FILE_ERROR_SOLUTION
|
211 | ),
|
212 | };
|
213 | }
|
214 |
|
215 |
|
216 | if (typeof decryptedData.version !== 'number') {
|
217 | return {
|
218 | error: new this.app.logger.ErrorWithSolution(
|
219 | 'License file has an invalid version.',
|
220 | LICENSE_FILE_ERROR_SOLUTION
|
221 | ),
|
222 | };
|
223 | }
|
224 |
|
225 | if (decryptedData.version < MINIMUM_LICENSE_VERSION) {
|
226 | return {
|
227 | error: new this.app.logger.ErrorWithSolution(
|
228 | `The license file version is too old for this version of ${
|
229 | this.app.getInfo().name
|
230 | }.`,
|
231 | `Please upgrade your license by running '${
|
232 | this.app.getInfo().name
|
233 | } license validate'.`
|
234 | ),
|
235 | };
|
236 | }
|
237 |
|
238 | if (decryptedData.version >= Math.floor(MINIMUM_LICENSE_VERSION) + 1) {
|
239 | return {
|
240 | error: new this.app.logger.ErrorWithSolution(
|
241 | `The license file version is too new for this version of ${
|
242 | this.app.getInfo().name
|
243 | }.`,
|
244 | `Please upgrade ${this.app.getInfo().name}.`
|
245 | ),
|
246 | };
|
247 | }
|
248 |
|
249 | return {
|
250 | data: decryptedData,
|
251 | };
|
252 | }
|
253 |
|
254 | |
255 |
|
256 |
|
257 |
|
258 |
|
259 | _loadLicenseFile() {
|
260 | const filename = this._getLicenseFilename();
|
261 | if (!filename) {
|
262 | return {
|
263 | error: new this.app.logger.ErrorWithSolution(
|
264 | 'The required license file could not be found.',
|
265 | LICENSE_FILE_ERROR_SOLUTION
|
266 | ),
|
267 | };
|
268 | }
|
269 |
|
270 | let data;
|
271 | try {
|
272 | data = fs.readFileSync(filename, 'utf8');
|
273 | } catch (error) {
|
274 | return {
|
275 | error: new this.app.logger.ErrorWithSolution(
|
276 | 'The required license file could not be read.',
|
277 | LICENSE_FILE_ERROR_SOLUTION,
|
278 | error
|
279 | ),
|
280 | };
|
281 | }
|
282 |
|
283 | const validationResult = this._decodeAndValidateLicenseFileData(data);
|
284 |
|
285 | if (validationResult.data) {
|
286 | validationResult.filename = filename;
|
287 | }
|
288 |
|
289 | return validationResult;
|
290 | }
|
291 |
|
292 | |
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | getLicenseInfo() {
|
300 | if (!this._license.data) {
|
301 | return null;
|
302 | }
|
303 |
|
304 | return {
|
305 | filepath: this._license.filename,
|
306 | licensee: this._license.data.licensee,
|
307 | products: this._license.data.products.map((product) => ({
|
308 | ...product,
|
309 | })),
|
310 | version: this._license.data.version,
|
311 | };
|
312 | }
|
313 |
|
314 | |
315 |
|
316 |
|
317 |
|
318 |
|
319 | getLicenseFileBuffer() {
|
320 | this.ensureLicenseFileExists();
|
321 | return readFile(this._license.filename).catch(() => {
|
322 | throw new Error('Could not read the license file.');
|
323 | });
|
324 | }
|
325 |
|
326 | |
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 | hasProductLicenses(productIds) {
|
337 | if (!this._license.data) {
|
338 | return false;
|
339 | }
|
340 |
|
341 | return productIds.every((productId) => {
|
342 | return this._license.data.products.find(
|
343 | (product) => product.id === productId
|
344 | );
|
345 | });
|
346 | }
|
347 |
|
348 | |
349 |
|
350 |
|
351 |
|
352 |
|
353 | ensureLicenseFileExists() {
|
354 | if (!this._license.data) {
|
355 | throw this._license.error;
|
356 | }
|
357 |
|
358 | return true;
|
359 | }
|
360 |
|
361 | |
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 |
|
371 | ensureProductLicenses(productIds) {
|
372 | this.ensureLicenseFileExists();
|
373 |
|
374 | const missingProductIds = productIds.filter((productId) => {
|
375 | return !this._license.data.products.find(
|
376 | (product) => product.id === productId
|
377 | );
|
378 | });
|
379 |
|
380 | if (missingProductIds.length) {
|
381 | throw new this.app.logger.ErrorWithSolution(
|
382 | `You appear to be missing one of the required product licenses (${missingProductIds.join(
|
383 | ', '
|
384 | )}).`,
|
385 | `Check if your license is valid and have the required product licenses by running '${
|
386 | this.app.getInfo().name
|
387 | } license validate', and afterwards try to run the current command again.`
|
388 | );
|
389 | }
|
390 |
|
391 | return true;
|
392 | }
|
393 |
|
394 | |
395 |
|
396 |
|
397 |
|
398 |
|
399 | _getSystemTelemetryData() {
|
400 | return {
|
401 | fdt: {
|
402 | version: this.app.version,
|
403 | },
|
404 | node: {
|
405 | version: process.version,
|
406 | platform: process.platform,
|
407 | architecture: process.arch,
|
408 | },
|
409 | };
|
410 | }
|
411 |
|
412 | |
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 | validateAndUpdateLicenseFile() {
|
419 | return this.getLicenseFileBuffer()
|
420 | .then((licenseFileBuffer) => {
|
421 |
|
422 | return requestPromise.post(
|
423 | `${this._backendBaseUrl}/license/validate`,
|
424 | {
|
425 | formData: {
|
426 | request: {
|
427 | value: JSON.stringify(
|
428 | this._getSystemTelemetryData()
|
429 | ),
|
430 | options: {
|
431 | contentType: 'application/json',
|
432 | },
|
433 | },
|
434 | license: {
|
435 | value: licenseFileBuffer,
|
436 | options: {
|
437 | filename: DEFAULT_LICENSE_FILENAME,
|
438 | contentType: 'application/octet-stream',
|
439 | },
|
440 | },
|
441 | },
|
442 | resolveWithFullResponse: true,
|
443 | simple: false,
|
444 | }
|
445 | );
|
446 | })
|
447 | .catch((error) => {
|
448 | if (error.hasOwnProperty('solution')) {
|
449 | throw error;
|
450 | }
|
451 |
|
452 | throw new Error(
|
453 | `Could not get response from license server. Please check if you have a working internet connection.`
|
454 | );
|
455 | })
|
456 | .then((response) => {
|
457 | switch (response.statusCode) {
|
458 | case 200: {
|
459 |
|
460 | const validationResult =
|
461 | this._decodeAndValidateLicenseFileData(
|
462 | response.body
|
463 | );
|
464 | if (validationResult.error) {
|
465 | throw new Error(
|
466 | 'Invalid updated license data received while checking license.'
|
467 | );
|
468 | }
|
469 |
|
470 |
|
471 | return writeFile(this._license.filename, response.body)
|
472 | .catch((_error) => {
|
473 | throw new Error(
|
474 | 'Could not update local license file.'
|
475 | );
|
476 | })
|
477 | .then(() => {
|
478 |
|
479 | this._license = this._loadLicenseFile();
|
480 |
|
481 | if (!this._license.data) {
|
482 | throw this._license.error;
|
483 | }
|
484 |
|
485 | return true;
|
486 | });
|
487 | }
|
488 | case 204:
|
489 | return true;
|
490 | case 400: {
|
491 | const error = new Error(
|
492 | `License is invalid. Run '${
|
493 | this.app.getInfo().name
|
494 | } license validate' to check your license.`
|
495 | );
|
496 | error.statusCode = response.statusCode;
|
497 | throw error;
|
498 | }
|
499 | case 401:
|
500 | case 403: {
|
501 | const error = new Error(
|
502 | `License is not valid (anymore). Run '${
|
503 | this.app.getInfo().name
|
504 | } license validate' to check your license.`
|
505 | );
|
506 | error.statusCode = response.statusCode;
|
507 | throw error;
|
508 | }
|
509 | default: {
|
510 | const error = new Error(
|
511 | `Error while checking license (${response.statusCode}).`
|
512 | );
|
513 | error.statusCode = response.statusCode;
|
514 | throw error;
|
515 | }
|
516 | }
|
517 | });
|
518 | }
|
519 |
|
520 | |
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 |
|
527 |
|
528 | getDataForProducts(productsWithData) {
|
529 | return this.getLicenseFileBuffer()
|
530 | .then((licenseFileBuffer) => {
|
531 |
|
532 | return requestPromise.post(
|
533 | `${this._backendBaseUrl}/product/data`,
|
534 | {
|
535 | formData: {
|
536 | request: {
|
537 | value: JSON.stringify(
|
538 | Object.assign(
|
539 | this._getSystemTelemetryData(),
|
540 | {
|
541 | products: productsWithData,
|
542 | }
|
543 | )
|
544 | ),
|
545 | options: {
|
546 | contentType: 'application/json',
|
547 | },
|
548 | },
|
549 | license: {
|
550 | value: licenseFileBuffer,
|
551 | options: {
|
552 | filename: DEFAULT_LICENSE_FILENAME,
|
553 | contentType: 'application/octet-stream',
|
554 | },
|
555 | },
|
556 | },
|
557 | resolveWithFullResponse: true,
|
558 | simple: false,
|
559 | }
|
560 | );
|
561 | })
|
562 | .catch((error) => {
|
563 | if (error.hasOwnProperty('solution')) {
|
564 | throw error;
|
565 | }
|
566 |
|
567 | throw new Error(
|
568 | `Could not get response from license server. Please check if you have a working internet connection.`
|
569 | );
|
570 | })
|
571 | .then((response) => {
|
572 | switch (response.statusCode) {
|
573 | case 200: {
|
574 | let data;
|
575 | try {
|
576 | data = JSON.parse(response.body);
|
577 | } catch (_error) {
|
578 | throw new Error(
|
579 | 'Invalid response data while getting data for products.'
|
580 | );
|
581 | }
|
582 | return data;
|
583 | }
|
584 | case 400: {
|
585 | const error = new Error(
|
586 | `License is invalid. Run '${
|
587 | this.app.getInfo().name
|
588 | } license validate' to check your license.`
|
589 | );
|
590 | error.statusCode = response.statusCode;
|
591 | throw error;
|
592 | }
|
593 | case 401:
|
594 | case 403: {
|
595 | const error = new Error(
|
596 | `License is not valid (anymore). Run '${
|
597 | this.app.getInfo().name
|
598 | } license validate' to check your license.`
|
599 | );
|
600 | error.statusCode = response.statusCode;
|
601 | throw error;
|
602 | }
|
603 | case 404: {
|
604 | const error = new Error(
|
605 | 'Could not get requested data, because one or more of the requested products or their data does not exist.'
|
606 | );
|
607 | error.statusCode = response.statusCode;
|
608 | throw error;
|
609 | }
|
610 | default: {
|
611 | const error = new Error(
|
612 | `Error while getting data for products (${response.statusCode}).`
|
613 | );
|
614 | error.statusCode = response.statusCode;
|
615 | throw error;
|
616 | }
|
617 | }
|
618 | });
|
619 | }
|
620 |
|
621 | |
622 |
|
623 |
|
624 |
|
625 |
|
626 |
|
627 |
|
628 | getVersionsForProduct(productName) {
|
629 | const requestObj = {};
|
630 | requestObj[productName] = {
|
631 | versions: true,
|
632 | };
|
633 |
|
634 | return this.getDataForProducts(requestObj).then((productData) => {
|
635 | return new ProductVersions(
|
636 | productName,
|
637 | productData.products[productName].versions
|
638 | );
|
639 | });
|
640 | }
|
641 |
|
642 | |
643 |
|
644 |
|
645 |
|
646 |
|
647 |
|
648 |
|
649 | sendTelemetry(data) {
|
650 | return this.getLicenseFileBuffer()
|
651 | .then((licenseFileBuffer) => {
|
652 | return requestPromise.post(
|
653 | `${this._backendBaseUrl}/telemetry`,
|
654 | {
|
655 | formData: {
|
656 | request: {
|
657 | value: JSON.stringify(
|
658 | Object.assign(
|
659 | this._getSystemTelemetryData(),
|
660 | {
|
661 | data,
|
662 | }
|
663 | )
|
664 | ),
|
665 | options: {
|
666 | contentType: 'application/json',
|
667 | },
|
668 | },
|
669 | license: {
|
670 | value: licenseFileBuffer,
|
671 | options: {
|
672 | filename: DEFAULT_LICENSE_FILENAME,
|
673 | contentType: 'application/octet-stream',
|
674 | },
|
675 | },
|
676 | },
|
677 | resolveWithFullResponse: true,
|
678 | simple: false,
|
679 | timeout: 10000,
|
680 | }
|
681 | );
|
682 | })
|
683 | .then((response) => {
|
684 | switch (response.statusCode) {
|
685 | case 200:
|
686 | case 204:
|
687 | break;
|
688 | default: {
|
689 | const error = new Error(
|
690 | `Error while sending telemetry (${response.statusCode}).`
|
691 | );
|
692 | error.statusCode = response.statusCode;
|
693 | throw error;
|
694 | }
|
695 | }
|
696 | })
|
697 | .catch((_error) => {
|
698 | if (!this.app.hideStacktraceOnErrors) {
|
699 | this.app.logger.notice('Error while sending telemetry.');
|
700 | }
|
701 | });
|
702 | }
|
703 | }
|