1 | 'use strict';
|
2 |
|
3 | const fs = require('fs');
|
4 | const os = require('os');
|
5 | const path = require('path');
|
6 | const util = require('util');
|
7 |
|
8 | const readFile = util.promisify(fs.readFile);
|
9 | const writeFile = util.promisify(fs.writeFile);
|
10 |
|
11 | const getParentDirectoryContainingFileSync = require('./getParentDirectoryContainingFileSync');
|
12 |
|
13 | const DEFAULT_LICENSE_FILENAME = 'fonto.lic';
|
14 |
|
15 | const MINIMUM_LICENSE_VERSION = 1.0;
|
16 |
|
17 | const LICENSE_FILE_ERROR_SOLUTION = 'Please check if you have a license file installed, or contact support for obtaining a license file.';
|
18 |
|
19 | class FdtLicense {
|
20 | constructor (app) {
|
21 | this.app = app;
|
22 |
|
23 | this._backendBaseUrl = process.env.LICENSE_SERVICE_URL || 'https://licenseserver.fontoxml.com';
|
24 |
|
25 | this._license = this._loadLicenseFile();
|
26 | }
|
27 |
|
28 | |
29 |
|
30 |
|
31 |
|
32 |
|
33 | _getLicenseFilename () {
|
34 | if (process.env.FDT_LICENSE_FILENAME) {
|
35 | return process.env.FDT_LICENSE_FILENAME;
|
36 | }
|
37 |
|
38 | const licenseFilenameInHomedir = path.join(os.homedir(), DEFAULT_LICENSE_FILENAME);
|
39 | const licenseFilenameInPathAncestry = getParentDirectoryContainingFileSync(process.cwd(), DEFAULT_LICENSE_FILENAME, true);
|
40 |
|
41 | if (licenseFilenameInPathAncestry) {
|
42 | return path.join(licenseFilenameInPathAncestry, DEFAULT_LICENSE_FILENAME);
|
43 | }
|
44 |
|
45 | try {
|
46 | fs.accessSync(licenseFilenameInHomedir, fs.constants.R_OK);
|
47 | return licenseFilenameInHomedir;
|
48 | }
|
49 | catch (_error) {
|
50 |
|
51 | }
|
52 |
|
53 | return null;
|
54 | }
|
55 |
|
56 | _decodeAndValidateLicenseFileData (data) {
|
57 | let decryptedData;
|
58 |
|
59 | try {
|
60 | decryptedData = JSON.parse(Buffer.from(data, 'base64').toString('utf8'));
|
61 | }
|
62 | catch (_error) {
|
63 | return { error: new this.app.cli.InputError('License file has invalid data.', LICENSE_FILE_ERROR_SOLUTION) };
|
64 | }
|
65 |
|
66 | if (typeof decryptedData !== 'object') {
|
67 | return { error: new this.app.cli.InputError('License file has invalid data.', LICENSE_FILE_ERROR_SOLUTION) };
|
68 | }
|
69 |
|
70 |
|
71 | if (!decryptedData.key || typeof decryptedData.key !== 'string') {
|
72 | return { error: new this.app.cli.InputError('License file has an invalid key.', LICENSE_FILE_ERROR_SOLUTION) };
|
73 | }
|
74 |
|
75 |
|
76 | if (!decryptedData.licensee || typeof decryptedData.licensee !== 'string') {
|
77 | return { error: new this.app.cli.InputError('License file has an invalid licensee.', LICENSE_FILE_ERROR_SOLUTION) };
|
78 | }
|
79 |
|
80 |
|
81 | if (!decryptedData.products || !Array.isArray(decryptedData.products)) {
|
82 | return { error: new this.app.cli.InputError('License file has invalid products.', LICENSE_FILE_ERROR_SOLUTION) };
|
83 | }
|
84 |
|
85 | const hasInvalidProducts = decryptedData.products.some(product => {
|
86 | return typeof product !== 'object' ||
|
87 | !product.id || typeof product.id !== 'string' ||
|
88 | !product.label || typeof product.label !== 'string';
|
89 | });
|
90 | if (hasInvalidProducts) {
|
91 | return { error: new this.app.cli.InputError('License file has invalid products.', LICENSE_FILE_ERROR_SOLUTION) };
|
92 | }
|
93 |
|
94 |
|
95 | if (typeof decryptedData.version !== 'number') {
|
96 | return { error: new this.app.cli.InputError('License file has an invalid version.', LICENSE_FILE_ERROR_SOLUTION) };
|
97 | }
|
98 |
|
99 | if (decryptedData.version < MINIMUM_LICENSE_VERSION) {
|
100 | return { error: new this.app.cli.InputError(`The license file version is too old for this version of ${this.app.getInfo().name}.`, `Please upgrade your license by running '${this.app.getInfo().name} license validate'.`) };
|
101 | }
|
102 |
|
103 | if (decryptedData.version >= Math.floor(MINIMUM_LICENSE_VERSION) + 1) {
|
104 | return { error: new this.app.cli.InputError(`The license file version is too new for this version of ${this.app.getInfo().name}.`, `Please upgrade ${this.app.getInfo().name}.`) };
|
105 | }
|
106 |
|
107 | return {
|
108 | data: decryptedData
|
109 | };
|
110 | }
|
111 |
|
112 | |
113 |
|
114 |
|
115 |
|
116 |
|
117 | _loadLicenseFile () {
|
118 | const filename = this._getLicenseFilename();
|
119 | if (!filename) {
|
120 | return {
|
121 | error: new this.app.cli.InputError(
|
122 | 'The required license file could not be found.',
|
123 | LICENSE_FILE_ERROR_SOLUTION)
|
124 | };
|
125 | }
|
126 |
|
127 | let data;
|
128 | try {
|
129 | data = fs.readFileSync(filename, 'utf8');
|
130 | }
|
131 | catch (_error) {
|
132 | return {
|
133 | error: new this.app.cli.InputError(
|
134 | 'The required license file could not be read.',
|
135 | LICENSE_FILE_ERROR_SOLUTION)
|
136 | };
|
137 | }
|
138 |
|
139 | const validationResult = this._decodeAndValidateLicenseFileData(data);
|
140 |
|
141 | if (validationResult.data) {
|
142 | validationResult.filename = filename;
|
143 | }
|
144 |
|
145 | return validationResult;
|
146 | }
|
147 |
|
148 | |
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 | getLicenseInfo () {
|
156 | if (!this._license.data) {
|
157 | return null;
|
158 | }
|
159 |
|
160 | return {
|
161 | licensee: this._license.data.licensee,
|
162 | products: this._license.data.products.map(product => Object.assign({}, product)),
|
163 | version: this._license.data.version
|
164 | };
|
165 | }
|
166 |
|
167 | |
168 |
|
169 |
|
170 |
|
171 |
|
172 | getLicenseFileBuffer () {
|
173 | this.ensureLicenseFileExists();
|
174 | return readFile(this._license.filename)
|
175 | .catch(() => {
|
176 | throw new Error('Could not read the license file.');
|
177 | });
|
178 | }
|
179 |
|
180 | |
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 | hasProductLicenses (productIds) {
|
191 | if (!this._license.data) {
|
192 | return false;
|
193 | }
|
194 |
|
195 | return productIds.every(productId => {
|
196 | return this._license.data.products.find(product => product.id === productId);
|
197 | });
|
198 | }
|
199 |
|
200 | |
201 |
|
202 |
|
203 |
|
204 |
|
205 | ensureLicenseFileExists () {
|
206 | if (!this._license.data) {
|
207 | throw this._license.error;
|
208 | }
|
209 |
|
210 | return true;
|
211 | }
|
212 |
|
213 | |
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | ensureProductLicenses (productIds) {
|
224 | this.ensureLicenseFileExists();
|
225 |
|
226 | const missingProductIds = productIds.filter(productId => {
|
227 | return !this._license.data.products.find(product => product.id === productId);
|
228 | });
|
229 |
|
230 | if (missingProductIds.length) {
|
231 | throw new this.app.cli.InputError(
|
232 | `You appear to be missing one of the required product licenses (${missingProductIds.join(', ')}).`,
|
233 | `Check if your license is valid and have the required product licenses by running '${this.app.getInfo().name} license validate', and afterwards try to run the current command again.`);
|
234 | }
|
235 |
|
236 | return true;
|
237 | }
|
238 |
|
239 | |
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 | validateAndUpdateLicenseFile () {
|
246 | return this.getLicenseFileBuffer()
|
247 | .then(licenseFileBuffer => {
|
248 | const requestPromise = require('request-promise-native');
|
249 |
|
250 | return requestPromise.post(this._backendBaseUrl + '/license/validate', {
|
251 | formData: {
|
252 | license: {
|
253 | value: licenseFileBuffer,
|
254 | options: {
|
255 | filename: DEFAULT_LICENSE_FILENAME,
|
256 | contentType: 'application/octet-stream'
|
257 | }
|
258 | }
|
259 | },
|
260 | resolveWithFullResponse: true,
|
261 | simple: false
|
262 | });
|
263 | })
|
264 | .catch(error => {
|
265 | if (error.hasOwnProperty('solution')) {
|
266 | throw error;
|
267 | }
|
268 |
|
269 | throw new Error(`Could not get response from license server. Please check if you have a working internet connection.`);
|
270 | })
|
271 | .then(response => {
|
272 | switch (response.statusCode) {
|
273 | case 200: {
|
274 |
|
275 | const validationResult = this._decodeAndValidateLicenseFileData(response.body);
|
276 | if (validationResult.error) {
|
277 | throw new Error('Invalid updated license data received while checking license.');
|
278 | }
|
279 |
|
280 |
|
281 | return writeFile(this._license.filename, response.body)
|
282 | .catch(_error => {
|
283 | throw new Error('Could not update local license file.');
|
284 | })
|
285 | .then(() => {
|
286 |
|
287 | this._license = this._loadLicenseFile();
|
288 |
|
289 | if (!this._license.data) {
|
290 | throw this._license.error;
|
291 | }
|
292 |
|
293 | return true;
|
294 | });
|
295 | }
|
296 | case 204:
|
297 | return true;
|
298 | case 400: {
|
299 | const error = new Error(`License is invalid. Run '${this.app.getInfo().name} license validate' to check your license.`);
|
300 | error.statusCode = response.statusCode;
|
301 | throw error;
|
302 | }
|
303 | case 401:
|
304 | case 403: {
|
305 | const error = new Error(`License is not valid (anymore). Run '${this.app.getInfo().name} license validate' to check your license.`);
|
306 | error.statusCode = response.statusCode;
|
307 | throw error;
|
308 | }
|
309 | default: {
|
310 | const error = new Error(`Error while checking license (${response.statusCode}).`);
|
311 | error.statusCode = response.statusCode;
|
312 | throw error;
|
313 | }
|
314 | }
|
315 | });
|
316 | }
|
317 |
|
318 | |
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 | getDataForProducts (productsWithData) {
|
327 | return this.getLicenseFileBuffer()
|
328 | .then(licenseFileBuffer => {
|
329 | const requestPromise = require('request-promise-native');
|
330 |
|
331 | return requestPromise.post(this._backendBaseUrl + '/product/data', {
|
332 | formData: {
|
333 | request: {
|
334 | value: JSON.stringify({
|
335 | products: productsWithData
|
336 | }),
|
337 | options: {
|
338 | contentType: 'application/json'
|
339 | }
|
340 | },
|
341 | license: {
|
342 | value: licenseFileBuffer,
|
343 | options: {
|
344 | filename: DEFAULT_LICENSE_FILENAME,
|
345 | contentType: 'application/octet-stream'
|
346 | }
|
347 | }
|
348 | },
|
349 | resolveWithFullResponse: true,
|
350 | simple: false
|
351 | });
|
352 | })
|
353 | .catch(error => {
|
354 | if (error.hasOwnProperty('solution')) {
|
355 | throw error;
|
356 | }
|
357 |
|
358 | throw new Error(`Could not get response from license server. Please check if you have a working internet connection.`);
|
359 | })
|
360 | .then(response => {
|
361 | switch (response.statusCode) {
|
362 | case 200: {
|
363 | let data;
|
364 | try {
|
365 | data = JSON.parse(response.body);
|
366 | }
|
367 | catch (_error) {
|
368 | throw new Error('Invalid response data while getting data for products.');
|
369 | }
|
370 | return data;
|
371 | }
|
372 | case 400: {
|
373 | const error = new Error(`License is invalid. Run '${this.app.getInfo().name} license validate' to check your license.`);
|
374 | error.statusCode = response.statusCode;
|
375 | throw error;
|
376 | }
|
377 | case 401:
|
378 | case 403: {
|
379 | const error = new Error(`License is not valid (anymore). Run '${this.app.getInfo().name} license validate' to check your license.`);
|
380 | error.statusCode = response.statusCode;
|
381 | throw error;
|
382 | }
|
383 | case 404: {
|
384 | const error = new Error('Could not get requested data, because one or more of the requested products or their data does not exist.');
|
385 | error.statusCode = response.statusCode;
|
386 | throw error;
|
387 | }
|
388 | default: {
|
389 | const error = new Error(`Error while getting data for products (${response.statusCode}).`);
|
390 | error.statusCode = response.statusCode;
|
391 | throw error;
|
392 | }
|
393 | }
|
394 | });
|
395 | }
|
396 | }
|
397 |
|
398 | module.exports = FdtLicense;
|