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