@iobroker/socket-classes 0.1.2
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/LICENSE +21 -0
- package/README.md +96 -0
- package/index.js +11 -0
- package/lib/passportSocket.js +100 -0
- package/lib/socketAdmin.js +178 -0
- package/lib/socketCommands.js +975 -0
- package/lib/socketCommandsAdmin.js +1150 -0
- package/lib/socketCommon.js +383 -0
- package/package.json +59 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class Socket
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2014-2022 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
|
+
}
|
|
22
|
+
|
|
23
|
+
__getIsNoDisconnect() {
|
|
24
|
+
throw new Error('"__getIsNoDisconnect" must be implemented in SocketCommon!');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
__initAuthentication(authOptions) {
|
|
28
|
+
throw new Error('"__initAuthentication" must be implemented in SocketCommon!');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract username from socket
|
|
32
|
+
__getUserFromSocket(socket, callback) {
|
|
33
|
+
throw new Error('"__getUserFromSocket" must be implemented in SocketCommon!');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
__getClientAddress(socket) {
|
|
37
|
+
throw new Error('"__getClientAddress" must be implemented in SocketCommon!');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// update session ID, but not ofter than 60 seconds
|
|
41
|
+
__updateSession(socket) {
|
|
42
|
+
throw new Error('"__updateSession" must be implemented in SocketCommon!');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
__getSessionID(socket) {
|
|
46
|
+
throw new Error('"__getSessionID" must be implemented in SocketCommon!');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
addEventHandler(eventName, handler) {
|
|
50
|
+
this.eventHandlers[eventName] = handler;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
start(server, socketClass, authOptions, socketOptions) {
|
|
54
|
+
this.serverMode = !!socketClass;
|
|
55
|
+
|
|
56
|
+
this.commands = this.commands || new SocketCommands(this.adapter, socket => this.__updateSession(socket));
|
|
57
|
+
|
|
58
|
+
this.server = server;
|
|
59
|
+
|
|
60
|
+
this.settings.defaultUser = this.settings.defaultUser || 'system.user.admin';
|
|
61
|
+
if (!this.settings.defaultUser.match(/^system\.user\./)) {
|
|
62
|
+
this.settings.defaultUser = 'system.user.' + this.settings.defaultUser;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.settings.ttl = parseInt(this.settings.ttl, 10) || 3600;
|
|
66
|
+
|
|
67
|
+
// it can be used as client too for cloud
|
|
68
|
+
if (socketClass) {
|
|
69
|
+
if (!server.__inited) {
|
|
70
|
+
this.server = socketClass.listen(server, socketOptions);
|
|
71
|
+
server.__inited = true;
|
|
72
|
+
this.adapter.log.info(`${this.settings.secure ? 'Secure ' : ''}socket.io server listening on port ${this.settings.port}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (this.settings.auth && this.server) {
|
|
76
|
+
this.__initAuthentication(authOptions);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Enable cross domain access
|
|
80
|
+
if (this.settings.crossDomain && this.server.set) {
|
|
81
|
+
this.server.set('origins', '*:*');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.server.on('connection', (socket, cb) => {
|
|
85
|
+
this.eventHandlers.connect && this.eventHandlers.connect(socket);
|
|
86
|
+
this._initSocket(socket, cb);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.server.on('error', (error, details) => {
|
|
91
|
+
// ignore "failed connection" as it already shown
|
|
92
|
+
if (!error || !error.message || !error.message.includes('failed connection')) {
|
|
93
|
+
this.adapter.log.error(`Error: ${(error && error.message) || JSON.stringify(error)}${details ? ' - ' + details : ''}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this._updateConnectedInfo();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_initSocket(socket, cb) {
|
|
101
|
+
this.commands.disableEventThreshold && this.commands.disableEventThreshold();
|
|
102
|
+
const address = this.__getClientAddress(socket);
|
|
103
|
+
|
|
104
|
+
if (!socket._acl) {
|
|
105
|
+
if (this.settings.auth) {
|
|
106
|
+
this.__getUserFromSocket(socket, (err, user) => {
|
|
107
|
+
if (err || !user) {
|
|
108
|
+
socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE);
|
|
109
|
+
this.adapter.log.error(`socket.io [init] ${err || 'No user found in cookies'}`);
|
|
110
|
+
// ws does not require disconnect
|
|
111
|
+
if (!this.noDisconnect) {
|
|
112
|
+
socket.disconnect();
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
socket._secure = true;
|
|
116
|
+
this.adapter.log.debug(`socket.io client ${user} connected`);
|
|
117
|
+
if (!user.startsWith('system.user.')) {
|
|
118
|
+
user = `system.user.${user}`;
|
|
119
|
+
}
|
|
120
|
+
this.adapter.calculatePermissions(user, SocketCommands.COMMANDS_PERMISSIONS, acl => {
|
|
121
|
+
socket._acl = SocketCommon._mergeACLs(address, acl, this.settings.whiteListSettings);
|
|
122
|
+
this._socketEvents(socket, address, cb);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
this.adapter.calculatePermissions(this.settings.defaultUser, SocketCommands.COMMANDS_PERMISSIONS, acl => {
|
|
128
|
+
socket._acl = SocketCommon._mergeACLs(address, acl, this.settings.whiteListSettings);
|
|
129
|
+
this._socketEvents(socket, address, cb);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
this._socketEvents(socket, address, cb);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
unsubscribeSocket(socket, type) {
|
|
138
|
+
return this.commands.unsubscribeSocket(socket, type);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_unsubscribeAll() {
|
|
142
|
+
if (this.server && this.server.ioBroker) {
|
|
143
|
+
// this could be an object or array
|
|
144
|
+
Object.keys(this.server.sockets.connected).forEach(i =>
|
|
145
|
+
this.commands.unsubscribeSocket(this.server.sockets.connected[i]));
|
|
146
|
+
} else
|
|
147
|
+
if (this.server && this.server.sockets) {
|
|
148
|
+
for (const socket in this.server.sockets) {
|
|
149
|
+
if (Object.prototype.hasOwnProperty.call(this.server.sockets, socket)) {
|
|
150
|
+
this.commands.unsubscribeSocket(socket);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
static getWhiteListIpForAddress(address, whiteList) {
|
|
157
|
+
if (!whiteList) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// check IPv6 or IPv4 direct match
|
|
162
|
+
if (Object.prototype.hasOwnProperty.call(whiteList, address)) {
|
|
163
|
+
return address;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// check if address is IPv4
|
|
167
|
+
const addressParts = address.split('.');
|
|
168
|
+
if (addressParts.length !== 4) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// do we have settings for wild carded ips?
|
|
173
|
+
const wildCardIps = Object.keys(whiteList).filter(key => key.includes('*'));
|
|
174
|
+
|
|
175
|
+
if (!wildCardIps.length) {
|
|
176
|
+
// no wild carded ips => no ip configured
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
wildCardIps.forEach(ip => {
|
|
181
|
+
const ipParts = ip.split('.');
|
|
182
|
+
if (ipParts.length === 4) {
|
|
183
|
+
for (let i = 0; i < 4; i++) {
|
|
184
|
+
if (ipParts[i] === '*' && i === 3) {
|
|
185
|
+
// match
|
|
186
|
+
return ip;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (ipParts[i] !== addressParts[i]) {
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static _getPermissionsForIp(address, whiteList) {
|
|
200
|
+
return whiteList[SocketCommon.getWhiteListIpForAddress(address, whiteList) || 'default'];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static _mergeACLs(address, acl, whiteList) {
|
|
204
|
+
if (whiteList && address) {
|
|
205
|
+
const whiteListAcl = SocketCommon._getPermissionsForIp(address, whiteList);
|
|
206
|
+
if (whiteListAcl) {
|
|
207
|
+
['object', 'state', 'file'].forEach(key => {
|
|
208
|
+
if (Object.prototype.hasOwnProperty.call(acl, key) && Object.prototype.hasOwnProperty.call(whiteListAcl, key)) {
|
|
209
|
+
Object.keys(acl[key]).forEach(permission => {
|
|
210
|
+
if (Object.prototype.hasOwnProperty.call(whiteListAcl[key], permission)) {
|
|
211
|
+
acl[key][permission] = acl[key][permission] && whiteListAcl[key][permission];
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (whiteListAcl.user !== 'auth') {
|
|
218
|
+
acl.user = 'system.user.' + whiteListAcl.user;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return acl;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// install event handlers on socket
|
|
227
|
+
_socketEvents(socket, address, cb) {
|
|
228
|
+
if (this.serverMode) {
|
|
229
|
+
this.adapter.log.info(`==> Connected ${socket._acl.user} from ${address}`);
|
|
230
|
+
} else {
|
|
231
|
+
this.adapter.log.info(`Trying to connect as ${socket._acl.user} to ${address}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this._updateConnectedInfo();
|
|
235
|
+
|
|
236
|
+
this.commands.applyCommands(socket);
|
|
237
|
+
|
|
238
|
+
// socket sends its name => update list of sockets
|
|
239
|
+
socket.on('name', (name, cb) => {
|
|
240
|
+
this.adapter.log.debug(`Connection from "${name}"`);
|
|
241
|
+
if (socket._name === undefined) {
|
|
242
|
+
socket._name = name;
|
|
243
|
+
this._updateConnectedInfo();
|
|
244
|
+
} else if (socket._name !== name) {
|
|
245
|
+
this.adapter.log.warn(`socket ${socket.id} changed socket name from ${socket._name} to ${name}`);
|
|
246
|
+
socket._name = name;
|
|
247
|
+
this._updateConnectedInfo();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
typeof cb === 'function' && cb();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// disconnect
|
|
254
|
+
socket.on('disconnect', error => {
|
|
255
|
+
this.commands.unsubscribeSocket(socket);
|
|
256
|
+
this._updateConnectedInfo();
|
|
257
|
+
|
|
258
|
+
// Disable logging if no one browser is connected
|
|
259
|
+
if (this.adapter.requireLog) {
|
|
260
|
+
this.adapter.log.debug('Disable logging, because no one socket connected');
|
|
261
|
+
this.adapter.requireLog(!!(this.server && this.server.engine && this.server.engine.clientsCount));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (socket._sessionTimer) {
|
|
265
|
+
clearTimeout(socket._sessionTimer);
|
|
266
|
+
socket._sessionTimer = null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (this.eventHandlers.disconnect) {
|
|
270
|
+
this.eventHandlers.disconnect(socket, error);
|
|
271
|
+
} else {
|
|
272
|
+
this.adapter.log.info(`<== Disconnect ${socket._acl.user} from ${this.__getClientAddress(socket)} ${socket._name || ''}`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (typeof this.settings.extensions === 'function') {
|
|
277
|
+
this.settings.extensions(socket);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// if server mode
|
|
281
|
+
if (this.serverMode) {
|
|
282
|
+
const sessionId = this.__getSessionID(socket);
|
|
283
|
+
if (sessionId) {
|
|
284
|
+
socket._secure = true;
|
|
285
|
+
socket._sessionID = sessionId;
|
|
286
|
+
// Get user for session
|
|
287
|
+
this.store && this.store.get(socket._sessionID, (err, obj) => {
|
|
288
|
+
if (!obj || !obj.passport) {
|
|
289
|
+
socket._acl.user = '';
|
|
290
|
+
socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE);
|
|
291
|
+
// ws does not require disconnect
|
|
292
|
+
if (!this.noDisconnect) {
|
|
293
|
+
socket.disconnect();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (socket._authPending) {
|
|
297
|
+
socket._authPending(!!socket._acl.user, true);
|
|
298
|
+
delete socket._authPending;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.commands.subscribeSocket(socket);
|
|
305
|
+
|
|
306
|
+
cb && cb();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_updateConnectedInfo() {
|
|
310
|
+
// only in server mode
|
|
311
|
+
if (this.serverMode) {
|
|
312
|
+
if (this.infoTimeout) {
|
|
313
|
+
clearTimeout(this.infoTimeout);
|
|
314
|
+
this.infoTimeout = null;
|
|
315
|
+
}
|
|
316
|
+
this.infoTimeout = setTimeout(() => {
|
|
317
|
+
this.infoTimeout = null;
|
|
318
|
+
|
|
319
|
+
if (this.server) {
|
|
320
|
+
let clientsArray = [];
|
|
321
|
+
if (this.server.sockets) {
|
|
322
|
+
// this could be an object or array
|
|
323
|
+
Object.keys(this.server.sockets.connected).forEach(i =>
|
|
324
|
+
clientsArray.push(this.server.sockets.connected[i]._name || 'noname'));
|
|
325
|
+
}
|
|
326
|
+
const text = `[${clientsArray.length}]${clientsArray.join(', ')}`;
|
|
327
|
+
this.adapter.setState('info.connected', text, true);
|
|
328
|
+
}
|
|
329
|
+
}, 1000);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
checkPermissions(socket, command, callback, arg) {
|
|
334
|
+
return this.commands._checkPermissions(socket, command, callback, arg);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
addCommandHandler(command, handler) {
|
|
338
|
+
this.commands.addCommandHandler(command, handler);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
sendLog(obj) {
|
|
342
|
+
// TODO Build in some threshold
|
|
343
|
+
if (this.server && this.server.sockets) {
|
|
344
|
+
this.server.sockets.emit('log', obj);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
publish(socket, type, id, obj) {
|
|
349
|
+
return this.commands.publish(socket, type, id, obj);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
close() {
|
|
353
|
+
this._unsubscribeAll();
|
|
354
|
+
|
|
355
|
+
this.commands.destroy();
|
|
356
|
+
|
|
357
|
+
if (this.server && this.server.sockets) {
|
|
358
|
+
// this could be an object or array
|
|
359
|
+
Object.keys(this.server.sockets.connected).forEach(i => {
|
|
360
|
+
const socket = this.server.sockets.connected[i];
|
|
361
|
+
if (socket._sessionTimer) {
|
|
362
|
+
clearTimeout(socket._sessionTimer);
|
|
363
|
+
socket._sessionTimer = null;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// IO server will be closed
|
|
369
|
+
try {
|
|
370
|
+
this.server && this.server.close && this.server.close();
|
|
371
|
+
this.server = null;
|
|
372
|
+
} catch (e) {
|
|
373
|
+
// ignore
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (this.infoTimeout) {
|
|
377
|
+
clearTimeout(this.infoTimeout);
|
|
378
|
+
this.infoTimeout = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = SocketCommon;
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iobroker/socket-classes",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "ioBroker server-side web sockets",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "bluefox",
|
|
7
|
+
"email": "dogafox@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ioBroker/ioBroker.socket-classes",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ioBroker",
|
|
15
|
+
"web",
|
|
16
|
+
"socket",
|
|
17
|
+
"web-socket"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/ioBroker/ioBroker.socket-classes"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"passport": "^0.5.2",
|
|
25
|
+
"cookie-parser": "^1.4.6",
|
|
26
|
+
"axios": "^0.26.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@alcalzone/release-script": "^3.5.6",
|
|
30
|
+
"@alcalzone/release-script-plugin-license": "^3.5.3",
|
|
31
|
+
"chai": "^4.3.6",
|
|
32
|
+
"mocha": "^9.2.2"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/ioBroker/ioBroker.web/issues"
|
|
36
|
+
},
|
|
37
|
+
"main": "index.js",
|
|
38
|
+
"files": [
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"lib/",
|
|
41
|
+
"index.js",
|
|
42
|
+
"lib/socketCommon.js",
|
|
43
|
+
"lib/socketAdmin.js"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "node node_modules/mocha/bin/mocha --exit",
|
|
47
|
+
"release": "release-script",
|
|
48
|
+
"release-patch": "release-script patch --yes",
|
|
49
|
+
"release-minor": "release-script minor --yes",
|
|
50
|
+
"release-major": "release-script major --yes"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"maintainers": [
|
|
54
|
+
{
|
|
55
|
+
"name": "bluefox",
|
|
56
|
+
"email": "dogafox@gmail.com"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|