@iobroker/socket-classes 2.7.0 → 2.8.1
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/README.md +24 -1
- package/build/index.d.ts +1 -1
- package/build/index.js.map +1 -1
- package/build/lib/socketCommands.d.ts +39 -7
- package/build/lib/socketCommands.js +299 -5
- package/build/lib/socketCommands.js.map +1 -1
- package/build/lib/socketCommandsAdmin.d.ts +17 -0
- package/build/lib/socketCommandsAdmin.js +223 -12
- package/build/lib/socketCommandsAdmin.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -526,6 +526,7 @@ Unsubscribe from file changes in ioBroker DB
|
|
|
526
526
|
* [`getCompactInstalled`](#getcompactinstalled_a)
|
|
527
527
|
* [`getCompactSystemRepositories`](#getcompactsystemrepositories_a)
|
|
528
528
|
* [`getCompactRepository`](#getcompactrepository_a)
|
|
529
|
+
* [`getReducedRepository`](#getreducedrepository_a)
|
|
529
530
|
* [`getCompactHosts`](#getcompacthosts_a)
|
|
530
531
|
* [`delState`](#delstate_a)
|
|
531
532
|
* [`getStates`](#getstates_a)
|
|
@@ -770,6 +771,17 @@ made every start of the GUI wait seconds for it.
|
|
|
770
771
|
* `host` *string*: - Host name, e.g., `system.host.raspberrypi`
|
|
771
772
|
* `callback` *(result: Record<string, {version: string; icon?: string}>) => void) => void*: - Callback function `(error: string | null, results?: Record<string, { version: string; icon?: string }>) => void`
|
|
772
773
|
|
|
774
|
+
#### <a name="getreducedrepository_a"></a>`getReducedRepository(host, options, callback)`
|
|
775
|
+
Get the repository with what the GUI shows of it.
|
|
776
|
+
The whole repository is several megabytes, most of it the `news` of every version in every
|
|
777
|
+
language. They are delivered only for the adapters installed on any host and for the
|
|
778
|
+
controller, as only an update shows them; with `lang` all translated texts keep only that
|
|
779
|
+
language and English. Answered from the objects admin holds in memory where possible.
|
|
780
|
+
Announced as the feature `REDUCED_REPOSITORY`.
|
|
781
|
+
* `host` *string*: - Host to ask if the objects in memory cannot answer, e.g., `system.host.raspberrypi`
|
|
782
|
+
* `options` *ReducedRepositoryOptions | null | undefined*: - `{ repo?: string | string[]; update?: boolean; lang?: ioBroker.Languages; adapters?: string[] }`
|
|
783
|
+
* `callback` *(error: string | null | Error | undefined, repository?: Record<string, RepoAdapterObject>) => void*: - Callback function `(error: string | null, repository?: Record<string, RepoAdapterObject>) => void`
|
|
784
|
+
|
|
773
785
|
#### <a name="getcompacthosts_a"></a>`getCompactHosts(callback)`
|
|
774
786
|
Get all hosts in a compact form to save bandwidth.
|
|
775
787
|
* `callback` *(error: string | null | Error | undefined, hosts?: CompactHost[]) => void) => void*: - Callback function `(error: string | null, results?: Record<string, { common: { name: string; icon: string; color: string; installedVersion: string }; native: { hardware: { networkInterfaces: any[] } } }>) => void`
|
|
@@ -1043,8 +1055,19 @@ Unsubscribe from file changes in ioBroker DB
|
|
|
1043
1055
|
Placeholder for the next version (at the beginning of the line):
|
|
1044
1056
|
### **WORK IN PROGRESS**
|
|
1045
1057
|
-->
|
|
1058
|
+
### 2.8.1 (2026-10-15)
|
|
1059
|
+
- (@GermanBluefox) Changed: `getCompactSystemRepositories` is answered from the objects in memory
|
|
1060
|
+
- (@GermanBluefox) Fixed: the outdated lock file that stopped the release of 2.8.0
|
|
1061
|
+
|
|
1062
|
+
### 2.8.0 (2026-10-15)
|
|
1063
|
+
- (@GermanBluefox) Added: command `getReducedRepository` - the repository without unneeded news and languages
|
|
1064
|
+
|
|
1065
|
+
### 2.7.1 (2026-10-07)
|
|
1066
|
+
- (@GermanBluefox) Added: an event only reaches a connection whose user may read what it is about. A subscription says which ids a client is interested in, never which ids it may see - `subscribe` asks whether the user may read states *at all*, so whoever subscribed to `*` was told about every state of the system, the ACL of the single objects notwithstanding, and the same went for objects and for files. The fan-out asks that question now, for state, object and file events alike. Members of the administrator group are not restricted, as everywhere else, and with authentication switched off every connection is the configured default user - so for most installations nothing changes at all. **Where restricted users exist, they will see less than before: that is the point, but it is the kind of change that comes back as "my user stopped getting updates".** The database decides, not this package: `mayRead` of js-controller 8.0 and newer is asked where it exists, which knows the ACL of a state rather than that of its object and the mode of a single file rather than that of the adapter; an older controller is asked for the object instead, which is coarser but refuses in the same direction. One question per user and thing, remembered until the object - or the rights themselves - change, and the objects are watched from the first decision on, so an installation whose connections are all administrators never asks anything
|
|
1067
|
+
- (@GermanBluefox) Changed: `publish` and `publishFile` answer `true` where the event is on its way to the client, which now includes the moment it waits for the decision described above; `false` means the client is not subscribed to it, or may not see it. Only the first event of a thing ever waits, and only the newest one of it, so nothing can overtake it
|
|
1068
|
+
|
|
1046
1069
|
### 2.7.0 (2026-10-05)
|
|
1047
|
-
- (@GermanBluefox) Added: every command that sends a message now names the user of the connection - `sendTo`, `sendToHost`, `cmdExec`, `clientSubscribe`, `clientUnsubscribe` and the "nobody subscribed" notice. Objects, states and files have always been read and written with `{ user }` so the database applies the ACLs of the logged-in user; a message had no such channel, so the receiving instance saw `from` and nothing else and had to act with its own rights - a GUI user who may use `sendTo` could have an adapter do things their own ACL forbids. It travels as a send option, which
|
|
1070
|
+
- (@GermanBluefox) Added: every command that sends a message now names the user of the connection - `sendTo`, `sendToHost`, `cmdExec`, `clientSubscribe`, `clientUnsubscribe` and the "nobody subscribed" notice. Objects, states and files have always been read and written with `{ user }` so the database applies the ACLs of the logged-in user; a message had no such channel, so the receiving instance saw `from` and nothing else and had to act with its own rights - a GUI user who may use `sendTo` could have an adapter do things their own ACL forbids. It travels as a send option, which js-controller 7.2.5 and newer pass on to the message as `obj.user`; an older one ignores it, so nothing breaks, and a receiving adapter treats the field as optional
|
|
1048
1071
|
|
|
1049
1072
|
### 2.6.2 (2026-10-02)
|
|
1050
1073
|
- (@GermanBluefox) Fixed: a websocket whose access token was not accepted stayed open without a single command handler. The client was asked to re-authenticate, fetched a new token within milliseconds and announced it with `updateTokenExpiration` - the one command that is exempt from the session check exactly for this - but nobody was listening. The client waited for an answer that could not come until its own three second timeout closed the connection, burnt its single-use refresh token for nothing and had to start over, which is why the GUI took seconds to come up with authentication enabled and sometimes did not come up at all. The handlers are installed now, with an empty ACL: every command that needs a permission is still refused, only the announcement of a token gets through
|
package/build/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import passport from 'passport';
|
|
2
2
|
import cookieParser from 'cookie-parser';
|
|
3
3
|
export type { SocketSubscribeTypes, InternalStorageToken, CommandFile } from './types';
|
|
4
|
-
export { SocketCommandsAdmin, type ObjectsCount } from './lib/socketCommandsAdmin';
|
|
4
|
+
export { SocketCommandsAdmin, type ObjectsCount, type ReducedRepositoryOptions } from './lib/socketCommandsAdmin';
|
|
5
5
|
export { SocketCommands, type SupportedFeature, COMMANDS_PERMISSIONS } from './lib/socketCommands';
|
|
6
6
|
export { SocketCommon, type Server, type SocketSettings, type SocketIoOptions, type WhiteListSettings, } from './lib/socketCommon';
|
|
7
7
|
export { SocketAdmin } from './lib/socketAdmin';
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,wDAAgC;AAoBvB,mBApBF,kBAAQ,CAoBE;AAnBjB,kEAAyC;AAmBtB,uBAnBZ,uBAAY,CAmBY;AAhB/B,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,wDAAgC;AAoBvB,mBApBF,kBAAQ,CAoBE;AAnBjB,kEAAyC;AAmBtB,uBAnBZ,uBAAY,CAmBY;AAhB/B,iEAAkH;AAAzG,0HAAA,mBAAmB,OAAA;AAC5B,uDAAmG;AAA1F,gHAAA,cAAc,OAAA;AAAyB,sHAAA,oBAAoB,OAAA;AACpE,mDAM4B;AALxB,4GAAA,YAAY,OAAA;AAMhB,iDAAgD;AAAvC,0GAAA,WAAW,OAAA;AACpB,yDAK8B;AACG,+FALhB,0BAAc,OAKgB"}
|
|
@@ -3,9 +3,9 @@ import { type PermissionCommands, type SocketSubscribeTypes, type SocketOperatio
|
|
|
3
3
|
/**
|
|
4
4
|
* The options of an outbound message, as far as this package needs them.
|
|
5
5
|
*
|
|
6
|
-
* `timeout` has always been there. `user` is read by
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* `timeout` has always been there. `user` is read by js-controller 7.2.5 and newer and silently
|
|
7
|
+
* ignored by older ones, and it is not in the older `@iobroker/types` this package builds against -
|
|
8
|
+
* hence the own declaration.
|
|
9
9
|
*/
|
|
10
10
|
interface MessageSendOptions {
|
|
11
11
|
/** Reject/err-callback if no answer arrives in time (single targets only) */
|
|
@@ -34,7 +34,9 @@ export type Ratings = {
|
|
|
34
34
|
};
|
|
35
35
|
export type SupportedFeature = 'ALIAS' | 'ALIAS_SEPARATE_READ_WRITE_ID' | 'ADAPTER_GETPORT_BIND' | 'ADAPTER_DEL_OBJECT_RECURSIVE' | 'ADAPTER_SET_OBJECT_SETS_DEFAULT_VALUE' | 'ADAPTER_AUTO_DECRYPT_NATIVE' | 'PLUGINS' | 'CONTROLLER_NPM_AUTO_REBUILD' | 'CONTROLLER_READWRITE_BASE_SETTINGS' | 'CONTROLLER_MULTI_REPO' | 'CONTROLLER_LICENSE_MANAGER' | 'CONTROLLER_OS_PACKAGE_UPGRADE' | 'DEL_INSTANCE_CUSTOM' | 'CUSTOM_FULL_VIEW' | 'ADAPTER_GET_OBJECTS_BY_ARRAY' | 'CONTROLLER_UI_UPGRADE' | 'ADAPTER_WEBSERVER_UPGRADE' | 'INSTANCE_MESSAGES' | 'PARTIAL_OBJECT_TREE'
|
|
36
36
|
/** The command `getObjectsCount` counts the objects instead of the client reading them all */
|
|
37
|
-
| 'OBJECTS_COUNT'
|
|
37
|
+
| 'OBJECTS_COUNT'
|
|
38
|
+
/** The command `getReducedRepository` delivers the repository without what the GUI does not show */
|
|
39
|
+
| 'REDUCED_REPOSITORY';
|
|
38
40
|
export interface SocketDataContext {
|
|
39
41
|
language?: ioBroker.Languages;
|
|
40
42
|
ratings: Ratings | null;
|
|
@@ -74,7 +76,36 @@ export declare class SocketCommands {
|
|
|
74
76
|
*/
|
|
75
77
|
static _fixCallback(callback: SocketCallback | null | undefined, error: string | Error | null | undefined, ...args: any[]): void;
|
|
76
78
|
_checkPermissions(socket: WebSocketClient, command: PermissionCommands, callback: ((error: string | null, ...args: any[]) => void) | undefined, ...args: any[]): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Give one event to one connection, if it is subscribed to it and may see it.
|
|
81
|
+
*
|
|
82
|
+
* Returns whether the event is on its way to this client: `true` where it was sent, and also
|
|
83
|
+
* where it is waiting for the database to decide whether this user may read the object - the
|
|
84
|
+
* answer to that is not worth holding up every other connection for. `false` means the client is
|
|
85
|
+
* not subscribed to it, or may not see it.
|
|
86
|
+
*
|
|
87
|
+
* @param socket the connection
|
|
88
|
+
* @param type what kind of event it is
|
|
89
|
+
* @param id the object it is about
|
|
90
|
+
* @param obj the state or object as it is sent to the client
|
|
91
|
+
*/
|
|
77
92
|
publish(socket: WebSocketClient, type: SocketSubscribeTypes, id: string, obj: ioBroker.Object | ioBroker.State | null | undefined): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Give one file event to one connection, if it is subscribed to it and may see it.
|
|
95
|
+
*
|
|
96
|
+
* The decision is taken per file and user. Where the controller has `mayRead`, it answers for the
|
|
97
|
+
* single file, with its own owner and mode as `chownFile` and `chmodFile` set them. An older
|
|
98
|
+
* controller cannot be asked about a file, so the meta object the files belong to - `vis.0` for
|
|
99
|
+
* every file of vis - stands in for it: still one question per file, but every one of them about
|
|
100
|
+
* the adapter. That also works where the file the event is about has just been deleted.
|
|
101
|
+
*
|
|
102
|
+
* The answer means the same as in {@link publish}: `true` where the event is on its way.
|
|
103
|
+
*
|
|
104
|
+
* @param socket the connection
|
|
105
|
+
* @param id the adapter the file belongs to, e.g. `vis.0`
|
|
106
|
+
* @param fileName the path of the file inside it
|
|
107
|
+
* @param size how big it is now, or null where it is gone
|
|
108
|
+
*/
|
|
78
109
|
publishFile(socket: WebSocketClient, id: string, fileName: string, size: number | null): boolean;
|
|
79
110
|
/**
|
|
80
111
|
* The send options for a message triggered by this socket: the user it is sent on behalf of.
|
|
@@ -82,9 +113,10 @@ export declare class SocketCommands {
|
|
|
82
113
|
* Objects, states and files have always been read and written with `{ user }` so the database
|
|
83
114
|
* applies the ACLs of the logged-in user. A message had no such channel: the receiving instance saw
|
|
84
115
|
* `from` and nothing else, so every adapter reachable over `sendTo` had to act with its own rights,
|
|
85
|
-
* and could not tell one caller from another. The user travels with the message now
|
|
86
|
-
*
|
|
87
|
-
* receiver
|
|
116
|
+
* and could not tell one caller from another. The user travels with the message now: js-controller
|
|
117
|
+
* 7.2.5 and newer put it into the message as `obj.user`, older ones ignore the option, so nothing
|
|
118
|
+
* breaks. A receiver treats the field as optional - where nobody was named there is nothing to
|
|
119
|
+
* check against, which is how every message looked before.
|
|
88
120
|
*
|
|
89
121
|
* @param socket the socket the command came in on
|
|
90
122
|
*/
|
|
@@ -3,6 +3,16 @@ var _a;
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.SocketCommands = exports.COMMANDS_PERMISSIONS = void 0;
|
|
5
5
|
const adapter_core_1 = require("@iobroker/adapter-core"); // Get common adapter utils
|
|
6
|
+
/**
|
|
7
|
+
* The events that are waiting for the database to say whether their socket may see them.
|
|
8
|
+
*
|
|
9
|
+
* Kept per connection and per event, and only ever the **latest** one: while the first event of an id
|
|
10
|
+
* waits for its answer, a newer value of the same state would otherwise overtake it and the client
|
|
11
|
+
* would end up with the older one. What is kept is how to deliver it - the event goes through
|
|
12
|
+
* `publish`/`publishFile` again, so a subscription that was given up in the meantime still counts. It
|
|
13
|
+
* lives in a `WeakMap`, so a connection that goes away takes its waiting events with it.
|
|
14
|
+
*/
|
|
15
|
+
const pendingEvents = new WeakMap();
|
|
6
16
|
exports.COMMANDS_PERMISSIONS = {
|
|
7
17
|
getObject: { type: 'object', operation: 'read' },
|
|
8
18
|
getObjects: { type: 'object', operation: 'list' },
|
|
@@ -71,6 +81,21 @@ class SocketCommands {
|
|
|
71
81
|
subscribes = {};
|
|
72
82
|
#logEnabled = false;
|
|
73
83
|
#clientSubscribes = {};
|
|
84
|
+
/**
|
|
85
|
+
* What each user may read, by object id. Filled while the events flow, emptied where an object -
|
|
86
|
+
* or the rights themselves - change. Per user, because every browser tab of one person would
|
|
87
|
+
* otherwise ask the same question again.
|
|
88
|
+
*/
|
|
89
|
+
#readable = new Map();
|
|
90
|
+
/** Questions that are with the database right now, so the same id is asked only once at a time */
|
|
91
|
+
#deciding = new Map();
|
|
92
|
+
/**
|
|
93
|
+
* Counts how often the decisions were thrown away. An answer that was asked for before the last
|
|
94
|
+
* change arrives too late to be trusted and is dropped instead of being remembered.
|
|
95
|
+
*/
|
|
96
|
+
#readableGeneration = 0;
|
|
97
|
+
/** Whether this instance watches the objects for the sake of its own decisions - see `#rememberDecision` */
|
|
98
|
+
#watchingObjects = false;
|
|
74
99
|
#updateSession;
|
|
75
100
|
adapterName;
|
|
76
101
|
_sendToHost;
|
|
@@ -260,10 +285,250 @@ class SocketCommands {
|
|
|
260
285
|
}
|
|
261
286
|
return true;
|
|
262
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Whether this connection belongs to somebody who may see everything.
|
|
290
|
+
*
|
|
291
|
+
* The administrator group is the whole point of the administrator group, and with authentication
|
|
292
|
+
* switched off every connection is the configured default user - usually exactly that one. So this
|
|
293
|
+
* is the answer for almost every connection there is, and it costs a look at an ACL that the
|
|
294
|
+
* socket layer calculated when the connection was opened.
|
|
295
|
+
*
|
|
296
|
+
* @param socket the connection in question
|
|
297
|
+
*/
|
|
298
|
+
static #seesEverything(socket) {
|
|
299
|
+
const acl = socket?._acl;
|
|
300
|
+
return acl?.user === 'system.user.admin' || !!acl?.groups?.includes('system.group.administrator');
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Whether the user of this connection may read `id`, as far as it is already known.
|
|
304
|
+
*
|
|
305
|
+
* `undefined` means "not decided yet" - the caller has to let `#decideReadable` ask the database
|
|
306
|
+
* and come back. Decisions are kept per user, not per connection: three browser tabs of the same
|
|
307
|
+
* person ask once.
|
|
308
|
+
*
|
|
309
|
+
* @param socket the connection the event would go to
|
|
310
|
+
* @param question what the event is about
|
|
311
|
+
*/
|
|
312
|
+
#mayRead(socket, question) {
|
|
313
|
+
const user = socket?._acl?.user;
|
|
314
|
+
if (!user) {
|
|
315
|
+
// a connection without a user has no rights at all, not even to be told that `id` exists
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (_a.#seesEverything(socket)) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
return this.#readable.get(user)?.get(_a.#questionKey(question));
|
|
322
|
+
}
|
|
323
|
+
/** What one question is remembered under: the kind, the id, and for a file its name */
|
|
324
|
+
static #questionKey(question) {
|
|
325
|
+
return question.fileName
|
|
326
|
+
? `${question.type}####${question.id}####${question.fileName}`
|
|
327
|
+
: `${question.type}####${question.id}`;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Ask the database whether the user may read `id`, and remember the answer.
|
|
331
|
+
*
|
|
332
|
+
* The database owns this decision - the ACL of the object, its owner, the groups of the user and
|
|
333
|
+
* the default ACL of the system all go into it, and a second implementation here would drift away
|
|
334
|
+
* from it sooner or later. Everybody who is waiting for the same id is answered together.
|
|
335
|
+
*
|
|
336
|
+
* @param socket the connection the event would go to
|
|
337
|
+
* @param question what the event is about
|
|
338
|
+
* @param andThen what to do once it is decided, with the decision
|
|
339
|
+
*/
|
|
340
|
+
#decideReadable(socket, question, andThen) {
|
|
341
|
+
// a connection without a user never gets this far: `#mayRead` refuses it outright
|
|
342
|
+
const user = socket._acl.user;
|
|
343
|
+
const questionKey = _a.#questionKey(question);
|
|
344
|
+
const key = `${user}####${questionKey}`;
|
|
345
|
+
const waiting = this.#deciding.get(key);
|
|
346
|
+
if (waiting) {
|
|
347
|
+
waiting.push(andThen);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
this.#deciding.set(key, [andThen]);
|
|
351
|
+
const generation = this.#readableGeneration;
|
|
352
|
+
const answer = (allowed) => {
|
|
353
|
+
if (generation === this.#readableGeneration) {
|
|
354
|
+
this.#rememberDecision(user, questionKey, allowed);
|
|
355
|
+
}
|
|
356
|
+
/*
|
|
357
|
+
* An answer that is too old to be remembered still answers the events that waited for it:
|
|
358
|
+
* a "no" drops them, a "yes" sends them back through `publish`, which - with nothing in
|
|
359
|
+
* memory - asks again under the rights as they are now.
|
|
360
|
+
*/
|
|
361
|
+
const callbacks = this.#deciding.get(key) || [];
|
|
362
|
+
this.#deciding.delete(key);
|
|
363
|
+
for (const callback of callbacks) {
|
|
364
|
+
callback(allowed);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const mayRead = this.adapter.mayRead;
|
|
368
|
+
if (mayRead) {
|
|
369
|
+
/*
|
|
370
|
+
* The controller answers this itself from js-controller 8.0 on, and it is the only place
|
|
371
|
+
* that knows the whole truth: the ACL of a state rather than of its object, the mode of a
|
|
372
|
+
* single file rather than of the adapter it belongs to.
|
|
373
|
+
*/
|
|
374
|
+
mayRead.call(this.adapter, { ...question, user }).then(answer, (e) => {
|
|
375
|
+
this.adapter.log.warn(`Cannot check what "${user}" may read of "${question.id}": ${e.message}`);
|
|
376
|
+
answer(false);
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
/*
|
|
381
|
+
* An older controller cannot be asked, so the object has to stand in for all three. It is
|
|
382
|
+
* coarser - the right on the object instead of the one on the state, the adapter instead of
|
|
383
|
+
* the single file - but it errs the same way: only a refusal of the database counts as "no".
|
|
384
|
+
* That an object does not exist is not a refusal, and `mayRead` of a newer controller says the
|
|
385
|
+
* same, because such a state can be read by anybody.
|
|
386
|
+
*/
|
|
387
|
+
// the callback form also returns a promise, which nobody here waits for
|
|
388
|
+
void this.adapter.getForeignObject(question.id, { user }, (error) => answer(!error));
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Keep one decision, and make sure the object changes that would invalidate it arrive here.
|
|
392
|
+
*
|
|
393
|
+
* Nothing of this is needed as long as everybody who is connected sees everything anyway, which is
|
|
394
|
+
* the normal case - so the subscription is taken out where the first decision is made and not
|
|
395
|
+
* before. The events themselves reach `publish` the way every other object change does.
|
|
396
|
+
*
|
|
397
|
+
* @param user whose decision it is
|
|
398
|
+
* @param questionKey what was asked - the kind, the id and for a file its name
|
|
399
|
+
* @param allowed what was decided
|
|
400
|
+
*/
|
|
401
|
+
#rememberDecision(user, questionKey, allowed) {
|
|
402
|
+
let decisions = this.#readable.get(user);
|
|
403
|
+
if (!decisions) {
|
|
404
|
+
decisions = new Map();
|
|
405
|
+
this.#readable.set(user, decisions);
|
|
406
|
+
}
|
|
407
|
+
decisions.set(questionKey, allowed);
|
|
408
|
+
if (!this.#watchingObjects) {
|
|
409
|
+
this.#watchingObjects = true;
|
|
410
|
+
/*
|
|
411
|
+
* The ACL of an object can change at any time, and without this the decision above would
|
|
412
|
+
* be kept until the connection goes away. The watch counts as one more subscriber of `*`:
|
|
413
|
+
* a client that subscribed to all objects and gives them up again would otherwise take
|
|
414
|
+
* the subscription of the database away from under it.
|
|
415
|
+
*/
|
|
416
|
+
this.subscribes.objectChange ||= {};
|
|
417
|
+
if (this.subscribes.objectChange['*'] === undefined) {
|
|
418
|
+
this.subscribes.objectChange['*'] = 1;
|
|
419
|
+
this.adapter
|
|
420
|
+
.subscribeForeignObjectsAsync('*')
|
|
421
|
+
.catch(e => this.adapter.log.warn(`Cannot watch the objects for the permissions of the clients: ${e.message}`));
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
this.subscribes.objectChange['*']++;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** Forget what was decided about `id`, because the object - and with it its ACL - changed. */
|
|
429
|
+
#forgetReadable(id) {
|
|
430
|
+
this.#readableGeneration++;
|
|
431
|
+
if (id.startsWith('system.user.') || id.startsWith('system.group.') || id === 'system.config') {
|
|
432
|
+
// the rights themselves moved, so nothing that was decided with them still counts
|
|
433
|
+
this.#readable.clear();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
for (const decisions of this.#readable.values()) {
|
|
437
|
+
// everything that was decided about this id, whatever was asked about it
|
|
438
|
+
for (const questionKey of decisions.keys()) {
|
|
439
|
+
if (questionKey.endsWith(`####${id}`) || questionKey.includes(`####${id}####`)) {
|
|
440
|
+
decisions.delete(questionKey);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Hold one event until it is decided whether its connection may see it.
|
|
447
|
+
*
|
|
448
|
+
* Only the latest event of the same kind and id waits; a newer one replaces it, so nothing can
|
|
449
|
+
* overtake it. The question is asked once - every further event of that id joins the one that is
|
|
450
|
+
* already open.
|
|
451
|
+
*
|
|
452
|
+
* @param socket the connection the event would go to
|
|
453
|
+
* @param question what the decision is about
|
|
454
|
+
* @param key what identifies this event - its kind and what it is about
|
|
455
|
+
* @param deliver how to send it once it is decided
|
|
456
|
+
*/
|
|
457
|
+
#holdUntilDecided(socket, question, key, deliver) {
|
|
458
|
+
let waiting = pendingEvents.get(socket);
|
|
459
|
+
if (!waiting) {
|
|
460
|
+
waiting = new Map();
|
|
461
|
+
pendingEvents.set(socket, waiting);
|
|
462
|
+
}
|
|
463
|
+
const first = !waiting.has(key);
|
|
464
|
+
waiting.set(key, deliver);
|
|
465
|
+
if (first) {
|
|
466
|
+
this.#decideReadable(socket, question, allowed => this.#flushPending(socket, key, allowed));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Send the event that waited for a decision, if it may go out after all.
|
|
471
|
+
*
|
|
472
|
+
* What is sent is whatever arrived last while the question was open - a client that subscribes to
|
|
473
|
+
* a state wants its value, not the one it had a moment ago. It goes through `publish`/`publishFile`
|
|
474
|
+
* again: the subscription may have been given up in the meantime. Usually the decision is in memory
|
|
475
|
+
* by then and the event goes out; only where the object changed while the question was open was
|
|
476
|
+
* the answer not remembered, and the event waits once more for a fresh one.
|
|
477
|
+
*
|
|
478
|
+
* @param socket the connection that waited
|
|
479
|
+
* @param key what identifies the event
|
|
480
|
+
* @param allowed what was decided
|
|
481
|
+
*/
|
|
482
|
+
#flushPending(socket, key, allowed) {
|
|
483
|
+
const waiting = pendingEvents.get(socket);
|
|
484
|
+
const deliver = waiting?.get(key);
|
|
485
|
+
if (!deliver || !waiting) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
waiting.delete(key);
|
|
489
|
+
if (allowed) {
|
|
490
|
+
deliver();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Give one event to one connection, if it is subscribed to it and may see it.
|
|
495
|
+
*
|
|
496
|
+
* Returns whether the event is on its way to this client: `true` where it was sent, and also
|
|
497
|
+
* where it is waiting for the database to decide whether this user may read the object - the
|
|
498
|
+
* answer to that is not worth holding up every other connection for. `false` means the client is
|
|
499
|
+
* not subscribed to it, or may not see it.
|
|
500
|
+
*
|
|
501
|
+
* @param socket the connection
|
|
502
|
+
* @param type what kind of event it is
|
|
503
|
+
* @param id the object it is about
|
|
504
|
+
* @param obj the state or object as it is sent to the client
|
|
505
|
+
*/
|
|
263
506
|
publish(socket, type, id, obj) {
|
|
507
|
+
if (type === 'objectChange') {
|
|
508
|
+
// the ACL of the object travelled with it, so what was decided about it is out of date
|
|
509
|
+
this.#forgetReadable(id);
|
|
510
|
+
}
|
|
264
511
|
if (socket?.subscribe?.[type] && this.#updateSession(socket)) {
|
|
265
512
|
return !!socket.subscribe[type].find(sub => {
|
|
266
513
|
if (sub.regex.test(id)) {
|
|
514
|
+
/*
|
|
515
|
+
* Who may know that this exists? A subscription says which ids a client is
|
|
516
|
+
* interested in, never which ids it may see - `subscribe` only asks whether the
|
|
517
|
+
* user may read states at all, so whoever subscribed to `*` was told about every
|
|
518
|
+
* state of the system, the ACL of the single objects notwithstanding.
|
|
519
|
+
*/
|
|
520
|
+
const question = { type: type === 'objectChange' ? 'object' : 'state', id };
|
|
521
|
+
const allowed = this.#mayRead(socket, question);
|
|
522
|
+
if (allowed === false) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
if (allowed === undefined) {
|
|
526
|
+
// the first event of an id for this user waits for the answer instead of
|
|
527
|
+
// being guessed at
|
|
528
|
+
this.#holdUntilDecided(socket, question, `${type}####${id}`, () => this.publish(socket, type, id, obj));
|
|
529
|
+
// on its way: either it goes out in a moment, or the user may not see it
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
267
532
|
// replace language
|
|
268
533
|
if (this.context.language &&
|
|
269
534
|
id === 'system.config' &&
|
|
@@ -277,11 +542,36 @@ class SocketCommands {
|
|
|
277
542
|
}
|
|
278
543
|
return false;
|
|
279
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Give one file event to one connection, if it is subscribed to it and may see it.
|
|
547
|
+
*
|
|
548
|
+
* The decision is taken per file and user. Where the controller has `mayRead`, it answers for the
|
|
549
|
+
* single file, with its own owner and mode as `chownFile` and `chmodFile` set them. An older
|
|
550
|
+
* controller cannot be asked about a file, so the meta object the files belong to - `vis.0` for
|
|
551
|
+
* every file of vis - stands in for it: still one question per file, but every one of them about
|
|
552
|
+
* the adapter. That also works where the file the event is about has just been deleted.
|
|
553
|
+
*
|
|
554
|
+
* The answer means the same as in {@link publish}: `true` where the event is on its way.
|
|
555
|
+
*
|
|
556
|
+
* @param socket the connection
|
|
557
|
+
* @param id the adapter the file belongs to, e.g. `vis.0`
|
|
558
|
+
* @param fileName the path of the file inside it
|
|
559
|
+
* @param size how big it is now, or null where it is gone
|
|
560
|
+
*/
|
|
280
561
|
publishFile(socket, id, fileName, size) {
|
|
281
562
|
if (socket?.subscribe?.fileChange && this.#updateSession(socket)) {
|
|
282
563
|
const key = `${id}####${fileName}`;
|
|
283
564
|
return !!socket.subscribe.fileChange.find(sub => {
|
|
284
565
|
if (sub.regex.test(key)) {
|
|
566
|
+
const question = { type: 'file', id, fileName };
|
|
567
|
+
const allowed = this.#mayRead(socket, question);
|
|
568
|
+
if (allowed === false) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
if (allowed === undefined) {
|
|
572
|
+
this.#holdUntilDecided(socket, question, `fileChange####${key}`, () => this.publishFile(socket, id, fileName, size));
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
285
575
|
socket.emit('fileChange', id, fileName, size);
|
|
286
576
|
return true;
|
|
287
577
|
}
|
|
@@ -295,9 +585,10 @@ class SocketCommands {
|
|
|
295
585
|
* Objects, states and files have always been read and written with `{ user }` so the database
|
|
296
586
|
* applies the ACLs of the logged-in user. A message had no such channel: the receiving instance saw
|
|
297
587
|
* `from` and nothing else, so every adapter reachable over `sendTo` had to act with its own rights,
|
|
298
|
-
* and could not tell one caller from another. The user travels with the message now
|
|
299
|
-
*
|
|
300
|
-
* receiver
|
|
588
|
+
* and could not tell one caller from another. The user travels with the message now: js-controller
|
|
589
|
+
* 7.2.5 and newer put it into the message as `obj.user`, older ones ignore the option, so nothing
|
|
590
|
+
* breaks. A receiver treats the field as optional - where nobody was named there is nothing to
|
|
591
|
+
* check against, which is how every message looked before.
|
|
301
592
|
*
|
|
302
593
|
* @param socket the socket the command came in on
|
|
303
594
|
*/
|
|
@@ -882,6 +1173,9 @@ class SocketCommands {
|
|
|
882
1173
|
// send a command that nobody answers - it would sit there until its timeout
|
|
883
1174
|
_a._fixCallback(callback, null, !!this.commands.getObjectsCount);
|
|
884
1175
|
}
|
|
1176
|
+
else if (feature === 'REDUCED_REPOSITORY') {
|
|
1177
|
+
_a._fixCallback(callback, null, !!this.commands.getReducedRepository);
|
|
1178
|
+
}
|
|
885
1179
|
else {
|
|
886
1180
|
_a._fixCallback(callback, null, this.adapter.supportsFeature(feature));
|
|
887
1181
|
}
|
|
@@ -1015,8 +1309,8 @@ class SocketCommands {
|
|
|
1015
1309
|
}
|
|
1016
1310
|
else {
|
|
1017
1311
|
try {
|
|
1018
|
-
// the 5th parameter exists from
|
|
1019
|
-
//
|
|
1312
|
+
// the 5th parameter exists from js-controller 7.2.5 on; an older one
|
|
1313
|
+
// ignores the extra argument
|
|
1020
1314
|
const sendToHost = this.adapter.sendToHost;
|
|
1021
1315
|
sendToHost.call(this.adapter, host, command, message, callback, _a.sendOptionsOf(socket));
|
|
1022
1316
|
}
|