@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.
@@ -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
+ }