@soundworks/core 4.0.0-alpha.9 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/README.md +14 -29
- package/package.json +24 -42
- package/src/client/Client.js +306 -256
- package/src/client/{Context.js → ClientContext.js} +63 -54
- package/src/client/ClientContextManager.js +115 -0
- package/src/client/{Plugin.js → ClientPlugin.js} +23 -19
- package/src/client/{PluginManager.js → ClientPluginManager.js} +43 -40
- package/src/client/ClientSocket.js +270 -0
- package/src/client/{StateManager.js → ClientStateManager.js} +13 -13
- package/src/client/index.js +36 -39
- package/src/common/BasePlugin.js +63 -58
- package/src/common/BasePluginManager.js +191 -142
- package/src/common/BaseStateManager.js +635 -145
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +90 -93
- package/src/common/PromiseStore.js +54 -0
- package/src/common/SharedState.js +821 -0
- package/src/common/SharedStateCollection.js +492 -0
- package/src/common/constants.js +14 -2
- package/src/common/logger.js +64 -68
- package/src/common/shared-state-types.js +82 -0
- package/src/common/sockets-utils.js +0 -64
- package/src/common/version.js +2 -0
- package/src/server/Server.js +552 -824
- package/src/server/ServerClient.js +74 -0
- package/src/server/{Context.js → ServerContext.js} +96 -77
- package/src/server/{ContextManager.js → ServerContextManager.js} +107 -87
- package/src/server/{Plugin.js → ServerPlugin.js} +30 -32
- package/src/server/ServerPluginManager.js +177 -0
- package/src/server/ServerSocket.js +256 -0
- package/src/server/ServerSockets.js +150 -0
- package/src/server/ServerStateManager.js +706 -0
- package/src/server/SharedStatePrivate.js +277 -0
- package/src/server/audit-network-latency.worker.js +19 -4
- package/src/server/{audit-schema.js → audit-state-class-description.js} +3 -2
- package/src/server/create-http-server.js +127 -0
- package/src/server/crypto.js +8 -7
- package/src/server/index.js +60 -17
- package/types/client/Client.d.ts +95 -70
- package/types/client/{Context.d.ts → ClientContext.d.ts} +59 -34
- package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
- package/types/client/ClientPlugin.d.ts +51 -0
- package/types/client/ClientPluginManager.d.ts +112 -0
- package/types/client/ClientSocket.d.ts +76 -0
- package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +18 -21
- package/types/client/index.d.ts +33 -6
- package/types/common/BasePlugin.d.ts +64 -53
- package/types/common/BasePluginManager.d.ts +84 -39
- package/types/common/BaseStateManager.d.ts +227 -49
- package/types/common/BatchedTransport.d.ts +17 -0
- package/types/common/ParameterBag.d.ts +73 -76
- package/types/common/PromiseStore.d.ts +11 -0
- package/types/common/SharedState.d.ts +291 -0
- package/types/common/SharedStateCollection.d.ts +311 -0
- package/types/common/constants.d.ts +10 -3
- package/types/common/logger.d.ts +8 -6
- package/types/common/shared-state-types.d.ts +95 -0
- package/types/common/sockets-utils.d.ts +0 -5
- package/types/common/version.d.ts +2 -0
- package/types/server/Server.d.ts +215 -136
- package/types/server/ServerClient.d.ts +45 -0
- package/types/server/ServerContext.d.ts +209 -0
- package/types/server/ServerContextManager.d.ts +75 -0
- package/types/server/ServerPlugin.d.ts +57 -0
- package/types/server/ServerPluginManager.d.ts +115 -0
- package/types/server/ServerSocket.d.ts +69 -0
- package/types/server/ServerSockets.d.ts +42 -0
- package/types/server/ServerStateManager.d.ts +284 -0
- package/types/server/SharedStatePrivate.d.ts +23 -0
- package/types/server/audit-network-latency.worker.d.ts +2 -2
- package/types/server/{audit-schema.d.ts → audit-state-class-description.d.ts} +8 -9
- package/types/server/create-http-server.d.ts +5 -0
- package/types/server/crypto.d.ts +4 -0
- package/types/server/index.d.ts +51 -3
- package/.eslintrc +0 -15
- package/.github/workflows/gh-pages.yml +0 -31
- package/.github/workflows/unit-tests.yml +0 -18
- package/src/client/ContextManager.js +0 -111
- package/src/client/SharedState.js +0 -84
- package/src/client/SharedStateCollection.js +0 -35
- package/src/client/Socket.js +0 -402
- package/src/common/BaseSharedState.js +0 -583
- package/src/common/BaseSharedStateCollection.js +0 -179
- package/src/common/SharedStatePrivate.js +0 -192
- package/src/common/promise-store.js +0 -25
- package/src/server/Client.js +0 -55
- package/src/server/PluginManager.js +0 -170
- package/src/server/SharedState.js +0 -85
- package/src/server/SharedStateCollection.js +0 -36
- package/src/server/Socket.js +0 -396
- package/src/server/Sockets.js +0 -224
- package/src/server/StateManager.js +0 -620
- package/tsconfig.json +0 -21
- package/types/client/Client.d.ts.map +0 -1
- package/types/client/Context.d.ts.map +0 -1
- package/types/client/ContextManager.d.ts.map +0 -1
- package/types/client/Plugin.d.ts +0 -53
- package/types/client/Plugin.d.ts.map +0 -1
- package/types/client/PluginManager.d.ts +0 -96
- package/types/client/PluginManager.d.ts.map +0 -1
- package/types/client/SharedState.d.ts +0 -85
- package/types/client/SharedState.d.ts.map +0 -1
- package/types/client/Socket.d.ts +0 -138
- package/types/client/Socket.d.ts.map +0 -1
- package/types/client/StateManager.d.ts.map +0 -1
- package/types/client/index.d.ts.map +0 -1
- package/types/common/BasePlugin.d.ts.map +0 -1
- package/types/common/BasePluginManager.d.ts.map +0 -1
- package/types/common/BaseSharedState.d.ts +0 -173
- package/types/common/BaseSharedState.d.ts.map +0 -1
- package/types/common/BaseStateManager.d.ts.map +0 -1
- package/types/common/ParameterBag.d.ts.map +0 -1
- package/types/common/SharedStatePrivate.d.ts +0 -20
- package/types/common/SharedStatePrivate.d.ts.map +0 -1
- package/types/common/constants.d.ts.map +0 -1
- package/types/common/logger.d.ts.map +0 -1
- package/types/common/promise-store.d.ts +0 -4
- package/types/common/promise-store.d.ts.map +0 -1
- package/types/common/sockets-utils.d.ts.map +0 -1
- package/types/common/utils.d.ts +0 -5
- package/types/common/utils.d.ts.map +0 -1
- package/types/server/Client.d.ts +0 -40
- package/types/server/Client.d.ts.map +0 -1
- package/types/server/Context.d.ts +0 -59
- package/types/server/Context.d.ts.map +0 -1
- package/types/server/ContextManager.d.ts +0 -69
- package/types/server/ContextManager.d.ts.map +0 -1
- package/types/server/Plugin.d.ts +0 -70
- package/types/server/Plugin.d.ts.map +0 -1
- package/types/server/PluginManager.d.ts +0 -91
- package/types/server/PluginManager.d.ts.map +0 -1
- package/types/server/Server.d.ts.map +0 -1
- package/types/server/SharedState.d.ts +0 -86
- package/types/server/SharedState.d.ts.map +0 -1
- package/types/server/Socket.d.ts +0 -128
- package/types/server/Socket.d.ts.map +0 -1
- package/types/server/Sockets.d.ts +0 -73
- package/types/server/Sockets.d.ts.map +0 -1
- package/types/server/StateManager.d.ts +0 -190
- package/types/server/StateManager.d.ts.map +0 -1
- package/types/server/audit-network-latency.worker.d.ts.map +0 -1
- package/types/server/audit-schema.d.ts.map +0 -1
- package/types/server/index.d.ts.map +0 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import ParameterBag from '../common/ParameterBag.js';
|
|
2
|
+
import {
|
|
3
|
+
DELETE_REQUEST,
|
|
4
|
+
DELETE_RESPONSE,
|
|
5
|
+
DELETE_NOTIFICATION,
|
|
6
|
+
DETACH_REQUEST,
|
|
7
|
+
DETACH_RESPONSE,
|
|
8
|
+
UPDATE_REQUEST,
|
|
9
|
+
UPDATE_RESPONSE,
|
|
10
|
+
UPDATE_ABORT,
|
|
11
|
+
UPDATE_NOTIFICATION,
|
|
12
|
+
} from '../common/constants.js';
|
|
13
|
+
import {
|
|
14
|
+
kServerStateManagerDeletePrivateState,
|
|
15
|
+
kServerStateManagerGetUpdateHooks,
|
|
16
|
+
} from '../server/ServerStateManager.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Filter update object according to filter list.
|
|
20
|
+
* Return identity is filter is null
|
|
21
|
+
* @param {object} updates
|
|
22
|
+
* @param {array|null} filter
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
function filterUpdates(updates, filter) {
|
|
26
|
+
if (filter === null) {
|
|
27
|
+
return updates;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return filter.reduce((acc, key) => {
|
|
31
|
+
if (key in updates) {
|
|
32
|
+
acc[key] = updates[key];
|
|
33
|
+
}
|
|
34
|
+
return acc;
|
|
35
|
+
}, {});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const kSharedStatePrivateAttachClient = Symbol('soundworks:shared-state-private-attach-client');
|
|
39
|
+
export const kSharedStatePrivateDetachClient = Symbol('soundworks:shared-state-private-detach-client');
|
|
40
|
+
export const kSharedStatePrivateGetValues = Symbol('soundworks:shared-state-private-get-values');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The "real" state, this instance is kept private by the server.StateManager.
|
|
44
|
+
* It can only be accessed through a SharedState proxy.
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
class SharedStatePrivate {
|
|
49
|
+
#id = null;
|
|
50
|
+
#className = null;
|
|
51
|
+
#manager = null;
|
|
52
|
+
#parameters = null;
|
|
53
|
+
#creatorId = null;
|
|
54
|
+
#creatorInstanceId = null;
|
|
55
|
+
#attachedClients = new Map();
|
|
56
|
+
|
|
57
|
+
constructor(manager, className, classDefinition, id, initValues = {}) {
|
|
58
|
+
this.#manager = manager;
|
|
59
|
+
this.#className = className;
|
|
60
|
+
this.#id = id;
|
|
61
|
+
// This can throw but will be catch in ServerStateManager CREATE_REQUEST handler
|
|
62
|
+
this.#parameters = new ParameterBag(classDefinition, initValues);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get id() {
|
|
66
|
+
return this.#id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get className() {
|
|
70
|
+
return this.#className;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get creatorId() {
|
|
74
|
+
return this.#creatorId;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get creatorInstanceId() {
|
|
78
|
+
return this.#creatorInstanceId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get attachedClients() {
|
|
82
|
+
return this.#attachedClients;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get parameters() {
|
|
86
|
+
return this.#parameters;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[kSharedStatePrivateGetValues]() {
|
|
90
|
+
return this.#parameters.getValues();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
[kSharedStatePrivateAttachClient](instanceId, client, isOwner, filter) {
|
|
94
|
+
const clientInfos = { client, isOwner, filter };
|
|
95
|
+
this.#attachedClients.set(instanceId, clientInfos);
|
|
96
|
+
|
|
97
|
+
if (isOwner) {
|
|
98
|
+
this.#creatorId = client.id;
|
|
99
|
+
this.#creatorInstanceId = instanceId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// attach client listeners
|
|
103
|
+
client.transport.addListener(`${UPDATE_REQUEST}-${this.id}-${instanceId}`, async (reqId, updates) => {
|
|
104
|
+
// apply registered hooks
|
|
105
|
+
const hooks = this.#manager[kServerStateManagerGetUpdateHooks](this.className);
|
|
106
|
+
const values = this.#parameters.getValues();
|
|
107
|
+
let hookAborted = false;
|
|
108
|
+
|
|
109
|
+
// cf. https://github.com/collective-soundworks/soundworks/issues/45
|
|
110
|
+
for (let hook of hooks.values()) {
|
|
111
|
+
const result = await hook(updates, values);
|
|
112
|
+
|
|
113
|
+
if (result === null) { // explicit abort if hook returns null
|
|
114
|
+
hookAborted = true;
|
|
115
|
+
break;
|
|
116
|
+
} else if (result === undefined) { // implicit continue if hook returns undefined
|
|
117
|
+
continue;
|
|
118
|
+
} else { // the hook returned an updates object
|
|
119
|
+
updates = result;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (hookAborted === false) {
|
|
124
|
+
let acknowledgedUpdates = {};
|
|
125
|
+
let hasUpdates = false;
|
|
126
|
+
|
|
127
|
+
for (let name in updates) {
|
|
128
|
+
const [newValue, changed] = this.#parameters.set(name, updates[name]);
|
|
129
|
+
// if `filterChange` is set to `false` we don't check if the value
|
|
130
|
+
// has been changed or not, it is always propagated to client states
|
|
131
|
+
const { event, filterChange } = this.#parameters.getDescription(name);
|
|
132
|
+
|
|
133
|
+
// if event type reset internal value to null
|
|
134
|
+
if (event) {
|
|
135
|
+
this.#parameters.set(name, null);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if ((filterChange && changed) || !filterChange) {
|
|
139
|
+
acknowledgedUpdates[name] = newValue;
|
|
140
|
+
hasUpdates = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (hasUpdates) {
|
|
145
|
+
// Normal case
|
|
146
|
+
//
|
|
147
|
+
// We need to handle cases where:
|
|
148
|
+
// - client state (client.id: 2) sends a request
|
|
149
|
+
// - server attached state (client.id: -1) spot a problem and overrides the value
|
|
150
|
+
// We want the remote client (id: 2) to receive in the right order:
|
|
151
|
+
// * 1. the value it requested,
|
|
152
|
+
// * 2. the value overridden by the server-side attached state (id: -1)
|
|
153
|
+
//
|
|
154
|
+
// This problem is now better solved with the the updateHook system,
|
|
155
|
+
// nevertheless we don't want to introduce inconsistencies here
|
|
156
|
+
//
|
|
157
|
+
// Then we propagate server-side last, because as the server transport
|
|
158
|
+
// is synchronous it can break ordering if a subscription function makes
|
|
159
|
+
// itself an update in reaction to an update. Propagating to server last
|
|
160
|
+
// allows to maintain network messages order consistent.
|
|
161
|
+
//
|
|
162
|
+
// @note - instanceId correspond to unique remote state id
|
|
163
|
+
|
|
164
|
+
// propagate RESPONSE to the client that originates the request if not the server
|
|
165
|
+
if (client.id !== -1) {
|
|
166
|
+
// no need to filter updates on requested, is blocked on client-side
|
|
167
|
+
client.transport.emit(
|
|
168
|
+
`${UPDATE_RESPONSE}-${this.id}-${instanceId}`,
|
|
169
|
+
reqId,
|
|
170
|
+
acknowledgedUpdates,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// propagate NOTIFICATION to all attached clients except server
|
|
175
|
+
for (let [peerInstanceId, clientInfos] of this.#attachedClients) {
|
|
176
|
+
const peer = clientInfos.client;
|
|
177
|
+
|
|
178
|
+
if (instanceId !== peerInstanceId && peer.id !== -1) {
|
|
179
|
+
const filter = clientInfos.filter;
|
|
180
|
+
const filteredUpdates = filterUpdates(acknowledgedUpdates, filter);
|
|
181
|
+
|
|
182
|
+
// propagate only if there something left after applying the filter
|
|
183
|
+
if (Object.keys(filteredUpdates).length > 0) {
|
|
184
|
+
peer.transport.emit(
|
|
185
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerInstanceId}`,
|
|
186
|
+
filteredUpdates,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// propagate RESPONSE to server if it is the requester
|
|
193
|
+
if (client.id === -1) {
|
|
194
|
+
// no need to filter updates on requested, is blocked on client-side
|
|
195
|
+
client.transport.emit(
|
|
196
|
+
`${UPDATE_RESPONSE}-${this.id}-${instanceId}`,
|
|
197
|
+
reqId,
|
|
198
|
+
acknowledgedUpdates,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// propagate NOTIFICATION to other state attached on the server
|
|
203
|
+
for (let [peerInstanceId, clientInfos] of this.#attachedClients) {
|
|
204
|
+
const peer = clientInfos.client;
|
|
205
|
+
|
|
206
|
+
if (instanceId !== peerInstanceId && peer.id === -1) {
|
|
207
|
+
const filter = clientInfos.filter;
|
|
208
|
+
const filteredUpdates = filterUpdates(acknowledgedUpdates, filter);
|
|
209
|
+
|
|
210
|
+
if (Object.keys(filteredUpdates).length > 0) {
|
|
211
|
+
peer.transport.emit(
|
|
212
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerInstanceId}`,
|
|
213
|
+
filteredUpdates,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
// propagate back to the requester that the update has been aborted
|
|
220
|
+
// ignore all other attached clients.
|
|
221
|
+
client.transport.emit(`${UPDATE_ABORT}-${this.id}-${instanceId}`, reqId, updates);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// retrieve values from inner state (also handle immediate appropriately)
|
|
225
|
+
const oldValues = {};
|
|
226
|
+
|
|
227
|
+
for (let name in updates) {
|
|
228
|
+
oldValues[name] = this.#parameters.get(name);
|
|
229
|
+
}
|
|
230
|
+
// aborted by hook (updates have been overridden to {})
|
|
231
|
+
client.transport.emit(`${UPDATE_ABORT}-${this.id}-${instanceId}`, reqId, oldValues);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (isOwner) {
|
|
236
|
+
// delete only if creator
|
|
237
|
+
client.transport.addListener(`${DELETE_REQUEST}-${this.id}-${instanceId}`, async reqId => {
|
|
238
|
+
// make sure hooks have been called when `delete()` fulfills
|
|
239
|
+
await this.#manager[kServerStateManagerDeletePrivateState](this);
|
|
240
|
+
// --------------------------------------------------------------------
|
|
241
|
+
// WARNING - MAKE SURE WE DON'T HAVE PROBLEM W/ THAT
|
|
242
|
+
// --------------------------------------------------------------------
|
|
243
|
+
// @todo - propagate server-side last, because if a subscription function sends a
|
|
244
|
+
// message to a client, network messages order are kept coherent
|
|
245
|
+
// this._subscriptions.forEach(func => func(updated));
|
|
246
|
+
for (let [instanceId, clientInfos] of this.#attachedClients) {
|
|
247
|
+
const attached = clientInfos.client;
|
|
248
|
+
this[kSharedStatePrivateDetachClient](instanceId, attached);
|
|
249
|
+
|
|
250
|
+
if (instanceId === this.#creatorInstanceId) {
|
|
251
|
+
attached.transport.emit(`${DELETE_RESPONSE}-${this.id}-${instanceId}`, reqId);
|
|
252
|
+
} else {
|
|
253
|
+
attached.transport.emit(`${DELETE_NOTIFICATION}-${this.id}-${instanceId}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
// detach only if not creator
|
|
259
|
+
client.transport.addListener(`${DETACH_REQUEST}-${this.id}-${instanceId}`, (reqId) => {
|
|
260
|
+
this[kSharedStatePrivateDetachClient](instanceId, client);
|
|
261
|
+
client.transport.emit(`${DETACH_RESPONSE}-${this.id}-${instanceId}`, reqId);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
[kSharedStatePrivateDetachClient](instanceId, client) {
|
|
267
|
+
this.#attachedClients.delete(instanceId);
|
|
268
|
+
// delete listeners
|
|
269
|
+
client.transport.removeAllListeners(`${UPDATE_REQUEST}-${this.id}-${instanceId}`);
|
|
270
|
+
client.transport.removeAllListeners(`${DELETE_REQUEST}-${this.id}-${instanceId}`);
|
|
271
|
+
client.transport.removeAllListeners(`${DETACH_REQUEST}-${this.id}-${instanceId}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default SharedStatePrivate;
|
|
276
|
+
|
|
277
|
+
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// @note - We use this common js syntax so that the server can be bundled to cjs
|
|
2
|
+
// which allows to build a server that can be accepted by Max `node.script`
|
|
3
|
+
//
|
|
4
|
+
// Should move back to regular esm module once Max has fixed its loader
|
|
5
|
+
export default `
|
|
6
|
+
const { parentPort } = require('node:worker_threads');
|
|
7
|
+
const { hrtime } = require('node:process');
|
|
4
8
|
|
|
5
9
|
let stack = [];
|
|
6
10
|
let averageLatencyWindow = 5;
|
|
@@ -9,7 +13,17 @@ let averageLatencyPeriod = 2;
|
|
|
9
13
|
let intervalId = null;
|
|
10
14
|
let meanLatency = 0;
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
// workaround that sc-utils is pure ecmascript module
|
|
17
|
+
// 2024/09/06 - Just copy getTime implementation so that we don't even need the node_modules
|
|
18
|
+
const start = hrtime.bigint();
|
|
19
|
+
|
|
20
|
+
function getTime() {
|
|
21
|
+
const now = hrtime.bigint();
|
|
22
|
+
const delta = now - start;
|
|
23
|
+
return Number(delta) * 1e-9;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
parentPort.on('message', async msg => {
|
|
13
27
|
switch (msg.type) {
|
|
14
28
|
case 'config': {
|
|
15
29
|
averageLatencyWindow = msg.value.averageLatencyWindow;
|
|
@@ -55,3 +69,4 @@ function computeAverageLatency() {
|
|
|
55
69
|
});
|
|
56
70
|
}
|
|
57
71
|
}
|
|
72
|
+
`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal
|
|
2
|
+
* Internal shared state class used to audit the application.
|
|
3
|
+
* @private
|
|
3
4
|
*/
|
|
4
5
|
export default {
|
|
5
6
|
/**
|
|
@@ -17,7 +18,7 @@ export default {
|
|
|
17
18
|
},
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
* Average latency in seconds computed from ping/pong.
|
|
21
|
+
* Average latency in seconds computed from ping/pong information.
|
|
21
22
|
*/
|
|
22
23
|
averageNetworkLatency: {
|
|
23
24
|
type: 'float',
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import {
|
|
5
|
+
X509Certificate,
|
|
6
|
+
createPrivateKey,
|
|
7
|
+
} from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
import pem from 'pem';
|
|
10
|
+
import equal from 'fast-deep-equal';
|
|
11
|
+
|
|
12
|
+
import logger from '../common/logger.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @private
|
|
16
|
+
* @return {HttpServer|HttpsServer}
|
|
17
|
+
*/
|
|
18
|
+
export async function createHttpServer(server) {
|
|
19
|
+
const serverConfig = server.config;
|
|
20
|
+
|
|
21
|
+
if (serverConfig.env.useHttps !== true) {
|
|
22
|
+
return http.createServer();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// https is more tricky
|
|
26
|
+
const httpsInfos = serverConfig.env.httpsInfos;
|
|
27
|
+
let useSelfSigned = false;
|
|
28
|
+
|
|
29
|
+
if (!httpsInfos || equal(httpsInfos, { cert: null, key: null })) {
|
|
30
|
+
useSelfSigned = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let httpsCertsInfos = null;
|
|
34
|
+
let httpsServer = null;
|
|
35
|
+
|
|
36
|
+
if (!useSelfSigned) {
|
|
37
|
+
try {
|
|
38
|
+
// existence of file is checked in constructor
|
|
39
|
+
let cert = fs.readFileSync(httpsInfos.cert);
|
|
40
|
+
let key = fs.readFileSync(httpsInfos.key);
|
|
41
|
+
|
|
42
|
+
let x509 = null;
|
|
43
|
+
// this fails with self-signed certificates for whatever reason...
|
|
44
|
+
try {
|
|
45
|
+
x509 = new X509Certificate(cert);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new TypeError(`Cannot create https.Server: Invalid https cert file`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const keyObj = createPrivateKey(key);
|
|
52
|
+
|
|
53
|
+
if (!x509.checkPrivateKey(keyObj)) {
|
|
54
|
+
throw new TypeError(`Cannot create https.Server: Invalid https key file`);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
throw new TypeError(`Cannot create https.Server: Invalid https key file`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// check is certificate is still valid
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const certExpire = Date.parse(x509.validTo);
|
|
63
|
+
const isValid = now < certExpire;
|
|
64
|
+
|
|
65
|
+
const diff = certExpire - now;
|
|
66
|
+
const daysRemaining = Math.round(diff / 1000 / 60 / 60 / 24);
|
|
67
|
+
|
|
68
|
+
httpsCertsInfos = {
|
|
69
|
+
selfSigned: false,
|
|
70
|
+
CN: x509.subject.split('=')[1],
|
|
71
|
+
altNames: x509.subjectAltName.split(',').map(e => e.trim().split(':')[1]),
|
|
72
|
+
validFrom: x509.validFrom,
|
|
73
|
+
validTo: x509.validTo,
|
|
74
|
+
isValid: isValid,
|
|
75
|
+
daysRemaining: daysRemaining,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
httpsServer = https.createServer({ key, cert });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.error(`
|
|
81
|
+
Invalid certificate files, please check your:
|
|
82
|
+
- key file: ${httpsInfos.key}
|
|
83
|
+
- cert file: ${httpsInfos.cert}
|
|
84
|
+
`);
|
|
85
|
+
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
// generate self signed certs or reused already existing ones
|
|
90
|
+
let cert = await server.db.get('httpsCert');
|
|
91
|
+
let key = await server.db.get('httpsKey');
|
|
92
|
+
|
|
93
|
+
if (!cert || !key) {
|
|
94
|
+
try {
|
|
95
|
+
const result = await new Promise((resolve, reject) => {
|
|
96
|
+
pem.createCertificate({ days: 1, selfSigned: true }, async (err, keys) => {
|
|
97
|
+
if (err) {
|
|
98
|
+
reject(err);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
resolve({
|
|
103
|
+
cert: keys.certificate,
|
|
104
|
+
key: keys.serviceKey,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
cert = result.cert;
|
|
110
|
+
key = result.key;
|
|
111
|
+
// store the generated certs to reuse on next start
|
|
112
|
+
await server.db.set('httpsCert', cert);
|
|
113
|
+
await server.db.set('httpsKey', key);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
logger.error(err.stack);
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
httpsCertsInfos = { selfSigned: true };
|
|
121
|
+
httpsServer = https.createServer({ cert, key });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.httpsCertsInfos(httpsCertsInfos);
|
|
125
|
+
|
|
126
|
+
return httpsServer;
|
|
127
|
+
}
|
package/src/server/crypto.js
CHANGED
|
@@ -21,18 +21,19 @@ const encryptionIV = crypto
|
|
|
21
21
|
.substring(0, 16);
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
/** @private */
|
|
24
25
|
export function encryptData(obj) {
|
|
25
26
|
const data = JSON.stringify(obj);
|
|
26
|
-
const cipher = crypto.createCipheriv(encryptionMethod, key, encryptionIV)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
).toString('base64')
|
|
27
|
+
const cipher = crypto.createCipheriv(encryptionMethod, key, encryptionIV);
|
|
28
|
+
|
|
29
|
+
return Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex')).toString('base64');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** @private */
|
|
32
33
|
export function decryptData(encryptedData) {
|
|
33
|
-
const buff = Buffer.from(encryptedData, 'base64')
|
|
34
|
-
const decipher = crypto.createDecipheriv(encryptionMethod, key, encryptionIV)
|
|
34
|
+
const buff = Buffer.from(encryptedData, 'base64');
|
|
35
|
+
const decipher = crypto.createDecipheriv(encryptionMethod, key, encryptionIV);
|
|
35
36
|
|
|
36
|
-
const decrypted = `${decipher.update(buff.toString('utf8'), 'hex', 'utf8')}${decipher.final('utf8')}
|
|
37
|
+
const decrypted = `${decipher.update(buff.toString('utf8'), 'hex', 'utf8')}${decipher.final('utf8')}`;
|
|
37
38
|
return JSON.parse(decrypted);
|
|
38
39
|
}
|
package/src/server/index.js
CHANGED
|
@@ -1,25 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2014-present IRCAM – Centre Pompidou (France, Paris)
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// these types must be defined here, so that they can be imported into the application
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The role of the client in the application.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
* // - API documentation: https://soundworks.dev/api
|
|
11
|
-
* // - Issue Tracker: https://github.com/collective-soundworks/soundworks/issues
|
|
12
|
-
* // - Wizard & Tools: `npx soundworks`
|
|
12
|
+
* For browser client, this information is used to create the URL endpoint.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* @typedef ClientRole
|
|
15
|
+
* @type {string}
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* @typedef ClientDescription
|
|
19
|
+
* @type {object}
|
|
20
|
+
* @property {'browser'|'node'} runtime - Runtime on which the client is aimed at running
|
|
21
|
+
* @property {boolean} [default=false] - For browser client, define is the client should be
|
|
22
|
+
* accessible at the default / root HTTP endpoint
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Application configuration object.
|
|
15
26
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
27
|
+
* @typedef ServerAppConfig
|
|
28
|
+
* @type {object}
|
|
29
|
+
* @property {object<ClientRole, ClientDescription>} clients - Definition of the application clients.
|
|
30
|
+
* @property {string} [name=''] - Name of the application.
|
|
31
|
+
* @property {string} [author=''] - Name of the author.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Environment configuration object.
|
|
18
35
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
36
|
+
* @typedef ServerEnvConfig
|
|
37
|
+
* @type {objecy}
|
|
38
|
+
* @property {'development'|'production'} type - If set to 'production', will enforce
|
|
39
|
+
* some security feature (e.g. blocking access to sensitive APIs, etc.) based on
|
|
40
|
+
* the `auth`.
|
|
41
|
+
* @property {number} port - Port on which the server is listening.
|
|
42
|
+
* @property {string} serverAddress - Domain name or IP of the server.
|
|
43
|
+
* Mandatory when node clients are declared
|
|
44
|
+
* @property {boolean} useHttps - Define is the server run in http or in https.
|
|
45
|
+
* @property {object} [httpsInfos=null] - Path to cert files ( cert, key } for https.
|
|
46
|
+
* If not given and `useHttps` is `true` self certified certificates will be created.
|
|
47
|
+
* @property {string} [httpsInfos.cert=null] - Path to cert file.
|
|
48
|
+
* @property {string} [httpsInfos.key=null] - Path to key file.
|
|
49
|
+
* @property {object} [auth=null] - Configuration object for authenticating clients
|
|
50
|
+
* using simple HTTP authentication.
|
|
51
|
+
* @property {array.<ClientRole>} [auth.client=[]] - List of client role that must authenticate.
|
|
52
|
+
* @property {login} [auth.login=''] - Login
|
|
53
|
+
* @property {login} [auth.password=''] - Password
|
|
54
|
+
* @property {string} [env.baseUrl=''] - If running behind a proxy, base URL of the application.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* Configuration object for the server.
|
|
21
58
|
*
|
|
22
|
-
* @
|
|
59
|
+
* @typedef ServerConfig
|
|
60
|
+
* @type {object}
|
|
61
|
+
* @property {AppConfig} app - Application configuration object.
|
|
62
|
+
* @property {ServerEnvConfig} env - Environment configuration object.
|
|
23
63
|
*/
|
|
64
|
+
|
|
24
65
|
export { default as Server } from './Server.js';
|
|
25
|
-
export { default as
|
|
66
|
+
export { default as ServerPlugin } from './ServerPlugin.js';
|
|
67
|
+
export { default as ServerContext } from './ServerContext.js';
|
|
68
|
+
export { default as version } from '../common/version.js';
|