@soundworks/core 4.0.0-alpha.2 → 4.0.0-alpha.21
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/.eslintrc +0 -1
- package/.jsdoc +57 -0
- package/CHANGELOG.md +33 -0
- package/README.md +17 -51
- package/package.json +24 -31
- package/src/client/Client.js +13 -7
- package/src/client/Context.js +12 -13
- package/src/client/ContextManager.js +1 -1
- package/src/client/PluginManager.js +1 -1
- package/src/client/SharedStateCollection.js +38 -0
- package/src/client/Socket.js +80 -44
- package/src/common/BasePlugin.js +31 -21
- package/src/common/BasePluginManager.js +34 -19
- package/src/common/BaseSharedState.js +304 -79
- package/src/common/BaseSharedStateCollection.js +324 -0
- package/src/common/BaseStateManager.js +353 -49
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +92 -30
- package/src/common/PromiseStore.js +53 -0
- package/src/common/SharedStatePrivate.js +87 -46
- package/src/common/constants.js +14 -1
- package/src/common/sockets-utils.js +3 -4
- package/src/server/Client.js +7 -2
- package/src/server/ContextManager.js +4 -4
- package/src/server/PluginManager.js +1 -1
- package/src/server/Server.js +263 -43
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +39 -0
- package/src/server/Socket.js +55 -49
- package/src/server/Sockets.js +33 -11
- package/src/server/StateManager.js +187 -100
- package/src/server/audit-network-latency.worker.js +16 -4
- package/src/server/crypto.js +37 -0
- package/src/common/promise-store.js +0 -25
- package/src/common/utils.js +0 -22
- package/tsconfig.json +0 -21
package/src/server/Server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import http from 'node:http';
|
|
3
4
|
import https from 'node:https';
|
|
@@ -5,17 +6,19 @@ import path from 'node:path';
|
|
|
5
6
|
import os from 'node:os';
|
|
6
7
|
import { X509Certificate, createPrivateKey } from 'node:crypto';
|
|
7
8
|
|
|
9
|
+
import { isPlainObject, idGenerator, getTime } from '@ircam/sc-utils';
|
|
8
10
|
import chalk from 'chalk';
|
|
9
11
|
import compression from 'compression';
|
|
10
12
|
import express from 'express';
|
|
11
|
-
import
|
|
13
|
+
import equal from 'fast-deep-equal';
|
|
12
14
|
import Keyv from 'keyv';
|
|
13
15
|
import { KeyvFile } from 'keyv-file';
|
|
14
|
-
import merge from 'lodash.
|
|
16
|
+
import merge from 'lodash/merge.js';
|
|
15
17
|
import pem from 'pem';
|
|
16
18
|
import compile from 'template-literal';
|
|
17
19
|
|
|
18
20
|
import auditSchema from './audit-schema.js';
|
|
21
|
+
import { encryptData, decryptData } from './crypto.js';
|
|
19
22
|
import Client from './Client.js';
|
|
20
23
|
import ContextManager from './ContextManager.js';
|
|
21
24
|
import PluginManager from './PluginManager.js';
|
|
@@ -23,12 +26,14 @@ import StateManager from './StateManager.js';
|
|
|
23
26
|
import Sockets from './Sockets.js';
|
|
24
27
|
import logger from '../common/logger.js';
|
|
25
28
|
import {
|
|
29
|
+
SERVER_ID,
|
|
26
30
|
CLIENT_HANDSHAKE_REQUEST,
|
|
27
31
|
CLIENT_HANDSHAKE_RESPONSE,
|
|
28
32
|
CLIENT_HANDSHAKE_ERROR,
|
|
29
33
|
AUDIT_STATE_NAME,
|
|
30
34
|
} from '../common/constants.js';
|
|
31
35
|
|
|
36
|
+
|
|
32
37
|
let _dbNamespaces = new Set();
|
|
33
38
|
|
|
34
39
|
/**
|
|
@@ -73,6 +78,8 @@ const DEFAULT_CONFIG = {
|
|
|
73
78
|
},
|
|
74
79
|
};
|
|
75
80
|
|
|
81
|
+
const TOKEN_VALID_DURATION = 20; // sec
|
|
82
|
+
|
|
76
83
|
// set terminal title
|
|
77
84
|
/** @private */
|
|
78
85
|
function setTerminalTitle(server) {
|
|
@@ -156,8 +163,8 @@ class Server {
|
|
|
156
163
|
throw new Error(`[soundworks:Server] Invalid argument for Server constructor, config should be an object`);
|
|
157
164
|
}
|
|
158
165
|
/**
|
|
159
|
-
* Given config object merged with the following defaults:
|
|
160
|
-
*
|
|
166
|
+
* @description Given config object merged with the following defaults:
|
|
167
|
+
* @example
|
|
161
168
|
* {
|
|
162
169
|
* env: {
|
|
163
170
|
* type: 'development',
|
|
@@ -178,7 +185,6 @@ class Server {
|
|
|
178
185
|
* clients: {},
|
|
179
186
|
* }
|
|
180
187
|
* }
|
|
181
|
-
* ```
|
|
182
188
|
*/
|
|
183
189
|
this.config = merge({}, DEFAULT_CONFIG, config);
|
|
184
190
|
|
|
@@ -210,11 +216,11 @@ class Server {
|
|
|
210
216
|
throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, should contain both "cert" and "key" entries`);
|
|
211
217
|
}
|
|
212
218
|
// @todo - move that to constructor
|
|
213
|
-
if (!fs.existsSync(httpsInfos.cert)) {
|
|
219
|
+
if (httpsInfos.cert !== null && !fs.existsSync(httpsInfos.cert)) {
|
|
214
220
|
throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, "cert" file not found`);
|
|
215
221
|
}
|
|
216
222
|
|
|
217
|
-
if (!fs.existsSync(httpsInfos.key)) {
|
|
223
|
+
if (httpsInfos.key !== null && !fs.existsSync(httpsInfos.key)) {
|
|
218
224
|
throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, "key" file not found`);
|
|
219
225
|
}
|
|
220
226
|
}
|
|
@@ -236,8 +242,10 @@ class Server {
|
|
|
236
242
|
* // expose assets located in the `soundfiles` directory on the network
|
|
237
243
|
* server.router.use('/soundfiles', express.static('soundfiles')));
|
|
238
244
|
*/
|
|
245
|
+
// @note: we use express() instead of express.Router() because all 404 and
|
|
246
|
+
// error stuff is handled by default
|
|
239
247
|
this.router = express();
|
|
240
|
-
// compression (must be set before
|
|
248
|
+
// compression (must be set before express.static())
|
|
241
249
|
this.router.use(compression());
|
|
242
250
|
|
|
243
251
|
/**
|
|
@@ -312,7 +320,15 @@ class Server {
|
|
|
312
320
|
/** @private */
|
|
313
321
|
this._onStatusChangeCallbacks = new Set();
|
|
314
322
|
/** @private */
|
|
323
|
+
this._onClientConnectCallbacks = new Set();
|
|
324
|
+
/** @private */
|
|
325
|
+
this._onClientDisconnectCallbacks = new Set();
|
|
326
|
+
/** @private */
|
|
315
327
|
this._auditState = null;
|
|
328
|
+
/** @private */
|
|
329
|
+
this._pendingConnectionTokens = new Set();
|
|
330
|
+
/** @private */
|
|
331
|
+
this._trustedClients = new Set();
|
|
316
332
|
|
|
317
333
|
// register audit state schema
|
|
318
334
|
this.stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema);
|
|
@@ -321,6 +337,15 @@ class Server {
|
|
|
321
337
|
setTerminalTitle(this);
|
|
322
338
|
}
|
|
323
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Id of the server, a constant set to -1
|
|
342
|
+
* @type {Number}
|
|
343
|
+
* @readonly
|
|
344
|
+
*/
|
|
345
|
+
get id() {
|
|
346
|
+
return SERVER_ID;
|
|
347
|
+
}
|
|
348
|
+
|
|
324
349
|
/**
|
|
325
350
|
* The `init` method is part of the initialization lifecycle of the `soundworks`
|
|
326
351
|
* server. Most of the time, the `init` method will be implicitly called by the
|
|
@@ -347,6 +372,11 @@ class Server {
|
|
|
347
372
|
* await server.start(); // init is called implicitely
|
|
348
373
|
*/
|
|
349
374
|
async init() {
|
|
375
|
+
// ------------------------------------------------------------
|
|
376
|
+
// INIT STATE MANAGER
|
|
377
|
+
// ------------------------------------------------------------
|
|
378
|
+
this.stateManager.init(SERVER_ID, new EventEmitter());
|
|
379
|
+
|
|
350
380
|
const numClients = {};
|
|
351
381
|
for (let name in this.config.app.clients) {
|
|
352
382
|
numClients[name] = 0;
|
|
@@ -357,11 +387,24 @@ class Server {
|
|
|
357
387
|
|
|
358
388
|
// basic http authentication
|
|
359
389
|
if (this.config.env.auth) {
|
|
360
|
-
|
|
390
|
+
const ids = idGenerator();
|
|
391
|
+
|
|
392
|
+
const soundworksAuth = (req, res, next) => {
|
|
393
|
+
let role = null;
|
|
394
|
+
|
|
395
|
+
for (let [_role, config] of Object.entries(this.config.app.clients)) {
|
|
396
|
+
if (req.path === config.route) {
|
|
397
|
+
role = _role;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// route that are not client entry points just pass through the middleware
|
|
402
|
+
if (role === null) {
|
|
403
|
+
next();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
361
406
|
|
|
362
|
-
const isProtected = this.
|
|
363
|
-
.map(type => req.path.endsWith(`/${type}`))
|
|
364
|
-
.reduce((acc, value) => acc || value, false);
|
|
407
|
+
const isProtected = this.isProtected(role);
|
|
365
408
|
|
|
366
409
|
if (isProtected) {
|
|
367
410
|
// authentication middleware
|
|
@@ -373,35 +416,57 @@ class Server {
|
|
|
373
416
|
// verify login and password are set and correct
|
|
374
417
|
if (login && password && login === auth.login && password === auth.password) {
|
|
375
418
|
// -> access granted...
|
|
419
|
+
// generate token for web socket to check connections
|
|
420
|
+
const id = ids.next().value;
|
|
421
|
+
const ip = req.ip;
|
|
422
|
+
const time = getTime();
|
|
423
|
+
const token = { id, ip, time };
|
|
424
|
+
const encryptedToken = encryptData(token);
|
|
425
|
+
|
|
426
|
+
this._pendingConnectionTokens.add(encryptedToken);
|
|
427
|
+
|
|
428
|
+
setTimeout(() => {
|
|
429
|
+
this._pendingConnectionTokens.delete(encryptedToken);
|
|
430
|
+
}, TOKEN_VALID_DURATION * 1000);
|
|
431
|
+
|
|
432
|
+
// pass to the response object to be send to the client
|
|
433
|
+
res.swToken = encryptedToken;
|
|
434
|
+
|
|
376
435
|
return next();
|
|
377
436
|
}
|
|
378
437
|
|
|
379
|
-
//
|
|
438
|
+
// show login / password modal
|
|
380
439
|
res.writeHead(401, {
|
|
381
440
|
'WWW-Authenticate':'Basic',
|
|
382
441
|
'Content-Type':'text/plain',
|
|
383
442
|
});
|
|
443
|
+
|
|
384
444
|
res.end('Authentication required.');
|
|
385
445
|
} else {
|
|
386
|
-
// route not protected
|
|
446
|
+
// route is not protected
|
|
387
447
|
return next();
|
|
388
448
|
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
449
|
+
};
|
|
391
450
|
|
|
392
|
-
|
|
393
|
-
|
|
451
|
+
this.router.use(soundworksAuth);
|
|
452
|
+
}
|
|
394
453
|
|
|
395
454
|
// ------------------------------------------------------------
|
|
396
455
|
// create HTTP(S) SERVER
|
|
397
456
|
// ------------------------------------------------------------
|
|
457
|
+
const useHttps = this.config.env.useHttps || false;
|
|
458
|
+
|
|
398
459
|
if (!useHttps) {
|
|
399
460
|
this.httpServer = http.createServer(this.router);
|
|
400
461
|
} else {
|
|
401
462
|
const httpsInfos = this.config.env.httpsInfos;
|
|
463
|
+
let useSelfSigned = false;
|
|
464
|
+
|
|
465
|
+
if (!httpsInfos || equal(httpsInfos, { cert: null, key: null })) {
|
|
466
|
+
useSelfSigned = true;
|
|
467
|
+
}
|
|
402
468
|
|
|
403
|
-
|
|
404
|
-
if (httpsInfos !== null) {
|
|
469
|
+
if (!useSelfSigned) {
|
|
405
470
|
try {
|
|
406
471
|
// existance of file is checked in contructor
|
|
407
472
|
let cert = fs.readFileSync(httpsInfos.cert);
|
|
@@ -411,7 +476,7 @@ class Server {
|
|
|
411
476
|
// this fails with self-signed certificates for whatever reason...
|
|
412
477
|
try {
|
|
413
478
|
x509 = new X509Certificate(cert);
|
|
414
|
-
} catch(err) {
|
|
479
|
+
} catch (err) {
|
|
415
480
|
this._dispatchStatus('errored');
|
|
416
481
|
throw new Error(`[soundworks:Server] Invalid https cert file`);
|
|
417
482
|
}
|
|
@@ -423,7 +488,7 @@ class Server {
|
|
|
423
488
|
this._dispatchStatus('errored');
|
|
424
489
|
throw new Error(`[soundworks:Server] Invalid https key file`);
|
|
425
490
|
}
|
|
426
|
-
} catch(err) {
|
|
491
|
+
} catch (err) {
|
|
427
492
|
this._dispatchStatus('errored');
|
|
428
493
|
throw new Error(`[soundworks:Server] Invalid https key file`);
|
|
429
494
|
}
|
|
@@ -447,7 +512,7 @@ class Server {
|
|
|
447
512
|
};
|
|
448
513
|
|
|
449
514
|
this.httpServer = https.createServer({ key, cert }, this.router);
|
|
450
|
-
} catch(err) {
|
|
515
|
+
} catch (err) {
|
|
451
516
|
logger.error(`
|
|
452
517
|
Invalid certificate files, please check your:
|
|
453
518
|
- key file: ${httpsInfos.key}
|
|
@@ -591,7 +656,7 @@ Invalid certificate files, please check your:
|
|
|
591
656
|
await this.sockets.start(
|
|
592
657
|
this,
|
|
593
658
|
this.config.env.websockets,
|
|
594
|
-
(
|
|
659
|
+
(...args) => this._onSocketConnection(...args),
|
|
595
660
|
);
|
|
596
661
|
|
|
597
662
|
// ------------------------------------------------------------
|
|
@@ -648,6 +713,10 @@ Invalid certificate files, please check your:
|
|
|
648
713
|
|
|
649
714
|
await this._dispatchStatus('started');
|
|
650
715
|
|
|
716
|
+
if (this.config.env.type === 'development') {
|
|
717
|
+
logger.log(`\n> press "${chalk.bold('Ctrl + C')}" to exit`);
|
|
718
|
+
}
|
|
719
|
+
|
|
651
720
|
resolve();
|
|
652
721
|
});
|
|
653
722
|
});
|
|
@@ -706,6 +775,8 @@ Invalid certificate files, please check your:
|
|
|
706
775
|
route += `${role}`;
|
|
707
776
|
}
|
|
708
777
|
|
|
778
|
+
this.config.app.clients[role].route = route;
|
|
779
|
+
|
|
709
780
|
// define template filename: `${role}.html` or `default.html`
|
|
710
781
|
const {
|
|
711
782
|
templatePath,
|
|
@@ -722,7 +793,7 @@ Invalid certificate files, please check your:
|
|
|
722
793
|
try {
|
|
723
794
|
const stats = fs.statSync(clientTmpl);
|
|
724
795
|
template = stats.isFile() ? clientTmpl : defaultTmpl;
|
|
725
|
-
} catch(err) {
|
|
796
|
+
} catch (err) {
|
|
726
797
|
template = defaultTmpl;
|
|
727
798
|
}
|
|
728
799
|
|
|
@@ -730,15 +801,21 @@ Invalid certificate files, please check your:
|
|
|
730
801
|
|
|
731
802
|
try {
|
|
732
803
|
tmplString = fs.readFileSync(template, 'utf8');
|
|
733
|
-
} catch(err) {
|
|
804
|
+
} catch (err) {
|
|
734
805
|
throw new Error(`[soundworks:Server] html template file "${template}" not found`);
|
|
735
806
|
}
|
|
736
807
|
|
|
737
808
|
const tmpl = templateEngine.compile(tmplString);
|
|
738
|
-
|
|
739
|
-
|
|
809
|
+
|
|
810
|
+
const soundworksClientHandler = (req, res) => {
|
|
740
811
|
const data = clientConfigFunction(role, this.config, req);
|
|
741
812
|
|
|
813
|
+
// if the client has gone through the connection middleware (add succedeed),
|
|
814
|
+
// add the token to the data object
|
|
815
|
+
if (res.swToken) {
|
|
816
|
+
data.token = res.swToken;
|
|
817
|
+
}
|
|
818
|
+
|
|
742
819
|
// CORS / COOP / COEP headers for `crossOriginIsolated pages,
|
|
743
820
|
// enables `sharedArrayBuffers` and high precision timers
|
|
744
821
|
// cf. https://web.dev/why-coop-coep/
|
|
@@ -752,34 +829,78 @@ Invalid certificate files, please check your:
|
|
|
752
829
|
|
|
753
830
|
const appIndex = tmpl(data);
|
|
754
831
|
res.end(appIndex);
|
|
755
|
-
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// http request
|
|
835
|
+
router.get(route, soundworksClientHandler);
|
|
756
836
|
|
|
757
837
|
// return route infos for logging on server start
|
|
758
838
|
return route;
|
|
759
839
|
}
|
|
760
840
|
|
|
841
|
+
onClientConnect(callback) {
|
|
842
|
+
this._onClientConnectCallbacks.add(callback);
|
|
843
|
+
|
|
844
|
+
return () => this._onClientConnectCallbacks.delete(callback);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
onClientDisconnect(callback) {
|
|
848
|
+
this._onClientDisconnectCallbacks.add(callback);
|
|
849
|
+
|
|
850
|
+
return () => this._onClientDisconnectCallbacks.delete(callback);
|
|
851
|
+
}
|
|
852
|
+
|
|
761
853
|
/**
|
|
762
854
|
* Socket connection callback.
|
|
763
855
|
* @private
|
|
764
856
|
*/
|
|
765
|
-
_onSocketConnection(role, socket) {
|
|
857
|
+
_onSocketConnection(role, socket, connectionToken) {
|
|
766
858
|
const client = new Client(role, socket);
|
|
767
859
|
const roles = Object.keys(this.config.app.clients);
|
|
768
860
|
|
|
861
|
+
// this has been validated
|
|
862
|
+
if (this.isProtected(role) && this.isValidConnectionToken(connectionToken)) {
|
|
863
|
+
const { ip } = decryptData(connectionToken);
|
|
864
|
+
const newData = {
|
|
865
|
+
ip: ip,
|
|
866
|
+
id: client.id,
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const newToken = encryptData(newData);
|
|
870
|
+
|
|
871
|
+
client.token = newToken;
|
|
872
|
+
|
|
873
|
+
this._pendingConnectionTokens.delete(connectionToken);
|
|
874
|
+
this._trustedClients.add(client);
|
|
875
|
+
}
|
|
876
|
+
|
|
769
877
|
socket.addListener('close', async () => {
|
|
770
|
-
// do
|
|
878
|
+
// do nothing if client role is invalid
|
|
771
879
|
if (roles.includes(role)) {
|
|
772
880
|
// decrement audit state counter
|
|
773
881
|
const numClients = this._auditState.get('numClients');
|
|
774
882
|
numClients[role] -= 1;
|
|
775
883
|
this._auditState.set({ numClients });
|
|
776
884
|
|
|
777
|
-
//
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
885
|
+
// delete token
|
|
886
|
+
if (this._trustedClients.has(client)) {
|
|
887
|
+
this._trustedClients.delete(client);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// if something goes wrong here, the 'close' event is called again and
|
|
891
|
+
// again and again... let's just log the error and terminate the socket
|
|
892
|
+
try {
|
|
893
|
+
// clean context manager, await before cleaning state manager
|
|
894
|
+
await this.contextManager.removeClient(client);
|
|
895
|
+
// remove client from pluginManager
|
|
896
|
+
await this.pluginManager.removeClient(client);
|
|
897
|
+
// clean state manager
|
|
898
|
+
await this.stateManager.removeClient(client.id);
|
|
899
|
+
|
|
900
|
+
this._onClientDisconnectCallbacks.forEach(callback => callback(client));
|
|
901
|
+
} catch (err) {
|
|
902
|
+
console.error(err);
|
|
903
|
+
}
|
|
783
904
|
}
|
|
784
905
|
|
|
785
906
|
// clean sockets
|
|
@@ -790,18 +911,18 @@ Invalid certificate files, please check your:
|
|
|
790
911
|
const { role, registeredPlugins } = payload;
|
|
791
912
|
|
|
792
913
|
if (!roles.includes(role)) {
|
|
793
|
-
console.error(`[soundworks.Server] A client with invalid
|
|
914
|
+
console.error(`[soundworks.Server] A client with invalid role ("${role}") attempted to connect`);
|
|
794
915
|
|
|
795
916
|
socket.send(CLIENT_HANDSHAKE_ERROR, {
|
|
796
917
|
type: 'invalid-client-type',
|
|
797
|
-
message: `Invalid client
|
|
918
|
+
message: `Invalid client role, please check server configuration (valid client roles are: ${roles.join(', ')})`,
|
|
798
919
|
});
|
|
799
920
|
return;
|
|
800
921
|
}
|
|
801
922
|
|
|
802
923
|
try {
|
|
803
924
|
this.pluginManager.checkRegisteredPlugins(registeredPlugins);
|
|
804
|
-
} catch(err) {
|
|
925
|
+
} catch (err) {
|
|
805
926
|
socket.send(CLIENT_HANDSHAKE_ERROR, {
|
|
806
927
|
type: 'invalid-plugin-list',
|
|
807
928
|
message: err.message,
|
|
@@ -826,8 +947,10 @@ Invalid certificate files, please check your:
|
|
|
826
947
|
// add client to context manager
|
|
827
948
|
await this.contextManager.addClient(client);
|
|
828
949
|
|
|
829
|
-
|
|
830
|
-
|
|
950
|
+
this._onClientConnectCallbacks.forEach(callback => callback(client));
|
|
951
|
+
|
|
952
|
+
const { id, uuid, token } = client;
|
|
953
|
+
socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid, token });
|
|
831
954
|
});
|
|
832
955
|
}
|
|
833
956
|
|
|
@@ -904,6 +1027,27 @@ Invalid certificate files, please check your:
|
|
|
904
1027
|
* you should rely on these defaults._
|
|
905
1028
|
*/
|
|
906
1029
|
useDefaultApplicationTemplate() {
|
|
1030
|
+
const buildDirectory = path.join('.build', 'public');
|
|
1031
|
+
|
|
1032
|
+
const useMinifiedFile = {};
|
|
1033
|
+
const roles = Object.keys(this.config.app.clients);
|
|
1034
|
+
|
|
1035
|
+
roles.forEach(role => {
|
|
1036
|
+
if (this.config.env.type === 'production') {
|
|
1037
|
+
// check if minified file exists
|
|
1038
|
+
const minifiedFilePath = path.join(buildDirectory, `${role}.min.js`);
|
|
1039
|
+
|
|
1040
|
+
if (fs.existsSync(minifiedFilePath)) {
|
|
1041
|
+
useMinifiedFile[role] = true;
|
|
1042
|
+
} else {
|
|
1043
|
+
console.log(chalk.yellow(` > Minified file not found for client "${role}", falling back to normal build file (use \`npm run build:production && npm start\` to use minified files)`));
|
|
1044
|
+
useMinifiedFile[role] = false;
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
useMinifiedFile[role] = false;
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
907
1051
|
this._applicationTemplateOptions = {
|
|
908
1052
|
templateEngine: { compile },
|
|
909
1053
|
templatePath: path.join('.build', 'server', 'tmpl'),
|
|
@@ -918,13 +1062,14 @@ Invalid certificate files, please check your:
|
|
|
918
1062
|
type: config.env.type,
|
|
919
1063
|
websockets: config.env.websockets,
|
|
920
1064
|
subpath: config.env.subpath,
|
|
1065
|
+
useMinifiedFile: useMinifiedFile[role],
|
|
921
1066
|
},
|
|
922
1067
|
};
|
|
923
1068
|
},
|
|
924
1069
|
};
|
|
925
1070
|
|
|
926
1071
|
this.router.use(express.static('public'));
|
|
927
|
-
this.router.use('/build', express.static(
|
|
1072
|
+
this.router.use('/build', express.static(buildDirectory));
|
|
928
1073
|
}
|
|
929
1074
|
|
|
930
1075
|
/**
|
|
@@ -960,6 +1105,81 @@ Invalid certificate files, please check your:
|
|
|
960
1105
|
|
|
961
1106
|
return this._auditState;
|
|
962
1107
|
}
|
|
1108
|
+
|
|
1109
|
+
/** @private */
|
|
1110
|
+
isProtected(role) {
|
|
1111
|
+
if (this.config.env.auth && Array.isArray(this.config.env.auth.clients)) {
|
|
1112
|
+
return this.config.env.auth.clients.includes(role);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** @private */
|
|
1119
|
+
isValidConnectionToken(token) {
|
|
1120
|
+
// token should be in pending token list
|
|
1121
|
+
if (!this._pendingConnectionTokens.has(token)) {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// check the token is not too old
|
|
1126
|
+
const data = decryptData(token);
|
|
1127
|
+
const now = getTime();
|
|
1128
|
+
|
|
1129
|
+
// token is valid only for 30 seconds (this is arbitrary)
|
|
1130
|
+
if (now > data.time + TOKEN_VALID_DURATION) {
|
|
1131
|
+
// delete the token, is too old
|
|
1132
|
+
this._pendingConnectionTokens.delete(token);
|
|
1133
|
+
return false;
|
|
1134
|
+
} else {
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Check if the given client is trusted, i.e. config.env.type == 'production'
|
|
1141
|
+
* and the client is protected behind a password.
|
|
1142
|
+
*
|
|
1143
|
+
* @param {server.Client} client - Client to be tested
|
|
1144
|
+
* @returns {Boolean}
|
|
1145
|
+
*/
|
|
1146
|
+
isTrustedClient(client) {
|
|
1147
|
+
if (this.config.env.type !== 'production') {
|
|
1148
|
+
return true;
|
|
1149
|
+
} else {
|
|
1150
|
+
return this._trustedClients.has(client);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Check if the token from a client is trusted, i.e. config.env.type == 'production'
|
|
1156
|
+
* and the client is protected behind a password.
|
|
1157
|
+
*
|
|
1158
|
+
* @param {Number} clientId - Id of the client
|
|
1159
|
+
* @param {Number} clientIp - Ip of the client
|
|
1160
|
+
* @param {String} token - Token to be tested
|
|
1161
|
+
* @returns {Boolean}
|
|
1162
|
+
*/
|
|
1163
|
+
// for stateless interactions, e.g. POST files
|
|
1164
|
+
isTrustedToken(clientId, clientIp, token) {
|
|
1165
|
+
if (this.config.env.type !== 'production') {
|
|
1166
|
+
return true;
|
|
1167
|
+
} else {
|
|
1168
|
+
for (let client of this._trustedClients) {
|
|
1169
|
+
if (client.id === clientId && client.token === token) {
|
|
1170
|
+
// check that given token is consistent with client ip and id
|
|
1171
|
+
const { id, ip } = decryptData(client.token);
|
|
1172
|
+
|
|
1173
|
+
if (clientId === id && clientIp === ip) {
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
963
1183
|
}
|
|
964
1184
|
|
|
965
1185
|
export default Server;
|
|
@@ -18,7 +18,6 @@ import BaseSharedState from '../common/BaseSharedState.js';
|
|
|
18
18
|
* @callback server.SharedState~deleteOnUpdateCallback
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
/**
|
|
23
22
|
* The `SharedState` is one of the most important and versatile abstraction provided
|
|
24
23
|
* by `soundworks`. It represents a set of parameters that are synchronized accross
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import BaseSharedStateCollection from '../common/BaseSharedStateCollection.js';
|
|
2
|
+
|
|
3
|
+
// this file exists only for documentation purposesSharedStateCollection.js
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @callback server.SharedStateCollection~onUpdateCallback
|
|
7
|
+
* @param {server.SharedState} state - State that triggered the update.
|
|
8
|
+
* @param {Object} newValues - Key / value pairs of the updates that have been
|
|
9
|
+
* applied to the state.
|
|
10
|
+
* @param {Object} oldValues - Key / value pairs of the updated params before
|
|
11
|
+
* the updates has been applied to the state.
|
|
12
|
+
* @param {Mixed} [context=null] - Optionnal context object that has been passed
|
|
13
|
+
* with the values updates in the `set` call.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The `SharedStateCollection` interface represent a collection of all states
|
|
18
|
+
* created from a given schema name on the network.
|
|
19
|
+
*
|
|
20
|
+
* It can optionnaly exclude the states created by the current node.
|
|
21
|
+
*
|
|
22
|
+
* See {@link server.StateManager#getCollection} for factory method API
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* const collection = await server.stateManager.getCollection('my-schema');
|
|
26
|
+
* const allValues = collection.getValues();
|
|
27
|
+
* collection.onUpdate((state, newValues, oldValues, context) => {
|
|
28
|
+
* // do something
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
* @memberof server
|
|
32
|
+
* @extends BaseSharedStateCollection
|
|
33
|
+
* @inheritdoc
|
|
34
|
+
* @hideconstructor
|
|
35
|
+
*/
|
|
36
|
+
class SharedStateCollection extends BaseSharedStateCollection {}
|
|
37
|
+
|
|
38
|
+
export default SharedStateCollection;
|
|
39
|
+
|