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