@soundworks/core 3.5.1 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server/Server.js CHANGED
@@ -5,28 +5,32 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = void 0;
7
7
 
8
- var _events = _interopRequireDefault(require("events"));
9
-
10
8
  var _fs = _interopRequireDefault(require("fs"));
11
9
 
12
10
  var _http = _interopRequireDefault(require("http"));
13
11
 
14
12
  var _https = _interopRequireDefault(require("https"));
15
13
 
16
- var _lodash = _interopRequireDefault(require("lodash.merge"));
17
-
18
14
  var _path = _interopRequireDefault(require("path"));
19
15
 
20
- var _pem = _interopRequireDefault(require("pem"));
21
-
22
16
  var _os = _interopRequireDefault(require("os"));
23
17
 
24
- var _polka = _interopRequireDefault(require("polka"));
18
+ var _crypto = require("crypto");
25
19
 
26
- var _serveStatic = _interopRequireDefault(require("serve-static"));
20
+ var _chalk = _interopRequireDefault(require("chalk"));
27
21
 
28
22
  var _compression = _interopRequireDefault(require("compression"));
29
23
 
24
+ var _keyv = _interopRequireDefault(require("keyv"));
25
+
26
+ var _keyvFile = _interopRequireDefault(require("keyv-file"));
27
+
28
+ var _lodash = _interopRequireDefault(require("lodash.merge"));
29
+
30
+ var _pem = _interopRequireDefault(require("pem"));
31
+
32
+ var _polka = _interopRequireDefault(require("polka"));
33
+
30
34
  var _Client = _interopRequireDefault(require("./Client.js"));
31
35
 
32
36
  var _PluginManager = _interopRequireDefault(require("./PluginManager.js"));
@@ -35,17 +39,11 @@ var _Sockets = _interopRequireDefault(require("./Sockets.js"));
35
39
 
36
40
  var _SharedStateManagerServer = _interopRequireDefault(require("../common/SharedStateManagerServer.js"));
37
41
 
38
- var _logger = _interopRequireDefault(require("./utils/logger.js"));
39
-
40
- var _keyv = _interopRequireDefault(require("keyv"));
41
-
42
- var _keyvFile = _interopRequireDefault(require("keyv-file"));
42
+ var _logger = _interopRequireDefault(require("../common/logger.js"));
43
43
 
44
44
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
45
45
 
46
46
  let _dbNamespaces = new Set();
47
-
48
- let _dbStore = null;
49
47
  /**
50
48
  * Server side entry point for a `soundworks` application.
51
49
  *
@@ -64,6 +62,7 @@ let _dbStore = null;
64
62
  * soundworks.server.start();
65
63
  */
66
64
 
65
+
67
66
  class Server {
68
67
  constructor() {
69
68
  /**
@@ -141,8 +140,13 @@ class Server {
141
140
  * @param {String}
142
141
  */
143
142
 
144
- this.templateDirectory = null; // private stuff
143
+ this.templateDirectory = null;
144
+ /**
145
+ * If https is required, will contain informations about the certificates
146
+ * (self-signed, validity dates, etc.)
147
+ */
145
148
 
149
+ this.httpsInfos = null;
146
150
  /**
147
151
  * Required activities that must be started. Only used in Experience
148
152
  * and Plugin - do not expose.
@@ -150,13 +154,6 @@ class Server {
150
154
  */
151
155
 
152
156
  this.activities = new Set();
153
- /**
154
- * Key and certificates (may be generated and self-signed) for https server.
155
- * @todo - put in config...
156
- * @private
157
- */
158
-
159
- this._httpsInfos = null;
160
157
  /**
161
158
  * Mapping between a `clientType` and its related activities.
162
159
  * @private
@@ -170,14 +167,15 @@ class Server {
170
167
  */
171
168
 
172
169
  this._routes = {};
173
- /**
174
- * @private
175
- */
170
+ /** @private */
176
171
 
177
172
  this._htmlTemplateConfig = {
178
173
  engine: null,
179
174
  directory: null
180
175
  };
176
+ /** @private */
177
+
178
+ this._listeners = new Map();
181
179
  }
182
180
  /**
183
181
  *
@@ -225,7 +223,7 @@ class Server {
225
223
  */
226
224
 
227
225
 
228
- async init(config, clientConfigFunction = (clientType, serverConfig, httpRequest) => ({
226
+ async init(config, clientConfigFunction = (clientType, _serverConfig, _httpRequest) => ({
229
227
  clientType
230
228
  })) {
231
229
  const defaultConfig = {
@@ -300,6 +298,7 @@ class Server {
300
298
  });
301
299
  }
302
300
 
301
+ await this._emit('inited');
303
302
  return Promise.resolve();
304
303
  }
305
304
  /**
@@ -310,222 +309,255 @@ class Server {
310
309
 
311
310
 
312
311
  async start() {
313
- try {
314
- // ------------------------------------------------------------
315
- // init acitvities
316
- // ------------------------------------------------------------
317
- // we should at least have one activity registered for each client here
318
- // i.e. the Experience so it's ok
319
- this.activities.forEach(activity => {
320
- activity.clientTypes.forEach(clientType => {
321
- if (!this._clientTypeActivitiesMap[clientType]) {
322
- this._clientTypeActivitiesMap[clientType] = new Set();
323
- }
312
+ // ------------------------------------------------------------
313
+ // init acitvities
314
+ // ------------------------------------------------------------
315
+ // we should at least have one activity registered for each client here
316
+ // i.e. the Experience so it's ok
317
+ this.activities.forEach(activity => {
318
+ activity.clientTypes.forEach(clientType => {
319
+ if (!(clientType in this.config.app.clients)) {
320
+ throw new Error(`[soundworks:core] no config found for client type ${clientType}`);
321
+ }
324
322
 
325
- this._clientTypeActivitiesMap[clientType].add(activity);
326
- });
327
- }); // start http server
323
+ if (!this._clientTypeActivitiesMap[clientType]) {
324
+ this._clientTypeActivitiesMap[clientType] = new Set();
325
+ }
328
326
 
329
- const useHttps = this.config.env.useHttps || false;
330
- return Promise.resolve() // ------------------------------------------------------------
331
- // create HTTP(S) SERVER
332
- // ------------------------------------------------------------
333
- .then(() => {
334
- // create http server
335
- if (!useHttps) {
336
- const httpServer = _http.default.createServer();
327
+ this._clientTypeActivitiesMap[clientType].add(activity);
328
+ });
329
+ }); // start http server
330
+
331
+ const useHttps = this.config.env.useHttps || false;
332
+ return Promise.resolve() // ------------------------------------------------------------
333
+ // create HTTP(S) SERVER
334
+ // ------------------------------------------------------------
335
+ .then(async () => {
336
+ // create http server
337
+ if (!useHttps) {
338
+ const httpServer = _http.default.createServer();
339
+
340
+ return Promise.resolve(httpServer);
341
+ } else {
342
+ const httpsInfos = this.config.env.httpsInfos;
337
343
 
338
- return Promise.resolve(httpServer);
339
- } else {
340
- const httpsInfos = this.config.env.httpsInfos;
344
+ if (httpsInfos.key && httpsInfos.cert) {
345
+ // use given certificate
346
+ try {
347
+ const key = _fs.default.readFileSync(httpsInfos.key);
341
348
 
342
- if (httpsInfos.key && httpsInfos.cert) {
343
- // use given certificate
344
- try {
345
- const key = _fs.default.readFileSync(httpsInfos.key);
349
+ const cert = _fs.default.readFileSync(httpsInfos.cert); // this fails with self-signed certificates for whatever reason...
346
350
 
347
- const cert = _fs.default.readFileSync(httpsInfos.cert);
348
351
 
349
- this._httpsInfos = {
350
- key,
351
- cert
352
- };
352
+ const x509 = new _crypto.X509Certificate(cert);
353
+ this.httpsInfos = {
354
+ selfSigned: false,
355
+ CN: x509.subject.split('=')[1],
356
+ altNames: x509.subjectAltName.split(',').map(e => e.trim().split(':')[1]),
357
+ validFrom: x509.validFrom,
358
+ validTo: x509.validTo
359
+ };
353
360
 
354
- const httpsServer = _https.default.createServer(this._httpsInfos);
361
+ const httpsServer = _https.default.createServer({
362
+ key,
363
+ cert
364
+ });
355
365
 
356
- return Promise.resolve(httpsServer);
357
- } catch (err) {
358
- console.error(`Invalid certificate files, please check your:
366
+ return Promise.resolve(httpsServer);
367
+ } catch (err) {
368
+ _logger.default.error(`
369
+ Invalid certificate files, please check your:
359
370
  - key file: ${httpsInfos.key}
360
371
  - cert file: ${httpsInfos.cert}
361
- `);
362
- throw err;
363
- }
364
- } else {
365
- return new Promise(async (resolve, reject) => {
366
- const key = await this.db.get('httpsKey');
367
- const cert = await this.db.get('httpsCert');
372
+ `);
368
373
 
369
- if (key && cert) {
370
- this._httpsInfos = {
371
- key,
372
- cert
373
- };
374
+ throw err;
375
+ }
376
+ } else {
377
+ const key = await this.db.get('httpsKey');
378
+ const cert = await this.db.get('httpsCert');
374
379
 
375
- const httpsServer = _https.default.createServer(this._httpsInfos);
380
+ if (key && cert) {
381
+ this.httpsInfos = {
382
+ selfSigned: true
383
+ };
376
384
 
377
- resolve(httpsServer);
378
- } else {
379
- // generate certificate on the fly (for development purposes)
380
- _pem.default.createCertificate({
381
- days: 1,
382
- selfSigned: true
383
- }, async (err, keys) => {
384
- if (err) {
385
- return console.error(err.stack);
386
- }
385
+ const httpsServer = _https.default.createServer({
386
+ key,
387
+ cert
388
+ });
387
389
 
388
- this._httpsInfos = {
389
- key: keys.serviceKey,
390
- cert: keys.certificate
391
- };
392
- await this.db.set('httpsKey', this._httpsInfos.key);
393
- await this.db.set('httpsCert', this._httpsInfos.cert);
390
+ return Promise.resolve(httpsServer);
391
+ } else {
392
+ return new Promise(resolve => {
393
+ // generate certificate on the fly (for development purposes)
394
+ _pem.default.createCertificate({
395
+ days: 1,
396
+ selfSigned: true
397
+ }, async (err, keys) => {
398
+ if (err) {
399
+ _logger.default.error(err.stack);
400
+
401
+ return;
402
+ }
394
403
 
395
- const httpsServer = _https.default.createServer(this._httpsInfos);
404
+ const key = keys.serviceKey;
405
+ const cert = keys.certificate;
406
+ this.httpsInfos = {
407
+ selfSigned: true
408
+ }; // we store the generated cert so that we don't have to re-accept
409
+ // the cert each time the server restarts
396
410
 
397
- resolve(httpsServer);
411
+ await this.db.set('httpsKey', key);
412
+ await this.db.set('httpsCert', cert);
413
+
414
+ const httpsServer = _https.default.createServer({
415
+ key,
416
+ cert
398
417
  });
399
- }
418
+
419
+ resolve(httpsServer);
420
+ });
400
421
  });
401
422
  }
402
423
  }
403
- }).then(httpServer => {
404
- this.httpServer = httpServer;
405
- this.router.server = httpServer;
406
- return Promise.resolve();
407
- }).then(() => {
408
- let nodeOnly = true; // do not throw if no browser clients are defined, very usefull for
409
- // cleaning tests in particular
410
-
411
- for (let clientType in this.config.app.clients) {
412
- if (this.config.app.clients[clientType].target === 'browser') {
413
- nodeOnly = false;
414
- }
424
+ }
425
+ }).then(httpServer => {
426
+ this.httpServer = httpServer;
427
+ this.router.server = httpServer;
428
+ return Promise.resolve();
429
+ }).then(() => {
430
+ let nodeOnly = true; // do not throw if no browser clients are defined, very usefull for
431
+ // cleaning tests in particular
432
+
433
+ for (let clientType in this.config.app.clients) {
434
+ if (this.config.app.clients[clientType].target === 'browser') {
435
+ nodeOnly = false;
415
436
  }
437
+ }
416
438
 
417
- if (!nodeOnly) {
418
- if (this.templateEngine === null) {
419
- throw new Error('Undefined "server.templateEngine": please provide a valid template engine');
420
- }
439
+ if (!nodeOnly) {
440
+ if (this.templateEngine === null) {
441
+ throw new Error('Undefined "server.templateEngine": please provide a valid template engine');
442
+ }
421
443
 
422
- if (this.templateDirectory === null) {
423
- throw new Error('Undefined "server.templateDirectory": please provide a valid template directory');
424
- }
425
- } // ------------------------------------------------------------
426
- // INIT ROUTING
427
- // ------------------------------------------------------------
444
+ if (this.templateDirectory === null) {
445
+ throw new Error('Undefined "server.templateDirectory": please provide a valid template directory');
446
+ }
447
+ } // ------------------------------------------------------------
448
+ // INIT ROUTING
449
+ // ------------------------------------------------------------
428
450
 
429
451
 
430
- _logger.default.title(`configured clients and routing`);
452
+ _logger.default.title(`configured clients and routing`);
431
453
 
432
- const routes = [];
433
- let defaultClientType = null;
454
+ const routes = [];
455
+ let defaultClientType = null;
434
456
 
435
- for (let clientType in this.config.app.clients) {
436
- if (this.config.app.clients[clientType].default === true) {
437
- defaultClientType = clientType;
438
- }
439
- } // we must open default route last
457
+ for (let clientType in this.config.app.clients) {
458
+ if (this.config.app.clients[clientType].default === true) {
459
+ defaultClientType = clientType;
460
+ }
461
+ } // we must open default route last
440
462
 
441
463
 
442
- for (let clientType in this._clientTypeActivitiesMap) {
443
- // do nothing if client type is not registered in config
444
- if (!(clientType in this.config.app.clients)) {
445
- continue;
446
- }
464
+ for (let clientType in this._clientTypeActivitiesMap) {
465
+ // do nothing if client type is not registered in config
466
+ if (!(clientType in this.config.app.clients)) {
467
+ continue;
468
+ }
447
469
 
448
- if (clientType !== defaultClientType) {
449
- const clientTarget = this.config.app.clients[clientType].target;
470
+ if (clientType !== defaultClientType) {
471
+ const clientTarget = this.config.app.clients[clientType].target;
450
472
 
451
- const path = this._openClientRoute(clientType, clientTarget, this.router);
473
+ const path = this._openClientRoute(clientType, clientTarget, this.router);
452
474
 
453
- routes.push({
454
- clientType,
455
- path
456
- });
457
- }
458
- } // open default route last
475
+ routes.push({
476
+ clientType,
477
+ path
478
+ });
479
+ }
480
+ } // open default route last
459
481
 
460
482
 
461
- for (let clientType in this._clientTypeActivitiesMap) {
462
- // do nothing if client type is not registered in config
463
- if (!(clientType in this.config.app.clients)) {
464
- continue;
465
- }
483
+ for (let clientType in this._clientTypeActivitiesMap) {
484
+ // do nothing if client type is not registered in config
485
+ if (!(clientType in this.config.app.clients)) {
486
+ continue;
487
+ }
466
488
 
467
- if (clientType === defaultClientType) {
468
- const clientTarget = this.config.app.clients[clientType].target;
489
+ if (clientType === defaultClientType) {
490
+ const clientTarget = this.config.app.clients[clientType].target;
469
491
 
470
- const path = this._openClientRoute(clientType, clientTarget, this.router, true);
492
+ const path = this._openClientRoute(clientType, clientTarget, this.router, true);
471
493
 
472
- routes.unshift({
473
- clientType,
474
- path
475
- });
476
- }
494
+ routes.unshift({
495
+ clientType,
496
+ path
497
+ });
477
498
  }
499
+ }
478
500
 
479
- _logger.default.clientConfigAndRouting(routes, this.config);
480
-
481
- return Promise.resolve();
482
- }).then(() => {
483
- // ------------------------------------------------------------
484
- // START SOCKET SERVER
485
- // ------------------------------------------------------------
486
- this.sockets.start(this.httpServer, this.config.env.websockets, (clientType, socket) => this._onSocketConnection(clientType, socket));
487
- return Promise.resolve();
488
- }).then(async () => {
489
- // ------------------------------------------------------------
490
- // START SERVICE MANAGER
491
- // ------------------------------------------------------------
492
- return this.pluginManager.start();
493
- }).then(() => {
494
- // ------------------------------------------------------------
495
- // START HTTP SERVER
496
- // ------------------------------------------------------------
497
- return new Promise((resolve, reject) => {
498
- const port = this.config.env.port;
499
- const useHttps = this.config.env.useHttps || false;
500
- const protocol = useHttps ? 'https' : 'http';
501
-
502
- const ifaces = _os.default.networkInterfaces();
503
-
504
- this.router.listen(port, () => {
505
- _logger.default.title(`${protocol} server listening on`);
506
-
507
- Object.keys(ifaces).forEach(dev => {
508
- ifaces[dev].forEach(details => {
509
- if (details.family === 'IPv4') {
510
- _logger.default.ip(protocol, details.address, port);
511
- }
512
- });
501
+ _logger.default.clientConfigAndRouting(routes, this.config);
502
+
503
+ return Promise.resolve();
504
+ }).then(() => {
505
+ // ------------------------------------------------------------
506
+ // START SOCKET SERVER
507
+ // ------------------------------------------------------------
508
+ this.sockets.start(this.httpServer, this.config.env.websockets, (clientType, socket) => this._onSocketConnection(clientType, socket));
509
+ return Promise.resolve();
510
+ }).then(async () => {
511
+ // ------------------------------------------------------------
512
+ // START SERVICE MANAGER
513
+ // ------------------------------------------------------------
514
+ return this.pluginManager.start();
515
+ }).then(() => {
516
+ // ------------------------------------------------------------
517
+ // START HTTP SERVER
518
+ // ------------------------------------------------------------
519
+ return new Promise(resolve => {
520
+ const port = this.config.env.port;
521
+ const useHttps = this.config.env.useHttps || false;
522
+ const protocol = useHttps ? 'https' : 'http';
523
+
524
+ const ifaces = _os.default.networkInterfaces();
525
+
526
+ this.router.listen(port, async () => {
527
+ _logger.default.title(`${protocol} server listening on`);
528
+
529
+ Object.keys(ifaces).forEach(dev => {
530
+ ifaces[dev].forEach(details => {
531
+ if (details.family === 'IPv4') {
532
+ _logger.default.ip(protocol, details.address, port);
533
+ }
513
534
  });
514
- resolve();
515
535
  });
536
+
537
+ if (this.httpsInfos !== null) {
538
+ _logger.default.title(`https certificates infos`);
539
+
540
+ if (this.httpsInfos.selfSigned) {
541
+ _logger.default.log(` self-signed: ${this.httpsInfos.selfSigned ? 'true' : 'false'}`);
542
+ } else {
543
+ _logger.default.log(` valid from: ${this.httpsInfos.validFrom}`);
544
+
545
+ _logger.default.log(` valid to: ${this.httpsInfos.validTo}`);
546
+ }
547
+ }
548
+
549
+ await this._emit('started');
550
+ resolve();
516
551
  });
517
552
  });
518
- await this.pluginManager.start();
519
- return Promise.resolve();
520
- } catch (err) {
521
- console.error(err);
522
- }
553
+ });
523
554
  } // @todo - handle gracefull close of the server
524
555
 
525
556
 
526
557
  async stop() {
527
558
  this.sockets.terminate();
528
559
  this.httpServer.close();
560
+ await this._emit('stopped');
529
561
  return Promise.resolve();
530
562
  }
531
563
  /**
@@ -618,39 +650,47 @@ class Server {
618
650
  const activities = this._clientTypeActivitiesMap[clientType];
619
651
 
620
652
  if (activities == undefined) {
621
- throw new Error(`No activity registered for "${clientType}" clients, make sure an Experience has been initialized for this client (see src/server/index.js)`);
653
+ _logger.default.error(`[soundworks:core] No activity registered for "${clientType}", make sure an Experience has been created for this client`);
622
654
  }
623
655
 
624
656
  socket.addListener('close', () => {
625
657
  // clean sockets
626
658
  socket.terminate(); // remove client from activities
627
659
 
628
- activities.forEach(activity => activity.disconnect(client)); // destroy client
660
+ if (activities) {
661
+ activities.forEach(activity => activity.disconnect(client));
662
+ } // destroy client
663
+
629
664
 
630
665
  client.destroy();
631
666
  });
632
667
  socket.addListener('s:client:handshake', data => {
633
- // in development, if plugin required client-side but not server-side,
634
- // complain properly client-side.
635
- if (this.config.env.type !== 'production') {
636
- // check coherence between client-side and server-side plugin requirements
637
- const clientRequiredPlugins = data.requiredPlugins || [];
638
- const serverRequiredPlugins = this.pluginManager.getRequiredPlugins(clientType);
639
- const missingPlugins = [];
640
- clientRequiredPlugins.forEach(pluginId => {
641
- if (serverRequiredPlugins.indexOf(pluginId) === -1) {
642
- missingPlugins.push(pluginId);
643
- }
644
- });
645
-
646
- if (missingPlugins.length > 0) {
647
- const err = {
648
- type: 'plugins',
649
- data: missingPlugins
650
- };
651
- socket.send('s:client:error', err);
652
- return;
668
+ // client should throw error if no activity registered server-side (cf. #16)
669
+ if (activities === undefined) {
670
+ const err = {
671
+ type: 'no-activity'
672
+ };
673
+ socket.send('s:client:error', err);
674
+ return;
675
+ } // check coherence between client-side and server-side plugin requirements
676
+
677
+
678
+ const clientRequiredPlugins = data.requiredPlugins || [];
679
+ const serverRequiredPlugins = this.pluginManager.getRequiredPlugins(clientType);
680
+ const missingPlugins = [];
681
+ clientRequiredPlugins.forEach(pluginId => {
682
+ if (serverRequiredPlugins.indexOf(pluginId) === -1) {
683
+ missingPlugins.push(pluginId);
653
684
  }
685
+ });
686
+
687
+ if (missingPlugins.length > 0) {
688
+ const err = {
689
+ type: 'plugins',
690
+ data: missingPlugins
691
+ };
692
+ socket.send('s:client:error', err);
693
+ return;
654
694
  }
655
695
 
656
696
  activities.forEach(activity => activity.connect(client));
@@ -699,10 +739,46 @@ class Server {
699
739
  namespace,
700
740
  store
701
741
  });
702
- db.on('error', err => console.log(chalk.red('[soundworks:core] db ${namespace} error:'), err));
742
+ db.on('error', err => _logger.default.error('[soundworks:core] db ${namespace} error: ${err}'));
703
743
  return db;
704
744
  }
705
745
 
746
+ addListener(channel, callback) {
747
+ if (!this._listeners.has(channel)) {
748
+ const listeners = new Set();
749
+
750
+ this._listeners.set(channel, listeners);
751
+ }
752
+
753
+ const listeners = this._listeners.get(channel);
754
+
755
+ listeners.add(callback);
756
+ }
757
+
758
+ removeListener(channel, callback) {
759
+ if (this._listeners.has(channel)) {
760
+ const listeners = this._listeners.get(channel);
761
+
762
+ listeners.delete(callback);
763
+
764
+ if (listeners.size === 0) {
765
+ this._listeners.delete(channel);
766
+ }
767
+ }
768
+ }
769
+ /** @private */
770
+
771
+
772
+ async _emit(channel) {
773
+ if (this._listeners.has(channel)) {
774
+ const listeners = this._listeners.get(channel);
775
+
776
+ for (let callback of listeners) {
777
+ await callback();
778
+ }
779
+ }
780
+ }
781
+
706
782
  }
707
783
 
708
784
  var _default = Server;