UNPKG

11.9 kBJavaScriptView Raw
1'use strict';
2
3const fs = require('fs');
4const os = require('os');
5const path = require('path');
6const util = require('util');
7
8const readFile = util.promisify(fs.readFile);
9const writeFile = util.promisify(fs.writeFile);
10
11const getParentDirectoryContainingFileSync = require('./getParentDirectoryContainingFileSync');
12
13const DEFAULT_LICENSE_FILENAME = 'fonto.lic';
14
15const MINIMUM_LICENSE_VERSION = 1.0;
16
17const LICENSE_FILE_ERROR_SOLUTION = 'Please check if you have a license file installed, or contact support for obtaining a license file.';
18
19class 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 * Determine the license file path.
30 *
31 * @return {string|null}
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 // Do nothing
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 // key
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 // licensee
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 // products
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 // version
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 * Load the local license file from disk and parse it.
114 *
115 * @return {Object} The license load data.
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 * Get all (public) license information.
150 *
151 * @return {Object|null}
152 * @return {string} .licensee
153 * @return {Array<Object>} .products
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 * Get a buffer for the license file.
169 *
170 * @return {Promise<Buffer>}
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 * Check if specific product licenses are present. This does not validate remotely.
182 *
183 * Set .setRequiresLicenseValidation() on your command first, if you need to make sure the
184 * license is up to date.
185 *
186 * @param {Array<string>} productIds
187 *
188 * @return {boolean}
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 * Check if there is a license file.
202 *
203 * @return {boolean} Returns true if there is a license file, throws otherwise.
204 */
205 ensureLicenseFileExists () {
206 if (!this._license.data) {
207 throw this._license.error;
208 }
209
210 return true;
211 }
212
213 /**
214 * Ensure the user has the specified products licenses by checking the license file.
215 *
216 * Validate the license with .validateAndUpdateLicenseFile() first if you need to make sure the
217 * license is up to date.
218 *
219 * @param {Array<string>} productIds
220 *
221 * @return {boolean} Returns true if the user has the specified products licenses, throws otherwise.
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 * Validate the license by checking for validity online. This will also update the local license
241 * file in case the license is changed remotely.
242 *
243 * @return {Promise<boolean>}
244 */
245 validateAndUpdateLicenseFile () {
246 return this.getLicenseFileBuffer()
247 .then(licenseFileBuffer => {
248 const requestPromise = require('request-promise-native');
249 // Remotely validate the license file.
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 // Validate response before writing to disk.
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 // Update license file on disk.
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 // Update license file in memory.
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 * Get data for a specific product(s). This can be, for example, credentials for specific systems or urls for specific systems.
320 *
321 * @param {Object} productsWithData
322 * @param {Object} productsWithData.<productId>
323 *
324 * @return {Promise<Object>} An Object containing the data for the products, in the same format as the request, but filled with response values instead of request values.
325 */
326 getDataForProducts (productsWithData) {
327 return this.getLicenseFileBuffer()
328 .then(licenseFileBuffer => {
329 const requestPromise = require('request-promise-native');
330 // Remotely get the required data.
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
398module.exports = FdtLicense;