@soundworks/core 4.0.0-alpha.9 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/README.md +14 -29
- package/package.json +24 -42
- package/src/client/Client.js +306 -256
- package/src/client/{Context.js → ClientContext.js} +63 -54
- package/src/client/ClientContextManager.js +115 -0
- package/src/client/{Plugin.js → ClientPlugin.js} +23 -19
- package/src/client/{PluginManager.js → ClientPluginManager.js} +43 -40
- package/src/client/ClientSocket.js +270 -0
- package/src/client/{StateManager.js → ClientStateManager.js} +13 -13
- package/src/client/index.js +36 -39
- package/src/common/BasePlugin.js +63 -58
- package/src/common/BasePluginManager.js +191 -142
- package/src/common/BaseStateManager.js +635 -145
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +90 -93
- package/src/common/PromiseStore.js +54 -0
- package/src/common/SharedState.js +821 -0
- package/src/common/SharedStateCollection.js +492 -0
- package/src/common/constants.js +14 -2
- package/src/common/logger.js +64 -68
- package/src/common/shared-state-types.js +82 -0
- package/src/common/sockets-utils.js +0 -64
- package/src/common/version.js +2 -0
- package/src/server/Server.js +552 -824
- package/src/server/ServerClient.js +74 -0
- package/src/server/{Context.js → ServerContext.js} +96 -77
- package/src/server/{ContextManager.js → ServerContextManager.js} +107 -87
- package/src/server/{Plugin.js → ServerPlugin.js} +30 -32
- package/src/server/ServerPluginManager.js +177 -0
- package/src/server/ServerSocket.js +256 -0
- package/src/server/ServerSockets.js +150 -0
- package/src/server/ServerStateManager.js +706 -0
- package/src/server/SharedStatePrivate.js +277 -0
- package/src/server/audit-network-latency.worker.js +19 -4
- package/src/server/{audit-schema.js → audit-state-class-description.js} +3 -2
- package/src/server/create-http-server.js +127 -0
- package/src/server/crypto.js +8 -7
- package/src/server/index.js +60 -17
- package/types/client/Client.d.ts +95 -70
- package/types/client/{Context.d.ts → ClientContext.d.ts} +59 -34
- package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
- package/types/client/ClientPlugin.d.ts +51 -0
- package/types/client/ClientPluginManager.d.ts +112 -0
- package/types/client/ClientSocket.d.ts +76 -0
- package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +18 -21
- package/types/client/index.d.ts +33 -6
- package/types/common/BasePlugin.d.ts +64 -53
- package/types/common/BasePluginManager.d.ts +84 -39
- package/types/common/BaseStateManager.d.ts +227 -49
- package/types/common/BatchedTransport.d.ts +17 -0
- package/types/common/ParameterBag.d.ts +73 -76
- package/types/common/PromiseStore.d.ts +11 -0
- package/types/common/SharedState.d.ts +291 -0
- package/types/common/SharedStateCollection.d.ts +311 -0
- package/types/common/constants.d.ts +10 -3
- package/types/common/logger.d.ts +8 -6
- package/types/common/shared-state-types.d.ts +95 -0
- package/types/common/sockets-utils.d.ts +0 -5
- package/types/common/version.d.ts +2 -0
- package/types/server/Server.d.ts +215 -136
- package/types/server/ServerClient.d.ts +45 -0
- package/types/server/ServerContext.d.ts +209 -0
- package/types/server/ServerContextManager.d.ts +75 -0
- package/types/server/ServerPlugin.d.ts +57 -0
- package/types/server/ServerPluginManager.d.ts +115 -0
- package/types/server/ServerSocket.d.ts +69 -0
- package/types/server/ServerSockets.d.ts +42 -0
- package/types/server/ServerStateManager.d.ts +284 -0
- package/types/server/SharedStatePrivate.d.ts +23 -0
- package/types/server/audit-network-latency.worker.d.ts +2 -2
- package/types/server/{audit-schema.d.ts → audit-state-class-description.d.ts} +8 -9
- package/types/server/create-http-server.d.ts +5 -0
- package/types/server/crypto.d.ts +4 -0
- package/types/server/index.d.ts +51 -3
- package/.eslintrc +0 -15
- package/.github/workflows/gh-pages.yml +0 -31
- package/.github/workflows/unit-tests.yml +0 -18
- package/src/client/ContextManager.js +0 -111
- package/src/client/SharedState.js +0 -84
- package/src/client/SharedStateCollection.js +0 -35
- package/src/client/Socket.js +0 -402
- package/src/common/BaseSharedState.js +0 -583
- package/src/common/BaseSharedStateCollection.js +0 -179
- package/src/common/SharedStatePrivate.js +0 -192
- package/src/common/promise-store.js +0 -25
- package/src/server/Client.js +0 -55
- package/src/server/PluginManager.js +0 -170
- package/src/server/SharedState.js +0 -85
- package/src/server/SharedStateCollection.js +0 -36
- package/src/server/Socket.js +0 -396
- package/src/server/Sockets.js +0 -224
- package/src/server/StateManager.js +0 -620
- package/tsconfig.json +0 -21
- 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 +0 -53
- package/types/client/Plugin.d.ts.map +0 -1
- package/types/client/PluginManager.d.ts +0 -96
- 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/Socket.d.ts +0 -138
- 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 -173
- package/types/common/BaseSharedState.d.ts.map +0 -1
- package/types/common/BaseStateManager.d.ts.map +0 -1
- package/types/common/ParameterBag.d.ts.map +0 -1
- package/types/common/SharedStatePrivate.d.ts +0 -20
- 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/promise-store.d.ts +0 -4
- package/types/common/promise-store.d.ts.map +0 -1
- package/types/common/sockets-utils.d.ts.map +0 -1
- package/types/common/utils.d.ts +0 -5
- package/types/common/utils.d.ts.map +0 -1
- package/types/server/Client.d.ts +0 -40
- package/types/server/Client.d.ts.map +0 -1
- package/types/server/Context.d.ts +0 -59
- 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 +0 -70
- package/types/server/Plugin.d.ts.map +0 -1
- package/types/server/PluginManager.d.ts +0 -91
- 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/Socket.d.ts +0 -128
- package/types/server/Socket.d.ts.map +0 -1
- package/types/server/Sockets.d.ts +0 -73
- package/types/server/Sockets.d.ts.map +0 -1
- package/types/server/StateManager.d.ts +0 -190
- 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/index.d.ts.map +0 -1
package/src/server/Server.js
CHANGED
|
@@ -1,28 +1,62 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
|
-
import http from 'node:http';
|
|
3
|
-
import https from 'node:https';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import os from 'node:os';
|
|
6
|
-
import { X509Certificate, createPrivateKey } from 'node:crypto';
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
import {
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isPlainObject,
|
|
9
|
+
isString,
|
|
10
|
+
counter,
|
|
11
|
+
getTime,
|
|
12
|
+
} from '@ircam/sc-utils';
|
|
10
13
|
import chalk from 'chalk';
|
|
11
|
-
import compression from 'compression';
|
|
12
|
-
import express from 'express';
|
|
13
14
|
import Keyv from 'keyv';
|
|
14
15
|
import { KeyvFile } from 'keyv-file';
|
|
15
|
-
import merge from 'lodash.
|
|
16
|
-
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import
|
|
16
|
+
import merge from 'lodash/merge.js';
|
|
17
|
+
|
|
18
|
+
import auditClassDescription from './audit-state-class-description.js';
|
|
19
|
+
import {
|
|
20
|
+
encryptData,
|
|
21
|
+
decryptData,
|
|
22
|
+
} from './crypto.js';
|
|
23
|
+
import {
|
|
24
|
+
createHttpServer,
|
|
25
|
+
} from './create-http-server.js';
|
|
26
|
+
import ServerClient, {
|
|
27
|
+
kServerClientToken,
|
|
28
|
+
} from './ServerClient.js';
|
|
29
|
+
import ServerContextManager, {
|
|
30
|
+
kServerContextManagerStart,
|
|
31
|
+
kServerContextManagerStop,
|
|
32
|
+
kServerContextManagerAddClient,
|
|
33
|
+
kServerContextManagerRemoveClient,
|
|
34
|
+
} from './ServerContextManager.js';
|
|
35
|
+
import ServerPluginManager, {
|
|
36
|
+
kServerPluginManagerCheckRegisteredPlugins,
|
|
37
|
+
kServerPluginManagerAddClient,
|
|
38
|
+
kServerPluginManagerRemoveClient,
|
|
39
|
+
} from './ServerPluginManager.js';
|
|
40
|
+
import {
|
|
41
|
+
kPluginManagerStart,
|
|
42
|
+
kPluginManagerStop,
|
|
43
|
+
} from '../common/BasePluginManager.js';
|
|
44
|
+
import ServerStateManager, {
|
|
45
|
+
kServerStateManagerAddClient,
|
|
46
|
+
kServerStateManagerRemoveClient,
|
|
47
|
+
kServerStateManagerHasClient,
|
|
48
|
+
} from './ServerStateManager.js';
|
|
49
|
+
import {
|
|
50
|
+
kStateManagerInit,
|
|
51
|
+
} from '../common/BaseStateManager.js';
|
|
52
|
+
import {
|
|
53
|
+
kSocketClientId,
|
|
54
|
+
kSocketTerminate,
|
|
55
|
+
} from './ServerSocket.js';
|
|
56
|
+
import ServerSockets, {
|
|
57
|
+
kSocketsStart,
|
|
58
|
+
kSocketsStop,
|
|
59
|
+
} from './ServerSockets.js';
|
|
26
60
|
import logger from '../common/logger.js';
|
|
27
61
|
import {
|
|
28
62
|
SERVER_ID,
|
|
@@ -31,43 +65,19 @@ import {
|
|
|
31
65
|
CLIENT_HANDSHAKE_ERROR,
|
|
32
66
|
AUDIT_STATE_NAME,
|
|
33
67
|
} from '../common/constants.js';
|
|
68
|
+
import VERSION from '../common/version.js';
|
|
34
69
|
|
|
35
|
-
|
|
36
|
-
let _dbNamespaces = new Set();
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Configuration object for the server.
|
|
40
|
-
*
|
|
41
|
-
* @typedef ServerConfig
|
|
42
|
-
* @memberof server
|
|
43
|
-
* @type {object}
|
|
44
|
-
* @property {object} [app] - Application configration object.
|
|
45
|
-
* @property {object} app.clients - Definition of the application clients.
|
|
46
|
-
* @property {string} [app.name=''] - Name of the application.
|
|
47
|
-
* @property {string} [app.author=''] - Name of the author.
|
|
48
|
-
* @property {object} [env] - Environment configration object.
|
|
49
|
-
* @property {boolean} env.port - Port on which the server is listening.
|
|
50
|
-
* @property {boolean} env.useHttps - Define is the server run in http or in https.
|
|
51
|
-
* @property {boolean} [env.httpsInfos={}] - Path to cert files for https.
|
|
52
|
-
* @property {boolean} env.serverAddress - Domain name or IP of the server.
|
|
53
|
-
* Mandatory if node clients are defined
|
|
54
|
-
* @property {string} [env.websockets={}] - Configuration options for websockets.
|
|
55
|
-
* @property {string} [env.subpath=''] - If running behind a proxy, path to the application.
|
|
56
|
-
*/
|
|
70
|
+
const dbNamespaces = new Set();
|
|
57
71
|
|
|
58
72
|
/** @private */
|
|
59
73
|
const DEFAULT_CONFIG = {
|
|
60
74
|
env: {
|
|
61
75
|
type: 'development',
|
|
62
76
|
port: 8000,
|
|
63
|
-
serverAddress:
|
|
64
|
-
subpath: '',
|
|
65
|
-
websockets: {
|
|
66
|
-
path: 'socket',
|
|
67
|
-
pingInterval: 5000,
|
|
68
|
-
},
|
|
77
|
+
serverAddress: '',
|
|
69
78
|
useHttps: false,
|
|
70
79
|
httpsInfos: null,
|
|
80
|
+
baseUrl: '',
|
|
71
81
|
crossOriginIsolated: true,
|
|
72
82
|
verbose: true,
|
|
73
83
|
},
|
|
@@ -77,36 +87,19 @@ const DEFAULT_CONFIG = {
|
|
|
77
87
|
},
|
|
78
88
|
};
|
|
79
89
|
|
|
80
|
-
const TOKEN_VALID_DURATION =
|
|
81
|
-
|
|
82
|
-
// set terminal title
|
|
83
|
-
/** @private */
|
|
84
|
-
function setTerminalTitle(server) {
|
|
85
|
-
let title = '';
|
|
86
|
-
|
|
87
|
-
if (server._auditState !== null) {
|
|
88
|
-
const numClients = server._auditState.get('numClients');
|
|
89
|
-
let numClientStrings = [];
|
|
90
|
-
for (let name in numClients) {
|
|
91
|
-
numClientStrings.push(`${name}: ${numClients[name]}`);
|
|
92
|
-
}
|
|
90
|
+
const TOKEN_VALID_DURATION = 10; // sec
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const msg = String.fromCharCode(27) + ']0;' + title + String.fromCharCode(7);
|
|
100
|
-
process.stdout.write(msg);
|
|
101
|
-
}
|
|
92
|
+
export const kServerOnSocketConnection = Symbol('soundworks:server-on-socket-connection');
|
|
93
|
+
export const kServerIsValidConnectionToken = Symbol('soundworks:server-is-valid-connection-token');
|
|
94
|
+
// protected and not private for testing purposes
|
|
95
|
+
export const kServerOnStatusChangeCallbacks = Symbol('soundworks:server-on-status-change-callbacks');
|
|
102
96
|
|
|
103
97
|
/**
|
|
104
|
-
* The `Server` class is the main entry point for
|
|
105
|
-
* application.
|
|
98
|
+
* The `Server` class is the main entry point for soundworks server-side project.
|
|
106
99
|
*
|
|
107
|
-
* The `Server` instance allows to access soundworks components such as {@link
|
|
108
|
-
* {@link
|
|
109
|
-
* Its is also responsible for handling the initialization
|
|
100
|
+
* The `Server` instance allows to access soundworks components such as {@link ServerStateManager},
|
|
101
|
+
* {@link ServerPluginManager}, {@link ServerSocket} or {@link ServerContextManager}.
|
|
102
|
+
* Its is also responsible for handling the initialization lifecycle of the different
|
|
110
103
|
* soundworks components.
|
|
111
104
|
*
|
|
112
105
|
* ```
|
|
@@ -116,9 +109,9 @@ function setTerminalTitle(server) {
|
|
|
116
109
|
* app: {
|
|
117
110
|
* name: 'my-example-app',
|
|
118
111
|
* clients: {
|
|
119
|
-
* player: {
|
|
120
|
-
* controller: {
|
|
121
|
-
* thing: {
|
|
112
|
+
* player: { runtime: 'browser', default: true },
|
|
113
|
+
* controller: { runtime: 'browser' },
|
|
114
|
+
* thing: { runtime: 'node' }
|
|
122
115
|
* },
|
|
123
116
|
* },
|
|
124
117
|
* env: {
|
|
@@ -128,28 +121,32 @@ function setTerminalTitle(server) {
|
|
|
128
121
|
*
|
|
129
122
|
* await server.start();
|
|
130
123
|
* ```
|
|
131
|
-
*
|
|
132
|
-
* According to the clients definitions provided in `config.app.clients`, the
|
|
133
|
-
* server will automatically create a dedicated route for each browser client role.
|
|
134
|
-
* For example, given the config object of the example above that defines two
|
|
135
|
-
* different client roles for browser targets (i.e. `player` and `controller`):
|
|
136
|
-
*
|
|
137
|
-
* ```
|
|
138
|
-
* config.app.clients = {
|
|
139
|
-
* player: { target: 'browser', default: true },
|
|
140
|
-
* controller: { target: 'browser' },
|
|
141
|
-
* }
|
|
142
|
-
* ```
|
|
143
|
-
*
|
|
144
|
-
* The server will listen to the following URLs:
|
|
145
|
-
* - `http://127.0.0.1:8000/` for the `player` role, which is defined as the default client.
|
|
146
|
-
* - `http://127.0.0.1:8000/controller` for the `controller` role.
|
|
147
|
-
*
|
|
148
|
-
* @memberof server
|
|
149
124
|
*/
|
|
150
125
|
class Server {
|
|
126
|
+
#config = null;
|
|
127
|
+
#version = null;
|
|
128
|
+
#status = null;
|
|
129
|
+
#router = null;
|
|
130
|
+
#httpServer = null;
|
|
131
|
+
#db = null;
|
|
132
|
+
|
|
133
|
+
#sockets = null;
|
|
134
|
+
#pluginManager = null;
|
|
135
|
+
#stateManager = null;
|
|
136
|
+
#contextManager = null;
|
|
137
|
+
|
|
138
|
+
#onClientConnectCallbacks = new Set();
|
|
139
|
+
#onClientDisconnectCallbacks = new Set();
|
|
140
|
+
#auditState = null;
|
|
141
|
+
#tokenIdGenerator = counter();
|
|
142
|
+
#pendingConnectionTokens = new Set();
|
|
143
|
+
#trustedClients = new Set();
|
|
144
|
+
|
|
145
|
+
// for backward compatibility
|
|
146
|
+
#useDefaultApplicationTemplate = false;
|
|
147
|
+
|
|
151
148
|
/**
|
|
152
|
-
* @param {
|
|
149
|
+
* @param {ServerConfig} config - Configuration object for the server.
|
|
153
150
|
* @throws
|
|
154
151
|
* - If `config.app.clients` is empty.
|
|
155
152
|
* - If a `node` client is defined but `config.env.serverAddress` is not defined.
|
|
@@ -159,182 +156,130 @@ class Server {
|
|
|
159
156
|
*/
|
|
160
157
|
constructor(config) {
|
|
161
158
|
if (!isPlainObject(config)) {
|
|
162
|
-
throw new
|
|
159
|
+
throw new TypeError(`Cannot construct 'Server': Parameter 1 must be an object`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
config = merge({}, DEFAULT_CONFIG, config);
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------
|
|
165
|
+
// Deprecation checks for config
|
|
166
|
+
// ---------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
// `target` renamed to `runtime`
|
|
169
|
+
for (let role in config.app.clients) {
|
|
170
|
+
const clientConfig = config.app.clients[role];
|
|
171
|
+
|
|
172
|
+
if (clientConfig.target) {
|
|
173
|
+
logger.deprecated('ClientDescription#target', 'ClientDescription#runtime (or run `npx soundworks --upgrade-config` to upgrade your config files)', '4.0.0-alpha.29');
|
|
174
|
+
clientConfig.runtime = clientConfig.target;
|
|
175
|
+
delete clientConfig.target;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// `env.subpath` to `env.baseUrl`
|
|
180
|
+
if ('subpath' in config.env) {
|
|
181
|
+
logger.deprecated('ServerConfig#subpath', 'ServerConfig#baseUrl (or run `npx soundworks --upgrade-config` to upgrade your config files)', '4.0.0-alpha.29');
|
|
182
|
+
config.env.baseUrl = config.env.subpath;
|
|
183
|
+
delete config.env.subpath;
|
|
163
184
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* port: 8000,
|
|
171
|
-
* serverAddress: null,
|
|
172
|
-
* subpath: '',
|
|
173
|
-
* websockets: {
|
|
174
|
-
* path: 'socket',
|
|
175
|
-
* pingInterval: 5000,
|
|
176
|
-
* },
|
|
177
|
-
* useHttps: false,
|
|
178
|
-
* httpsInfos: null,
|
|
179
|
-
* crossOriginIsolated: true,
|
|
180
|
-
* verbose: true,
|
|
181
|
-
* },
|
|
182
|
-
* app: {
|
|
183
|
-
* name: 'soundworks',
|
|
184
|
-
* clients: {},
|
|
185
|
-
* }
|
|
186
|
-
* }
|
|
187
|
-
*/
|
|
188
|
-
this.config = merge({}, DEFAULT_CONFIG, config);
|
|
189
|
-
|
|
190
|
-
// parse config
|
|
191
|
-
if (Object.keys(this.config.app.clients).length === 0) {
|
|
192
|
-
throw new Error(`[soundworks:Server] Invalid "app.clients" config, at least one client should be declared`);
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------
|
|
187
|
+
// ---------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
if (Object.keys(config.app.clients).length === 0) {
|
|
190
|
+
throw new DOMException(`Cannot construct 'Server': At least one ClientDescription must be declared in 'config.app.clients'`, 'NotSupportedError');
|
|
193
191
|
}
|
|
194
192
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
hasNodeClient = true;
|
|
193
|
+
for (let name in config.app.clients) {
|
|
194
|
+
// runtime property is mandatory
|
|
195
|
+
if (!['node', 'browser'].includes(config.app.clients[name].runtime)) {
|
|
196
|
+
throw new TypeError(`Cannot construct 'Server': Invalid 'ClientDescription' for client '${name}': 'runtime' property must be either 'node' or 'browser'`);
|
|
200
197
|
}
|
|
201
198
|
}
|
|
202
199
|
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
// @peeka - remove this check
|
|
201
|
+
// [2024-05-29] Override default `config.env.serverAddress`` provided from
|
|
202
|
+
// template `loadConfig` to '' so that browser clients can default to
|
|
203
|
+
// window.location.hostname and node clients to `127.0.0.1`
|
|
204
|
+
if (process.env.ENV === undefined && config.env.serverAddress === '127.0.0.1') {
|
|
205
|
+
config.env.serverAddress = '';
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
if (
|
|
208
|
-
const httpsInfos =
|
|
208
|
+
if (config.env.useHttps && config.env.httpsInfos !== null) {
|
|
209
|
+
const httpsInfos = config.env.httpsInfos;
|
|
209
210
|
|
|
210
|
-
if (!isPlainObject(
|
|
211
|
-
throw new
|
|
211
|
+
if (!isPlainObject(config.env.httpsInfos)) {
|
|
212
|
+
throw new TypeError(`Cannot construct 'Server': Invalid 'ServerEnvConfig': 'httpsInfos' must be an object: { cert, key }`);
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
if (!('cert' in httpsInfos) || !('key' in httpsInfos)) {
|
|
215
|
-
throw new
|
|
216
|
+
throw new TypeError(`Cannot construct 'Server': Invalid 'ServerEnvConfig': 'httpsInfos' must contain both "cert" and "key" entries`);
|
|
216
217
|
}
|
|
217
|
-
|
|
218
|
-
if (!fs.existsSync(httpsInfos.cert)) {
|
|
219
|
-
throw new
|
|
218
|
+
|
|
219
|
+
if (httpsInfos.cert !== null && !fs.existsSync(httpsInfos.cert)) {
|
|
220
|
+
throw new DOMException(`Cannot construct 'Server': Invalid 'ServerEnvConfig': 'httpsInfos.cert' file not found`, 'NotFoundError');
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
if (!fs.existsSync(httpsInfos.key)) {
|
|
223
|
-
throw new
|
|
223
|
+
if (httpsInfos.key !== null && !fs.existsSync(httpsInfos.key)) {
|
|
224
|
+
throw new DOMException(`Cannot construct 'Server': Invalid 'ServerEnvConfig': 'httpsInfos.key' file not found`, 'NotFoundError');
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
* import express from 'express';
|
|
237
|
-
*
|
|
238
|
-
* // create the soundworks server instance
|
|
239
|
-
* const server = new Server(config);
|
|
240
|
-
*
|
|
241
|
-
* // expose assets located in the `soundfiles` directory on the network
|
|
242
|
-
* server.router.use('/soundfiles', express.static('soundfiles')));
|
|
243
|
-
*/
|
|
244
|
-
// @note: we use express() instead of express.Router() because all 404 and
|
|
245
|
-
// error stuff is handled by default
|
|
246
|
-
this.router = express();
|
|
247
|
-
// compression (must be set before express.static())
|
|
248
|
-
this.router.use(compression());
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Raw Node.js `http` or `https` instance
|
|
252
|
-
*
|
|
253
|
-
* @see {@link https://nodejs.org/api/http.html}
|
|
254
|
-
* @see {@link https://nodejs.org/api/https.html}
|
|
255
|
-
*/
|
|
256
|
-
this.httpServer = null;
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Instance of the {@link server.Sockets} class.
|
|
260
|
-
*
|
|
261
|
-
* @see {@link server.Sockets}
|
|
262
|
-
* @type {server.Sockets}
|
|
263
|
-
*/
|
|
264
|
-
this.sockets = new Sockets();
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Instance of the {@link server.PluginManager} class.
|
|
268
|
-
*
|
|
269
|
-
* @see {@link server.PluginManager}
|
|
270
|
-
* @type {server.PluginManager}
|
|
271
|
-
*/
|
|
272
|
-
this.pluginManager = new PluginManager(this);
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Instance of the {@link server.StateManager} class.
|
|
276
|
-
*
|
|
277
|
-
* @see {@link server.StateManager}
|
|
278
|
-
* @type {server.StateManager}
|
|
279
|
-
*/
|
|
280
|
-
this.stateManager = new StateManager();
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Instance of the {@link server.ContextManager} class.
|
|
284
|
-
*
|
|
285
|
-
* @see {@link server.ContextManager}
|
|
286
|
-
* @type {server.ContextManager}
|
|
287
|
-
*/
|
|
288
|
-
this.contextManager = new ContextManager(this);
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* If `https` is required, hold informations about the certificates, e.g. if
|
|
292
|
-
* self-signed, the dates of validity of the certificates, etc.
|
|
293
|
-
*/
|
|
294
|
-
this.httpsInfos = null;
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Status of the server, 'idle', 'inited', 'started' or 'errored'.
|
|
298
|
-
*
|
|
299
|
-
* @type {string}
|
|
300
|
-
*/
|
|
301
|
-
this.status = 'idle';
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Simple key / value database with Promise based Map API store on filesystem,
|
|
305
|
-
* basically a tiny wrapper around the `kvey` package.
|
|
306
|
-
*
|
|
307
|
-
* @private
|
|
308
|
-
* @see {@link https://github.com/lukechilds/keyv}
|
|
309
|
-
*/
|
|
310
|
-
this.db = this.createNamespacedDb('core');
|
|
311
|
-
|
|
228
|
+
// private
|
|
229
|
+
this.#config = config;
|
|
230
|
+
this.#version = VERSION;
|
|
231
|
+
this.#sockets = new ServerSockets(this, { path: 'socket' });
|
|
232
|
+
this.#pluginManager = new ServerPluginManager(this);
|
|
233
|
+
this.#stateManager = new ServerStateManager();
|
|
234
|
+
this.#contextManager = new ServerContextManager(this);
|
|
235
|
+
this.#status = 'idle';
|
|
236
|
+
this.#db = this.createNamespacedDb('core');
|
|
312
237
|
/** @private */
|
|
313
|
-
this
|
|
314
|
-
templateEngine: null,
|
|
315
|
-
templatePath: null,
|
|
316
|
-
clientConfigFunction: null,
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
/** @private */
|
|
320
|
-
this._onStatusChangeCallbacks = new Set();
|
|
321
|
-
/** @private */
|
|
322
|
-
this._auditState = null;
|
|
323
|
-
/** @private */
|
|
324
|
-
this._pendingConnectionTokens = new Set();
|
|
325
|
-
/** @private */
|
|
326
|
-
this._trustedClients = new Set();
|
|
238
|
+
this[kServerOnStatusChangeCallbacks] = new Set();
|
|
327
239
|
|
|
328
240
|
// register audit state schema
|
|
329
|
-
this
|
|
241
|
+
this.#stateManager.defineClass(AUDIT_STATE_NAME, auditClassDescription);
|
|
242
|
+
|
|
243
|
+
logger.configure(this.#config.env.verbose);
|
|
244
|
+
}
|
|
330
245
|
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
* baseUrl: '',
|
|
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;
|
|
333
269
|
}
|
|
334
270
|
|
|
335
271
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
272
|
+
* Package version.
|
|
273
|
+
*
|
|
274
|
+
* @type {string}
|
|
275
|
+
*/
|
|
276
|
+
get version() {
|
|
277
|
+
return this.#version;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Id of the server, a constant set to `-1`
|
|
282
|
+
* @type {number}
|
|
338
283
|
* @readonly
|
|
339
284
|
*/
|
|
340
285
|
get id() {
|
|
@@ -342,271 +287,248 @@ class Server {
|
|
|
342
287
|
}
|
|
343
288
|
|
|
344
289
|
/**
|
|
345
|
-
*
|
|
346
|
-
* server. Most of the time, the `init` method will be implicitly called by the
|
|
347
|
-
* {@link server.Server#start} method.
|
|
290
|
+
* Status of the server.
|
|
348
291
|
*
|
|
349
|
-
*
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
*
|
|
292
|
+
* @type {'idle'|'inited'|'started'|'errored'}
|
|
293
|
+
*/
|
|
294
|
+
get status() {
|
|
295
|
+
return this.#status;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Instance of the router if any.
|
|
357
300
|
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
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).
|
|
360
303
|
*
|
|
361
304
|
* @example
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* //
|
|
366
|
-
* const server = new Server(
|
|
367
|
-
*
|
|
305
|
+
* import { Server } from '@soundworks/core/server.js';
|
|
306
|
+
* import { loadConfig, configureHttpRouter } from '@soundworks/helpers/server.js';
|
|
307
|
+
*
|
|
308
|
+
* // create the server instance
|
|
309
|
+
* const server = new Server(loadConfig());
|
|
310
|
+
* // configure the express router provided by the helpers
|
|
311
|
+
* configureHttpRouter(server);
|
|
312
|
+
*
|
|
313
|
+
* // expose assets located in the `soundfiles` directory on the network
|
|
314
|
+
* server.router.use('/soundfiles', express.static('soundfiles')));
|
|
368
315
|
*/
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
numClients[name] = 0;
|
|
373
|
-
}
|
|
374
|
-
/** @private */
|
|
375
|
-
this._auditState = await this.stateManager.create(AUDIT_STATE_NAME, { numClients });
|
|
376
|
-
this._auditState.onUpdate(() => setTerminalTitle(this));
|
|
377
|
-
|
|
378
|
-
// basic http authentication
|
|
379
|
-
if (this.config.env.auth) {
|
|
380
|
-
const ids = idGenerator();
|
|
381
|
-
|
|
382
|
-
const soundworksAuth = (req, res, next) => {
|
|
383
|
-
let role = null;
|
|
316
|
+
get router() {
|
|
317
|
+
return this.#router;
|
|
318
|
+
}
|
|
384
319
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
role = _role;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
320
|
+
set router(router) {
|
|
321
|
+
this.#router = router;
|
|
390
322
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
323
|
+
if (this.httpServer) {
|
|
324
|
+
this.httpServer.on('request', router);
|
|
325
|
+
} else {
|
|
326
|
+
// register router on HTTP server when ready
|
|
327
|
+
this.onStatusChange(status => {
|
|
328
|
+
if (status === 'http-server-ready') {
|
|
329
|
+
this.httpServer.on('request', router);
|
|
395
330
|
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
396
334
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// verify login and password are set and correct
|
|
407
|
-
if (login && password && login === auth.login && password === auth.password) {
|
|
408
|
-
// -> access granted...
|
|
409
|
-
// generate token for web socket to check connections
|
|
410
|
-
const id = ids.next().value;
|
|
411
|
-
const ip = req.ip;
|
|
412
|
-
const time = getTime();
|
|
413
|
-
const token = { id, ip, time };
|
|
414
|
-
const encryptedToken = encryptData(token);
|
|
335
|
+
/**
|
|
336
|
+
* Instance of the Node.js `http.Server` or `https.Server`
|
|
337
|
+
*
|
|
338
|
+
* @see {@link https://nodejs.org/api/http.html#class-httpserver}
|
|
339
|
+
* @see {@link https://nodejs.org/api/https.html#class-httpsserver}
|
|
340
|
+
*/
|
|
341
|
+
get httpServer() {
|
|
342
|
+
return this.#httpServer;
|
|
343
|
+
}
|
|
415
344
|
|
|
416
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Simple key / value filesystem database with Promise based Map API.
|
|
347
|
+
*
|
|
348
|
+
* Basically a tiny wrapper around the {@link https://github.com/lukechilds/keyv} package.
|
|
349
|
+
*/
|
|
350
|
+
get db() {
|
|
351
|
+
return this.#db;
|
|
352
|
+
}
|
|
417
353
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Instance of the {@link ServerSockets} class.
|
|
356
|
+
*
|
|
357
|
+
* @type {ServerSockets}
|
|
358
|
+
*/
|
|
359
|
+
get sockets() {
|
|
360
|
+
return this.#sockets;
|
|
361
|
+
}
|
|
421
362
|
|
|
422
|
-
|
|
423
|
-
|
|
363
|
+
/**
|
|
364
|
+
* Instance of the {@link ServerPluginManager} class.
|
|
365
|
+
*
|
|
366
|
+
* @type {ServerPluginManager}
|
|
367
|
+
*/
|
|
368
|
+
get pluginManager() {
|
|
369
|
+
return this.#pluginManager;
|
|
370
|
+
}
|
|
424
371
|
|
|
425
|
-
|
|
426
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Instance of the {@link ServerStateManager} class.
|
|
374
|
+
*
|
|
375
|
+
* @type {ServerStateManager}
|
|
376
|
+
*/
|
|
377
|
+
get stateManager() {
|
|
378
|
+
return this.#stateManager;
|
|
379
|
+
}
|
|
427
380
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Instance of the {@link ServerContextManager} class.
|
|
383
|
+
*
|
|
384
|
+
* @type {ServerContextManager}
|
|
385
|
+
*/
|
|
386
|
+
get contextManager() {
|
|
387
|
+
return this.#contextManager;
|
|
388
|
+
}
|
|
433
389
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return next();
|
|
438
|
-
}
|
|
439
|
-
};
|
|
390
|
+
/** @private */
|
|
391
|
+
async #dispatchStatus(status) {
|
|
392
|
+
this.#status = status;
|
|
440
393
|
|
|
441
|
-
|
|
394
|
+
// if launched in a child process, forward status to parent process
|
|
395
|
+
if (process.send !== undefined) {
|
|
396
|
+
process.send(`soundworks:server:${status}`);
|
|
442
397
|
}
|
|
443
398
|
|
|
444
|
-
//
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
// ------------------------------------------------------------
|
|
448
|
-
// create HTTP(S) SERVER
|
|
449
|
-
// ------------------------------------------------------------
|
|
450
|
-
if (!useHttps) {
|
|
451
|
-
this.httpServer = http.createServer(this.router);
|
|
452
|
-
} else {
|
|
453
|
-
const httpsInfos = this.config.env.httpsInfos;
|
|
399
|
+
// execute all callbacks in parallel
|
|
400
|
+
const promises = [];
|
|
454
401
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
// existance of file is checked in contructor
|
|
459
|
-
let cert = fs.readFileSync(httpsInfos.cert);
|
|
460
|
-
let key = fs.readFileSync(httpsInfos.key);
|
|
461
|
-
|
|
462
|
-
let x509 = null;
|
|
463
|
-
// this fails with self-signed certificates for whatever reason...
|
|
464
|
-
try {
|
|
465
|
-
x509 = new X509Certificate(cert);
|
|
466
|
-
} catch (err) {
|
|
467
|
-
this._dispatchStatus('errored');
|
|
468
|
-
throw new Error(`[soundworks:Server] Invalid https cert file`);
|
|
469
|
-
}
|
|
402
|
+
for (let callback of this[kServerOnStatusChangeCallbacks]) {
|
|
403
|
+
promises.push(callback(status));
|
|
404
|
+
}
|
|
470
405
|
|
|
471
|
-
|
|
472
|
-
|
|
406
|
+
await Promise.all(promises);
|
|
407
|
+
}
|
|
473
408
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
409
|
+
/**
|
|
410
|
+
* Register a callback to execute when status change.
|
|
411
|
+
*
|
|
412
|
+
* Status are dispatched in the following order:
|
|
413
|
+
* - 'http-server-ready'
|
|
414
|
+
* - 'inited'
|
|
415
|
+
* - 'started'
|
|
416
|
+
* - 'stopped'
|
|
417
|
+
* during the lifecycle of the server. If an error occurs the 'errored' status is propagated.
|
|
418
|
+
*
|
|
419
|
+
* @param {function} callback
|
|
420
|
+
*/
|
|
421
|
+
onStatusChange(callback) {
|
|
422
|
+
this[kServerOnStatusChangeCallbacks].add(callback);
|
|
423
|
+
return () => this[kServerOnStatusChangeCallbacks].delete(callback);
|
|
424
|
+
}
|
|
482
425
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
} catch (err) {
|
|
503
|
-
logger.error(`
|
|
504
|
-
Invalid certificate files, please check your:
|
|
505
|
-
- key file: ${httpsInfos.key}
|
|
506
|
-
- cert file: ${httpsInfos.cert}
|
|
507
|
-
`);
|
|
508
|
-
|
|
509
|
-
this._dispatchStatus('errored');
|
|
510
|
-
throw err;
|
|
511
|
-
}
|
|
512
|
-
} else {
|
|
513
|
-
// generate certs
|
|
514
|
-
// --------------------------------------------------------
|
|
515
|
-
const cert = await this.db.get('httpsCert');
|
|
516
|
-
const key = await this.db.get('httpsKey');
|
|
517
|
-
|
|
518
|
-
if (key && cert) {
|
|
519
|
-
this.httpsInfos = { selfSigned: true };
|
|
520
|
-
this.httpServer = https.createServer({ cert, key }, this.router);
|
|
521
|
-
} else {
|
|
522
|
-
this.httpServer = await new Promise((resolve, reject) => {
|
|
523
|
-
// generate certificate on the fly (for development purposes)
|
|
524
|
-
pem.createCertificate({ days: 1, selfSigned: true }, async (err, keys) => {
|
|
525
|
-
if (err) {
|
|
526
|
-
logger.error(err.stack);
|
|
527
|
-
this._dispatchStatus('errored');
|
|
528
|
-
|
|
529
|
-
reject(err);
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const cert = keys.certificate;
|
|
534
|
-
const key = keys.serviceKey;
|
|
535
|
-
|
|
536
|
-
this.httpsInfos = { selfSigned: true };
|
|
537
|
-
// we store the generated cert so that we don't have to re-accept
|
|
538
|
-
// the cert each time the server restarts in development
|
|
539
|
-
await this.db.set('httpsCert', cert);
|
|
540
|
-
await this.db.set('httpsKey', key);
|
|
541
|
-
|
|
542
|
-
const httpsServer = https.createServer({ cert, key }, this.router);
|
|
543
|
-
|
|
544
|
-
resolve(httpsServer);
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
}
|
|
426
|
+
/**
|
|
427
|
+
* Attach and retrieve the global audit state of the application.
|
|
428
|
+
*
|
|
429
|
+
* The audit state is a {@link SharedState} instance that keeps track of
|
|
430
|
+
* global information about the application such as, the number of connected
|
|
431
|
+
* clients, network latency estimation, etc.
|
|
432
|
+
*
|
|
433
|
+
* The audit state is created by the server on start up.
|
|
434
|
+
*
|
|
435
|
+
* @returns {Promise<SharedState>}
|
|
436
|
+
* @throws Will throw if called before `server.init()`
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* const auditState = await server.getAuditState();
|
|
440
|
+
* auditState.onUpdate(() => console.log(auditState.getValues()), true);
|
|
441
|
+
*/
|
|
442
|
+
async getAuditState() {
|
|
443
|
+
if (this.#status === 'idle') {
|
|
444
|
+
throw new DOMException(`Cannot execute 'getAuditState' on Server: 'init' must be called first`, 'InvalidAccessError');
|
|
549
445
|
}
|
|
550
446
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
// cleaning tests in particular
|
|
554
|
-
for (let role in this.config.app.clients) {
|
|
555
|
-
if (this.config.app.clients[role].target === 'browser') {
|
|
556
|
-
nodeOnly = false;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
447
|
+
return this.#auditState;
|
|
448
|
+
}
|
|
559
449
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
450
|
+
/**
|
|
451
|
+
* The `init` method is part of the initialization lifecycle of the `soundworks`
|
|
452
|
+
* server. Most of the time, the `init` method will be implicitly called by the
|
|
453
|
+
* {@link Server#start} method.
|
|
454
|
+
*
|
|
455
|
+
* In some situations you might want to call this method manually, in such cases
|
|
456
|
+
* the method should be called before the {@link Server#start} method.
|
|
457
|
+
*
|
|
458
|
+
* What it does:
|
|
459
|
+
* 1) Create the audit state
|
|
460
|
+
* 2) Create the HTTP(s) server
|
|
461
|
+
* 3) Initialize registered plugins
|
|
462
|
+
*
|
|
463
|
+
* Between steps 2 and 3, the 'http-server-ready' event status is dispatched so
|
|
464
|
+
* that consumer code can register its router before plugin initialization:
|
|
465
|
+
* ```js
|
|
466
|
+
* server.onStatusChange(status => {
|
|
467
|
+
* if (status === 'http-server-ready') {
|
|
468
|
+
* server.httpServer.on('request', router);
|
|
469
|
+
* }
|
|
470
|
+
* });
|
|
471
|
+
* ```
|
|
472
|
+
*
|
|
473
|
+
* After `await server.init()` is fulfilled, the {@link Server#stateManager}
|
|
474
|
+
* and all registered plugins can be safely used.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* const server = new Server(config);
|
|
478
|
+
* await server.init();
|
|
479
|
+
* await server.start();
|
|
480
|
+
* // or implicitly called by start
|
|
481
|
+
* const server = new Server(config);
|
|
482
|
+
* await server.start(); // init is called implicitly
|
|
483
|
+
*/
|
|
484
|
+
async init() {
|
|
485
|
+
if (this.#status !== 'idle') {
|
|
486
|
+
throw new DOMException(`Cannot execute 'init' on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
|
|
567
487
|
}
|
|
568
488
|
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
// ------------------------------------------------------------
|
|
572
|
-
logger.title(`configured clients and routing`);
|
|
489
|
+
// init `ServerStateManager` and global "audit" state
|
|
490
|
+
this.#stateManager[kStateManagerInit](SERVER_ID, new EventEmitter());
|
|
573
491
|
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
for (let role in this.config.app.clients) {
|
|
578
|
-
const config = Object.assign({}, this.config.app.clients[role]);
|
|
579
|
-
config.role = role;
|
|
580
|
-
clientsConfig.push(config);
|
|
492
|
+
const numClients = {};
|
|
493
|
+
for (let name in this.#config.app.clients) {
|
|
494
|
+
numClients[name] = 0;
|
|
581
495
|
}
|
|
582
496
|
|
|
583
|
-
|
|
584
|
-
clientsConfig
|
|
585
|
-
.sort(a => a.default === true ? 1 : -1)
|
|
586
|
-
.forEach(config => {
|
|
587
|
-
const path = this._openClientRoute(this.router, config);
|
|
588
|
-
routes.push({ role: config.role, path });
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
logger.clientConfigAndRouting(routes, this.config);
|
|
497
|
+
this.#auditState = await this.#stateManager.create(AUDIT_STATE_NAME, { numClients });
|
|
592
498
|
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
499
|
+
// backward compatibility for `useDefaultApplicationTemplate`
|
|
500
|
+
if (this.#useDefaultApplicationTemplate === true) {
|
|
501
|
+
try {
|
|
502
|
+
const { configureHttpRouter } = await import('@soundworks/helpers/server.js');
|
|
503
|
+
configureHttpRouter(this);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
logger.warn('Could not apply patch for deprecated `useDefaultApplicationTemplate` method. Please use `configureHttpRouter` from helpers instead.');
|
|
506
|
+
throw err;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
597
509
|
|
|
598
|
-
|
|
510
|
+
// create HTTP(S) SERVER
|
|
511
|
+
try {
|
|
512
|
+
this.#httpServer = await createHttpServer(this);
|
|
513
|
+
await this.#dispatchStatus('http-server-ready');
|
|
514
|
+
} catch (err) {
|
|
515
|
+
logger.error(err.message);
|
|
516
|
+
await this.#dispatchStatus('errored');
|
|
517
|
+
throw err;
|
|
518
|
+
}
|
|
599
519
|
|
|
600
|
-
|
|
520
|
+
// start `ServerPluginManager`
|
|
521
|
+
await this.#pluginManager[kPluginManagerStart]();
|
|
522
|
+
await this.#dispatchStatus('inited');
|
|
601
523
|
}
|
|
602
524
|
|
|
603
525
|
/**
|
|
604
526
|
* The `start` method is part of the initialization lifecycle of the `soundworks`
|
|
605
|
-
* server. The `start` method will implicitly call the {@link
|
|
527
|
+
* server. The `start` method will implicitly call the {@link Server#init}
|
|
606
528
|
* method if it has not been called manually.
|
|
607
529
|
*
|
|
608
530
|
* What it does:
|
|
609
|
-
* -
|
|
531
|
+
* - implicitly call {@link Server#init} if not done manually
|
|
610
532
|
* - launch the HTTP and WebSocket servers
|
|
611
533
|
* - start all created contexts. To this end, you will have to call `server.init`
|
|
612
534
|
* manually and instantiate the contexts between `server.init()` and `server.start()`
|
|
@@ -620,87 +542,39 @@ Invalid certificate files, please check your:
|
|
|
620
542
|
* await server.start();
|
|
621
543
|
*/
|
|
622
544
|
async start() {
|
|
623
|
-
|
|
545
|
+
// lazily call init for convenience
|
|
546
|
+
if (this.#status === 'idle') {
|
|
624
547
|
await this.init();
|
|
625
548
|
}
|
|
626
549
|
|
|
627
|
-
if (this
|
|
628
|
-
throw new
|
|
550
|
+
if (this.#status !== 'inited') {
|
|
551
|
+
throw new DOMException(`Cannot execute 'start' on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
|
|
629
552
|
}
|
|
630
553
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
//
|
|
636
|
-
// START CONTEXT MANAGER
|
|
637
|
-
// ------------------------------------------------------------
|
|
638
|
-
await this.contextManager.start();
|
|
639
|
-
|
|
640
|
-
// ------------------------------------------------------------
|
|
641
|
-
// START SOCKET SERVER
|
|
642
|
-
// ------------------------------------------------------------
|
|
643
|
-
await this.sockets.start(
|
|
644
|
-
this,
|
|
645
|
-
this.config.env.websockets,
|
|
646
|
-
(...args) => this._onSocketConnection(...args),
|
|
647
|
-
);
|
|
648
|
-
|
|
649
|
-
// ------------------------------------------------------------
|
|
650
|
-
// START HTTP SERVER
|
|
651
|
-
// ------------------------------------------------------------
|
|
554
|
+
// state `ServerContextManager`
|
|
555
|
+
await this.#contextManager[kServerContextManagerStart]();
|
|
556
|
+
// start `SocketServer`
|
|
557
|
+
await this.#sockets[kSocketsStart]();
|
|
558
|
+
// start httpServer
|
|
652
559
|
return new Promise(resolve => {
|
|
653
|
-
const port = this
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
const ifaces = os.networkInterfaces();
|
|
560
|
+
const port = this.#config.env.port;
|
|
561
|
+
const protocol = this.#config.env.useHttps ? 'https' : 'http';
|
|
562
|
+
const interfaces = os.networkInterfaces();
|
|
657
563
|
|
|
658
|
-
this
|
|
564
|
+
this.#httpServer.listen(port, async () => {
|
|
659
565
|
logger.title(`${protocol} server listening on`);
|
|
660
566
|
|
|
661
|
-
Object.keys(
|
|
662
|
-
|
|
567
|
+
Object.keys(interfaces).forEach(dev => {
|
|
568
|
+
interfaces[dev].forEach(details => {
|
|
663
569
|
if (details.family === 'IPv4') {
|
|
664
570
|
logger.ip(protocol, details.address, port);
|
|
665
571
|
}
|
|
666
572
|
});
|
|
667
573
|
});
|
|
668
574
|
|
|
669
|
-
|
|
670
|
-
logger.title(`https certificates infos`);
|
|
671
|
-
|
|
672
|
-
// this.httpsInfos.selfSigned = true;
|
|
673
|
-
if (this.httpsInfos.selfSigned) {
|
|
674
|
-
logger.log(` self-signed: ${this.httpsInfos.selfSigned ? 'true' : 'false'}`);
|
|
675
|
-
logger.log(chalk.yellow` > INVALID CERTIFICATE (self-signed)`);
|
|
676
|
-
|
|
677
|
-
} else {
|
|
678
|
-
logger.log(` valid from: ${this.httpsInfos.validFrom}`);
|
|
679
|
-
logger.log(` valid to: ${this.httpsInfos.validTo}`);
|
|
680
|
-
|
|
681
|
-
// this.httpsInfos.isValid = false; // for testing
|
|
682
|
-
if (!this.httpsInfos.isValid) {
|
|
683
|
-
logger.error(chalk.red` -------------------------------------------`);
|
|
684
|
-
logger.error(chalk.red` > INVALID CERTIFICATE`);
|
|
685
|
-
logger.error(chalk.red` i.e. you pretend to be safe but you are not`);
|
|
686
|
-
logger.error(chalk.red` -------------------------------------------`);
|
|
687
|
-
} else {
|
|
688
|
-
// this.httpsInfos.daysRemaining = 2; // for testing
|
|
689
|
-
if (this.httpsInfos.daysRemaining < 5) {
|
|
690
|
-
logger.log(chalk.red` > CERTIFICATE IS VALID... BUT ONLY ${this.httpsInfos.daysRemaining} DAYS LEFT, PLEASE CONSIDER UPDATING YOUR CERTS!`);
|
|
691
|
-
} else if (this.httpsInfos.daysRemaining < 15) {
|
|
692
|
-
logger.log(chalk.yellow` > CERTIFICATE IS VALID - only ${this.httpsInfos.daysRemaining} days left, be careful...`);
|
|
693
|
-
} else {
|
|
694
|
-
logger.log(chalk.green` > CERTIFICATE IS VALID (${this.httpsInfos.daysRemaining} days left)`);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
await this._dispatchStatus('started');
|
|
575
|
+
await this.#dispatchStatus('started');
|
|
702
576
|
|
|
703
|
-
if (this
|
|
577
|
+
if (this.#config.env.type === 'development') {
|
|
704
578
|
logger.log(`\n> press "${chalk.bold('Ctrl + C')}" to exit`);
|
|
705
579
|
}
|
|
706
580
|
|
|
@@ -714,7 +588,7 @@ Invalid certificate files, please check your:
|
|
|
714
588
|
* the http(s) server.
|
|
715
589
|
*
|
|
716
590
|
* In most situations, you might not need to call this method. However, it can
|
|
717
|
-
* be
|
|
591
|
+
* be useful for unit testing or similar situations where you want to create
|
|
718
592
|
* and delete several servers in the same process.
|
|
719
593
|
*
|
|
720
594
|
* @example
|
|
@@ -727,167 +601,107 @@ Invalid certificate files, please check your:
|
|
|
727
601
|
* await server.stop();
|
|
728
602
|
*/
|
|
729
603
|
async stop() {
|
|
730
|
-
if (this
|
|
731
|
-
throw new
|
|
604
|
+
if (this.#status !== 'started') {
|
|
605
|
+
throw new DOMException(`Cannot execute 'stop' on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
|
|
732
606
|
}
|
|
733
607
|
|
|
734
|
-
await this
|
|
735
|
-
await this
|
|
608
|
+
await this.#contextManager[kServerContextManagerStop]();
|
|
609
|
+
await this.#pluginManager[kPluginManagerStop]();
|
|
610
|
+
|
|
611
|
+
this.#sockets[kSocketsStop]();
|
|
736
612
|
|
|
737
|
-
this.
|
|
738
|
-
this.httpServer.close(err => {
|
|
613
|
+
this.#httpServer.close(err => {
|
|
739
614
|
if (err) {
|
|
740
615
|
throw new Error(err.message);
|
|
741
616
|
}
|
|
742
617
|
});
|
|
743
618
|
|
|
744
|
-
await this
|
|
619
|
+
await this.#dispatchStatus('stopped');
|
|
745
620
|
}
|
|
746
621
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
_openClientRoute(router, config) {
|
|
752
|
-
const { role, target } = config;
|
|
753
|
-
const isDefault = (config.default === true);
|
|
754
|
-
// only browser targets need a route
|
|
755
|
-
if (target === 'node') {
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
let route = '/';
|
|
760
|
-
|
|
761
|
-
if (!isDefault) {
|
|
762
|
-
route += `${role}`;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
this.config.app.clients[role].route = route;
|
|
766
|
-
|
|
767
|
-
// define template filename: `${role}.html` or `default.html`
|
|
768
|
-
const {
|
|
769
|
-
templatePath,
|
|
770
|
-
templateEngine,
|
|
771
|
-
clientConfigFunction,
|
|
772
|
-
} = this._applicationTemplateOptions;
|
|
773
|
-
|
|
774
|
-
const clientTmpl = path.join(templatePath, `${role}.tmpl`);
|
|
775
|
-
const defaultTmpl = path.join(templatePath, `default.tmpl`);
|
|
776
|
-
|
|
777
|
-
// make it sync
|
|
778
|
-
let template;
|
|
779
|
-
|
|
780
|
-
try {
|
|
781
|
-
const stats = fs.statSync(clientTmpl);
|
|
782
|
-
template = stats.isFile() ? clientTmpl : defaultTmpl;
|
|
783
|
-
} catch (err) {
|
|
784
|
-
template = defaultTmpl;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
let tmplString;
|
|
788
|
-
|
|
789
|
-
try {
|
|
790
|
-
tmplString = fs.readFileSync(template, 'utf8');
|
|
791
|
-
} catch (err) {
|
|
792
|
-
throw new Error(`[soundworks:Server] html template file "${template}" not found`);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
const tmpl = templateEngine.compile(tmplString);
|
|
796
|
-
|
|
797
|
-
const soundworksClientHandler = (req, res) => {
|
|
798
|
-
const data = clientConfigFunction(role, this.config, req);
|
|
799
|
-
|
|
800
|
-
// if the client has gone through the connection middleware (add succedeed),
|
|
801
|
-
// add the token to the data object
|
|
802
|
-
if (res.swToken) {
|
|
803
|
-
data.token = res.swToken;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// CORS / COOP / COEP headers for `crossOriginIsolated pages,
|
|
807
|
-
// enables `sharedArrayBuffers` and high precision timers
|
|
808
|
-
// cf. https://web.dev/why-coop-coep/
|
|
809
|
-
if (this.config.env.crossOriginIsolated) {
|
|
810
|
-
res.writeHead(200, {
|
|
811
|
-
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
812
|
-
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
813
|
-
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
const appIndex = tmpl(data);
|
|
818
|
-
res.end(appIndex);
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
// http request
|
|
822
|
-
router.get(route, soundworksClientHandler);
|
|
622
|
+
onClientConnect(callback) {
|
|
623
|
+
this.#onClientConnectCallbacks.add(callback);
|
|
624
|
+
return () => this.#onClientConnectCallbacks.delete(callback);
|
|
625
|
+
}
|
|
823
626
|
|
|
824
|
-
|
|
825
|
-
|
|
627
|
+
onClientDisconnect(callback) {
|
|
628
|
+
this.#onClientDisconnectCallbacks.add(callback);
|
|
629
|
+
return () => this.#onClientDisconnectCallbacks.delete(callback);
|
|
826
630
|
}
|
|
827
631
|
|
|
828
632
|
/**
|
|
829
633
|
* Socket connection callback.
|
|
830
634
|
* @private
|
|
831
635
|
*/
|
|
832
|
-
|
|
833
|
-
const client = new
|
|
834
|
-
|
|
636
|
+
[kServerOnSocketConnection](role, socket, connectionToken) {
|
|
637
|
+
const client = new ServerClient(role, socket);
|
|
638
|
+
socket[kSocketClientId] = client.id;
|
|
639
|
+
const roles = Object.keys(this.#config.app.clients);
|
|
835
640
|
|
|
836
641
|
// this has been validated
|
|
837
|
-
if (this.
|
|
642
|
+
if (this.isProtectedClientRole(role) && this[kServerIsValidConnectionToken](connectionToken)) {
|
|
838
643
|
const { ip } = decryptData(connectionToken);
|
|
839
|
-
const newData = {
|
|
840
|
-
ip: ip,
|
|
841
|
-
id: client.id,
|
|
842
|
-
};
|
|
843
|
-
|
|
644
|
+
const newData = { ip, id: client.id };
|
|
844
645
|
const newToken = encryptData(newData);
|
|
845
646
|
|
|
846
|
-
client
|
|
647
|
+
client[kServerClientToken] = newToken;
|
|
847
648
|
|
|
848
|
-
this.
|
|
849
|
-
this.
|
|
649
|
+
this.#pendingConnectionTokens.delete(connectionToken);
|
|
650
|
+
this.#trustedClients.add(client);
|
|
850
651
|
}
|
|
851
652
|
|
|
852
653
|
socket.addListener('close', async () => {
|
|
853
|
-
//
|
|
854
|
-
if (roles.includes(role)) {
|
|
654
|
+
// cleanup if role is valid and client finished handshake
|
|
655
|
+
if (roles.includes(role) && this.#stateManager[kServerStateManagerHasClient](client.id)) {
|
|
855
656
|
// decrement audit state counter
|
|
856
|
-
const numClients = this.
|
|
657
|
+
const numClients = this.#auditState.get('numClients');
|
|
857
658
|
numClients[role] -= 1;
|
|
858
|
-
this.
|
|
659
|
+
this.#auditState.set({ numClients });
|
|
859
660
|
|
|
860
661
|
// delete token
|
|
861
|
-
if (this.
|
|
862
|
-
this.
|
|
662
|
+
if (this.#trustedClients.has(client)) {
|
|
663
|
+
this.#trustedClients.delete(client);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// if something goes wrong here, the 'close' event is called again and
|
|
667
|
+
// again and again... let's just log the error and terminate the socket
|
|
668
|
+
try {
|
|
669
|
+
// clean context manager, await before cleaning state manager
|
|
670
|
+
await this.#contextManager[kServerContextManagerRemoveClient](client);
|
|
671
|
+
// remove client from pluginManager
|
|
672
|
+
await this.#pluginManager[kServerPluginManagerRemoveClient](client);
|
|
673
|
+
// clean state manager
|
|
674
|
+
await this.#stateManager[kServerStateManagerRemoveClient](client.id);
|
|
675
|
+
|
|
676
|
+
this.#onClientDisconnectCallbacks.forEach(callback => callback(client));
|
|
677
|
+
} catch (err) {
|
|
678
|
+
console.error(err);
|
|
863
679
|
}
|
|
864
|
-
// clean context manager, await before cleaning state manager
|
|
865
|
-
await this.contextManager.removeClient(client);
|
|
866
|
-
// remove client from pluginManager
|
|
867
|
-
await this.pluginManager.removeClient(client);
|
|
868
|
-
// clean state manager
|
|
869
|
-
await this.stateManager.removeClient(client.id);
|
|
870
680
|
}
|
|
871
681
|
|
|
872
682
|
// clean sockets
|
|
873
|
-
socket
|
|
683
|
+
socket[kSocketTerminate]();
|
|
874
684
|
});
|
|
875
685
|
|
|
876
686
|
socket.addListener(CLIENT_HANDSHAKE_REQUEST, async payload => {
|
|
877
|
-
const { role, registeredPlugins } = payload;
|
|
687
|
+
const { role, version, registeredPlugins } = payload;
|
|
878
688
|
|
|
879
689
|
if (!roles.includes(role)) {
|
|
880
|
-
console.error(`
|
|
690
|
+
console.error(`A client with undefined role ("${role}") attempted to connect`);
|
|
881
691
|
|
|
882
692
|
socket.send(CLIENT_HANDSHAKE_ERROR, {
|
|
883
693
|
type: 'invalid-client-type',
|
|
884
|
-
message: `Invalid client
|
|
694
|
+
message: `Invalid client role, please check server configuration (valid client roles are: ${roles.join(', ')})`,
|
|
885
695
|
});
|
|
886
696
|
return;
|
|
887
697
|
}
|
|
888
698
|
|
|
699
|
+
if (version !== this.#version) {
|
|
700
|
+
logger.warnVersionDiscrepancies(role, version, this.#version);
|
|
701
|
+
}
|
|
702
|
+
|
|
889
703
|
try {
|
|
890
|
-
this
|
|
704
|
+
this.#pluginManager[kServerPluginManagerCheckRegisteredPlugins](registeredPlugins);
|
|
891
705
|
} catch (err) {
|
|
892
706
|
socket.send(CLIENT_HANDSHAKE_ERROR, {
|
|
893
707
|
type: 'invalid-plugin-list',
|
|
@@ -897,192 +711,71 @@ Invalid certificate files, please check your:
|
|
|
897
711
|
}
|
|
898
712
|
|
|
899
713
|
// increment audit state
|
|
900
|
-
const numClients = this.
|
|
714
|
+
const numClients = this.#auditState.get('numClients');
|
|
901
715
|
numClients[role] += 1;
|
|
902
|
-
this.
|
|
716
|
+
this.#auditState.set({ numClients });
|
|
903
717
|
|
|
904
|
-
|
|
905
|
-
await this.stateManager.addClient(client.id, {
|
|
718
|
+
const transport = {
|
|
906
719
|
emit: client.socket.send.bind(client.socket),
|
|
907
720
|
addListener: client.socket.addListener.bind(client.socket),
|
|
908
721
|
removeAllListeners: client.socket.removeAllListeners.bind(client.socket),
|
|
909
|
-
}
|
|
722
|
+
};
|
|
723
|
+
// add client to state manager
|
|
724
|
+
await this.#stateManager[kServerStateManagerAddClient](client.id, transport);
|
|
910
725
|
// add client to plugin manager
|
|
911
726
|
// server-side, all plugins are active for the lifetime of the client
|
|
912
|
-
await this
|
|
727
|
+
await this.#pluginManager[kServerPluginManagerAddClient](client, registeredPlugins);
|
|
913
728
|
// add client to context manager
|
|
914
|
-
await this
|
|
915
|
-
|
|
916
|
-
const { id, uuid, token } = client;
|
|
917
|
-
socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid, token });
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* Create namespaced databases for core and plugins
|
|
923
|
-
* (kind of experimental API do not expose in doc for now)
|
|
924
|
-
*
|
|
925
|
-
* @note - introduced in v3.1.0-beta.1
|
|
926
|
-
* @note - used by core and plugin-audio-streams
|
|
927
|
-
* @private
|
|
928
|
-
*/
|
|
929
|
-
createNamespacedDb(namespace = null) {
|
|
930
|
-
if (namespace === null || !(typeof namespace === 'string')) {
|
|
931
|
-
throw new Error(`[soundworks:Server] Invalid namespace for ".createNamespacedDb(namespace)", namespace is mandatory and should be a string`);
|
|
932
|
-
}
|
|
729
|
+
await this.#contextManager[kServerContextManagerAddClient](client);
|
|
933
730
|
|
|
934
|
-
|
|
935
|
-
throw new Error(`[soundworks:Server] Invalid namespace for ".createNamespacedDb(namespace)", namespace "${namespace}" already exists`);
|
|
936
|
-
}
|
|
731
|
+
this.#onClientConnectCallbacks.forEach(callback => callback(client));
|
|
937
732
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
db.on('error', err => logger.error(`[soundworks:Server] db ${namespace} error: ${err}`));
|
|
946
|
-
|
|
947
|
-
return db;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
onStatusChange(callback) {
|
|
951
|
-
this._onStatusChangeCallbacks.add(callback);
|
|
952
|
-
|
|
953
|
-
return () => this._onStatusChangeCallbacks.delete(callback);
|
|
733
|
+
socket.send(CLIENT_HANDSHAKE_RESPONSE, {
|
|
734
|
+
id: client.id,
|
|
735
|
+
uuid: client.uuid,
|
|
736
|
+
token: client[kServerClientToken],
|
|
737
|
+
version: this.#version,
|
|
738
|
+
});
|
|
739
|
+
});
|
|
954
740
|
}
|
|
955
741
|
|
|
742
|
+
// make public
|
|
956
743
|
/** @private */
|
|
957
|
-
|
|
958
|
-
this.
|
|
959
|
-
|
|
960
|
-
// if launched in a child process, forward status to parent process
|
|
961
|
-
if (process.send !== undefined) {
|
|
962
|
-
process.send(`soundworks:server:${status}`);
|
|
744
|
+
isProtectedClientRole(role) {
|
|
745
|
+
if (this.#config.env.auth && Array.isArray(this.#config.env.auth.clients)) {
|
|
746
|
+
return this.#config.env.auth.clients.includes(role);
|
|
963
747
|
}
|
|
964
748
|
|
|
965
|
-
|
|
966
|
-
const promises = [];
|
|
967
|
-
|
|
968
|
-
for (let callback of this._onStatusChangeCallbacks) {
|
|
969
|
-
promises.push(callback(status));
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
await Promise.all(promises);
|
|
749
|
+
return false;
|
|
973
750
|
}
|
|
974
751
|
|
|
975
752
|
/**
|
|
976
|
-
*
|
|
977
|
-
* template provided by `@soundworks/create.
|
|
978
|
-
*
|
|
979
|
-
* - uses [template-literal](https://www.npmjs.com/package/template-literal) package
|
|
980
|
-
* as html templateEngine
|
|
981
|
-
* - define `.build/server/tmpl` as the directory in which html template can be
|
|
982
|
-
* found
|
|
983
|
-
* - define the `clientConfigFunction` function that return client compliant
|
|
984
|
-
* config object to be injected in the html template.
|
|
985
|
-
*
|
|
986
|
-
* Also expose two public directory:
|
|
987
|
-
* - the `public` directory which is exposed behind the root path
|
|
988
|
-
* - the `./.build/public` directory which is exposed behind the `build` path
|
|
753
|
+
* Generate a token to secure client connection.
|
|
989
754
|
*
|
|
990
|
-
*
|
|
991
|
-
*
|
|
755
|
+
* The token should be passed to the client-side `Client` config object, it will
|
|
756
|
+
* be internally used to check the WebSocket connection and reject it if the
|
|
757
|
+
* token is invalid.
|
|
992
758
|
*/
|
|
993
|
-
|
|
994
|
-
const
|
|
759
|
+
generateAuthToken(req) {
|
|
760
|
+
const id = this.#tokenIdGenerator;
|
|
761
|
+
const ip = req.ip;
|
|
762
|
+
const time = getTime();
|
|
763
|
+
const token = { id, ip, time };
|
|
764
|
+
const encryptedToken = encryptData(token);
|
|
995
765
|
|
|
996
|
-
|
|
997
|
-
const roles = Object.keys(this.config.app.clients);
|
|
766
|
+
this.#pendingConnectionTokens.add(encryptedToken);
|
|
998
767
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const minifiedFilePath = path.join(buildDirectory, `${role}.min.js`);
|
|
768
|
+
setTimeout(() => {
|
|
769
|
+
this.#pendingConnectionTokens.delete(encryptedToken);
|
|
770
|
+
}, TOKEN_VALID_DURATION * 1000);
|
|
1003
771
|
|
|
1004
|
-
|
|
1005
|
-
useMinifiedFile[role] = true;
|
|
1006
|
-
} else {
|
|
1007
|
-
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)`));
|
|
1008
|
-
useMinifiedFile[role] = false;
|
|
1009
|
-
}
|
|
1010
|
-
} else {
|
|
1011
|
-
useMinifiedFile[role] = false;
|
|
1012
|
-
}
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
this._applicationTemplateOptions = {
|
|
1016
|
-
templateEngine: { compile },
|
|
1017
|
-
templatePath: path.join('.build', 'server', 'tmpl'),
|
|
1018
|
-
clientConfigFunction: (role, config, _httpRequest) => {
|
|
1019
|
-
return {
|
|
1020
|
-
role: role,
|
|
1021
|
-
app: {
|
|
1022
|
-
name: config.app.name,
|
|
1023
|
-
author: config.app.author,
|
|
1024
|
-
},
|
|
1025
|
-
env: {
|
|
1026
|
-
type: config.env.type,
|
|
1027
|
-
websockets: config.env.websockets,
|
|
1028
|
-
subpath: config.env.subpath,
|
|
1029
|
-
useMinifiedFile: useMinifiedFile[role],
|
|
1030
|
-
},
|
|
1031
|
-
};
|
|
1032
|
-
},
|
|
1033
|
-
};
|
|
1034
|
-
|
|
1035
|
-
this.router.use(express.static('public'));
|
|
1036
|
-
this.router.use('/build', express.static(buildDirectory));
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* Define custom template path, template engine, and clientConfig function.
|
|
1041
|
-
* This method is proposed for very advanced use-cases and should very probably
|
|
1042
|
-
* be improved. If you consider using this for some reason, please get in touch
|
|
1043
|
-
* first to explain your use-case :)
|
|
1044
|
-
*/
|
|
1045
|
-
setCustomApplicationTemplateOptions(options) {
|
|
1046
|
-
Object.assign(this._applicationTemplateOptions, options);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
/**
|
|
1050
|
-
* Attach and retrieve the global audit state of the application.
|
|
1051
|
-
*
|
|
1052
|
-
* The audit state is a {@link server.SharedState} instance that keeps track of
|
|
1053
|
-
* global informations about the application such as, the number of connected
|
|
1054
|
-
* clients, network latency estimation, etc.
|
|
1055
|
-
*
|
|
1056
|
-
* The audit state is created by the server on start up.
|
|
1057
|
-
*
|
|
1058
|
-
* @returns {Promise<server.SharedState>}
|
|
1059
|
-
* @throws Will throw if called before `server.init()`
|
|
1060
|
-
* @see {@link server.SharedState}
|
|
1061
|
-
* @example
|
|
1062
|
-
* const auditState = await server.getAuditState();
|
|
1063
|
-
* auditState.onUpdate(() => console.log(auditState.getValues()), true);
|
|
1064
|
-
*/
|
|
1065
|
-
async getAuditState() {
|
|
1066
|
-
if (this.status === 'idle') {
|
|
1067
|
-
throw new Error(`[soundworks.Server] Cannot access audit state before init`);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
return this._auditState;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
/** @private */
|
|
1074
|
-
isProtected(role) {
|
|
1075
|
-
if (this.config.env.auth && Array.isArray(this.config.env.auth.clients)) {
|
|
1076
|
-
return this.config.env.auth.clients.includes(role);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
return false;
|
|
772
|
+
return encryptedToken;
|
|
1080
773
|
}
|
|
1081
774
|
|
|
1082
775
|
/** @private */
|
|
1083
|
-
|
|
776
|
+
[kServerIsValidConnectionToken](token) {
|
|
1084
777
|
// token should be in pending token list
|
|
1085
|
-
if (!this.
|
|
778
|
+
if (!this.#pendingConnectionTokens.has(token)) {
|
|
1086
779
|
return false;
|
|
1087
780
|
}
|
|
1088
781
|
|
|
@@ -1090,10 +783,8 @@ Invalid certificate files, please check your:
|
|
|
1090
783
|
const data = decryptData(token);
|
|
1091
784
|
const now = getTime();
|
|
1092
785
|
|
|
1093
|
-
// token is valid only for 30 seconds (this is arbitrary)
|
|
1094
786
|
if (now > data.time + TOKEN_VALID_DURATION) {
|
|
1095
|
-
|
|
1096
|
-
this._pendingConnectionTokens.delete(token);
|
|
787
|
+
this.#pendingConnectionTokens.delete(token);
|
|
1097
788
|
return false;
|
|
1098
789
|
} else {
|
|
1099
790
|
return true;
|
|
@@ -1104,14 +795,14 @@ Invalid certificate files, please check your:
|
|
|
1104
795
|
* Check if the given client is trusted, i.e. config.env.type == 'production'
|
|
1105
796
|
* and the client is protected behind a password.
|
|
1106
797
|
*
|
|
1107
|
-
* @param {
|
|
1108
|
-
* @returns {
|
|
798
|
+
* @param {ServerClient} client - Client to be tested
|
|
799
|
+
* @returns {boolean}
|
|
1109
800
|
*/
|
|
1110
801
|
isTrustedClient(client) {
|
|
1111
|
-
if (this
|
|
802
|
+
if (this.#config.env.type !== 'production') {
|
|
1112
803
|
return true;
|
|
1113
804
|
} else {
|
|
1114
|
-
return this.
|
|
805
|
+
return this.#trustedClients.has(client);
|
|
1115
806
|
}
|
|
1116
807
|
}
|
|
1117
808
|
|
|
@@ -1119,20 +810,20 @@ Invalid certificate files, please check your:
|
|
|
1119
810
|
* Check if the token from a client is trusted, i.e. config.env.type == 'production'
|
|
1120
811
|
* and the client is protected behind a password.
|
|
1121
812
|
*
|
|
1122
|
-
* @param {
|
|
1123
|
-
* @param {
|
|
1124
|
-
* @param {
|
|
1125
|
-
* @returns {
|
|
813
|
+
* @param {number} clientId - Id of the client
|
|
814
|
+
* @param {string} clientIp - Ip of the client
|
|
815
|
+
* @param {string} token - Token to be tested
|
|
816
|
+
* @returns {boolean}
|
|
1126
817
|
*/
|
|
1127
818
|
// for stateless interactions, e.g. POST files
|
|
1128
819
|
isTrustedToken(clientId, clientIp, token) {
|
|
1129
|
-
if (this
|
|
820
|
+
if (this.#config.env.type !== 'production') {
|
|
1130
821
|
return true;
|
|
1131
822
|
} else {
|
|
1132
|
-
for (let client of this
|
|
1133
|
-
if (client.id === clientId && client
|
|
823
|
+
for (let client of this.#trustedClients) {
|
|
824
|
+
if (client.id === clientId && client[kServerClientToken] === token) {
|
|
1134
825
|
// check that given token is consistent with client ip and id
|
|
1135
|
-
const { id, ip } = decryptData(client
|
|
826
|
+
const { id, ip } = decryptData(client[kServerClientToken]);
|
|
1136
827
|
|
|
1137
828
|
if (clientId === id && clientIp === ip) {
|
|
1138
829
|
return true;
|
|
@@ -1144,6 +835,43 @@ Invalid certificate files, please check your:
|
|
|
1144
835
|
return false;
|
|
1145
836
|
}
|
|
1146
837
|
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Create namespaced databases for core and plugins
|
|
841
|
+
* (kind of experimental API do not expose in doc for now)
|
|
842
|
+
*
|
|
843
|
+
* @note - introduced in v3.1.0-beta.1
|
|
844
|
+
* @note - used by core and plugin-audio-streams
|
|
845
|
+
* @private
|
|
846
|
+
*/
|
|
847
|
+
createNamespacedDb(namespace = null) {
|
|
848
|
+
if (!isString(namespace)) {
|
|
849
|
+
throw new TypeError(`Cannot execute "createNamespacedDb(namespace)" on Server: argument 1 must be a string`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (dbNamespaces.has(namespace)) {
|
|
853
|
+
throw new DOMException(`Cannot execute "createNamespacedDb(namespace)" on Server: namespace "${namespace}" already exists`, 'NotSupportedError');
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// KeyvFile uses fs-extra.outputFile internally so we don't need to create
|
|
857
|
+
// the directory, it will be lazily created if something is written in the db
|
|
858
|
+
// @see https://github.com/zaaack/keyv-file/blob/52502077c78226b3d69a615c80b88e53be096979/index.ts#L157
|
|
859
|
+
const filename = path.join(process.cwd(), '.data', `soundworks-${namespace}.db`);
|
|
860
|
+
// @note - keyv-file doesn't seems to works
|
|
861
|
+
const store = new KeyvFile({ filename });
|
|
862
|
+
const db = new Keyv({ namespace, store });
|
|
863
|
+
db.on('error', err => logger.error(`[soundworks:Server] db ${namespace} error: ${err}`));
|
|
864
|
+
|
|
865
|
+
return db;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* @deprecated
|
|
870
|
+
*/
|
|
871
|
+
useDefaultApplicationTemplate() {
|
|
872
|
+
logger.deprecated('Server#useDefaultApplicationTemplate', '`configureHttpRouter(server)` from the `@soundworks/helpers/server.js` package', '4.0.0-alpha.29');
|
|
873
|
+
this.#useDefaultApplicationTemplate = true;
|
|
874
|
+
}
|
|
1147
875
|
}
|
|
1148
876
|
|
|
1149
877
|
export default Server;
|