@iobroker/socket-classes 1.6.1 → 2.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.
@@ -1,480 +0,0 @@
1
- /**
2
- * Class Socket
3
- *
4
- * Copyright 2014-2024 bluefox <dogafox@gmail.com>,
5
- * MIT License
6
- *
7
- */
8
- const SocketCommands = require('./socketCommands');
9
-
10
- class SocketCommon {
11
- static COMMAND_RE_AUTHENTICATE = 'reauthenticate';
12
-
13
- constructor(settings, adapter) {
14
- this.settings = settings || {};
15
- this.adapter = adapter;
16
- this.commands = null;
17
- this.noDisconnect = this.__getIsNoDisconnect();
18
- this.infoTimeout = null;
19
- this.eventHandlers = {};
20
- this.store = null; // will be set in __initAuthentication
21
- this.adapter._language = this.settings.language;
22
- }
23
-
24
- __getIsNoDisconnect() {
25
- throw new Error('"__getIsNoDisconnect" must be implemented in SocketCommon!');
26
- }
27
-
28
- __initAuthentication(_authOptions) {
29
- throw new Error('"__initAuthentication" must be implemented in SocketCommon!');
30
- }
31
-
32
- // Extract username from socket
33
- __getUserFromSocket(_socket, _callback) {
34
- throw new Error('"__getUserFromSocket" must be implemented in SocketCommon!');
35
- }
36
-
37
- __getClientAddress(_socket) {
38
- throw new Error('"__getClientAddress" must be implemented in SocketCommon!');
39
- }
40
-
41
- // update session ID, but not ofter than 60 seconds
42
- __updateSession(_socket) {
43
- throw new Error('"__updateSession" must be implemented in SocketCommon!');
44
- }
45
-
46
- __getSessionID(_socket) {
47
- throw new Error('"__getSessionID" must be implemented in SocketCommon!');
48
- }
49
-
50
- addEventHandler(eventName, handler) {
51
- this.eventHandlers[eventName] = handler;
52
- }
53
-
54
- start(server, socketClass, authOptions, socketOptions) {
55
- this.serverMode = !!socketClass;
56
-
57
- this.commands = this.commands || new SocketCommands(this.adapter, socket => this.__updateSession(socket));
58
-
59
- this.server = server;
60
-
61
- this.settings.defaultUser = this.settings.defaultUser || 'system.user.admin';
62
- if (!this.settings.defaultUser.match(/^system\.user\./)) {
63
- this.settings.defaultUser = 'system.user.' + this.settings.defaultUser;
64
- }
65
-
66
- this.settings.ttl = parseInt(this.settings.ttl, 10) || 3600;
67
-
68
- // it can be used as a client too for cloud
69
- if (socketClass) {
70
- if (!server.__inited) {
71
- if (typeof socketClass.listen === 'function') {
72
- // old socket.io@2.x and ws
73
- this.server = socketClass.listen(server, socketOptions);
74
- } else if (socketClass.constructor) {
75
- // iobroker socket
76
- this.server = new socketClass(server, socketOptions);
77
- } else {
78
- // socket.io 4.x
79
- this.server = socketClass(server, socketOptions);
80
- }
81
-
82
- if (typeof this.server.of === 'function') {
83
- this.allNamespaces = this.server.of(/.*/);
84
- }
85
-
86
- server.__inited = true;
87
- this.adapter.log.info(
88
- `${this.settings.secure ? 'Secure ' : ''}socket.io server listening on port ${this.settings.port}`
89
- );
90
- }
91
-
92
- if (this.settings.auth && this.server) {
93
- this.__initAuthentication(authOptions);
94
- }
95
-
96
- // Enable cross-domain access
97
- // deprecated, because no more used in socket.io@4 only(in @2)
98
- if (this.settings.crossDomain && this.server.set) {
99
- this.server.set('origins', '*:*');
100
- }
101
-
102
- this.server.on('connection', (socket, cb) => {
103
- this.eventHandlers.connect && this.eventHandlers.connect(socket);
104
- this._initSocket(socket, cb);
105
- });
106
- // support of dynamic namespaces (because of reverse proxy)
107
- this.allNamespaces &&
108
- this.allNamespaces.on('connection', (socket, cb) => {
109
- this.eventHandlers.connect && this.eventHandlers.connect(socket);
110
- this._initSocket(socket, cb);
111
- });
112
- }
113
-
114
- this.server.on('error', (error, details) => {
115
- // ignore "failed connection" as it already shown
116
- if (!error || !error.message || !error.message.includes('failed connection')) {
117
- if (
118
- (error && error.message && error.message.includes('authentication failed')) ||
119
- (details && details.toString().includes('authentication failed'))
120
- ) {
121
- this.adapter.log.debug(
122
- `Error: ${(error && error.message) || JSON.stringify(error)}${details ? ` - ${details}` : ''}`
123
- );
124
- } else {
125
- this.adapter.log.error(
126
- `Error: ${(error && error.message) || JSON.stringify(error)}${details ? ` - ${details}` : ''}`
127
- );
128
- }
129
- }
130
- });
131
-
132
- // support of dynamic namespaces (because of reverse proxy)
133
- this.allNamespaces &&
134
- this.allNamespaces.on('error', (error, details) => {
135
- // ignore "failed connection" as it already shown
136
- if (!error || !error.message || !error.message.includes('failed connection')) {
137
- if (error && error.message && error.message.includes('authentication failed')) {
138
- this.adapter.log.debug(
139
- `Error: ${(error && error.message) || JSON.stringify(error)}${
140
- details ? ` - ${details}` : ''
141
- }`
142
- );
143
- } else {
144
- this.adapter.log.error(
145
- `Error: ${(error && error.message) || JSON.stringify(error)}${
146
- details ? ` - ${details}` : ''
147
- }`
148
- );
149
- }
150
- }
151
- });
152
-
153
- this._updateConnectedInfo();
154
- }
155
-
156
- _initSocket(socket, cb) {
157
- this.commands.disableEventThreshold && this.commands.disableEventThreshold();
158
- const address = this.__getClientAddress(socket);
159
-
160
- if (!socket._acl) {
161
- if (this.settings.auth) {
162
- this.__getUserFromSocket(socket, (err, user) => {
163
- if (err || !user) {
164
- socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE);
165
- this.adapter.log.error(`socket.io [init] ${err || 'No user found in cookies'}`);
166
- // ws does not require disconnect
167
- if (!this.noDisconnect) {
168
- socket.disconnect();
169
- }
170
- } else {
171
- socket._secure = true;
172
- this.adapter.log.debug(`socket.io client ${user} connected`);
173
- if (!user.startsWith('system.user.')) {
174
- user = `system.user.${user}`;
175
- }
176
- this.adapter.calculatePermissions(user, SocketCommands.COMMANDS_PERMISSIONS, acl => {
177
- socket._acl = SocketCommon._mergeACLs(address, acl, this.settings.whiteListSettings);
178
- this._socketEvents(socket, address, cb);
179
- });
180
- }
181
- });
182
- } else {
183
- this.adapter.calculatePermissions(
184
- this.settings.defaultUser,
185
- SocketCommands.COMMANDS_PERMISSIONS,
186
- acl => {
187
- socket._acl = SocketCommon._mergeACLs(address, acl, this.settings.whiteListSettings);
188
- this._socketEvents(socket, address, cb);
189
- }
190
- );
191
- }
192
- } else {
193
- this._socketEvents(socket, address, cb);
194
- }
195
- }
196
-
197
- unsubscribeSocket(socket, type) {
198
- return this.commands.unsubscribeSocket(socket, type);
199
- }
200
-
201
- _unsubscribeAll() {
202
- if (this.server && this.server.ioBroker) {
203
- // this could be an object or array
204
- const sockets = this.getSocketsList();
205
- if (sockets) {
206
- Object.keys(sockets).forEach(i => this.commands.unsubscribeSocket(sockets[i]));
207
- }
208
- } else if (this.server && this.server.sockets) {
209
- for (const socket in this.server.sockets) {
210
- if (Object.prototype.hasOwnProperty.call(this.server.sockets, socket)) {
211
- this.commands.unsubscribeSocket(socket);
212
- }
213
- }
214
- }
215
- }
216
-
217
- static getWhiteListIpForAddress(address, whiteList) {
218
- if (!whiteList) {
219
- return null;
220
- }
221
-
222
- // check IPv6 or IPv4 direct match
223
- if (Object.prototype.hasOwnProperty.call(whiteList, address)) {
224
- return address;
225
- }
226
-
227
- // check if the address is IPv4
228
- const addressParts = address.split('.');
229
- if (addressParts.length !== 4) {
230
- return null;
231
- }
232
-
233
- // do we have settings for wild-carded ips?
234
- const wildCardIps = Object.keys(whiteList).filter(key => key.includes('*'));
235
-
236
- if (!wildCardIps.length) {
237
- // no wild-carded ips => no ip configured
238
- return null;
239
- }
240
-
241
- wildCardIps.forEach(ip => {
242
- const ipParts = ip.split('.');
243
- if (ipParts.length === 4) {
244
- for (let i = 0; i < 4; i++) {
245
- if (ipParts[i] === '*' && i === 3) {
246
- // match
247
- return ip;
248
- }
249
-
250
- if (ipParts[i] !== addressParts[i]) {
251
- break;
252
- }
253
- }
254
- }
255
- });
256
-
257
- return null;
258
- }
259
-
260
- static _getPermissionsForIp(address, whiteList) {
261
- return whiteList[SocketCommon.getWhiteListIpForAddress(address, whiteList) || 'default'];
262
- }
263
-
264
- static _mergeACLs(address, acl, whiteList) {
265
- if (whiteList && address) {
266
- const whiteListAcl = SocketCommon._getPermissionsForIp(address, whiteList);
267
- if (whiteListAcl) {
268
- ['object', 'state', 'file'].forEach(key => {
269
- if (
270
- Object.prototype.hasOwnProperty.call(acl, key) &&
271
- Object.prototype.hasOwnProperty.call(whiteListAcl, key)
272
- ) {
273
- Object.keys(acl[key]).forEach(permission => {
274
- if (Object.prototype.hasOwnProperty.call(whiteListAcl[key], permission)) {
275
- acl[key][permission] = acl[key][permission] && whiteListAcl[key][permission];
276
- }
277
- });
278
- }
279
- });
280
-
281
- if (whiteListAcl.user !== 'auth') {
282
- acl.user = `system.user.${whiteListAcl.user}`;
283
- }
284
- }
285
- }
286
-
287
- return acl;
288
- }
289
-
290
- // install event handlers on socket
291
- _socketEvents(socket, address, cb) {
292
- if (this.serverMode) {
293
- this.adapter.log.info(`==> Connected ${socket._acl.user} from ${address}`);
294
- } else {
295
- this.adapter.log.info(`Trying to connect as ${socket._acl.user} to ${address}`);
296
- }
297
-
298
- this._updateConnectedInfo();
299
-
300
- if (!this.commands.getCommandHandler('name')) {
301
- // socket sends its name => update list of sockets
302
- this.addCommandHandler('name', (_socket, name, cb) => {
303
- this.adapter.log.debug(`Connection from "${name}"`);
304
- if (_socket._name === undefined) {
305
- _socket._name = name;
306
- this._updateConnectedInfo();
307
- } else if (_socket._name !== name) {
308
- this.adapter.log.warn(`socket ${_socket.id} changed socket name from ${_socket._name} to ${name}`);
309
- _socket._name = name;
310
- this._updateConnectedInfo();
311
- }
312
-
313
- typeof cb === 'function' && cb();
314
- });
315
- }
316
-
317
- this.commands.applyCommands(socket);
318
-
319
- // disconnect
320
- socket.on('disconnect', error => {
321
- this.commands.unsubscribeSocket(socket);
322
- this._updateConnectedInfo();
323
-
324
- // Disable logging if no one browser is connected
325
- if (this.adapter.requireLog && this.commands && this.commands.isLogEnabled()) {
326
- this.adapter.log.debug('Disable logging, because no one socket connected');
327
- this.adapter.requireLog(!!(this.server && this.server.engine && this.server.engine.clientsCount));
328
- }
329
-
330
- if (socket._sessionTimer) {
331
- clearTimeout(socket._sessionTimer);
332
- socket._sessionTimer = null;
333
- }
334
-
335
- if (this.eventHandlers.disconnect) {
336
- this.eventHandlers.disconnect(socket, error);
337
- } else {
338
- this.adapter.log.info(
339
- `<== Disconnect ${socket._acl.user} from ${this.__getClientAddress(socket)} ${socket._name || ''}`
340
- );
341
- }
342
- });
343
-
344
- if (typeof this.settings.extensions === 'function') {
345
- this.settings.extensions(socket);
346
- }
347
-
348
- // if server mode
349
- if (this.serverMode) {
350
- const sessionId = this.__getSessionID(socket);
351
- if (sessionId) {
352
- socket._secure = true;
353
- socket._sessionID = sessionId;
354
- // Get user for session
355
- this.store &&
356
- this.store.get(socket._sessionID, (err, obj) => {
357
- if (!obj || !obj.passport) {
358
- socket._acl.user = '';
359
- socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE);
360
- // ws does not require disconnect
361
- if (!this.noDisconnect) {
362
- socket.disconnect();
363
- }
364
- }
365
- if (socket._authPending) {
366
- socket._authPending(!!socket._acl.user, true);
367
- delete socket._authPending;
368
- }
369
- });
370
- }
371
- }
372
-
373
- this.commands.subscribeSocket(socket);
374
-
375
- cb && cb();
376
- }
377
-
378
- _updateConnectedInfo() {
379
- // only in server mode
380
- if (this.serverMode) {
381
- if (this.infoTimeout) {
382
- clearTimeout(this.infoTimeout);
383
- this.infoTimeout = null;
384
- }
385
- this.infoTimeout = setTimeout(() => {
386
- this.infoTimeout = null;
387
-
388
- if (this.server) {
389
- const clientsArray = [];
390
- const sockets = this.getSocketsList();
391
- if (sockets) {
392
- // this could be an object or array
393
- Object.keys(sockets).forEach(i => clientsArray.push(sockets[i]._name || 'noname'));
394
- }
395
- const text = `[${clientsArray.length}]${clientsArray.join(', ')}`;
396
- this.adapter.setState('info.connected', text, true);
397
- }
398
- }, 1000);
399
- }
400
- }
401
-
402
- checkPermissions(socket, command, callback, arg) {
403
- return this.commands._checkPermissions(socket, command, callback, arg);
404
- }
405
-
406
- addCommandHandler(command, handler) {
407
- this.commands.addCommandHandler(command, handler);
408
- }
409
-
410
- sendLog(obj) {
411
- // TODO Build in some threshold
412
- if (this.server && this.server.sockets) {
413
- this.server.sockets.emit('log', obj);
414
- }
415
- }
416
-
417
- publish(socket, type, id, obj) {
418
- return this.commands.publish(socket, type, id, obj);
419
- }
420
-
421
- publishInstanceMessage(socket, sourceInstance, messageType, data) {
422
- return this.commands.publishInstanceMessage(socket, sourceInstance, messageType, data);
423
- }
424
-
425
- publishFile(socket, id, fileName, size) {
426
- return this.commands.publishFile(socket, id, fileName, size);
427
- }
428
-
429
- getSocketsList() {
430
- if (this.server && this.server.sockets) {
431
- // this could be an object or array
432
- return this.server.sockets.sockets || this.server.sockets.connected;
433
- }
434
- return null;
435
- }
436
-
437
- publishInstanceMessageAll(sourceInstance, messageType, sid, data) {
438
- const sockets = this.getSocketsList();
439
- if (sockets) {
440
- Object.keys(sockets).forEach(i => {
441
- if (sockets[i].id === sid) {
442
- this.publishInstanceMessage(sockets[i], sourceInstance, messageType, data);
443
- }
444
- });
445
- }
446
- }
447
-
448
- close() {
449
- this._unsubscribeAll();
450
-
451
- this.commands.destroy();
452
-
453
- const sockets = this.getSocketsList();
454
- if (sockets) {
455
- // this could be an object or array
456
- Object.keys(sockets).forEach(i => {
457
- const socket = sockets[i];
458
- if (socket._sessionTimer) {
459
- clearTimeout(socket._sessionTimer);
460
- socket._sessionTimer = null;
461
- }
462
- });
463
- }
464
-
465
- // IO server will be closed
466
- try {
467
- this.server && this.server.close && this.server.close();
468
- this.server = null;
469
- } catch {
470
- // ignore
471
- }
472
-
473
- if (this.infoTimeout) {
474
- clearTimeout(this.infoTimeout);
475
- this.infoTimeout = null;
476
- }
477
- }
478
- }
479
-
480
- module.exports = SocketCommon;