@soundworks/core 4.0.0-alpha.27 → 4.0.0-alpha.29
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/.jsdoc +3 -1
- package/CHANGELOG.md +15 -5
- package/package.json +5 -5
- package/src/client/Client.js +78 -83
- package/src/client/{Context.js → ClientContext.js} +48 -38
- package/src/client/ClientContextManager.js +115 -0
- package/src/client/{Plugin.js → ClientPlugin.js} +22 -18
- package/src/client/{PluginManager.js → ClientPluginManager.js} +39 -35
- package/src/client/{Socket.js → ClientSocket.js} +54 -65
- package/src/client/{StateManager.js → ClientStateManager.js} +7 -8
- package/src/client/index.js +17 -41
- package/src/common/BasePlugin.js +56 -51
- package/src/common/BasePluginManager.js +175 -139
- package/src/common/BaseStateManager.js +316 -120
- package/src/common/BatchedTransport.js +24 -24
- package/src/common/ParameterBag.js +18 -6
- package/src/common/PromiseStore.js +1 -0
- package/src/common/{BaseSharedState.js → SharedState.js} +271 -199
- package/src/common/{BaseSharedStateCollection.js → SharedStateCollection.js} +92 -61
- package/src/common/logger.js +5 -0
- package/src/common/shared-state-types.js +76 -0
- package/src/common/version.js +1 -1
- package/src/server/Server.js +391 -354
- package/src/server/ServerClient.js +73 -0
- package/src/server/{Context.js → ServerContext.js} +85 -66
- package/src/server/{ContextManager.js → ServerContextManager.js} +98 -85
- package/src/server/{Plugin.js → ServerPlugin.js} +10 -24
- package/src/server/{PluginManager.js → ServerPluginManager.js} +76 -69
- package/src/server/{Socket.js → ServerSocket.js} +19 -18
- package/src/server/{Sockets.js → ServerSockets.js} +36 -27
- package/src/server/ServerStateManager.js +515 -0
- package/src/{common → server}/SharedStatePrivate.js +61 -26
- package/src/server/audit-network-latency.worker.js +9 -7
- package/src/server/audit-schema.js +1 -1
- package/src/server/crypto.js +2 -0
- package/src/server/index.js +18 -21
- package/types/client/Client.d.ts +74 -95
- package/types/client/{Context.d.ts → ClientContext.d.ts} +25 -26
- package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
- package/types/client/{Plugin.d.ts → ClientPlugin.d.ts} +17 -20
- package/types/client/{PluginManager.d.ts → ClientPluginManager.d.ts} +13 -36
- package/types/client/ClientSocket.d.ts +74 -0
- package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +11 -15
- package/types/client/index.d.ts +26 -2
- package/types/common/BasePlugin.d.ts +52 -46
- package/types/common/BasePluginManager.d.ts +68 -44
- package/types/common/BaseStateManager.d.ts +207 -97
- package/types/common/BatchedTransport.d.ts +1 -5
- package/types/common/ParameterBag.d.ts +16 -18
- package/types/common/PromiseStore.d.ts +1 -1
- package/types/common/SharedState.d.ts +297 -0
- package/types/common/{BaseSharedStateCollection.d.ts → SharedStateCollection.d.ts} +34 -18
- package/types/common/constants.d.ts +0 -2
- package/types/common/logger.d.ts +2 -1
- package/types/common/shared-state-types.d.ts +83 -0
- package/types/common/sockets-utils.d.ts +0 -5
- package/types/common/version.d.ts +1 -2
- package/types/server/Server.d.ts +78 -132
- package/types/server/ServerClient.d.ts +45 -0
- package/types/server/{Context.d.ts → ServerContext.d.ts} +53 -43
- package/types/server/ServerContextManager.d.ts +80 -0
- package/types/server/{Plugin.d.ts → ServerPlugin.d.ts} +19 -33
- package/types/server/{PluginManager.d.ts → ServerPluginManager.d.ts} +39 -37
- package/types/server/ServerSocket.d.ts +85 -0
- package/types/server/ServerSockets.d.ts +50 -0
- package/types/server/ServerStateManager.d.ts +189 -0
- package/types/server/SharedStatePrivate.d.ts +21 -0
- package/types/server/audit-network-latency.worker.d.ts +1 -2
- package/types/server/audit-schema.d.ts +0 -1
- package/types/server/crypto.d.ts +2 -1
- package/types/server/index.d.ts +24 -2
- package/src/client/ContextManager.js +0 -111
- package/src/client/SharedState.js +0 -84
- package/src/client/SharedStateCollection.js +0 -38
- package/src/server/Client.js +0 -55
- package/src/server/SharedState.js +0 -85
- package/src/server/SharedStateCollection.js +0 -39
- package/src/server/StateManager.js +0 -696
- package/types/client/Client.d.ts.map +0 -1
- package/types/client/Context.d.ts.map +0 -1
- package/types/client/ContextManager.d.ts.map +0 -1
- package/types/client/Plugin.d.ts.map +0 -1
- package/types/client/PluginManager.d.ts.map +0 -1
- package/types/client/SharedState.d.ts +0 -85
- package/types/client/SharedState.d.ts.map +0 -1
- package/types/client/SharedStateCollection.d.ts +0 -41
- package/types/client/SharedStateCollection.d.ts.map +0 -1
- package/types/client/Socket.d.ts +0 -149
- package/types/client/Socket.d.ts.map +0 -1
- package/types/client/StateManager.d.ts.map +0 -1
- package/types/client/index.d.ts.map +0 -1
- package/types/common/BasePlugin.d.ts.map +0 -1
- package/types/common/BasePluginManager.d.ts.map +0 -1
- package/types/common/BaseSharedState.d.ts +0 -244
- package/types/common/BaseSharedState.d.ts.map +0 -1
- package/types/common/BaseSharedStateCollection.d.ts.map +0 -1
- package/types/common/BaseStateManager.d.ts.map +0 -1
- package/types/common/BatchedTransport.d.ts.map +0 -1
- package/types/common/ParameterBag.d.ts.map +0 -1
- package/types/common/PromiseStore.d.ts.map +0 -1
- package/types/common/SharedStatePrivate.d.ts +0 -21
- package/types/common/SharedStatePrivate.d.ts.map +0 -1
- package/types/common/constants.d.ts.map +0 -1
- package/types/common/logger.d.ts.map +0 -1
- package/types/common/sockets-utils.d.ts.map +0 -1
- package/types/common/version.d.ts.map +0 -1
- package/types/server/Client.d.ts +0 -45
- package/types/server/Client.d.ts.map +0 -1
- package/types/server/Context.d.ts.map +0 -1
- package/types/server/ContextManager.d.ts +0 -69
- package/types/server/ContextManager.d.ts.map +0 -1
- package/types/server/Plugin.d.ts.map +0 -1
- package/types/server/PluginManager.d.ts.map +0 -1
- package/types/server/Server.d.ts.map +0 -1
- package/types/server/SharedState.d.ts +0 -86
- package/types/server/SharedState.d.ts.map +0 -1
- package/types/server/SharedStateCollection.d.ts +0 -41
- package/types/server/SharedStateCollection.d.ts.map +0 -1
- package/types/server/Socket.d.ts +0 -164
- package/types/server/Socket.d.ts.map +0 -1
- package/types/server/Sockets.d.ts +0 -80
- package/types/server/Sockets.d.ts.map +0 -1
- package/types/server/StateManager.d.ts +0 -405
- package/types/server/StateManager.d.ts.map +0 -1
- package/types/server/audit-network-latency.worker.d.ts.map +0 -1
- package/types/server/audit-schema.d.ts.map +0 -1
- package/types/server/crypto.d.ts.map +0 -1
- package/types/server/index.d.ts.map +0 -1
package/src/server/Server.js
CHANGED
|
@@ -29,15 +29,39 @@ import {
|
|
|
29
29
|
encryptData,
|
|
30
30
|
decryptData,
|
|
31
31
|
} from './crypto.js';
|
|
32
|
-
import
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import
|
|
32
|
+
import ServerClient, {
|
|
33
|
+
kServerClientToken,
|
|
34
|
+
} from './ServerClient.js';
|
|
35
|
+
import ServerContextManager, {
|
|
36
|
+
kServerContextManagerStart,
|
|
37
|
+
kServerContextManagerStop,
|
|
38
|
+
kServerContextManagerAddClient,
|
|
39
|
+
kServerContextManagerRemoveClient,
|
|
40
|
+
} from './ServerContextManager.js';
|
|
41
|
+
import ServerPluginManager, {
|
|
42
|
+
kServerPluginManagerCheckRegisteredPlugins,
|
|
43
|
+
kServerPluginManagerAddClient,
|
|
44
|
+
kServerPluginManagerRemoveClient,
|
|
45
|
+
} from './ServerPluginManager.js';
|
|
46
|
+
import {
|
|
47
|
+
kPluginManagerStart,
|
|
48
|
+
kPluginManagerStop,
|
|
49
|
+
} from '../common/BasePluginManager.js';
|
|
50
|
+
import ServerStateManager, {
|
|
51
|
+
kServerStateManagerAddClient,
|
|
52
|
+
kServerStateManagerRemoveClient,
|
|
53
|
+
} from './ServerStateManager.js';
|
|
54
|
+
import {
|
|
55
|
+
kStateManagerInit,
|
|
56
|
+
} from '../common/BaseStateManager.js';
|
|
36
57
|
import {
|
|
37
58
|
kSocketClientId,
|
|
38
59
|
kSocketTerminate,
|
|
39
|
-
} from './
|
|
40
|
-
import
|
|
60
|
+
} from './ServerSocket.js';
|
|
61
|
+
import ServerSockets, {
|
|
62
|
+
kSocketsStart,
|
|
63
|
+
kSocketsStop,
|
|
64
|
+
} from './ServerSockets.js';
|
|
41
65
|
import logger from '../common/logger.js';
|
|
42
66
|
import {
|
|
43
67
|
SERVER_ID,
|
|
@@ -46,30 +70,10 @@ import {
|
|
|
46
70
|
CLIENT_HANDSHAKE_ERROR,
|
|
47
71
|
AUDIT_STATE_NAME,
|
|
48
72
|
} from '../common/constants.js';
|
|
49
|
-
import
|
|
73
|
+
import VERSION from '../common/version.js';
|
|
50
74
|
|
|
51
75
|
let _dbNamespaces = new Set();
|
|
52
76
|
|
|
53
|
-
/**
|
|
54
|
-
* Configuration object for the server.
|
|
55
|
-
*
|
|
56
|
-
* @typedef ServerConfig
|
|
57
|
-
* @memberof server
|
|
58
|
-
* @type {object}
|
|
59
|
-
* @property {object} [app] - Application configration object.
|
|
60
|
-
* @property {object} app.clients - Definition of the application clients.
|
|
61
|
-
* @property {string} [app.name=''] - Name of the application.
|
|
62
|
-
* @property {string} [app.author=''] - Name of the author.
|
|
63
|
-
* @property {object} [env] - Environment configration object.
|
|
64
|
-
* @property {boolean} env.port - Port on which the server is listening.
|
|
65
|
-
* @property {boolean} env.useHttps - Define is the server run in http or in https.
|
|
66
|
-
* @property {boolean} [env.httpsInfos={}] - Path to cert files for https.
|
|
67
|
-
* @property {boolean} env.serverAddress - Domain name or IP of the server.
|
|
68
|
-
* Mandatory if node clients are defined
|
|
69
|
-
* @property {string} [env.websockets={}] - Configuration options for websockets.
|
|
70
|
-
* @property {string} [env.subpath=''] - If running behind a proxy, path to the application.
|
|
71
|
-
*/
|
|
72
|
-
|
|
73
77
|
/** @private */
|
|
74
78
|
const DEFAULT_CONFIG = {
|
|
75
79
|
env: {
|
|
@@ -77,10 +81,6 @@ const DEFAULT_CONFIG = {
|
|
|
77
81
|
port: 8000,
|
|
78
82
|
serverAddress: '',
|
|
79
83
|
subpath: '',
|
|
80
|
-
websockets: {
|
|
81
|
-
path: 'socket',
|
|
82
|
-
pingInterval: 5000,
|
|
83
|
-
},
|
|
84
84
|
useHttps: false,
|
|
85
85
|
httpsInfos: null,
|
|
86
86
|
crossOriginIsolated: true,
|
|
@@ -94,12 +94,19 @@ const DEFAULT_CONFIG = {
|
|
|
94
94
|
|
|
95
95
|
const TOKEN_VALID_DURATION = 20; // sec
|
|
96
96
|
|
|
97
|
+
export const kServerOnSocketConnection = Symbol('soundworks:server-on-socket-connection');
|
|
98
|
+
export const kServerIsProtectedRole = Symbol('soundworks:server-is-protected-role');
|
|
99
|
+
export const kServerIsValidConnectionToken = Symbol('soundworks:server-is-valid-connection-token');
|
|
100
|
+
// for testing purposes
|
|
101
|
+
export const kServerOnStatusChangeCallbacks = Symbol('soundworks:server-on-status-change-callbacks');
|
|
102
|
+
export const kServerApplicationTemplateOptions = Symbol('soundworks:server-application-template-options');
|
|
103
|
+
|
|
97
104
|
/**
|
|
98
105
|
* The `Server` class is the main entry point for the server-side of a soundworks
|
|
99
106
|
* application.
|
|
100
107
|
*
|
|
101
|
-
* The `Server` instance allows to access soundworks components such as {@link
|
|
102
|
-
* {@link
|
|
108
|
+
* The `Server` instance allows to access soundworks components such as {@link ServerStateManager},
|
|
109
|
+
* {@link ServerPluginManager}, {@link ServerSocket} or {@link ServerContextManager}.
|
|
103
110
|
* Its is also responsible for handling the initialization lifecycles of the different
|
|
104
111
|
* soundworks components.
|
|
105
112
|
*
|
|
@@ -138,12 +145,31 @@ const TOKEN_VALID_DURATION = 20; // sec
|
|
|
138
145
|
* The server will listen to the following URLs:
|
|
139
146
|
* - `http://127.0.0.1:8000/` for the `player` role, which is defined as the default client.
|
|
140
147
|
* - `http://127.0.0.1:8000/controller` for the `controller` role.
|
|
141
|
-
*
|
|
142
|
-
* @memberof server
|
|
143
148
|
*/
|
|
144
149
|
class Server {
|
|
150
|
+
#config = null;
|
|
151
|
+
#version = null;
|
|
152
|
+
#status = null;
|
|
153
|
+
#router = null;
|
|
154
|
+
#httpServer = null;
|
|
155
|
+
#db = null;
|
|
156
|
+
|
|
157
|
+
#sockets = null;
|
|
158
|
+
#pluginManager = null;
|
|
159
|
+
#stateManager = null;
|
|
160
|
+
#contextManager = null;
|
|
161
|
+
|
|
162
|
+
// If `https` is required, hold informations about the certificates, e.g. if
|
|
163
|
+
// self-signed, the dates of validity of the certificates, etc.
|
|
164
|
+
#httpsInfos = null;
|
|
165
|
+
#onClientConnectCallbacks = new Set();
|
|
166
|
+
#onClientDisconnectCallbacks = new Set();
|
|
167
|
+
#auditState = null;
|
|
168
|
+
#pendingConnectionTokens = new Set();
|
|
169
|
+
#trustedClients = new Set();
|
|
170
|
+
|
|
145
171
|
/**
|
|
146
|
-
* @param {
|
|
172
|
+
* @param {ServerConfig} config - Configuration object for the server.
|
|
147
173
|
* @throws
|
|
148
174
|
* - If `config.app.clients` is empty.
|
|
149
175
|
* - If a `node` client is defined but `config.env.serverAddress` is not defined.
|
|
@@ -156,38 +182,14 @@ class Server {
|
|
|
156
182
|
throw new Error(`[soundworks:Server] Invalid argument for Server constructor, config should be an object`);
|
|
157
183
|
}
|
|
158
184
|
|
|
159
|
-
|
|
160
|
-
* @description Given config object merged with the following defaults:
|
|
161
|
-
* @example
|
|
162
|
-
* {
|
|
163
|
-
* env: {
|
|
164
|
-
* type: 'development',
|
|
165
|
-
* port: 8000,
|
|
166
|
-
* serverAddress: '',
|
|
167
|
-
* subpath: '',
|
|
168
|
-
* websockets: {
|
|
169
|
-
* path: 'socket',
|
|
170
|
-
* pingInterval: 5000,
|
|
171
|
-
* },
|
|
172
|
-
* useHttps: false,
|
|
173
|
-
* httpsInfos: null,
|
|
174
|
-
* crossOriginIsolated: true,
|
|
175
|
-
* verbose: true,
|
|
176
|
-
* },
|
|
177
|
-
* app: {
|
|
178
|
-
* name: 'soundworks',
|
|
179
|
-
* clients: {},
|
|
180
|
-
* }
|
|
181
|
-
* }
|
|
182
|
-
*/
|
|
183
|
-
this.config = merge({}, DEFAULT_CONFIG, config);
|
|
185
|
+
this.#config = merge({}, DEFAULT_CONFIG, config);
|
|
184
186
|
|
|
185
187
|
// parse config
|
|
186
|
-
if (Object.keys(this
|
|
188
|
+
if (Object.keys(this.#config.app.clients).length === 0) {
|
|
187
189
|
throw new Error(`[soundworks:Server] Invalid "app.clients" config, at least one client should be declared`);
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
// @peeka - remove
|
|
192
|
+
// @peeka - remove this check
|
|
191
193
|
// [2024-05-29] Override default `config.env.serverAddress`` provided from
|
|
192
194
|
// template `loadConfig` to '' so that browser clients can default to
|
|
193
195
|
// window.location.hostname and node clients to `127.0.0.1`
|
|
@@ -195,10 +197,10 @@ class Server {
|
|
|
195
197
|
this.config.env.serverAddress = '';
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
if (this
|
|
199
|
-
const httpsInfos = this
|
|
200
|
+
if (this.#config.env.useHttps && this.#config.env.httpsInfos !== null) {
|
|
201
|
+
const httpsInfos = this.#config.env.httpsInfos;
|
|
200
202
|
|
|
201
|
-
if (!isPlainObject(this
|
|
203
|
+
if (!isPlainObject(this.#config.env.httpsInfos)) {
|
|
202
204
|
throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, should be null or object { cert, key }`);
|
|
203
205
|
}
|
|
204
206
|
|
|
@@ -215,135 +217,221 @@ class Server {
|
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
219
|
|
|
218
|
-
this
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Instance of the express router.
|
|
222
|
-
*
|
|
223
|
-
* The router can be used to open new route, for example to expose a directory
|
|
224
|
-
* of static assets (in default soundworks applications only the `public` is exposed).
|
|
225
|
-
*
|
|
226
|
-
* @see {@link https://github.com/expressjs/express}
|
|
227
|
-
* @example
|
|
228
|
-
* import { Server } from '@soundworks/core/server.js';
|
|
229
|
-
* import express from 'express';
|
|
230
|
-
*
|
|
231
|
-
* // create the soundworks server instance
|
|
232
|
-
* const server = new Server(config);
|
|
233
|
-
*
|
|
234
|
-
* // expose assets located in the `soundfiles` directory on the network
|
|
235
|
-
* server.router.use('/soundfiles', express.static('soundfiles')));
|
|
236
|
-
*/
|
|
220
|
+
this.#version = VERSION;
|
|
237
221
|
// @note: we use express() instead of express.Router() because all 404 and
|
|
238
222
|
// error stuff is handled by default
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Instance of the {@link server.Sockets} class.
|
|
253
|
-
*
|
|
254
|
-
* @see {@link server.Sockets}
|
|
255
|
-
* @type {server.Sockets}
|
|
256
|
-
*/
|
|
257
|
-
this.sockets = new Sockets();
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Instance of the {@link server.PluginManager} class.
|
|
261
|
-
*
|
|
262
|
-
* @see {@link server.PluginManager}
|
|
263
|
-
* @type {server.PluginManager}
|
|
264
|
-
*/
|
|
265
|
-
this.pluginManager = new PluginManager(this);
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Instance of the {@link server.StateManager} class.
|
|
269
|
-
*
|
|
270
|
-
* @see {@link server.StateManager}
|
|
271
|
-
* @type {server.StateManager}
|
|
272
|
-
*/
|
|
273
|
-
this.stateManager = new StateManager();
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Instance of the {@link server.ContextManager} class.
|
|
277
|
-
*
|
|
278
|
-
* @see {@link server.ContextManager}
|
|
279
|
-
* @type {server.ContextManager}
|
|
280
|
-
*/
|
|
281
|
-
this.contextManager = new ContextManager(this);
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* If `https` is required, hold informations about the certificates, e.g. if
|
|
285
|
-
* self-signed, the dates of validity of the certificates, etc.
|
|
286
|
-
*/
|
|
287
|
-
this.httpsInfos = null;
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Status of the server, 'idle', 'inited', 'started' or 'errored'.
|
|
291
|
-
*
|
|
292
|
-
* @type {string}
|
|
293
|
-
*/
|
|
294
|
-
this.status = 'idle';
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Simple key / value database with Promise based Map API store on filesystem,
|
|
298
|
-
* basically a tiny wrapper around the `kvey` package.
|
|
299
|
-
*
|
|
300
|
-
* @private
|
|
301
|
-
* @see {@link https://github.com/lukechilds/keyv}
|
|
302
|
-
*/
|
|
303
|
-
this.db = this.createNamespacedDb('core');
|
|
304
|
-
|
|
305
|
-
/** @private */
|
|
306
|
-
this._applicationTemplateOptions = {
|
|
223
|
+
// compression must be set before `express.static()`
|
|
224
|
+
this.#router = express();
|
|
225
|
+
this.#router.use(compression());
|
|
226
|
+
this.#sockets = new ServerSockets(this, { path: 'socket' });
|
|
227
|
+
this.#pluginManager = new ServerPluginManager(this);
|
|
228
|
+
this.#stateManager = new ServerStateManager();
|
|
229
|
+
this.#contextManager = new ServerContextManager(this);
|
|
230
|
+
this.#status = 'idle';
|
|
231
|
+
this.#db = this.createNamespacedDb('core');
|
|
232
|
+
|
|
233
|
+
this[kServerOnStatusChangeCallbacks] = new Set();
|
|
234
|
+
this[kServerApplicationTemplateOptions] = {
|
|
307
235
|
templateEngine: null,
|
|
308
236
|
templatePath: null,
|
|
309
237
|
clientConfigFunction: null,
|
|
310
238
|
};
|
|
311
239
|
|
|
312
|
-
/** @private */
|
|
313
|
-
this._onStatusChangeCallbacks = new Set();
|
|
314
|
-
/** @private */
|
|
315
|
-
this._onClientConnectCallbacks = new Set();
|
|
316
|
-
/** @private */
|
|
317
|
-
this._onClientDisconnectCallbacks = new Set();
|
|
318
|
-
/** @private */
|
|
319
|
-
this._auditState = null;
|
|
320
|
-
/** @private */
|
|
321
|
-
this._pendingConnectionTokens = new Set();
|
|
322
|
-
/** @private */
|
|
323
|
-
this._trustedClients = new Set();
|
|
324
|
-
|
|
325
240
|
// register audit state schema
|
|
326
|
-
this
|
|
241
|
+
this.#stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema);
|
|
242
|
+
|
|
243
|
+
logger.configure(this.#config.env.verbose);
|
|
244
|
+
}
|
|
327
245
|
|
|
328
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Given config object merged with the following defaults:
|
|
248
|
+
* @example
|
|
249
|
+
* {
|
|
250
|
+
* env: {
|
|
251
|
+
* type: 'development',
|
|
252
|
+
* port: 8000,
|
|
253
|
+
* serverAddress: null,
|
|
254
|
+
* subpath: '',
|
|
255
|
+
* useHttps: false,
|
|
256
|
+
* httpsInfos: null,
|
|
257
|
+
* crossOriginIsolated: true,
|
|
258
|
+
* verbose: true,
|
|
259
|
+
* },
|
|
260
|
+
* app: {
|
|
261
|
+
* name: 'soundworks',
|
|
262
|
+
* clients: {},
|
|
263
|
+
* }
|
|
264
|
+
* }
|
|
265
|
+
* @type {ServerConfig}
|
|
266
|
+
*/
|
|
267
|
+
get config() {
|
|
268
|
+
return this.#config;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Package version.
|
|
273
|
+
*
|
|
274
|
+
* @type {string}
|
|
275
|
+
*/
|
|
276
|
+
get version() {
|
|
277
|
+
return this.#version;
|
|
329
278
|
}
|
|
330
279
|
|
|
331
280
|
/**
|
|
332
281
|
* Id of the server, a constant set to -1
|
|
333
|
-
* @type {
|
|
282
|
+
* @type {number}
|
|
334
283
|
* @readonly
|
|
335
284
|
*/
|
|
336
285
|
get id() {
|
|
337
286
|
return SERVER_ID;
|
|
338
287
|
}
|
|
339
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Status of the server.
|
|
291
|
+
*
|
|
292
|
+
* @type {'idle'|'inited'|'started'|'errored'}
|
|
293
|
+
*/
|
|
294
|
+
get status() {
|
|
295
|
+
return this.#status;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Instance of the express router.
|
|
300
|
+
*
|
|
301
|
+
* The router can be used to open new route, for example to expose a directory
|
|
302
|
+
* of static assets (in default soundworks applications only the `public` is exposed).
|
|
303
|
+
*
|
|
304
|
+
* @see {@link https://github.com/expressjs/express}
|
|
305
|
+
* @example
|
|
306
|
+
* import { Server } from '@soundworks/core/server.js';
|
|
307
|
+
* import express from 'express';
|
|
308
|
+
*
|
|
309
|
+
* // create the soundworks server instance
|
|
310
|
+
* const server = new Server(config);
|
|
311
|
+
*
|
|
312
|
+
* // expose assets located in the `soundfiles` directory on the network
|
|
313
|
+
* server.router.use('/soundfiles', express.static('soundfiles')));
|
|
314
|
+
*/
|
|
315
|
+
get router() {
|
|
316
|
+
return this.#router;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Raw Node.js `http` or `https` instance
|
|
321
|
+
*
|
|
322
|
+
* @see {@link https://nodejs.org/api/http.html}
|
|
323
|
+
* @see {@link https://nodejs.org/api/https.html}
|
|
324
|
+
*/
|
|
325
|
+
get httpServer() {
|
|
326
|
+
return this.#httpServer;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Simple key / value filesystem database with Promise based Map API.
|
|
332
|
+
*
|
|
333
|
+
* Basically a tiny wrapper around the {@link https://github.com/lukechilds/keyv} package.
|
|
334
|
+
*/
|
|
335
|
+
get db() {
|
|
336
|
+
return this.#db;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Instance of the {@link ServerSockets} class.
|
|
341
|
+
*
|
|
342
|
+
* @type {ServerSockets}
|
|
343
|
+
*/
|
|
344
|
+
get sockets() {
|
|
345
|
+
return this.#sockets;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Instance of the {@link ServerPluginManager} class.
|
|
350
|
+
*
|
|
351
|
+
* @type {ServerPluginManager}
|
|
352
|
+
*/
|
|
353
|
+
get pluginManager() {
|
|
354
|
+
return this.#pluginManager;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Instance of the {@link ServerStateManager} class.
|
|
359
|
+
*
|
|
360
|
+
* @type {ServerStateManager}
|
|
361
|
+
*/
|
|
362
|
+
get stateManager() {
|
|
363
|
+
return this.#stateManager;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Instance of the {@link ServerContextManager} class.
|
|
368
|
+
*
|
|
369
|
+
* @type {ServerContextManager}
|
|
370
|
+
*/
|
|
371
|
+
get contextManager() {
|
|
372
|
+
return this.#contextManager;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** @private */
|
|
376
|
+
async #dispatchStatus(status) {
|
|
377
|
+
this.#status = status;
|
|
378
|
+
|
|
379
|
+
// if launched in a child process, forward status to parent process
|
|
380
|
+
if (process.send !== undefined) {
|
|
381
|
+
process.send(`soundworks:server:${status}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// execute all callbacks in parallel
|
|
385
|
+
const promises = [];
|
|
386
|
+
|
|
387
|
+
for (let callback of this[kServerOnStatusChangeCallbacks]) {
|
|
388
|
+
promises.push(callback(status));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await Promise.all(promises);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Register a callback to execute when status change
|
|
396
|
+
*
|
|
397
|
+
* @param {function} callback
|
|
398
|
+
*/
|
|
399
|
+
onStatusChange(callback) {
|
|
400
|
+
this[kServerOnStatusChangeCallbacks].add(callback);
|
|
401
|
+
return () => this[kServerOnStatusChangeCallbacks].delete(callback);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Attach and retrieve the global audit state of the application.
|
|
406
|
+
*
|
|
407
|
+
* The audit state is a {@link SharedState} instance that keeps track of
|
|
408
|
+
* global informations about the application such as, the number of connected
|
|
409
|
+
* clients, network latency estimation, etc.
|
|
410
|
+
*
|
|
411
|
+
* The audit state is created by the server on start up.
|
|
412
|
+
*
|
|
413
|
+
* @returns {Promise<SharedState>}
|
|
414
|
+
* @throws Will throw if called before `server.init()`
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* const auditState = await server.getAuditState();
|
|
418
|
+
* auditState.onUpdate(() => console.log(auditState.getValues()), true);
|
|
419
|
+
*/
|
|
420
|
+
async getAuditState() {
|
|
421
|
+
if (this.#status === 'idle') {
|
|
422
|
+
throw new Error(`[soundworks.Server] Cannot access audit state before init`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return this.#auditState;
|
|
426
|
+
}
|
|
427
|
+
|
|
340
428
|
/**
|
|
341
429
|
* The `init` method is part of the initialization lifecycle of the `soundworks`
|
|
342
430
|
* server. Most of the time, the `init` method will be implicitly called by the
|
|
343
|
-
* {@link
|
|
431
|
+
* {@link Server#start} method.
|
|
344
432
|
*
|
|
345
433
|
* In some situations you might want to call this method manually, in such cases
|
|
346
|
-
* the method should be called before the {@link
|
|
434
|
+
* the method should be called before the {@link Server#start} method.
|
|
347
435
|
*
|
|
348
436
|
* What it does:
|
|
349
437
|
* - create the audit state
|
|
@@ -351,7 +439,7 @@ class Server {
|
|
|
351
439
|
* declared in `config.app.clients`
|
|
352
440
|
* - initialize all registered plugins
|
|
353
441
|
*
|
|
354
|
-
* After `await server.init()` is fulfilled, the {@link
|
|
442
|
+
* After `await server.init()` is fulfilled, the {@link Server#stateManager}
|
|
355
443
|
* and all registered plugins can be safely used.
|
|
356
444
|
*
|
|
357
445
|
* @example
|
|
@@ -363,26 +451,23 @@ class Server {
|
|
|
363
451
|
* await server.start(); // init is called implicitely
|
|
364
452
|
*/
|
|
365
453
|
async init() {
|
|
366
|
-
|
|
367
|
-
// INIT STATE MANAGER
|
|
368
|
-
// ------------------------------------------------------------
|
|
369
|
-
this.stateManager.init(SERVER_ID, new EventEmitter());
|
|
454
|
+
this.#stateManager[kStateManagerInit](SERVER_ID, new EventEmitter());
|
|
370
455
|
|
|
371
456
|
const numClients = {};
|
|
372
|
-
for (let name in this
|
|
457
|
+
for (let name in this.#config.app.clients) {
|
|
373
458
|
numClients[name] = 0;
|
|
374
459
|
}
|
|
375
460
|
/** @private */
|
|
376
|
-
this
|
|
461
|
+
this.#auditState = await this.#stateManager.create(AUDIT_STATE_NAME, { numClients });
|
|
377
462
|
|
|
378
463
|
// basic http authentication
|
|
379
|
-
if (this
|
|
464
|
+
if (this.#config.env.auth) {
|
|
380
465
|
const ids = idGenerator();
|
|
381
466
|
|
|
382
467
|
const soundworksAuth = (req, res, next) => {
|
|
383
468
|
let role = null;
|
|
384
469
|
|
|
385
|
-
for (let [_role, config] of Object.entries(this
|
|
470
|
+
for (let [_role, config] of Object.entries(this.#config.app.clients)) {
|
|
386
471
|
if (req.path === config.route) {
|
|
387
472
|
role = _role;
|
|
388
473
|
}
|
|
@@ -394,11 +479,11 @@ class Server {
|
|
|
394
479
|
return;
|
|
395
480
|
}
|
|
396
481
|
|
|
397
|
-
const isProtected = this
|
|
482
|
+
const isProtected = this[kServerIsProtectedRole](role);
|
|
398
483
|
|
|
399
484
|
if (isProtected) {
|
|
400
485
|
// authentication middleware
|
|
401
|
-
const auth = this
|
|
486
|
+
const auth = this.#config.env.auth;
|
|
402
487
|
// parse login and password from headers
|
|
403
488
|
const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
|
|
404
489
|
const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':');
|
|
@@ -413,10 +498,10 @@ class Server {
|
|
|
413
498
|
const token = { id, ip, time };
|
|
414
499
|
const encryptedToken = encryptData(token);
|
|
415
500
|
|
|
416
|
-
this.
|
|
501
|
+
this.#pendingConnectionTokens.add(encryptedToken);
|
|
417
502
|
|
|
418
503
|
setTimeout(() => {
|
|
419
|
-
this.
|
|
504
|
+
this.#pendingConnectionTokens.delete(encryptedToken);
|
|
420
505
|
}, TOKEN_VALID_DURATION * 1000);
|
|
421
506
|
|
|
422
507
|
// pass to the response object to be send to the client
|
|
@@ -438,18 +523,18 @@ class Server {
|
|
|
438
523
|
}
|
|
439
524
|
};
|
|
440
525
|
|
|
441
|
-
this
|
|
526
|
+
this.#router.use(soundworksAuth);
|
|
442
527
|
}
|
|
443
528
|
|
|
444
529
|
// ------------------------------------------------------------
|
|
445
530
|
// create HTTP(S) SERVER
|
|
446
531
|
// ------------------------------------------------------------
|
|
447
|
-
const useHttps = this
|
|
532
|
+
const useHttps = this.#config.env.useHttps || false;
|
|
448
533
|
|
|
449
534
|
if (!useHttps) {
|
|
450
|
-
this
|
|
535
|
+
this.#httpServer = http.createServer(this.#router);
|
|
451
536
|
} else {
|
|
452
|
-
const httpsInfos = this
|
|
537
|
+
const httpsInfos = this.#config.env.httpsInfos;
|
|
453
538
|
let useSelfSigned = false;
|
|
454
539
|
|
|
455
540
|
if (!httpsInfos || equal(httpsInfos, { cert: null, key: null })) {
|
|
@@ -467,7 +552,7 @@ class Server {
|
|
|
467
552
|
try {
|
|
468
553
|
x509 = new X509Certificate(cert);
|
|
469
554
|
} catch (err) {
|
|
470
|
-
this
|
|
555
|
+
this.#dispatchStatus('errored');
|
|
471
556
|
throw new Error(`[soundworks:Server] Invalid https cert file`);
|
|
472
557
|
}
|
|
473
558
|
|
|
@@ -475,11 +560,11 @@ class Server {
|
|
|
475
560
|
const keyObj = createPrivateKey(key);
|
|
476
561
|
|
|
477
562
|
if (!x509.checkPrivateKey(keyObj)) {
|
|
478
|
-
this
|
|
563
|
+
this.#dispatchStatus('errored');
|
|
479
564
|
throw new Error(`[soundworks:Server] Invalid https key file`);
|
|
480
565
|
}
|
|
481
566
|
} catch (err) {
|
|
482
|
-
this
|
|
567
|
+
this.#dispatchStatus('errored');
|
|
483
568
|
throw new Error(`[soundworks:Server] Invalid https key file`);
|
|
484
569
|
}
|
|
485
570
|
|
|
@@ -491,7 +576,7 @@ class Server {
|
|
|
491
576
|
const diff = certExpire - now;
|
|
492
577
|
const daysRemaining = Math.round(diff / 1000 / 60 / 60 / 24);
|
|
493
578
|
|
|
494
|
-
this
|
|
579
|
+
this.#httpsInfos = {
|
|
495
580
|
selfSigned: false,
|
|
496
581
|
CN: x509.subject.split('=')[1],
|
|
497
582
|
altNames: x509.subjectAltName.split(',').map(e => e.trim().split(':')[1]),
|
|
@@ -501,7 +586,7 @@ class Server {
|
|
|
501
586
|
daysRemaining: daysRemaining,
|
|
502
587
|
};
|
|
503
588
|
|
|
504
|
-
this
|
|
589
|
+
this.#httpServer = https.createServer({ key, cert }, this.#router);
|
|
505
590
|
} catch (err) {
|
|
506
591
|
logger.error(`
|
|
507
592
|
Invalid certificate files, please check your:
|
|
@@ -509,25 +594,25 @@ Invalid certificate files, please check your:
|
|
|
509
594
|
- cert file: ${httpsInfos.cert}
|
|
510
595
|
`);
|
|
511
596
|
|
|
512
|
-
this
|
|
597
|
+
this.#dispatchStatus('errored');
|
|
513
598
|
throw err;
|
|
514
599
|
}
|
|
515
600
|
} else {
|
|
516
601
|
// generate certs
|
|
517
602
|
// --------------------------------------------------------
|
|
518
|
-
const cert = await this
|
|
519
|
-
const key = await this
|
|
603
|
+
const cert = await this.#db.get('httpsCert');
|
|
604
|
+
const key = await this.#db.get('httpsKey');
|
|
520
605
|
|
|
521
606
|
if (key && cert) {
|
|
522
|
-
this
|
|
523
|
-
this
|
|
607
|
+
this.#httpsInfos = { selfSigned: true };
|
|
608
|
+
this.#httpServer = https.createServer({ cert, key }, this.#router);
|
|
524
609
|
} else {
|
|
525
|
-
this
|
|
610
|
+
this.#httpServer = await new Promise((resolve, reject) => {
|
|
526
611
|
// generate certificate on the fly (for development purposes)
|
|
527
612
|
pem.createCertificate({ days: 1, selfSigned: true }, async (err, keys) => {
|
|
528
613
|
if (err) {
|
|
529
614
|
logger.error(err.stack);
|
|
530
|
-
this
|
|
615
|
+
this.#dispatchStatus('errored');
|
|
531
616
|
|
|
532
617
|
reject(err);
|
|
533
618
|
return;
|
|
@@ -536,13 +621,13 @@ Invalid certificate files, please check your:
|
|
|
536
621
|
const cert = keys.certificate;
|
|
537
622
|
const key = keys.serviceKey;
|
|
538
623
|
|
|
539
|
-
this
|
|
624
|
+
this.#httpsInfos = { selfSigned: true };
|
|
540
625
|
// we store the generated cert so that we don't have to re-accept
|
|
541
626
|
// the cert each time the server restarts in development
|
|
542
|
-
await this
|
|
543
|
-
await this
|
|
627
|
+
await this.#db.set('httpsCert', cert);
|
|
628
|
+
await this.#db.set('httpsKey', key);
|
|
544
629
|
|
|
545
|
-
const httpsServer = https.createServer({ cert, key }, this
|
|
630
|
+
const httpsServer = https.createServer({ cert, key }, this.#router);
|
|
546
631
|
|
|
547
632
|
resolve(httpsServer);
|
|
548
633
|
});
|
|
@@ -554,18 +639,18 @@ Invalid certificate files, please check your:
|
|
|
554
639
|
let nodeOnly = true;
|
|
555
640
|
// do not throw if no browser clients are defined, very usefull for
|
|
556
641
|
// cleaning tests in particular
|
|
557
|
-
for (let role in this
|
|
558
|
-
if (this
|
|
642
|
+
for (let role in this.#config.app.clients) {
|
|
643
|
+
if (this.#config.app.clients[role].target === 'browser') {
|
|
559
644
|
nodeOnly = false;
|
|
560
645
|
}
|
|
561
646
|
}
|
|
562
647
|
|
|
563
648
|
if (!nodeOnly) {
|
|
564
|
-
if (this.
|
|
565
|
-
|| this.
|
|
566
|
-
|| this.
|
|
649
|
+
if (this[kServerApplicationTemplateOptions].templateEngine === null
|
|
650
|
+
|| this[kServerApplicationTemplateOptions].templatePath === null
|
|
651
|
+
|| this[kServerApplicationTemplateOptions].clientConfigFunction === null
|
|
567
652
|
) {
|
|
568
|
-
throw new Error('[soundworks:Server] A browser client has been found in "config.app.clients" but configuration for html templating is missing. You should probably call `server.
|
|
653
|
+
throw new Error('[soundworks:Server] A browser client has been found in "config.app.clients" but configuration for html templating is missing. You should probably call `server.useDefaultApplicationTemplate()` if you use the soundworks-template and/or refer (at your own risks) to the documentation of `setCustomTemplateConfig()`');
|
|
569
654
|
}
|
|
570
655
|
}
|
|
571
656
|
|
|
@@ -577,8 +662,8 @@ Invalid certificate files, please check your:
|
|
|
577
662
|
const routes = [];
|
|
578
663
|
const clientsConfig = [];
|
|
579
664
|
|
|
580
|
-
for (let role in this
|
|
581
|
-
const config = Object.assign({}, this
|
|
665
|
+
for (let role in this.#config.app.clients) {
|
|
666
|
+
const config = Object.assign({}, this.#config.app.clients[role]);
|
|
582
667
|
config.role = role;
|
|
583
668
|
clientsConfig.push(config);
|
|
584
669
|
}
|
|
@@ -587,29 +672,29 @@ Invalid certificate files, please check your:
|
|
|
587
672
|
clientsConfig
|
|
588
673
|
.sort(a => a.default === true ? 1 : -1)
|
|
589
674
|
.forEach(config => {
|
|
590
|
-
const path = this
|
|
675
|
+
const path = this.#openClientRoute(this.#router, config);
|
|
591
676
|
routes.push({ role: config.role, path });
|
|
592
677
|
});
|
|
593
678
|
|
|
594
|
-
logger.clientConfigAndRouting(routes, this
|
|
679
|
+
logger.clientConfigAndRouting(routes, this.#config);
|
|
595
680
|
|
|
596
681
|
// ------------------------------------------------------------
|
|
597
682
|
// START PLUGIN MANAGER
|
|
598
683
|
// ------------------------------------------------------------
|
|
599
|
-
await this
|
|
684
|
+
await this.#pluginManager[kPluginManagerStart]();
|
|
600
685
|
|
|
601
|
-
await this
|
|
686
|
+
await this.#dispatchStatus('inited');
|
|
602
687
|
|
|
603
688
|
return Promise.resolve();
|
|
604
689
|
}
|
|
605
690
|
|
|
606
691
|
/**
|
|
607
692
|
* The `start` method is part of the initialization lifecycle of the `soundworks`
|
|
608
|
-
* server. The `start` method will implicitly call the {@link
|
|
693
|
+
* server. The `start` method will implicitly call the {@link Server#init}
|
|
609
694
|
* method if it has not been called manually.
|
|
610
695
|
*
|
|
611
696
|
* What it does:
|
|
612
|
-
* - implicitely call {@link
|
|
697
|
+
* - implicitely call {@link Server#init} if not done manually
|
|
613
698
|
* - launch the HTTP and WebSocket servers
|
|
614
699
|
* - start all created contexts. To this end, you will have to call `server.init`
|
|
615
700
|
* manually and instantiate the contexts between `server.init()` and `server.start()`
|
|
@@ -623,42 +708,38 @@ Invalid certificate files, please check your:
|
|
|
623
708
|
* await server.start();
|
|
624
709
|
*/
|
|
625
710
|
async start() {
|
|
626
|
-
if (this
|
|
711
|
+
if (this.#status === 'idle') {
|
|
627
712
|
await this.init();
|
|
628
713
|
}
|
|
629
714
|
|
|
630
|
-
if (this
|
|
715
|
+
if (this.#status === 'started') {
|
|
631
716
|
throw new Error(`[soundworks:Server] Cannot call "server.start()" twice`);
|
|
632
717
|
}
|
|
633
718
|
|
|
634
|
-
if (this
|
|
719
|
+
if (this.#status !== 'inited') {
|
|
635
720
|
throw new Error(`[soundworks:Server] Cannot "server.start()" before "server.init()"`);
|
|
636
721
|
}
|
|
637
722
|
|
|
638
723
|
// ------------------------------------------------------------
|
|
639
724
|
// START CONTEXT MANAGER
|
|
640
725
|
// ------------------------------------------------------------
|
|
641
|
-
await this
|
|
726
|
+
await this.#contextManager[kServerContextManagerStart]();
|
|
642
727
|
|
|
643
728
|
// ------------------------------------------------------------
|
|
644
729
|
// START SOCKET SERVER
|
|
645
730
|
// ------------------------------------------------------------
|
|
646
|
-
await this
|
|
647
|
-
this,
|
|
648
|
-
this.config.env.websockets,
|
|
649
|
-
(...args) => this._onSocketConnection(...args),
|
|
650
|
-
);
|
|
731
|
+
await this.#sockets[kSocketsStart]();
|
|
651
732
|
|
|
652
733
|
// ------------------------------------------------------------
|
|
653
734
|
// START HTTP SERVER
|
|
654
735
|
// ------------------------------------------------------------
|
|
655
736
|
return new Promise(resolve => {
|
|
656
|
-
const port = this
|
|
657
|
-
const useHttps = this
|
|
737
|
+
const port = this.#config.env.port;
|
|
738
|
+
const useHttps = this.#config.env.useHttps || false;
|
|
658
739
|
const protocol = useHttps ? 'https' : 'http';
|
|
659
740
|
const ifaces = os.networkInterfaces();
|
|
660
741
|
|
|
661
|
-
this
|
|
742
|
+
this.#httpServer.listen(port, async () => {
|
|
662
743
|
logger.title(`${protocol} server listening on`);
|
|
663
744
|
|
|
664
745
|
Object.keys(ifaces).forEach(dev => {
|
|
@@ -669,41 +750,41 @@ Invalid certificate files, please check your:
|
|
|
669
750
|
});
|
|
670
751
|
});
|
|
671
752
|
|
|
672
|
-
if (this
|
|
753
|
+
if (this.#httpsInfos !== null) {
|
|
673
754
|
logger.title(`https certificates infos`);
|
|
674
755
|
|
|
675
|
-
// this
|
|
676
|
-
if (this
|
|
677
|
-
logger.log(` self-signed: ${this
|
|
756
|
+
// this.#httpsInfos.selfSigned = true;
|
|
757
|
+
if (this.#httpsInfos.selfSigned) {
|
|
758
|
+
logger.log(` self-signed: ${this.#httpsInfos.selfSigned ? 'true' : 'false'}`);
|
|
678
759
|
logger.log(chalk.yellow` > INVALID CERTIFICATE (self-signed)`);
|
|
679
760
|
|
|
680
761
|
} else {
|
|
681
|
-
logger.log(` valid from: ${this
|
|
682
|
-
logger.log(` valid to: ${this
|
|
762
|
+
logger.log(` valid from: ${this.#httpsInfos.validFrom}`);
|
|
763
|
+
logger.log(` valid to: ${this.#httpsInfos.validTo}`);
|
|
683
764
|
|
|
684
|
-
// this
|
|
685
|
-
if (!this
|
|
765
|
+
// this.#httpsInfos.isValid = false; // for testing
|
|
766
|
+
if (!this.#httpsInfos.isValid) {
|
|
686
767
|
logger.error(chalk.red` -------------------------------------------`);
|
|
687
768
|
logger.error(chalk.red` > INVALID CERTIFICATE `);
|
|
688
769
|
logger.error(chalk.red` i.e. you pretend to be safe but you are not`);
|
|
689
770
|
logger.error(chalk.red` -------------------------------------------`);
|
|
690
771
|
} else {
|
|
691
|
-
// this
|
|
692
|
-
if (this
|
|
693
|
-
logger.log(chalk.red` > CERTIFICATE IS VALID... BUT ONLY ${this
|
|
694
|
-
} else if (this
|
|
695
|
-
logger.log(chalk.yellow` > CERTIFICATE IS VALID - only ${this
|
|
772
|
+
// this.#httpsInfos.daysRemaining = 2; // for testing
|
|
773
|
+
if (this.#httpsInfos.daysRemaining < 5) {
|
|
774
|
+
logger.log(chalk.red` > CERTIFICATE IS VALID... BUT ONLY ${this.#httpsInfos.daysRemaining} DAYS LEFT, PLEASE CONSIDER UPDATING YOUR CERTS!`);
|
|
775
|
+
} else if (this.#httpsInfos.daysRemaining < 15) {
|
|
776
|
+
logger.log(chalk.yellow` > CERTIFICATE IS VALID - only ${this.#httpsInfos.daysRemaining} days left, be careful...`);
|
|
696
777
|
} else {
|
|
697
|
-
logger.log(chalk.green` > CERTIFICATE IS VALID (${this
|
|
778
|
+
logger.log(chalk.green` > CERTIFICATE IS VALID (${this.#httpsInfos.daysRemaining} days left)`);
|
|
698
779
|
}
|
|
699
780
|
}
|
|
700
781
|
|
|
701
782
|
}
|
|
702
783
|
}
|
|
703
784
|
|
|
704
|
-
await this
|
|
785
|
+
await this.#dispatchStatus('started');
|
|
705
786
|
|
|
706
|
-
if (this
|
|
787
|
+
if (this.#config.env.type === 'development') {
|
|
707
788
|
logger.log(`\n> press "${chalk.bold('Ctrl + C')}" to exit`);
|
|
708
789
|
}
|
|
709
790
|
|
|
@@ -730,28 +811,29 @@ Invalid certificate files, please check your:
|
|
|
730
811
|
* await server.stop();
|
|
731
812
|
*/
|
|
732
813
|
async stop() {
|
|
733
|
-
if (this
|
|
814
|
+
if (this.#status !== 'started') {
|
|
734
815
|
throw new Error(`[soundworks:Server] Cannot stop() before start()`);
|
|
735
816
|
}
|
|
736
817
|
|
|
737
|
-
await this
|
|
738
|
-
await this
|
|
818
|
+
await this.#contextManager[kServerContextManagerStop]();
|
|
819
|
+
await this.#pluginManager[kPluginManagerStop]();
|
|
739
820
|
|
|
740
|
-
this
|
|
741
|
-
|
|
821
|
+
this.#sockets[kSocketsStop]();
|
|
822
|
+
|
|
823
|
+
this.#httpServer.close(err => {
|
|
742
824
|
if (err) {
|
|
743
825
|
throw new Error(err.message);
|
|
744
826
|
}
|
|
745
827
|
});
|
|
746
828
|
|
|
747
|
-
await this
|
|
829
|
+
await this.#dispatchStatus('stopped');
|
|
748
830
|
}
|
|
749
831
|
|
|
750
832
|
/**
|
|
751
833
|
* Open the route for a given client.
|
|
752
834
|
* @private
|
|
753
835
|
*/
|
|
754
|
-
|
|
836
|
+
#openClientRoute(router, config) {
|
|
755
837
|
const { role, target } = config;
|
|
756
838
|
const isDefault = (config.default === true);
|
|
757
839
|
// only browser targets need a route
|
|
@@ -765,14 +847,14 @@ Invalid certificate files, please check your:
|
|
|
765
847
|
route += `${role}`;
|
|
766
848
|
}
|
|
767
849
|
|
|
768
|
-
this
|
|
850
|
+
this.#config.app.clients[role].route = route;
|
|
769
851
|
|
|
770
852
|
// define template filename: `${role}.html` or `default.html`
|
|
771
853
|
const {
|
|
772
854
|
templatePath,
|
|
773
855
|
templateEngine,
|
|
774
856
|
clientConfigFunction,
|
|
775
|
-
} = this
|
|
857
|
+
} = this[kServerApplicationTemplateOptions];
|
|
776
858
|
|
|
777
859
|
const clientTmpl = path.join(templatePath, `${role}.tmpl`);
|
|
778
860
|
const defaultTmpl = path.join(templatePath, `default.tmpl`);
|
|
@@ -798,7 +880,7 @@ Invalid certificate files, please check your:
|
|
|
798
880
|
const tmpl = templateEngine.compile(tmplString);
|
|
799
881
|
|
|
800
882
|
const soundworksClientHandler = (req, res) => {
|
|
801
|
-
const data = clientConfigFunction(role, this
|
|
883
|
+
const data = clientConfigFunction(role, this.#config, req);
|
|
802
884
|
|
|
803
885
|
// if the client has gone through the connection middleware (add succedeed),
|
|
804
886
|
// add the token to the data object
|
|
@@ -809,7 +891,7 @@ Invalid certificate files, please check your:
|
|
|
809
891
|
// CORS / COOP / COEP headers for `crossOriginIsolated pages,
|
|
810
892
|
// enables `sharedArrayBuffers` and high precision timers
|
|
811
893
|
// cf. https://web.dev/why-coop-coep/
|
|
812
|
-
if (this
|
|
894
|
+
if (this.#config.env.crossOriginIsolated) {
|
|
813
895
|
res.writeHead(200, {
|
|
814
896
|
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
815
897
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
@@ -829,62 +911,62 @@ Invalid certificate files, please check your:
|
|
|
829
911
|
}
|
|
830
912
|
|
|
831
913
|
onClientConnect(callback) {
|
|
832
|
-
this.
|
|
914
|
+
this.#onClientConnectCallbacks.add(callback);
|
|
833
915
|
|
|
834
|
-
return () => this.
|
|
916
|
+
return () => this.#onClientConnectCallbacks.delete(callback);
|
|
835
917
|
}
|
|
836
918
|
|
|
837
919
|
onClientDisconnect(callback) {
|
|
838
|
-
this.
|
|
920
|
+
this.#onClientDisconnectCallbacks.add(callback);
|
|
839
921
|
|
|
840
|
-
return () => this.
|
|
922
|
+
return () => this.#onClientDisconnectCallbacks.delete(callback);
|
|
841
923
|
}
|
|
842
924
|
|
|
843
925
|
/**
|
|
844
926
|
* Socket connection callback.
|
|
845
927
|
* @private
|
|
846
928
|
*/
|
|
847
|
-
|
|
848
|
-
const client = new
|
|
929
|
+
[kServerOnSocketConnection](role, socket, connectionToken) {
|
|
930
|
+
const client = new ServerClient(role, socket);
|
|
849
931
|
socket[kSocketClientId] = client.id;
|
|
850
|
-
const roles = Object.keys(this
|
|
932
|
+
const roles = Object.keys(this.#config.app.clients);
|
|
851
933
|
|
|
852
934
|
// this has been validated
|
|
853
|
-
if (this
|
|
935
|
+
if (this[kServerIsProtectedRole](role) && this[kServerIsValidConnectionToken](connectionToken)) {
|
|
854
936
|
const { ip } = decryptData(connectionToken);
|
|
855
937
|
const newData = { ip, id: client.id };
|
|
856
938
|
const newToken = encryptData(newData);
|
|
857
939
|
|
|
858
|
-
client
|
|
940
|
+
client[kServerClientToken] = newToken;
|
|
859
941
|
|
|
860
|
-
this.
|
|
861
|
-
this.
|
|
942
|
+
this.#pendingConnectionTokens.delete(connectionToken);
|
|
943
|
+
this.#trustedClients.add(client);
|
|
862
944
|
}
|
|
863
945
|
|
|
864
946
|
socket.addListener('close', async () => {
|
|
865
947
|
// do nothing if client role is invalid
|
|
866
948
|
if (roles.includes(role)) {
|
|
867
949
|
// decrement audit state counter
|
|
868
|
-
const numClients = this.
|
|
950
|
+
const numClients = this.#auditState.get('numClients');
|
|
869
951
|
numClients[role] -= 1;
|
|
870
|
-
this.
|
|
952
|
+
this.#auditState.set({ numClients });
|
|
871
953
|
|
|
872
954
|
// delete token
|
|
873
|
-
if (this.
|
|
874
|
-
this.
|
|
955
|
+
if (this.#trustedClients.has(client)) {
|
|
956
|
+
this.#trustedClients.delete(client);
|
|
875
957
|
}
|
|
876
958
|
|
|
877
959
|
// if something goes wrong here, the 'close' event is called again and
|
|
878
960
|
// again and again... let's just log the error and terminate the socket
|
|
879
961
|
try {
|
|
880
962
|
// clean context manager, await before cleaning state manager
|
|
881
|
-
await this
|
|
963
|
+
await this.#contextManager[kServerContextManagerRemoveClient](client);
|
|
882
964
|
// remove client from pluginManager
|
|
883
|
-
await this
|
|
965
|
+
await this.#pluginManager[kServerPluginManagerRemoveClient](client);
|
|
884
966
|
// clean state manager
|
|
885
|
-
await this
|
|
967
|
+
await this.#stateManager[kServerStateManagerRemoveClient](client.id);
|
|
886
968
|
|
|
887
|
-
this.
|
|
969
|
+
this.#onClientDisconnectCallbacks.forEach(callback => callback(client));
|
|
888
970
|
} catch (err) {
|
|
889
971
|
console.error(err);
|
|
890
972
|
}
|
|
@@ -907,12 +989,12 @@ Invalid certificate files, please check your:
|
|
|
907
989
|
return;
|
|
908
990
|
}
|
|
909
991
|
|
|
910
|
-
if (version !== this
|
|
911
|
-
logger.warnVersionDiscepancies(role, version, this
|
|
992
|
+
if (version !== this.#version) {
|
|
993
|
+
logger.warnVersionDiscepancies(role, version, this.#version);
|
|
912
994
|
}
|
|
913
995
|
|
|
914
996
|
try {
|
|
915
|
-
this
|
|
997
|
+
this.#pluginManager[kServerPluginManagerCheckRegisteredPlugins](registeredPlugins);
|
|
916
998
|
} catch (err) {
|
|
917
999
|
socket.send(CLIENT_HANDSHAKE_ERROR, {
|
|
918
1000
|
type: 'invalid-plugin-list',
|
|
@@ -922,26 +1004,27 @@ Invalid certificate files, please check your:
|
|
|
922
1004
|
}
|
|
923
1005
|
|
|
924
1006
|
// increment audit state
|
|
925
|
-
const numClients = this.
|
|
1007
|
+
const numClients = this.#auditState.get('numClients');
|
|
926
1008
|
numClients[role] += 1;
|
|
927
|
-
this.
|
|
1009
|
+
this.#auditState.set({ numClients });
|
|
928
1010
|
|
|
929
|
-
|
|
930
|
-
await this.stateManager.addClient(client.id, {
|
|
1011
|
+
const transport = {
|
|
931
1012
|
emit: client.socket.send.bind(client.socket),
|
|
932
1013
|
addListener: client.socket.addListener.bind(client.socket),
|
|
933
1014
|
removeAllListeners: client.socket.removeAllListeners.bind(client.socket),
|
|
934
|
-
}
|
|
1015
|
+
};
|
|
1016
|
+
// add client to state manager
|
|
1017
|
+
await this.#stateManager[kServerStateManagerAddClient](client.id, transport);
|
|
935
1018
|
// add client to plugin manager
|
|
936
1019
|
// server-side, all plugins are active for the lifetime of the client
|
|
937
|
-
await this
|
|
1020
|
+
await this.#pluginManager[kServerPluginManagerAddClient](client, registeredPlugins);
|
|
938
1021
|
// add client to context manager
|
|
939
|
-
await this
|
|
1022
|
+
await this.#contextManager[kServerContextManagerAddClient](client);
|
|
940
1023
|
|
|
941
|
-
this.
|
|
1024
|
+
this.#onClientConnectCallbacks.forEach(callback => callback(client));
|
|
942
1025
|
|
|
943
1026
|
const { id, uuid, token } = client;
|
|
944
|
-
socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid, token, version: this
|
|
1027
|
+
socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid, token, version: this.#version });
|
|
945
1028
|
});
|
|
946
1029
|
}
|
|
947
1030
|
|
|
@@ -974,31 +1057,6 @@ Invalid certificate files, please check your:
|
|
|
974
1057
|
return db;
|
|
975
1058
|
}
|
|
976
1059
|
|
|
977
|
-
onStatusChange(callback) {
|
|
978
|
-
this._onStatusChangeCallbacks.add(callback);
|
|
979
|
-
|
|
980
|
-
return () => this._onStatusChangeCallbacks.delete(callback);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/** @private */
|
|
984
|
-
async _dispatchStatus(status) {
|
|
985
|
-
this.status = status;
|
|
986
|
-
|
|
987
|
-
// if launched in a child process, forward status to parent process
|
|
988
|
-
if (process.send !== undefined) {
|
|
989
|
-
process.send(`soundworks:server:${status}`);
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// execute all callbacks in parallel
|
|
993
|
-
const promises = [];
|
|
994
|
-
|
|
995
|
-
for (let callback of this._onStatusChangeCallbacks) {
|
|
996
|
-
promises.push(callback(status));
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
await Promise.all(promises);
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
1060
|
/**
|
|
1003
1061
|
* Configure the server to work _out-of-the-box_ within the soundworks application
|
|
1004
1062
|
* template provided by `@soundworks/create.
|
|
@@ -1021,10 +1079,10 @@ Invalid certificate files, please check your:
|
|
|
1021
1079
|
const buildDirectory = path.join('.build', 'public');
|
|
1022
1080
|
|
|
1023
1081
|
const useMinifiedFile = {};
|
|
1024
|
-
const roles = Object.keys(this
|
|
1082
|
+
const roles = Object.keys(this.#config.app.clients);
|
|
1025
1083
|
|
|
1026
1084
|
roles.forEach(role => {
|
|
1027
|
-
if (this
|
|
1085
|
+
if (this.#config.env.type === 'production') {
|
|
1028
1086
|
// check if minified file exists
|
|
1029
1087
|
const minifiedFilePath = path.join(buildDirectory, `${role}.min.js`);
|
|
1030
1088
|
|
|
@@ -1039,7 +1097,7 @@ Invalid certificate files, please check your:
|
|
|
1039
1097
|
}
|
|
1040
1098
|
});
|
|
1041
1099
|
|
|
1042
|
-
this
|
|
1100
|
+
this[kServerApplicationTemplateOptions] = {
|
|
1043
1101
|
templateEngine: { compile },
|
|
1044
1102
|
templatePath: path.join('.build', 'server', 'tmpl'),
|
|
1045
1103
|
clientConfigFunction: (role, config, _httpRequest) => {
|
|
@@ -1065,8 +1123,8 @@ Invalid certificate files, please check your:
|
|
|
1065
1123
|
},
|
|
1066
1124
|
};
|
|
1067
1125
|
|
|
1068
|
-
this
|
|
1069
|
-
this
|
|
1126
|
+
this.#router.use(express.static('public'));
|
|
1127
|
+
this.#router.use('/build', express.static(buildDirectory));
|
|
1070
1128
|
}
|
|
1071
1129
|
|
|
1072
1130
|
/**
|
|
@@ -1076,46 +1134,25 @@ Invalid certificate files, please check your:
|
|
|
1076
1134
|
* first to explain your use-case :)
|
|
1077
1135
|
*/
|
|
1078
1136
|
setCustomApplicationTemplateOptions(options) {
|
|
1079
|
-
Object.assign(this
|
|
1137
|
+
Object.assign(this[kServerApplicationTemplateOptions], options);
|
|
1080
1138
|
}
|
|
1081
1139
|
|
|
1082
|
-
/**
|
|
1083
|
-
* Attach and retrieve the global audit state of the application.
|
|
1084
|
-
*
|
|
1085
|
-
* The audit state is a {@link server.SharedState} instance that keeps track of
|
|
1086
|
-
* global informations about the application such as, the number of connected
|
|
1087
|
-
* clients, network latency estimation, etc.
|
|
1088
|
-
*
|
|
1089
|
-
* The audit state is created by the server on start up.
|
|
1090
|
-
*
|
|
1091
|
-
* @returns {Promise<server.SharedState>}
|
|
1092
|
-
* @throws Will throw if called before `server.init()`
|
|
1093
|
-
* @see {@link server.SharedState}
|
|
1094
|
-
* @example
|
|
1095
|
-
* const auditState = await server.getAuditState();
|
|
1096
|
-
* auditState.onUpdate(() => console.log(auditState.getValues()), true);
|
|
1097
|
-
*/
|
|
1098
|
-
async getAuditState() {
|
|
1099
|
-
if (this.status === 'idle') {
|
|
1100
|
-
throw new Error(`[soundworks.Server] Cannot access audit state before init`);
|
|
1101
|
-
}
|
|
1102
1140
|
|
|
1103
|
-
return this._auditState;
|
|
1104
|
-
}
|
|
1105
1141
|
|
|
1142
|
+
//
|
|
1106
1143
|
/** @private */
|
|
1107
|
-
|
|
1108
|
-
if (this
|
|
1109
|
-
return this
|
|
1144
|
+
[kServerIsProtectedRole](role) {
|
|
1145
|
+
if (this.#config.env.auth && Array.isArray(this.#config.env.auth.clients)) {
|
|
1146
|
+
return this.#config.env.auth.clients.includes(role);
|
|
1110
1147
|
}
|
|
1111
1148
|
|
|
1112
1149
|
return false;
|
|
1113
1150
|
}
|
|
1114
1151
|
|
|
1115
1152
|
/** @private */
|
|
1116
|
-
|
|
1153
|
+
[kServerIsValidConnectionToken](token) {
|
|
1117
1154
|
// token should be in pending token list
|
|
1118
|
-
if (!this.
|
|
1155
|
+
if (!this.#pendingConnectionTokens.has(token)) {
|
|
1119
1156
|
return false;
|
|
1120
1157
|
}
|
|
1121
1158
|
|
|
@@ -1126,7 +1163,7 @@ Invalid certificate files, please check your:
|
|
|
1126
1163
|
// token is valid only for 30 seconds (this is arbitrary)
|
|
1127
1164
|
if (now > data.time + TOKEN_VALID_DURATION) {
|
|
1128
1165
|
// delete the token, is too old
|
|
1129
|
-
this.
|
|
1166
|
+
this.#pendingConnectionTokens.delete(token);
|
|
1130
1167
|
return false;
|
|
1131
1168
|
} else {
|
|
1132
1169
|
return true;
|
|
@@ -1137,14 +1174,14 @@ Invalid certificate files, please check your:
|
|
|
1137
1174
|
* Check if the given client is trusted, i.e. config.env.type == 'production'
|
|
1138
1175
|
* and the client is protected behind a password.
|
|
1139
1176
|
*
|
|
1140
|
-
* @param {
|
|
1177
|
+
* @param {ServerClient} client - Client to be tested
|
|
1141
1178
|
* @returns {Boolean}
|
|
1142
1179
|
*/
|
|
1143
1180
|
isTrustedClient(client) {
|
|
1144
|
-
if (this
|
|
1181
|
+
if (this.#config.env.type !== 'production') {
|
|
1145
1182
|
return true;
|
|
1146
1183
|
} else {
|
|
1147
|
-
return this.
|
|
1184
|
+
return this.#trustedClients.has(client);
|
|
1148
1185
|
}
|
|
1149
1186
|
}
|
|
1150
1187
|
|
|
@@ -1159,13 +1196,13 @@ Invalid certificate files, please check your:
|
|
|
1159
1196
|
*/
|
|
1160
1197
|
// for stateless interactions, e.g. POST files
|
|
1161
1198
|
isTrustedToken(clientId, clientIp, token) {
|
|
1162
|
-
if (this
|
|
1199
|
+
if (this.#config.env.type !== 'production') {
|
|
1163
1200
|
return true;
|
|
1164
1201
|
} else {
|
|
1165
|
-
for (let client of this
|
|
1166
|
-
if (client.id === clientId && client
|
|
1202
|
+
for (let client of this.#trustedClients) {
|
|
1203
|
+
if (client.id === clientId && client[kServerClientToken] === token) {
|
|
1167
1204
|
// check that given token is consistent with client ip and id
|
|
1168
|
-
const { id, ip } = decryptData(client
|
|
1205
|
+
const { id, ip } = decryptData(client[kServerClientToken]);
|
|
1169
1206
|
|
|
1170
1207
|
if (clientId === id && clientIp === ip) {
|
|
1171
1208
|
return true;
|