@soundworks/core 3.7.0 → 4.0.0-alpha.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/.eslintrc +16 -0
- package/LICENSE +1 -1
- package/README.md +32 -30
- package/package.json +48 -28
- package/src/client/Client.js +391 -0
- package/src/client/Context.js +225 -0
- package/src/client/ContextManager.js +111 -0
- package/src/client/Plugin.js +66 -0
- package/src/client/PluginManager.js +123 -0
- package/src/client/SharedState.js +84 -0
- package/src/client/Socket.js +389 -0
- package/src/client/StateManager.js +67 -0
- package/src/client/index.js +77 -0
- package/src/common/BasePlugin.js +163 -0
- package/src/common/BasePluginManager.js +227 -0
- package/{common/SharedState.js → src/common/BaseSharedState.js} +198 -234
- package/src/common/BaseStateManager.js +200 -0
- package/{common → src/common}/ParameterBag.js +74 -112
- package/{common → src/common}/SharedStatePrivate.js +56 -62
- package/src/common/constants.js +51 -0
- package/src/common/logger.js +154 -0
- package/src/common/promise-store.js +25 -0
- package/src/common/sockets-utils.js +73 -0
- package/src/common/utils.js +22 -0
- package/src/server/Client.js +50 -0
- package/src/server/Context.js +99 -0
- package/src/server/ContextManager.js +266 -0
- package/src/server/Plugin.js +77 -0
- package/src/server/PluginManager.js +150 -0
- package/src/server/Server.js +849 -0
- package/src/server/SharedState.js +86 -0
- package/{server → src/server}/Socket.js +114 -88
- package/{server → src/server}/Sockets.js +73 -60
- package/{common/SharedStateManagerServer.js → src/server/StateManager.js} +181 -129
- package/src/server/audit-network-latency.worker.js +57 -0
- package/src/server/audit-schema.js +42 -0
- package/src/server/index.js +45 -0
- package/tsconfig.json +21 -0
- package/types/client/Client.d.ts +194 -0
- package/types/client/Client.d.ts.map +1 -0
- package/types/client/Context.d.ts +139 -0
- package/types/client/Context.d.ts.map +1 -0
- package/types/client/ContextManager.d.ts +45 -0
- package/types/client/ContextManager.d.ts.map +1 -0
- package/types/client/Plugin.d.ts +53 -0
- package/types/client/Plugin.d.ts.map +1 -0
- package/types/client/PluginManager.d.ts +96 -0
- package/types/client/PluginManager.d.ts.map +1 -0
- package/types/client/SharedState.d.ts +85 -0
- package/types/client/SharedState.d.ts.map +1 -0
- package/types/client/Socket.d.ts +138 -0
- package/types/client/Socket.d.ts.map +1 -0
- package/types/client/StateManager.d.ts +72 -0
- package/types/client/StateManager.d.ts.map +1 -0
- package/types/client/index.d.ts +11 -0
- package/types/client/index.d.ts.map +1 -0
- package/types/common/BasePlugin.d.ts +145 -0
- package/types/common/BasePlugin.d.ts.map +1 -0
- package/types/common/BasePluginManager.d.ts +88 -0
- package/types/common/BasePluginManager.d.ts.map +1 -0
- package/types/common/BaseSharedState.d.ts +173 -0
- package/types/common/BaseSharedState.d.ts.map +1 -0
- package/types/common/BaseStateManager.d.ts +70 -0
- package/types/common/BaseStateManager.d.ts.map +1 -0
- package/types/common/ParameterBag.d.ts +143 -0
- package/types/common/ParameterBag.d.ts.map +1 -0
- package/types/common/SharedStatePrivate.d.ts +20 -0
- package/types/common/SharedStatePrivate.d.ts.map +1 -0
- package/types/common/constants.d.ts +35 -0
- package/types/common/constants.d.ts.map +1 -0
- package/types/common/logger.d.ts +16 -0
- package/types/common/logger.d.ts.map +1 -0
- package/types/common/promise-store.d.ts +4 -0
- package/types/common/promise-store.d.ts.map +1 -0
- package/types/common/sockets-utils.d.ts +9 -0
- package/types/common/sockets-utils.d.ts.map +1 -0
- package/types/common/utils.d.ts +5 -0
- package/types/common/utils.d.ts.map +1 -0
- package/types/server/Client.d.ts +40 -0
- package/types/server/Client.d.ts.map +1 -0
- package/types/server/Context.d.ts +59 -0
- package/types/server/Context.d.ts.map +1 -0
- package/types/server/ContextManager.d.ts +69 -0
- package/types/server/ContextManager.d.ts.map +1 -0
- package/types/server/Plugin.d.ts +70 -0
- package/types/server/Plugin.d.ts.map +1 -0
- package/types/server/PluginManager.d.ts +91 -0
- package/types/server/PluginManager.d.ts.map +1 -0
- package/types/server/Server.d.ts +223 -0
- package/types/server/Server.d.ts.map +1 -0
- package/types/server/SharedState.d.ts +86 -0
- package/types/server/SharedState.d.ts.map +1 -0
- package/types/server/Socket.d.ts +128 -0
- package/types/server/Socket.d.ts.map +1 -0
- package/types/server/Sockets.d.ts +73 -0
- package/types/server/Sockets.d.ts.map +1 -0
- package/types/server/StateManager.d.ts +190 -0
- package/types/server/StateManager.d.ts.map +1 -0
- package/types/server/audit-network-latency.worker.d.ts +2 -0
- package/types/server/audit-network-latency.worker.d.ts.map +1 -0
- package/types/server/audit-schema.d.ts +27 -0
- package/types/server/audit-schema.d.ts.map +1 -0
- package/types/server/index.d.ts +7 -0
- package/types/server/index.d.ts.map +1 -0
- package/client/AbstractExperience.js +0 -54
- package/client/AbstractPlugin.js +0 -230
- package/client/Client.js +0 -193
- package/client/PluginManager.js +0 -279
- package/client/Socket.js +0 -346
- package/client/index.js +0 -48
- package/client.js +0 -28
- package/common/SharedStateManagerClient.js +0 -194
- package/common/Signal.js +0 -40
- package/common/SignalAll.js +0 -44
- package/common/logger.js +0 -162
- package/common/shared-state-utils.js +0 -89
- package/common/sockets-encoder-decoder.js +0 -73
- package/server/AbstractExperience.js +0 -126
- package/server/AbstractPlugin.js +0 -214
- package/server/Client.js +0 -70
- package/server/PluginManager.js +0 -273
- package/server/Server.js +0 -785
- package/server/index.js +0 -49
- package/server.js +0 -28
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { X509Certificate, createPrivateKey } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import compression from 'compression';
|
|
10
|
+
import express from 'express';
|
|
11
|
+
import isPlainObject from 'is-plain-obj';
|
|
12
|
+
import Keyv from 'keyv';
|
|
13
|
+
import { KeyvFile } from 'keyv-file';
|
|
14
|
+
import merge from 'lodash.merge';
|
|
15
|
+
import pem from 'pem';
|
|
16
|
+
import compile from 'template-literal';
|
|
17
|
+
|
|
18
|
+
import auditSchema from './audit-schema.js';
|
|
19
|
+
import Client from './Client.js';
|
|
20
|
+
import ContextManager from './ContextManager.js';
|
|
21
|
+
import PluginManager from './PluginManager.js';
|
|
22
|
+
import StateManager from './StateManager.js';
|
|
23
|
+
import Sockets from './Sockets.js';
|
|
24
|
+
import logger from '../common/logger.js';
|
|
25
|
+
import {
|
|
26
|
+
CLIENT_HANDSHAKE_REQUEST,
|
|
27
|
+
CLIENT_HANDSHAKE_RESPONSE,
|
|
28
|
+
CLIENT_HANDSHAKE_ERROR,
|
|
29
|
+
AUDIT_STATE_NAME,
|
|
30
|
+
} from '../common/constants.js';
|
|
31
|
+
|
|
32
|
+
let _dbNamespaces = new Set();
|
|
33
|
+
|
|
34
|
+
const DEFAULT_CONFIG = {
|
|
35
|
+
env: {
|
|
36
|
+
type: 'development',
|
|
37
|
+
port: 8000,
|
|
38
|
+
serverAddress: null,
|
|
39
|
+
subpath: '',
|
|
40
|
+
websockets: {
|
|
41
|
+
path: 'socket',
|
|
42
|
+
pingInterval: 5000,
|
|
43
|
+
},
|
|
44
|
+
useHttps: false,
|
|
45
|
+
httpsInfos: null,
|
|
46
|
+
crossOriginIsolated: true,
|
|
47
|
+
verbose: true,
|
|
48
|
+
},
|
|
49
|
+
app: {
|
|
50
|
+
name: 'soundworks',
|
|
51
|
+
clients: {},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// set terminal title
|
|
56
|
+
/** @private */
|
|
57
|
+
function setTerminalTitle(server) {
|
|
58
|
+
let title = '';
|
|
59
|
+
|
|
60
|
+
if (server._auditState !== null) {
|
|
61
|
+
const numClients = server._auditState.get('numClients');
|
|
62
|
+
let numClientStrings = [];
|
|
63
|
+
for (let name in numClients) {
|
|
64
|
+
numClientStrings.push(`${name}: ${numClients[name]}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
title = `${server.config.app.name} | ${numClientStrings.join(' - ')}`;
|
|
68
|
+
} else {
|
|
69
|
+
title = `${server.config.app.name}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const msg = String.fromCharCode(27) + ']0;' + title + String.fromCharCode(7);
|
|
73
|
+
process.stdout.write(msg);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Server side entry point for a `soundworks` application.
|
|
78
|
+
*
|
|
79
|
+
* This object hosts configuration informations, as well as methods to
|
|
80
|
+
* initialize and start the application. It is also responsible for creating
|
|
81
|
+
* the static file (http) server as well as the socket server.
|
|
82
|
+
*
|
|
83
|
+
* @memberof server
|
|
84
|
+
*
|
|
85
|
+
* @param {server.ServerConfig} config
|
|
86
|
+
* @example
|
|
87
|
+
* import { Server } from '@soundworks/core/server';
|
|
88
|
+
* const server = new Server({
|
|
89
|
+
* app: {
|
|
90
|
+
* name: 'my-example-app',
|
|
91
|
+
* clients: { myClient: { target: 'node' } },
|
|
92
|
+
* },
|
|
93
|
+
* env: {
|
|
94
|
+
* port: 8888,
|
|
95
|
+
* },
|
|
96
|
+
* });
|
|
97
|
+
* await server.init();
|
|
98
|
+
* await server.start();
|
|
99
|
+
*/
|
|
100
|
+
class Server {
|
|
101
|
+
constructor(config) {
|
|
102
|
+
if (!isPlainObject(config)) {
|
|
103
|
+
throw new Error(`[soundworks:Server] Invalid argument for Server constructor, config should be an object`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Configuration informations.
|
|
107
|
+
* Defaults to:
|
|
108
|
+
* ```
|
|
109
|
+
* {
|
|
110
|
+
* env: {
|
|
111
|
+
* type: 'development',
|
|
112
|
+
* port: 8000,
|
|
113
|
+
* subfolder: '',
|
|
114
|
+
* useHttps: false,
|
|
115
|
+
* httpsInfos: null,
|
|
116
|
+
* },
|
|
117
|
+
* app: {
|
|
118
|
+
* name: 'soundworks',
|
|
119
|
+
* clients: {},
|
|
120
|
+
* },
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
this.config = merge({}, DEFAULT_CONFIG, config);
|
|
125
|
+
|
|
126
|
+
// parse config
|
|
127
|
+
if (Object.keys(this.config.app.clients).length === 0) {
|
|
128
|
+
throw new Error(`[soundworks:Server] Invalid "app.clients" config, at least one client should be declared`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// if a node client is defined, serverAddress should be defined
|
|
132
|
+
let hasNodeClient = false;
|
|
133
|
+
for (let name in this.config.app.clients) {
|
|
134
|
+
if (this.config.app.clients[name].target === 'node') {
|
|
135
|
+
hasNodeClient = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (hasNodeClient && this.config.env.serverAddress === null) {
|
|
140
|
+
throw new Error(`[soundworks:Server] Invalid "env.serverAddress" config, is mandatory when a node client target is defined`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.config.env.useHttps && this.config.env.httpsInfos !== null) {
|
|
144
|
+
const httpsInfos = this.config.env.httpsInfos;
|
|
145
|
+
|
|
146
|
+
if (!isPlainObject(this.config.env.httpsInfos)) {
|
|
147
|
+
throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, should be null or object { cert, key }`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!('cert' in httpsInfos) || !('key' in httpsInfos)) {
|
|
151
|
+
throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, should contain both "cert" and "key" entries`);
|
|
152
|
+
}
|
|
153
|
+
// @todo - move that to constructor
|
|
154
|
+
if (!fs.existsSync(httpsInfos.cert)) {
|
|
155
|
+
throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, "cert" file not found`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(httpsInfos.key)) {
|
|
159
|
+
throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, "key" file not found`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Router. Internally use polka.
|
|
165
|
+
* (cf. {@link https://github.com/lukeed/polka})
|
|
166
|
+
*/
|
|
167
|
+
this.router = express();
|
|
168
|
+
// compression (must be set before serve-static)
|
|
169
|
+
this.router.use(compression());
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Http(s) server instance. The node `http` or `https` module instance
|
|
173
|
+
* (cf. {@link https://nodejs.org/api/http.html})
|
|
174
|
+
*/
|
|
175
|
+
this.httpServer = null;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Key / value storage with Promise based Map API
|
|
179
|
+
* basically a wrapper around kvey (cf. {@link https://github.com/lukechilds/keyv})
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
182
|
+
this.db = this.createNamespacedDb('core');
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The {@link server.Sockets} instance. A small wrapper around
|
|
186
|
+
* [`ws`](https://github.com/websockets/ws) server.
|
|
187
|
+
* @see {@link server.Sockets}
|
|
188
|
+
* @type {server.Sockets}
|
|
189
|
+
*/
|
|
190
|
+
this.sockets = new Sockets();
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The {@link server.PluginManager} instance.
|
|
194
|
+
* @see {@link server.PluginManager}
|
|
195
|
+
* @type {server.PluginManager}
|
|
196
|
+
*/
|
|
197
|
+
this.pluginManager = new PluginManager(this);
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The {@link server.StateManager} instance.
|
|
201
|
+
* @see {@link server.StateManager}
|
|
202
|
+
* @type {server.StateManager}
|
|
203
|
+
*/
|
|
204
|
+
this.stateManager = new StateManager();
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The {@link server.ContextManager} instance.
|
|
208
|
+
*
|
|
209
|
+
*/
|
|
210
|
+
this.contextManager = new ContextManager(this);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* If https is required, will contain informations about the certificates
|
|
214
|
+
* (self-signed, validity dates, etc.)
|
|
215
|
+
*/
|
|
216
|
+
this.httpsInfos = null;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Current status of the server ['idle', 'inited', 'started']
|
|
220
|
+
*/
|
|
221
|
+
this.status = 'idle';
|
|
222
|
+
|
|
223
|
+
/** @private */
|
|
224
|
+
this._applicationTemplateConfig = {
|
|
225
|
+
templateEngine: null,
|
|
226
|
+
templatePath: null,
|
|
227
|
+
clientConfigFunction: null,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** @private */
|
|
231
|
+
this._onStatusChangeCallbacks = new Set();
|
|
232
|
+
/** @private */
|
|
233
|
+
this._auditState = null;
|
|
234
|
+
|
|
235
|
+
// create audit state
|
|
236
|
+
this.stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema);
|
|
237
|
+
|
|
238
|
+
logger.configure(this.config.env.verbose);
|
|
239
|
+
setTerminalTitle(this);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Method to be called before `start` in the initialization lifecycle of the
|
|
244
|
+
* soundworks server. Note that if `init()`` is not explicitely called, `start()`
|
|
245
|
+
* will call it implicitely.
|
|
246
|
+
*
|
|
247
|
+
* What it does:
|
|
248
|
+
* - create the audit state
|
|
249
|
+
* - prepapre http(s) server and routing according to the informations
|
|
250
|
+
* declared in `config.app.clients`
|
|
251
|
+
* - starts all registered plugins
|
|
252
|
+
*
|
|
253
|
+
* After `await server.init()`, you can safely use the StateManager, as well
|
|
254
|
+
* as any registered Plugins.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* const server = new Server(config);
|
|
258
|
+
* await server.init();
|
|
259
|
+
* await server.start();
|
|
260
|
+
* // or implicitly called by start
|
|
261
|
+
* const server = new Server(config);
|
|
262
|
+
* await server.start(); // init is called implicitely
|
|
263
|
+
*/
|
|
264
|
+
async init() {
|
|
265
|
+
const numClients = {};
|
|
266
|
+
for (let name in this.config.app.clients) {
|
|
267
|
+
numClients[name] = 0;
|
|
268
|
+
}
|
|
269
|
+
/** @private */
|
|
270
|
+
this._auditState = await this.stateManager.create(AUDIT_STATE_NAME, { numClients });
|
|
271
|
+
this._auditState.onUpdate(() => setTerminalTitle(this));
|
|
272
|
+
|
|
273
|
+
// basic http authentication
|
|
274
|
+
if (this.config.env.auth) {
|
|
275
|
+
this.router.use((req, res, next) => {
|
|
276
|
+
|
|
277
|
+
const isProtected = this.config.env.auth.clients
|
|
278
|
+
.map(type => req.path.endsWith(`/${type}`))
|
|
279
|
+
.reduce((acc, value) => acc || value, false);
|
|
280
|
+
|
|
281
|
+
if (isProtected) {
|
|
282
|
+
// authentication middleware
|
|
283
|
+
const auth = this.config.env.auth;
|
|
284
|
+
// parse login and password from headers
|
|
285
|
+
const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
|
|
286
|
+
const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':');
|
|
287
|
+
|
|
288
|
+
// verify login and password are set and correct
|
|
289
|
+
if (login && password && login === auth.login && password === auth.password) {
|
|
290
|
+
// -> access granted...
|
|
291
|
+
return next();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// -> access denied...
|
|
295
|
+
res.writeHead(401, {
|
|
296
|
+
'WWW-Authenticate':'Basic',
|
|
297
|
+
'Content-Type':'text/plain',
|
|
298
|
+
});
|
|
299
|
+
res.end('Authentication required.');
|
|
300
|
+
} else {
|
|
301
|
+
// route not protected
|
|
302
|
+
return next();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// start http server
|
|
308
|
+
const useHttps = this.config.env.useHttps || false;
|
|
309
|
+
|
|
310
|
+
// ------------------------------------------------------------
|
|
311
|
+
// create HTTP(S) SERVER
|
|
312
|
+
// ------------------------------------------------------------
|
|
313
|
+
if (!useHttps) {
|
|
314
|
+
this.httpServer = http.createServer(this.router);
|
|
315
|
+
} else {
|
|
316
|
+
const httpsInfos = this.config.env.httpsInfos;
|
|
317
|
+
|
|
318
|
+
// if certs have been given in config
|
|
319
|
+
if (httpsInfos !== null) {
|
|
320
|
+
try {
|
|
321
|
+
// existance of file is checked in contructor
|
|
322
|
+
let cert = fs.readFileSync(httpsInfos.cert);
|
|
323
|
+
let key = fs.readFileSync(httpsInfos.key);
|
|
324
|
+
|
|
325
|
+
let x509 = null;
|
|
326
|
+
// this fails with self-signed certificates for whatever reason...
|
|
327
|
+
try {
|
|
328
|
+
x509 = new X509Certificate(cert);
|
|
329
|
+
} catch(err) {
|
|
330
|
+
this._dispatchStatus('errored');
|
|
331
|
+
throw new Error(`[soundworks:Server] Invalid https cert file`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const keyObj = createPrivateKey(key);
|
|
336
|
+
|
|
337
|
+
if (!x509.checkPrivateKey(keyObj)) {
|
|
338
|
+
this._dispatchStatus('errored');
|
|
339
|
+
throw new Error(`[soundworks:Server] Invalid https key file`);
|
|
340
|
+
}
|
|
341
|
+
} catch(err) {
|
|
342
|
+
this._dispatchStatus('errored');
|
|
343
|
+
throw new Error(`[soundworks:Server] Invalid https key file`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// check is certificate is still valid
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
const certExpire = Date.parse(x509.validTo);
|
|
349
|
+
const isValid = now < certExpire;
|
|
350
|
+
|
|
351
|
+
const diff = certExpire - now;
|
|
352
|
+
const daysRemaining = Math.round(diff / 1000 / 60 / 60 / 24);
|
|
353
|
+
|
|
354
|
+
this.httpsInfos = {
|
|
355
|
+
selfSigned: false,
|
|
356
|
+
CN: x509.subject.split('=')[1],
|
|
357
|
+
altNames: x509.subjectAltName.split(',').map(e => e.trim().split(':')[1]),
|
|
358
|
+
validFrom: x509.validFrom,
|
|
359
|
+
validTo: x509.validTo,
|
|
360
|
+
isValid: isValid,
|
|
361
|
+
daysRemaining: daysRemaining,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
this.httpServer = https.createServer({ key, cert }, this.router);
|
|
365
|
+
} catch(err) {
|
|
366
|
+
logger.error(`
|
|
367
|
+
Invalid certificate files, please check your:
|
|
368
|
+
- key file: ${httpsInfos.key}
|
|
369
|
+
- cert file: ${httpsInfos.cert}
|
|
370
|
+
`);
|
|
371
|
+
|
|
372
|
+
this._dispatchStatus('errored');
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
// generate certs
|
|
377
|
+
// --------------------------------------------------------
|
|
378
|
+
const cert = await this.db.get('httpsCert');
|
|
379
|
+
const key = await this.db.get('httpsKey');
|
|
380
|
+
|
|
381
|
+
if (key && cert) {
|
|
382
|
+
this.httpsInfos = { selfSigned: true };
|
|
383
|
+
this.httpServer = https.createServer({ cert, key }, this.router);
|
|
384
|
+
} else {
|
|
385
|
+
this.httpServer = await new Promise((resolve, reject) => {
|
|
386
|
+
// generate certificate on the fly (for development purposes)
|
|
387
|
+
pem.createCertificate({ days: 1, selfSigned: true }, async (err, keys) => {
|
|
388
|
+
if (err) {
|
|
389
|
+
logger.error(err.stack);
|
|
390
|
+
this._dispatchStatus('errored');
|
|
391
|
+
|
|
392
|
+
reject(err);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const cert = keys.certificate;
|
|
397
|
+
const key = keys.serviceKey;
|
|
398
|
+
|
|
399
|
+
this.httpsInfos = { selfSigned: true };
|
|
400
|
+
// we store the generated cert so that we don't have to re-accept
|
|
401
|
+
// the cert each time the server restarts in development
|
|
402
|
+
await this.db.set('httpsCert', cert);
|
|
403
|
+
await this.db.set('httpsKey', key);
|
|
404
|
+
|
|
405
|
+
const httpsServer = https.createServer({ cert, key }, this.router);
|
|
406
|
+
|
|
407
|
+
resolve(httpsServer);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let nodeOnly = true;
|
|
415
|
+
// do not throw if no browser clients are defined, very usefull for
|
|
416
|
+
// cleaning tests in particular
|
|
417
|
+
for (let role in this.config.app.clients) {
|
|
418
|
+
if (this.config.app.clients[role].target === 'browser') {
|
|
419
|
+
nodeOnly = false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!nodeOnly) {
|
|
424
|
+
if (this._applicationTemplateConfig.templateEngine === null
|
|
425
|
+
|| this._applicationTemplateConfig.templatePath === null
|
|
426
|
+
|| this._applicationTemplateConfig.clientConfigFunction === null
|
|
427
|
+
) {
|
|
428
|
+
throw new Error('[soundworks:Server] A browser client has been found in "config.app.clients" but configuration for html templating is missing. You should probably call `server.setDefaultTemplateConfig()` if you use the soundworks-template and/or refer (at your own risks) to the documentation of `setCustomTemplateConfig()`');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ------------------------------------------------------------
|
|
433
|
+
// INIT ROUTING
|
|
434
|
+
// ------------------------------------------------------------
|
|
435
|
+
logger.title(`configured clients and routing`);
|
|
436
|
+
|
|
437
|
+
const routes = [];
|
|
438
|
+
const clientsConfig = [];
|
|
439
|
+
|
|
440
|
+
for (let role in this.config.app.clients) {
|
|
441
|
+
const config = Object.assign({}, this.config.app.clients[role]);
|
|
442
|
+
config.role = role;
|
|
443
|
+
clientsConfig.push(config);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// sort default client last to open the route at the end
|
|
447
|
+
clientsConfig
|
|
448
|
+
.sort(a => a.default === true ? 1 : -1)
|
|
449
|
+
.forEach(config => {
|
|
450
|
+
const path = this._openClientRoute(this.router, config);
|
|
451
|
+
routes.push({ role: config.role, path });
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
logger.clientConfigAndRouting(routes, this.config);
|
|
455
|
+
|
|
456
|
+
// ------------------------------------------------------------
|
|
457
|
+
// START PLUGIN MANAGER
|
|
458
|
+
// ------------------------------------------------------------
|
|
459
|
+
await this.pluginManager.start();
|
|
460
|
+
|
|
461
|
+
await this._dispatchStatus('inited');
|
|
462
|
+
|
|
463
|
+
return Promise.resolve();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Method to be called when `init` step is done in the initialization
|
|
468
|
+
* lifecycle of the soundworks server. If `server.init()` has not been called
|
|
469
|
+
* explicitely, `server.start()` will call it automatically.
|
|
470
|
+
*
|
|
471
|
+
* What it does:
|
|
472
|
+
* - starts all registered contexts (context are automatically registered
|
|
473
|
+
* when instantiated)
|
|
474
|
+
* - start the web socket server
|
|
475
|
+
* - launch the HTTP server on given port
|
|
476
|
+
*
|
|
477
|
+
* After `await server.start()` the server is ready to accept incoming connexions
|
|
478
|
+
*/
|
|
479
|
+
async start() {
|
|
480
|
+
if (this.status === 'idle') {
|
|
481
|
+
await this.init();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (this.status === 'started') {
|
|
485
|
+
throw new Error(`[soundworks:Server] Cannot call "server.start()" twice`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (this.status !== 'inited') {
|
|
489
|
+
throw new Error(`[soundworks:Server] Cannot "server.start()" before "server.init()"`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ------------------------------------------------------------
|
|
493
|
+
// START CONTEXT MANAGER
|
|
494
|
+
// ------------------------------------------------------------
|
|
495
|
+
await this.contextManager.start();
|
|
496
|
+
|
|
497
|
+
// ------------------------------------------------------------
|
|
498
|
+
// START SOCKET SERVER
|
|
499
|
+
// ------------------------------------------------------------
|
|
500
|
+
await this.sockets.start(
|
|
501
|
+
this,
|
|
502
|
+
this.config.env.websockets,
|
|
503
|
+
(role, socket) => this._onSocketConnection(role, socket),
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// ------------------------------------------------------------
|
|
507
|
+
// START HTTP SERVER
|
|
508
|
+
// ------------------------------------------------------------
|
|
509
|
+
return new Promise(resolve => {
|
|
510
|
+
const port = this.config.env.port;
|
|
511
|
+
const useHttps = this.config.env.useHttps || false;
|
|
512
|
+
const protocol = useHttps ? 'https' : 'http';
|
|
513
|
+
const ifaces = os.networkInterfaces();
|
|
514
|
+
|
|
515
|
+
this.httpServer.listen(port, async () => {
|
|
516
|
+
logger.title(`${protocol} server listening on`);
|
|
517
|
+
|
|
518
|
+
Object.keys(ifaces).forEach(dev => {
|
|
519
|
+
ifaces[dev].forEach(details => {
|
|
520
|
+
if (details.family === 'IPv4') {
|
|
521
|
+
logger.ip(protocol, details.address, port);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
if (this.httpsInfos !== null) {
|
|
527
|
+
logger.title(`https certificates infos`);
|
|
528
|
+
|
|
529
|
+
// this.httpsInfos.selfSigned = true;
|
|
530
|
+
if (this.httpsInfos.selfSigned) {
|
|
531
|
+
logger.log(` self-signed: ${this.httpsInfos.selfSigned ? 'true' : 'false'}`);
|
|
532
|
+
logger.log(chalk.yellow` > INVALID CERTIFICATE (self-signed)`);
|
|
533
|
+
|
|
534
|
+
} else {
|
|
535
|
+
logger.log(` valid from: ${this.httpsInfos.validFrom}`);
|
|
536
|
+
logger.log(` valid to: ${this.httpsInfos.validTo}`);
|
|
537
|
+
|
|
538
|
+
// this.httpsInfos.isValid = false; // for testing
|
|
539
|
+
if (!this.httpsInfos.isValid) {
|
|
540
|
+
logger.error(chalk.red` -------------------------------------------`);
|
|
541
|
+
logger.error(chalk.red` > INVALID CERTIFICATE`);
|
|
542
|
+
logger.error(chalk.red` i.e. you pretend to be safe but you are not`);
|
|
543
|
+
logger.error(chalk.red` -------------------------------------------`);
|
|
544
|
+
} else {
|
|
545
|
+
// this.httpsInfos.daysRemaining = 2; // for testing
|
|
546
|
+
if (this.httpsInfos.daysRemaining < 5) {
|
|
547
|
+
logger.log(chalk.red` > CERTIFICATE IS VALID... BUT ONLY ${this.httpsInfos.daysRemaining} DAYS LEFT, PLEASE CONSIDER UPDATING YOUR CERTS!`);
|
|
548
|
+
} else if (this.httpsInfos.daysRemaining < 15) {
|
|
549
|
+
logger.log(chalk.yellow` > CERTIFICATE IS VALID - only ${this.httpsInfos.daysRemaining} days left, be careful...`);
|
|
550
|
+
} else {
|
|
551
|
+
logger.log(chalk.green` > CERTIFICATE IS VALID (${this.httpsInfos.daysRemaining} days left)`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
await this._dispatchStatus('started');
|
|
559
|
+
|
|
560
|
+
resolve();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// @todo - handle gracefull close of the server (but define what it means first...)
|
|
566
|
+
/**
|
|
567
|
+
* Stop the server, close all existing WebSocket connections.
|
|
568
|
+
* Mainly usefull for test.
|
|
569
|
+
*/
|
|
570
|
+
async stop() {
|
|
571
|
+
if (this.status !== 'started') {
|
|
572
|
+
throw new Error(`[soundworks:Server] Cannot stop() before start()`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
await this.contextManager.stop();
|
|
576
|
+
await this.pluginManager.stop();
|
|
577
|
+
|
|
578
|
+
this.sockets.terminate();
|
|
579
|
+
this.httpServer.close(err => {
|
|
580
|
+
if (err) {
|
|
581
|
+
throw new Error(err.message);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
await this._dispatchStatus('stopped');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Open the route for a given client.
|
|
590
|
+
* @private
|
|
591
|
+
*/
|
|
592
|
+
_openClientRoute(router, config) {
|
|
593
|
+
const { role, target } = config;
|
|
594
|
+
const isDefault = (config.default === true);
|
|
595
|
+
// only browser targets need a route
|
|
596
|
+
if (target === 'node') {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
let route = '/';
|
|
601
|
+
|
|
602
|
+
if (!isDefault) {
|
|
603
|
+
route += `${role}`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// define template filename: `${role}.html` or `default.html`
|
|
607
|
+
const {
|
|
608
|
+
templatePath,
|
|
609
|
+
templateEngine,
|
|
610
|
+
clientConfigFunction,
|
|
611
|
+
} = this._applicationTemplateConfig;
|
|
612
|
+
|
|
613
|
+
const clientTmpl = path.join(templatePath, `${role}.tmpl`);
|
|
614
|
+
const defaultTmpl = path.join(templatePath, `default.tmpl`);
|
|
615
|
+
|
|
616
|
+
// make it sync
|
|
617
|
+
let template;
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
const stats = fs.statSync(clientTmpl);
|
|
621
|
+
template = stats.isFile() ? clientTmpl : defaultTmpl;
|
|
622
|
+
} catch(err) {
|
|
623
|
+
template = defaultTmpl;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let tmplString;
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
tmplString = fs.readFileSync(template, 'utf8');
|
|
630
|
+
} catch(err) {
|
|
631
|
+
throw new Error(`[soundworks:Server] html template file "${template}" not found`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const tmpl = templateEngine.compile(tmplString);
|
|
635
|
+
// http request
|
|
636
|
+
router.get(route, (req, res) => {
|
|
637
|
+
const data = clientConfigFunction(role, this.config, req);
|
|
638
|
+
|
|
639
|
+
// CORS / COOP / COEP headers for `crossOriginIsolated pages,
|
|
640
|
+
// enables `sharedArrayBuffers` and high precision timers
|
|
641
|
+
// cf. https://web.dev/why-coop-coep/
|
|
642
|
+
if (this.config.env.crossOriginIsolated) {
|
|
643
|
+
res.writeHead(200, {
|
|
644
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
645
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
646
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const appIndex = tmpl(data);
|
|
651
|
+
res.end(appIndex);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// return route infos for logging on server start
|
|
655
|
+
return route;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Socket connection callback.
|
|
660
|
+
* @private
|
|
661
|
+
*/
|
|
662
|
+
_onSocketConnection(role, socket) {
|
|
663
|
+
const client = new Client(role, socket);
|
|
664
|
+
const roles = Object.keys(this.config.app.clients);
|
|
665
|
+
|
|
666
|
+
socket.addListener('close', async () => {
|
|
667
|
+
// do nothin if client type was invalid
|
|
668
|
+
if (roles.includes(role)) {
|
|
669
|
+
// decrement audit state counter
|
|
670
|
+
const numClients = this._auditState.get('numClients');
|
|
671
|
+
numClients[role] -= 1;
|
|
672
|
+
this._auditState.set({ numClients });
|
|
673
|
+
|
|
674
|
+
// clean context manager, await before cleaning state manager
|
|
675
|
+
await this.contextManager.removeClient(client);
|
|
676
|
+
// remove client from pluginManager
|
|
677
|
+
await this.pluginManager.removeClient(client);
|
|
678
|
+
// clean state manager
|
|
679
|
+
await this.stateManager.removeClient(client.id);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// clean sockets
|
|
683
|
+
socket.terminate();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
socket.addListener(CLIENT_HANDSHAKE_REQUEST, async payload => {
|
|
687
|
+
const { role, registeredPlugins } = payload;
|
|
688
|
+
|
|
689
|
+
if (!roles.includes(role)) {
|
|
690
|
+
console.error(`[soundworks.Server] A client with invalid type ("${role}") attempted to connect`);
|
|
691
|
+
|
|
692
|
+
socket.send(CLIENT_HANDSHAKE_ERROR, {
|
|
693
|
+
type: 'invalid-client-type',
|
|
694
|
+
message: `Invalid client type, please check server configuration (valid client types are: ${roles.join(', ')})`,
|
|
695
|
+
});
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
this.pluginManager.checkRegisteredPlugins(registeredPlugins);
|
|
701
|
+
} catch(err) {
|
|
702
|
+
socket.send(CLIENT_HANDSHAKE_ERROR, {
|
|
703
|
+
type: 'invalid-plugin-list',
|
|
704
|
+
message: err.message,
|
|
705
|
+
});
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// increment audit state
|
|
710
|
+
const numClients = this._auditState.get('numClients');
|
|
711
|
+
numClients[role] += 1;
|
|
712
|
+
this._auditState.set({ numClients });
|
|
713
|
+
|
|
714
|
+
// add client to state manager
|
|
715
|
+
await this.stateManager.addClient(client.id, {
|
|
716
|
+
emit: client.socket.send.bind(client.socket),
|
|
717
|
+
addListener: client.socket.addListener.bind(client.socket),
|
|
718
|
+
removeAllListeners: client.socket.removeAllListeners.bind(client.socket),
|
|
719
|
+
});
|
|
720
|
+
// add client to plugin manager
|
|
721
|
+
// server-side, all plugins are active for the lifetime of the client
|
|
722
|
+
await this.pluginManager.addClient(client, registeredPlugins);
|
|
723
|
+
// add client to context manager
|
|
724
|
+
await this.contextManager.addClient(client);
|
|
725
|
+
|
|
726
|
+
const { id, uuid } = client;
|
|
727
|
+
socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid });
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Create namespaced databases for core and plugins
|
|
733
|
+
* (kind of experimental API do not expose in doc for now)
|
|
734
|
+
*
|
|
735
|
+
* @note - introduced in v3.1.0-beta.1
|
|
736
|
+
* @note - used by core and plugin-audio-streams
|
|
737
|
+
* @private
|
|
738
|
+
*/
|
|
739
|
+
createNamespacedDb(namespace = null) {
|
|
740
|
+
if (namespace === null || !(typeof namespace === 'string')) {
|
|
741
|
+
throw new Error(`[soundworks:Server] Invalid namespace for ".createNamespacedDb(namespace)", namespace is mandatory and should be a string`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (_dbNamespaces.has(namespace)) {
|
|
745
|
+
throw new Error(`[soundworks:Server] Invalid namespace for ".createNamespacedDb(namespace)", namespace "${namespace}" already exists`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// KeyvFile uses fs-extra.outputFile internally so we don't need to create
|
|
749
|
+
// the directory, it will be lazily created if something is written in the db
|
|
750
|
+
// @see https://github.com/zaaack/keyv-file/blob/52502077c78226b3d69a615c80b88e53be096979/index.ts#L157
|
|
751
|
+
const filename = path.join(process.cwd(), '.data', `soundworks-${namespace}.db`);
|
|
752
|
+
// @note - keyv-file doesn't seems to works
|
|
753
|
+
const store = new KeyvFile({ filename });
|
|
754
|
+
const db = new Keyv({ namespace, store });
|
|
755
|
+
db.on('error', err => logger.error(`[soundworks:Server] db ${namespace} error: ${err}`));
|
|
756
|
+
|
|
757
|
+
return db;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
onStatusChange(callback) {
|
|
761
|
+
this._onStatusChangeCallbacks.add(callback);
|
|
762
|
+
|
|
763
|
+
return () => this._onStatusChangeCallbacks.delete(callback);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** @private */
|
|
767
|
+
async _dispatchStatus(status) {
|
|
768
|
+
this.status = status;
|
|
769
|
+
|
|
770
|
+
// if launched in a child process, forward status to parent process
|
|
771
|
+
if (process.send !== undefined) {
|
|
772
|
+
process.send(`soundworks:server:${status}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// execute all callbacks in parallel
|
|
776
|
+
const promises = [];
|
|
777
|
+
|
|
778
|
+
for (let callback of this._onStatusChangeCallbacks) {
|
|
779
|
+
promises.push(callback(status));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
await Promise.all(promises);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Configure the server to work out-of-the box with the soundworks-template
|
|
787
|
+
* directory tree structure.
|
|
788
|
+
*
|
|
789
|
+
* - uses [template-literal](https://www.npmjs.com/package/template-literal) package
|
|
790
|
+
* as html templateEngine
|
|
791
|
+
* - define `.build/server/tmpl` as the directory in which html template can be
|
|
792
|
+
* found
|
|
793
|
+
* - define the `clientConfigFunction` function that return client compliant
|
|
794
|
+
* config object to be injected in the html template.
|
|
795
|
+
*
|
|
796
|
+
* Also expose two public directory:
|
|
797
|
+
* - the `public` directory which is exposed behind the root path
|
|
798
|
+
* - the `./.build/public` directory which is exposed behind the `build` path
|
|
799
|
+
*/
|
|
800
|
+
setDefaultTemplateConfig() {
|
|
801
|
+
this._applicationTemplateConfig = {
|
|
802
|
+
templateEngine: { compile },
|
|
803
|
+
templatePath: path.join('.build', 'server', 'tmpl'),
|
|
804
|
+
clientConfigFunction: (role, config, _httpRequest) => {
|
|
805
|
+
return {
|
|
806
|
+
role: role,
|
|
807
|
+
app: {
|
|
808
|
+
name: config.app.name,
|
|
809
|
+
author: config.app.author,
|
|
810
|
+
},
|
|
811
|
+
env: {
|
|
812
|
+
type: config.env.type,
|
|
813
|
+
websockets: config.env.websockets,
|
|
814
|
+
subpath: config.env.subpath,
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
this.router.use(express.static('public'));
|
|
821
|
+
this.router.use('/build', express.static(path.join('.build', 'public')));
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Define your own template path, template engine, and clientConfig function.
|
|
826
|
+
* This method is for very advanced use-cases and only be used if you know what
|
|
827
|
+
* you are doing. As such its behavior could probably be improved a lot...
|
|
828
|
+
*
|
|
829
|
+
* If you end up using this, please contact me to explain your use-case :)
|
|
830
|
+
*/
|
|
831
|
+
setCustomTemplateConfig(options) {
|
|
832
|
+
Object.assign(this._applicationTemplateConfig, options);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Get the global audit state of the application.
|
|
837
|
+
*
|
|
838
|
+
* @throws Will throw if called before `server.init()`
|
|
839
|
+
*/
|
|
840
|
+
async getAuditState() {
|
|
841
|
+
if (this.status === 'idle') {
|
|
842
|
+
throw new Error(`[soundworks.Server] Cannot access audit state before init`);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return this._auditState;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export default Server;
|