@soundworks/core 4.0.0-alpha.5 → 4.0.0-alpha.7

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.
@@ -1,4 +1,6 @@
1
+ import { isString, isFunction } from '@ircam/sc-utils';
1
2
  import SharedState from './BaseSharedState.js';
3
+ import SharedStateCollection from './BaseSharedStateCollection.js';
2
4
  import {
3
5
  CREATE_REQUEST,
4
6
  CREATE_RESPONSE,
@@ -31,7 +33,7 @@ class BaseStateManager {
31
33
  this.client = { id, transport };
32
34
 
33
35
  this._statesById = new Map();
34
- this._observeListeners = new Set();
36
+ this._observeListeners = new Map(); // Map <callback, filterSchemaName>
35
37
  this._cachedSchemas = new Map();
36
38
  this._observeRequestCallbacks = new Map();
37
39
 
@@ -88,28 +90,35 @@ class BaseStateManager {
88
90
  this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
89
91
  // retrieve the callback that have been stored in observe to make sure
90
92
  // we don't call another callback that may have been registered earlier.
91
- const callback = this._observeRequestCallbacks.get(reqId);
93
+ const [callback, filterSchemaName] = this._observeRequestCallbacks.get(reqId);
92
94
  this._observeRequestCallbacks.delete(reqId);
93
95
 
94
96
  const promises = list.map(([schemaName, stateId, nodeId]) => {
95
- return callback(schemaName, stateId, nodeId);
97
+ if (filterSchemaName === '*' || filterSchemaName === schemaName) {
98
+ return callback(schemaName, stateId, nodeId);
99
+ } else {
100
+ return Promise.resolve();
101
+ }
96
102
  });
97
103
 
98
104
  await Promise.all(promises);
99
105
 
100
106
  const unsubscribe = () => {
101
107
  this._observeListeners.delete(callback);
102
-
108
+ // no more listeners, we can stop receiving notification from the server
103
109
  if (this._observeListeners.size === 0) {
104
110
  this.client.transport.emit(UNOBSERVE_NOTIFICATION);
105
111
  }
106
112
  };
113
+
107
114
  resolveRequest(reqId, unsubscribe);
108
115
  });
109
116
 
110
- this.client.transport.addListener(OBSERVE_NOTIFICATION, (...list) => {
111
- list.forEach(([schemaName, stateId, nodeId]) => {
112
- this._observeListeners.forEach(callback => callback(schemaName, stateId, nodeId));
117
+ this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
118
+ this._observeListeners.forEach((filterSchemaName, callback) => {
119
+ if (filterSchemaName === '*' || filterSchemaName === schemaName) {
120
+ callback(schemaName, stateId, nodeId);
121
+ }
113
122
  });
114
123
  });
115
124
 
@@ -170,12 +179,16 @@ class BaseStateManager {
170
179
  * the state of all other clients of the application, to monitor them and/or take
171
180
  * control over them from a single point.
172
181
  *
173
- * @todo Optionnal schema name
182
+ * Note that the states that are created by the same node are not propagated through
183
+ * the observe callback.
184
+ *
185
+ * @param {String} [schemaName] - optionnal schema name to filter the observed
186
+ * states.
174
187
  * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
175
188
  * callback - Function to be called when a new state is created on the network.
176
- * @returns {Promise<Function>} - Return a Promise that resolves when the callback
177
- * as been executed for the first time. The promise value is a function which
178
- * allows to stop observing the network.
189
+ * @returns {Promise<Function>} - Returns a Promise that resolves when the given
190
+ * callback as been executed on each existing states. The promise value is a
191
+ * function which allows to stop observing the states on the network.
179
192
  * @example
180
193
  * client.stateManager.observe(async (schemaName, stateId, nodeId) => {
181
194
  * if (schemaName === 'something') {
@@ -184,16 +197,57 @@ class BaseStateManager {
184
197
  * }
185
198
  * });
186
199
  */
187
- async observe(callback) {
188
- this._observeListeners.add(callback);
189
- // store function
200
+ // note: all filtering is done only on client-side as it is really more simple to
201
+ // handle this way and the network overhead is very low for observe notifications:
202
+ // i.e. schemaName, stateId, nodeId
203
+ async observe(...args) {
204
+ let filterSchemaName;
205
+ let callback;
206
+
207
+ if (args.length === 1) {
208
+ filterSchemaName = '*';
209
+ callback = args[0];
210
+
211
+ if (!isFunction(callback)) {
212
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
213
+ }
214
+ } else if (args.length === 2) {
215
+ filterSchemaName = args[0];
216
+ callback = args[1];
217
+
218
+ if (!isString(filterSchemaName) || !isFunction(callback)) {
219
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
220
+ }
221
+ } else {
222
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
223
+ }
224
+
225
+ // resend request to get updated list of states
190
226
  return new Promise((resolve, reject) => {
191
227
  const reqId = storeRequestPromise(resolve, reject);
192
- // store the callback to be executed on the response
193
- this._observeRequestCallbacks.set(reqId, callback);
228
+ // store the callback for execution on the response. the returned Promise
229
+ // is fullfiled once callback has been executed with each existing states
230
+ this._observeRequestCallbacks.set(reqId, [callback, filterSchemaName]);
231
+ // store the callback for execution on subsequent notifications
232
+ this._observeListeners.set(callback, filterSchemaName);
233
+
194
234
  this.client.transport.emit(OBSERVE_REQUEST, reqId);
195
235
  });
196
236
  }
237
+
238
+ /**
239
+ * Returns a collection of all the states created from the schema name. Except
240
+ * the ones created by the current node.
241
+ *
242
+ * @param {string} schemaName - Name of the schema.
243
+ * @returns {server.SharedStateCollection|client.SharedStateCollection}
244
+ */
245
+ async getCollection(schemaName) {
246
+ const collection = new SharedStateCollection(this, schemaName);
247
+ await collection._init();
248
+
249
+ return collection;
250
+ }
197
251
  }
198
252
 
199
253
  export default BaseStateManager;
@@ -17,7 +17,7 @@ export const types = {
17
17
  },
18
18
  coerceFunction: (name, def, value) => {
19
19
  if (typeof value !== 'boolean') {
20
- throw new TypeError(`[SharedState] Invalid value for boolean param "${name}": ${value}`);
20
+ throw new TypeError(`[SharedState] Invalid value "${value}" for boolean parameter "${name}"`);
21
21
  }
22
22
 
23
23
  return value;
@@ -30,7 +30,7 @@ export const types = {
30
30
  },
31
31
  coerceFunction: (name, def, value) => {
32
32
  if (typeof value !== 'string') {
33
- throw new TypeError(`[SharedState] Invalid value for string param "${name}": ${value}`);
33
+ throw new TypeError(`[SharedState] Invalid value "${value}" for string parameter "${name}"`);
34
34
  }
35
35
 
36
36
  return value;
@@ -63,7 +63,7 @@ export const types = {
63
63
  },
64
64
  coerceFunction: (name, def, value) => {
65
65
  if (!(typeof value === 'number' && Math.floor(value) === value)) {
66
- throw new TypeError(`[SharedState] Invalid value for integer param "${name}": ${value}`);
66
+ throw new TypeError(`[SharedState] Invalid value "${value}" for integer parameter "${name}"`);
67
67
  }
68
68
 
69
69
  return Math.max(def.min, Math.min(def.max, value));
@@ -96,7 +96,7 @@ export const types = {
96
96
  },
97
97
  coerceFunction: (name, def, value) => {
98
98
  if (typeof value !== 'number' || value !== value) { // reject NaN
99
- throw new TypeError(`[SharedState] Invalid value for float param "${name}": ${value}`);
99
+ throw new TypeError(`[SharedState] Invalid value "${value}" for float parameter "${name}"`);
100
100
  }
101
101
 
102
102
  return Math.max(def.min, Math.min(def.max, value));
@@ -109,7 +109,7 @@ export const types = {
109
109
  },
110
110
  coerceFunction: (name, def, value) => {
111
111
  if (def.list.indexOf(value) === -1) {
112
- throw new TypeError(`[SharedState] Invalid value for enum param "${name}": ${value}`);
112
+ throw new TypeError(`[SharedState] Invalid value "${value}" for enum parameter "${name}"`);
113
113
  }
114
114
 
115
115
 
@@ -241,7 +241,8 @@ class ParameterBag {
241
241
  }
242
242
 
243
243
  /**
244
- * Return values of all parameters as a flat object.
244
+ * Return values of all parameters as a flat object. If a parameter is of `any`
245
+ * type, a deep copy is made.
245
246
  *
246
247
  * @return {object}
247
248
  */
@@ -255,6 +256,25 @@ class ParameterBag {
255
256
  return values;
256
257
  }
257
258
 
259
+ /**
260
+ * Return values of all parameters as a flat object. Similar to `getValues` but
261
+ * returns a reference to the underlying value in case of `any` type. May be
262
+ * usefull if the underlying value is big (e.g. sensors recordings, etc.) and
263
+ * deep cloning expensive. Be aware that if changes are made on the returned
264
+ * object, the state of your application will become inconsistent.
265
+ *
266
+ * @return {object}
267
+ */
268
+ getValuesUnsafe() {
269
+ let values = {};
270
+
271
+ for (let name in this._values) {
272
+ values[name] = this.getUnsafe(name);
273
+ }
274
+
275
+ return values;
276
+ }
277
+
258
278
  /**
259
279
  * Return the value of the given parameter. If the parameter is of `any` type,
260
280
  * a deep copy is returned.
@@ -277,39 +297,66 @@ class ParameterBag {
277
297
  }
278
298
 
279
299
  /**
280
- * Set the value of a parameter. If the value of the parameter is updated
281
- * (aka if previous value is different from new value) all registered
282
- * callbacks are registered.
300
+ * Similar to `get` but returns a reference to the underlying value in case of
301
+ * `any` type. May be usefull if the underlying value is big (e.g. sensors
302
+ * recordings, etc.) and deep cloning expensive. Be aware that if changes are
303
+ * made on the returned object, the state of your application will become
304
+ * inconsistent.
283
305
  *
284
306
  * @param {string} name - Name of the parameter.
307
+ * @return {Mixed} - Value of the parameter.
308
+ */
309
+ getUnsafe(name) {
310
+ if (!this.has(name)) {
311
+ throw new ReferenceError(`[SharedState] Cannot get value of undefined parameter "${name}"`);
312
+ }
313
+
314
+ return this._values[name];
315
+ }
316
+
317
+ /**
318
+ * Check that the value is valid according to the schema and return it coerced
319
+ * to the schema definition
320
+ *
321
+ * @param {String} name - Name of the parameter.
285
322
  * @param {Mixed} value - Value of the parameter.
286
- * @param {boolean} [forcePropagation=false] - if true, propagate value even
287
- * if the value has not changed.
288
- * @return {Array} - [new value, updated flag].
289
323
  */
290
- set(name, value) {
324
+ coerceValue(name, value) {
291
325
  if (!this.has(name)) {
292
326
  throw new ReferenceError(`[SharedState] Cannot set value of undefined parameter "${name}"`);
293
327
  }
294
328
 
295
329
  const def = this._schema[name];
296
- const { coerceFunction } = types[def.type];
297
330
 
298
331
  if (value === null && def.nullable === false) {
299
332
  throw new TypeError(`[SharedState] Invalid value for ${def.type} param "${name}": value is null and param is not nullable`);
300
333
  } else if (value === null && def.nullable === true) {
301
334
  value = null;
302
335
  } else {
336
+ const { coerceFunction } = types[def.type];
303
337
  value = coerceFunction(name, def, value);
304
338
  }
305
339
 
340
+ return value;
341
+ }
342
+
343
+ /**
344
+ * Set the value of a parameter. If the value of the parameter is updated
345
+ * (aka if previous value is different from new value) all registered
346
+ * callbacks are registered.
347
+ *
348
+ * @param {string} name - Name of the parameter.
349
+ * @param {Mixed} value - Value of the parameter.
350
+ * @return {Array} - [new value, updated flag].
351
+ */
352
+ set(name, value) {
353
+ value = this.coerceValue(name, value);
306
354
  const currentValue = this._values[name];
307
355
  const updated = !equal(currentValue, value);
308
356
 
309
357
  // we store a deep copy of the object as we don't want the client to be able
310
- // to modify our underlying data, which leads unexpected behavior where the
358
+ // to modify our underlying data, which leads to unexpected behavior where the
311
359
  // deep equal check to returns true, and therefore the update is not triggered.
312
- //
313
360
  // @see tests/common.state-manager.spec.js
314
361
  // 'should copy stored value for "any" type to have a predictable behavior'
315
362
  if (this._schema[name].type === 'any') {
@@ -43,6 +43,12 @@ class Client {
43
43
  * @type {server.Socket}
44
44
  */
45
45
  this.socket = socket;
46
+
47
+ /**
48
+ * Is set in server._onSocketConnection
49
+ * @private
50
+ */
51
+ this.token = null;
46
52
  }
47
53
  }
48
54
 
@@ -78,7 +78,7 @@ import Server from './Server.js';
78
78
  class PluginManager extends BasePluginManager {
79
79
  constructor(server) {
80
80
  if (!(server instanceof Server)) {
81
- throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server as argument"`);
81
+ throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server" as argument`);
82
82
  }
83
83
 
84
84
  super(server);
@@ -5,7 +5,8 @@ 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 } from '@ircam/sc-utils';
8
+ import { getTime } from '@ircam/sc-gettime';
9
+ import { isPlainObject, idGenerator } from '@ircam/sc-utils';
9
10
  import chalk from 'chalk';
10
11
  import compression from 'compression';
11
12
  import express from 'express';
@@ -16,6 +17,7 @@ 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';
@@ -29,6 +31,7 @@ import {
29
31
  AUDIT_STATE_NAME,
30
32
  } from '../common/constants.js';
31
33
 
34
+
32
35
  let _dbNamespaces = new Set();
33
36
 
34
37
  /**
@@ -73,6 +76,8 @@ const DEFAULT_CONFIG = {
73
76
  },
74
77
  };
75
78
 
79
+ const TOKEN_VALID_DURATION = 20; // sec
80
+
76
81
  // set terminal title
77
82
  /** @private */
78
83
  function setTerminalTitle(server) {
@@ -156,8 +161,8 @@ class Server {
156
161
  throw new Error(`[soundworks:Server] Invalid argument for Server constructor, config should be an object`);
157
162
  }
158
163
  /**
159
- * Given config object merged with the following defaults:
160
- * ```
164
+ * @description Given config object merged with the following defaults:
165
+ * @example
161
166
  * {
162
167
  * env: {
163
168
  * type: 'development',
@@ -178,7 +183,6 @@ class Server {
178
183
  * clients: {},
179
184
  * }
180
185
  * }
181
- * ```
182
186
  */
183
187
  this.config = merge({}, DEFAULT_CONFIG, config);
184
188
 
@@ -236,8 +240,10 @@ class Server {
236
240
  * // expose assets located in the `soundfiles` directory on the network
237
241
  * server.router.use('/soundfiles', express.static('soundfiles')));
238
242
  */
239
- this.router = express.Router();
240
- // compression (must be set before serve-static)
243
+ // @note: we use express() instead of express.Router() because all 404 and
244
+ // error stuff is handled by default
245
+ this.router = express();
246
+ // compression (must be set before express.static())
241
247
  this.router.use(compression());
242
248
 
243
249
  /**
@@ -313,6 +319,10 @@ class Server {
313
319
  this._onStatusChangeCallbacks = new Set();
314
320
  /** @private */
315
321
  this._auditState = null;
322
+ /** @private */
323
+ this._pendingConnectionTokens = new Set();
324
+ /** @private */
325
+ this._trustedClients = new Set();
316
326
 
317
327
  // register audit state schema
318
328
  this.stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema);
@@ -357,11 +367,24 @@ class Server {
357
367
 
358
368
  // basic http authentication
359
369
  if (this.config.env.auth) {
370
+ const ids = idGenerator();
371
+
360
372
  const soundworksAuth = (req, res, next) => {
373
+ let role = null;
374
+
375
+ for (let [_role, config] of Object.entries(this.config.app.clients)) {
376
+ if (req.path === config.route) {
377
+ role = _role;
378
+ }
379
+ }
361
380
 
362
- const isProtected = this.config.env.auth.clients
363
- .map(type => req.path.endsWith(`/${type}`))
364
- .reduce((acc, value) => acc || value, false);
381
+ // route that are not client entry points just pass through the middleware
382
+ if (role === null) {
383
+ next();
384
+ return;
385
+ }
386
+
387
+ const isProtected = this.isProtected(role);
365
388
 
366
389
  if (isProtected) {
367
390
  // authentication middleware
@@ -373,17 +396,34 @@ class Server {
373
396
  // verify login and password are set and correct
374
397
  if (login && password && login === auth.login && password === auth.password) {
375
398
  // -> access granted...
399
+ // generate token for web socket to check connections
400
+ const id = ids.next().value;
401
+ const ip = req.ip;
402
+ const time = getTime();
403
+ const token = { id, ip, time };
404
+ const encryptedToken = encryptData(token);
405
+
406
+ this._pendingConnectionTokens.add(encryptedToken);
407
+
408
+ setTimeout(() => {
409
+ this._pendingConnectionTokens.delete(encryptedToken);
410
+ }, TOKEN_VALID_DURATION * 1000);
411
+
412
+ // pass to the response object to be send to the client
413
+ res.swToken = encryptedToken;
414
+
376
415
  return next();
377
416
  }
378
417
 
379
- // -> access denied...
418
+ // show login / password modal
380
419
  res.writeHead(401, {
381
420
  'WWW-Authenticate':'Basic',
382
421
  'Content-Type':'text/plain',
383
422
  });
423
+
384
424
  res.end('Authentication required.');
385
425
  } else {
386
- // route not protected
426
+ // route is not protected
387
427
  return next();
388
428
  }
389
429
  };
@@ -593,7 +633,7 @@ Invalid certificate files, please check your:
593
633
  await this.sockets.start(
594
634
  this,
595
635
  this.config.env.websockets,
596
- (role, socket) => this._onSocketConnection(role, socket),
636
+ (...args) => this._onSocketConnection(...args),
597
637
  );
598
638
 
599
639
  // ------------------------------------------------------------
@@ -712,6 +752,8 @@ Invalid certificate files, please check your:
712
752
  route += `${role}`;
713
753
  }
714
754
 
755
+ this.config.app.clients[role].route = route;
756
+
715
757
  // define template filename: `${role}.html` or `default.html`
716
758
  const {
717
759
  templatePath,
@@ -745,6 +787,12 @@ Invalid certificate files, please check your:
745
787
  const soundworksClientHandler = (req, res) => {
746
788
  const data = clientConfigFunction(role, this.config, req);
747
789
 
790
+ // if the client has gone through the connection middleware (add succedeed),
791
+ // add the token to the data object
792
+ if (res.swToken) {
793
+ data.token = res.swToken;
794
+ }
795
+
748
796
  // CORS / COOP / COEP headers for `crossOriginIsolated pages,
749
797
  // enables `sharedArrayBuffers` and high precision timers
750
798
  // cf. https://web.dev/why-coop-coep/
@@ -759,6 +807,7 @@ Invalid certificate files, please check your:
759
807
  const appIndex = tmpl(data);
760
808
  res.end(appIndex);
761
809
  };
810
+
762
811
  // http request
763
812
  router.get(route, soundworksClientHandler);
764
813
 
@@ -770,18 +819,38 @@ Invalid certificate files, please check your:
770
819
  * Socket connection callback.
771
820
  * @private
772
821
  */
773
- _onSocketConnection(role, socket) {
822
+ _onSocketConnection(role, socket, connectionToken) {
774
823
  const client = new Client(role, socket);
775
824
  const roles = Object.keys(this.config.app.clients);
776
825
 
826
+ // this has been validated
827
+ if (this.isProtected(role) && this.isValidConnectionToken(connectionToken)) {
828
+ const { ip } = decryptData(connectionToken);
829
+ const newData = {
830
+ ip: ip,
831
+ id: client.id,
832
+ };
833
+
834
+ const newToken = encryptData(newData);
835
+
836
+ client.token = newToken;
837
+
838
+ this._pendingConnectionTokens.delete(connectionToken);
839
+ this._trustedClients.add(client);
840
+ }
841
+
777
842
  socket.addListener('close', async () => {
778
- // do nothin if client type was invalid
843
+ // do nothing if client role is invalid
779
844
  if (roles.includes(role)) {
780
845
  // decrement audit state counter
781
846
  const numClients = this._auditState.get('numClients');
782
847
  numClients[role] -= 1;
783
848
  this._auditState.set({ numClients });
784
849
 
850
+ // delete token
851
+ if (this._trustedClients.has(client)) {
852
+ this._trustedClients.delete(client);
853
+ }
785
854
  // clean context manager, await before cleaning state manager
786
855
  await this.contextManager.removeClient(client);
787
856
  // remove client from pluginManager
@@ -834,8 +903,8 @@ Invalid certificate files, please check your:
834
903
  // add client to context manager
835
904
  await this.contextManager.addClient(client);
836
905
 
837
- const { id, uuid } = client;
838
- socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid });
906
+ const { id, uuid, token } = client;
907
+ socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid, token });
839
908
  });
840
909
  }
841
910
 
@@ -912,6 +981,27 @@ Invalid certificate files, please check your:
912
981
  * you should rely on these defaults._
913
982
  */
914
983
  useDefaultApplicationTemplate() {
984
+ const buildDirectory = path.join('.build', 'public');
985
+
986
+ const useMinifiedFile = {};
987
+ const roles = Object.keys(this.config.app.clients);
988
+
989
+ roles.forEach(role => {
990
+ if (this.config.env.type === 'production') {
991
+ // check if minified file exists
992
+ const minifiedFilePath = path.join(buildDirectory, `${role}.min.js`);
993
+
994
+ if (fs.existsSync(minifiedFilePath)) {
995
+ useMinifiedFile[role] = true;
996
+ } else {
997
+ 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)`));
998
+ useMinifiedFile[role] = false;
999
+ }
1000
+ } else {
1001
+ useMinifiedFile[role] = false;
1002
+ }
1003
+ });
1004
+
915
1005
  this._applicationTemplateOptions = {
916
1006
  templateEngine: { compile },
917
1007
  templatePath: path.join('.build', 'server', 'tmpl'),
@@ -926,13 +1016,14 @@ Invalid certificate files, please check your:
926
1016
  type: config.env.type,
927
1017
  websockets: config.env.websockets,
928
1018
  subpath: config.env.subpath,
1019
+ useMinifiedFile: useMinifiedFile[role],
929
1020
  },
930
1021
  };
931
1022
  },
932
1023
  };
933
1024
 
934
1025
  this.router.use(express.static('public'));
935
- this.router.use('/build', express.static(path.join('.build', 'public')));
1026
+ this.router.use('/build', express.static(buildDirectory));
936
1027
  }
937
1028
 
938
1029
  /**
@@ -968,6 +1059,81 @@ Invalid certificate files, please check your:
968
1059
 
969
1060
  return this._auditState;
970
1061
  }
1062
+
1063
+ /** @private */
1064
+ isProtected(role) {
1065
+ if (this.config.env.auth && Array.isArray(this.config.env.auth.clients)) {
1066
+ return this.config.env.auth.clients.includes(role);
1067
+ }
1068
+
1069
+ return false;
1070
+ }
1071
+
1072
+ /** @private */
1073
+ isValidConnectionToken(token) {
1074
+ // token should be in pending token list
1075
+ if (!this._pendingConnectionTokens.has(token)) {
1076
+ return false;
1077
+ }
1078
+
1079
+ // check the token is not too old
1080
+ const data = decryptData(token);
1081
+ const now = getTime();
1082
+
1083
+ // token is valid only for 30 seconds (this is arbitrary)
1084
+ if (now > data.time + TOKEN_VALID_DURATION) {
1085
+ // delete the token, is too old
1086
+ this._pendingConnectionTokens.delete(token);
1087
+ return false;
1088
+ } else {
1089
+ return true;
1090
+ }
1091
+ }
1092
+
1093
+ /**
1094
+ * Check if the given client is trusted, i.e. config.env.type == 'production'
1095
+ * and the client is protected behind a password.
1096
+ *
1097
+ * @param {server.Client} client - Client to be tested
1098
+ * @returns {Boolean}
1099
+ */
1100
+ isTrustedClient(client) {
1101
+ if (this.config.env.type !== 'production') {
1102
+ return true;
1103
+ } else {
1104
+ return this._trustedClients.has(client);
1105
+ }
1106
+ }
1107
+
1108
+ /**
1109
+ * Check if the token from a client is trusted, i.e. config.env.type == 'production'
1110
+ * and the client is protected behind a password.
1111
+ *
1112
+ * @param {Number} clientId - Id of the client
1113
+ * @param {Number} clientIp - Ip of the client
1114
+ * @param {String} token - Token to be tested
1115
+ * @returns {Boolean}
1116
+ */
1117
+ // for stateless interactions, e.g. POST files
1118
+ isTrustedToken(clientId, clientIp, token) {
1119
+ if (this.config.env.type !== 'production') {
1120
+ return true;
1121
+ } else {
1122
+ for (let client of this._trustedClients) {
1123
+ if (client.id === clientId && client.token === token) {
1124
+ // check that given token is consistent with client ip and id
1125
+ const { id, ip } = decryptData(client.token);
1126
+
1127
+ if (clientId === id && clientIp === ip) {
1128
+ return true;
1129
+ }
1130
+ }
1131
+
1132
+ }
1133
+
1134
+ return false;
1135
+ }
1136
+ }
971
1137
  }
972
1138
 
973
1139
  export default Server;