homematic-manager 3.0.0-beta.17 → 3.0.0-beta.19
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 +1 -1
- package/dist/occulite.d.ts +14 -3
- package/dist/occulite.js +35 -4
- package/dist/server.js +31 -10
- package/node_modules/@homematic-manager/backend/dist/api/backend.d.ts +6 -0
- package/node_modules/@homematic-manager/backend/dist/api/backend.js +65 -24
- package/node_modules/@homematic-manager/backend/dist/index.d.ts +2 -1
- package/node_modules/@homematic-manager/backend/dist/index.js +2 -1
- package/node_modules/@homematic-manager/backend/dist/interfaces/manager.d.ts +23 -3
- package/node_modules/@homematic-manager/backend/dist/interfaces/manager.js +129 -13
- package/node_modules/@homematic-manager/backend/dist/meta/service.js +23 -1
- package/node_modules/@homematic-manager/backend/dist/rpc/client.d.ts +18 -5
- package/node_modules/@homematic-manager/backend/dist/rpc/client.js +11 -5
- package/node_modules/@homematic-manager/backend/dist/rpc/log.d.ts +86 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/log.js +241 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/origin.d.ts +30 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/origin.js +35 -0
- package/node_modules/@homematic-manager/backend/dist/util/net.d.ts +67 -0
- package/node_modules/@homematic-manager/backend/dist/util/net.js +201 -0
- package/node_modules/@homematic-manager/backend/package.json +1 -1
- package/node_modules/@homematic-manager/core/dist/api/types.d.ts +91 -5
- package/node_modules/@homematic-manager/core/dist/api/types.js +2 -1
- package/node_modules/@homematic-manager/core/dist/i18n/messages.js +45 -0
- package/node_modules/@homematic-manager/core/dist/rssi/index.d.ts +31 -14
- package/node_modules/@homematic-manager/core/dist/rssi/index.js +28 -30
- package/node_modules/@homematic-manager/core/package.json +1 -1
- package/package.json +3 -3
- package/ui/assets/index-BQpPoLG-.css +1 -0
- package/ui/assets/index-D-hDPi8h.js +16 -0
- package/ui/favicon.ico +0 -0
- package/ui/index.html +3 -2
- package/node_modules/@homematic-manager/backend/dist/write/log.d.ts +0 -51
- package/node_modules/@homematic-manager/backend/dist/write/log.js +0 -151
- package/ui/assets/index-BqOnC2jh.js +0 -14
- package/ui/assets/index-DHvC2NX0.css +0 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ hard-coding it.
|
|
|
63
63
|
| `--issue-cookie` | loopback binds only | hand the token to the browser on the page load |
|
|
64
64
|
| `-a, --ccu` | - | CCU address; written to the configuration when it differs |
|
|
65
65
|
| `--local` | - | we run on the CCU itself: talk to the interface processes directly |
|
|
66
|
-
| `--callback-ip` | auto | address the interfaces call back to; needed where this host cannot see it (Docker) |
|
|
66
|
+
| `--callback-ip` | auto | address the interfaces call back to; needed where this host cannot see it (Docker). Auto is this host's address in the CCU's network, else the one on the route to the CCU |
|
|
67
67
|
| `--callback-xmlrpc-port`, `--callback-binrpc-port` | free ports | fixed callback ports, so a container can publish them |
|
|
68
68
|
| `--callback-xmlrpc-default-port`, `--callback-binrpc-default-port` | - | callback ports while the configured ones are 0, a free port when one is taken (the CCU addon: 2031, 2032; never written to the configuration) |
|
|
69
69
|
| `--demo` | off | serve the UI on its demo fixture and start no backend |
|
package/dist/occulite.d.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* On openccu-lite nobody logs in *here*. The box's shell has already done it, lighttpd's gate has
|
|
10
10
|
* already refused everyone who has no session (their D-28), and the shell opens an addon page with
|
|
11
|
-
* the session on the URL: `?sid=@xxxxxxxxxx@`, the CCU convention.
|
|
12
|
-
* credential for the box's own APIs, so the check is one
|
|
11
|
+
* the session on the URL: `?sid=@xxxxxxxxxx@`, the CCU convention. On an image from before their
|
|
12
|
+
* task 125 that session id **is** a valid credential for the box's own APIs, so the check is one
|
|
13
|
+
* request (on a newer image the shell sends the gate's `X-Occulite-Session` instead, see below):
|
|
13
14
|
*
|
|
14
15
|
* ```
|
|
15
16
|
* GET /api/meta/v1/enums Authorization: Bearer <sid> -> 200 valid, 401 not
|
|
@@ -58,8 +59,18 @@ export interface OcculiteCheckOptions {
|
|
|
58
59
|
*/
|
|
59
60
|
readonly requireState?: boolean;
|
|
60
61
|
}
|
|
61
|
-
/** A session id as the shell hands it over, `@…@` and all, or `undefined` when it is not one. */
|
|
62
|
+
/** A session id as the shell or the gate hands it over, `@…@` and all, or `undefined` when it is not one. */
|
|
62
63
|
export declare function parseSid(value: string | null | undefined): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* B-36: can the `?sid=` of a request be a session of the box whose gate named `gate`?
|
|
66
|
+
*
|
|
67
|
+
* A gate header of the 26-character shape says the box issues session ids of that shape (task 125),
|
|
68
|
+
* so a `?sid=` of any other shape there is the session's legacy alias, which the box's API refuses:
|
|
69
|
+
* the answer is known without asking. Without such a header - a CCU, an image from before the header,
|
|
70
|
+
* a ten-character header - nothing can be concluded, and the box decides as before. This only ever
|
|
71
|
+
* *refuses*: a `true` still has to be confirmed by the box.
|
|
72
|
+
*/
|
|
73
|
+
export declare function sidCanBeSession(sid: string, gate: string | undefined): boolean;
|
|
63
74
|
/** Checks a session id against the box that issued it. */
|
|
64
75
|
export declare class OcculiteAuthenticator {
|
|
65
76
|
#private;
|
package/dist/occulite.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* On openccu-lite nobody logs in *here*. The box's shell has already done it, lighttpd's gate has
|
|
10
10
|
* already refused everyone who has no session (their D-28), and the shell opens an addon page with
|
|
11
|
-
* the session on the URL: `?sid=@xxxxxxxxxx@`, the CCU convention.
|
|
12
|
-
* credential for the box's own APIs, so the check is one
|
|
11
|
+
* the session on the URL: `?sid=@xxxxxxxxxx@`, the CCU convention. On an image from before their
|
|
12
|
+
* task 125 that session id **is** a valid credential for the box's own APIs, so the check is one
|
|
13
|
+
* request (on a newer image the shell sends the gate's `X-Occulite-Session` instead, see below):
|
|
13
14
|
*
|
|
14
15
|
* ```
|
|
15
16
|
* GET /api/meta/v1/enums Authorization: Bearer <sid> -> 200 valid, 401 not
|
|
@@ -32,10 +33,40 @@ export const OCCULITE_TIMEOUT_MS = 5000;
|
|
|
32
33
|
* box - so the header is only a claim, and the box is asked about it like about any other session.
|
|
33
34
|
*/
|
|
34
35
|
export const OCCULITE_SESSION_HEADER = 'x-occulite-session';
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* B-36: what an openccu-lite session id looks like, and nothing else is one.
|
|
38
|
+
*
|
|
39
|
+
* - **Since openccu-lite task 125** (occulited `1f71597`, their D-77) a session id is what Go's
|
|
40
|
+
* `crypto/rand.Text` hands out: 26 characters of base32, `A-Z` and `2-7`. The box's API and its gate
|
|
41
|
+
* match exactly this (`sidRe`, the gate's `SID_PATTERN`).
|
|
42
|
+
* - **Before task 125** it was the CCU's shape, ten of `[0-9a-zA-Z]` (auth-off's fixed id too). Since
|
|
43
|
+
* then that shape is only a session's *legacy alias*, which the addon CGIs parse and the box's API
|
|
44
|
+
* refuses as a credential from anywhere. The two cannot be told apart by their characters: a
|
|
45
|
+
* ten-character value is a session on an older box and never one on a newer box.
|
|
46
|
+
* - **An API token** (`olt_` and 32 hex digits) is neither and is never a session: the header path
|
|
47
|
+
* asks `/api/auth/v1/state`, which also answers for a token.
|
|
48
|
+
*
|
|
49
|
+
* A value of another shape or length cannot be an openccu-lite session on any image, so it is refused
|
|
50
|
+
* here, without asking the box.
|
|
51
|
+
*/
|
|
52
|
+
const SESSION_ID = /^[A-Z2-7]{26}$/;
|
|
53
|
+
const LEGACY_ID = /^[0-9a-zA-Z]{10}$/;
|
|
54
|
+
/** A session id as the shell or the gate hands it over, `@…@` and all, or `undefined` when it is not one. */
|
|
36
55
|
export function parseSid(value) {
|
|
37
56
|
const bare = (value ?? '').replace(/^@|@$/g, '').trim();
|
|
38
|
-
return
|
|
57
|
+
return SESSION_ID.test(bare) || LEGACY_ID.test(bare) ? bare : undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* B-36: can the `?sid=` of a request be a session of the box whose gate named `gate`?
|
|
61
|
+
*
|
|
62
|
+
* A gate header of the 26-character shape says the box issues session ids of that shape (task 125),
|
|
63
|
+
* so a `?sid=` of any other shape there is the session's legacy alias, which the box's API refuses:
|
|
64
|
+
* the answer is known without asking. Without such a header - a CCU, an image from before the header,
|
|
65
|
+
* a ten-character header - nothing can be concluded, and the box decides as before. This only ever
|
|
66
|
+
* *refuses*: a `true` still has to be confirmed by the box.
|
|
67
|
+
*/
|
|
68
|
+
export function sidCanBeSession(sid, gate) {
|
|
69
|
+
return gate === undefined || !SESSION_ID.test(gate) || SESSION_ID.test(sid);
|
|
39
70
|
}
|
|
40
71
|
/** Checks a session id against the box that issued it. */
|
|
41
72
|
export class OcculiteAuthenticator {
|
package/dist/server.js
CHANGED
|
@@ -29,7 +29,7 @@ import { applyCookieToken, applySessionToken, clearedSessionCookie, createToken,
|
|
|
29
29
|
import { DeviceImageService, readIconMapFile } from './images.js';
|
|
30
30
|
import { clientAddress, parseLoginForm, pickLanguage, readBody, renderLoginPage, } from './login.js';
|
|
31
31
|
import { createLogger, silentLogger } from './log.js';
|
|
32
|
-
import { OCCULITE_SESSION_HEADER, OcculiteAuthenticator, parseSid } from './occulite.js';
|
|
32
|
+
import { OCCULITE_SESSION_HEADER, OcculiteAuthenticator, parseSid, sidCanBeSession, } from './occulite.js';
|
|
33
33
|
import { defaultDataDir, defaultMetadataDir, defaultUiDir, packageVersion } from './paths.js';
|
|
34
34
|
import { proxyRequest, proxyUpgrade } from './proxy.js';
|
|
35
35
|
import { RateLimiter, SessionStore } from './sessions.js';
|
|
@@ -184,8 +184,27 @@ export async function createWebHost(options = {}) {
|
|
|
184
184
|
// session is checked against the box, turned into one of ours, and taken off the URL again
|
|
185
185
|
// so that it does not sit in the address bar, in a bookmark and in every referrer.
|
|
186
186
|
if (boxSessions && sessions && (method === 'GET' || method === 'HEAD')) {
|
|
187
|
-
const offered = url.searchParams.get('sid');
|
|
188
|
-
if (
|
|
187
|
+
const offered = parseSid(url.searchParams.get('sid'));
|
|
188
|
+
if (offered !== undefined) {
|
|
189
|
+
// B-36: the gate header first. Since openccu-lite task 125 the `?sid=` a shell puts on
|
|
190
|
+
// the URL is the session's legacy alias, which the box's API refuses, while the same
|
|
191
|
+
// request carries the session the gate validated: a confirmed header signs the request
|
|
192
|
+
// in, whatever `?sid=` says, and the `?sid=` only comes off the URL.
|
|
193
|
+
const gate = gateSid(request);
|
|
194
|
+
if (gate !== undefined) {
|
|
195
|
+
const session = await sessionFromGate(request, sessions.get(readCookie(request.headers.cookie, SESSION_COOKIE)));
|
|
196
|
+
if (session?.credential === gate) {
|
|
197
|
+
redirectWithoutSid(request, response, session, url, rest);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!sidCanBeSession(offered, gate)) {
|
|
202
|
+
// what the box would answer, without asking it: see sidCanBeSession
|
|
203
|
+
log.warn(`login: the session offered from ${clientAddress(request)} cannot be one of this openccu-lite's (not asked)`);
|
|
204
|
+
response.writeHead(302, { Location: '/', 'Cache-Control': 'no-store' });
|
|
205
|
+
response.end();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
189
208
|
await handleHandover(request, response, offered, url, rest);
|
|
190
209
|
return;
|
|
191
210
|
}
|
|
@@ -356,17 +375,19 @@ export async function createWebHost(options = {}) {
|
|
|
356
375
|
response.end();
|
|
357
376
|
return;
|
|
358
377
|
}
|
|
359
|
-
const
|
|
360
|
-
const session = store.create(checked.name, checked.level, checked.sid);
|
|
378
|
+
const session = sessions.create(checked.name, checked.level, checked.sid);
|
|
361
379
|
backend?.noteMetaSession(checked.sid);
|
|
362
380
|
log.info(`login: ${checked.name} (level ${String(checked.level)}) through the openccu-lite shell`);
|
|
363
|
-
|
|
381
|
+
redirectWithoutSid(request, response, session, url, rest);
|
|
382
|
+
}
|
|
383
|
+
/** The same page without the session in the URL, with the cookie of the session it was let in by. */
|
|
384
|
+
function redirectWithoutSid(request, response, session, url, rest) {
|
|
364
385
|
const query = new URLSearchParams(url.searchParams);
|
|
365
386
|
query.delete('sid');
|
|
366
387
|
const search = query.size === 0 ? '' : `?${query.toString()}`;
|
|
367
388
|
response.writeHead(302, {
|
|
368
389
|
Location: `${base}${rest}${search}`,
|
|
369
|
-
'Set-Cookie': sessionCookie(session.id, base,
|
|
390
|
+
'Set-Cookie': sessionCookie(session.id, base, sessions.ttlMs / 1000, isHttps(request)),
|
|
370
391
|
'Cache-Control': 'no-store',
|
|
371
392
|
});
|
|
372
393
|
response.end();
|
|
@@ -403,9 +424,9 @@ export async function createWebHost(options = {}) {
|
|
|
403
424
|
/**
|
|
404
425
|
* B-94, D-65: the session of a request that came through openccu-lite's gate.
|
|
405
426
|
*
|
|
406
|
-
* The
|
|
407
|
-
*
|
|
408
|
-
*
|
|
427
|
+
* The cookie stays as it is; this adds a third way in, for a request that has none - a bookmark,
|
|
428
|
+
* a reload after our session expired - or whose cookie belongs to another box session than the
|
|
429
|
+
* one the gate found. A request with `?sid=` asks this first too (B-36). The header is a claim until the box
|
|
409
430
|
* confirms it (`GET /api/auth/v1/state`): a CCU and an older openccu-lite image pass a client's
|
|
410
431
|
* header through, and the backend's port is open to every process on the box.
|
|
411
432
|
*/
|
|
@@ -20,6 +20,7 @@ import { InterfaceManager, type InterfaceManagerOptions } from '../interfaces/ma
|
|
|
20
20
|
import { type MetaServiceOptions } from '../meta/service.js';
|
|
21
21
|
import { RegaService, type RegaServiceOptions } from '../rega/client.js';
|
|
22
22
|
import { ApiEventEmitter } from '../util/emitter.js';
|
|
23
|
+
import { type CallbackNetwork } from '../util/net.js';
|
|
23
24
|
/** How often the BidCos service messages are polled while the connection is up. */
|
|
24
25
|
export declare const SERVICE_MESSAGE_POLL_MS = 300000;
|
|
25
26
|
/** How long the HmIP `getParamset(:0, VALUES)` sweep waits for the device list to settle. */
|
|
@@ -45,6 +46,11 @@ export interface BackendOptions extends Omit<ConfigStoreOptions, 'version'> {
|
|
|
45
46
|
* popup to say that the callback ports must be published unchanged.
|
|
46
47
|
*/
|
|
47
48
|
readonly inContainer?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* B-53: the interfaces, name lookup and route probe the automatic callback address comes from.
|
|
51
|
+
* Absent, the machine's own - or, where `localAddresses` is injected, that list alone.
|
|
52
|
+
*/
|
|
53
|
+
readonly network?: CallbackNetwork;
|
|
48
54
|
readonly rpcTimeoutMs?: number;
|
|
49
55
|
readonly watchdogIntervalMs?: number;
|
|
50
56
|
readonly serviceMessagePollMs?: number;
|
|
@@ -28,8 +28,9 @@ import { MetaService } from '../meta/service.js';
|
|
|
28
28
|
import { RegaService } from '../rega/client.js';
|
|
29
29
|
import { listDevicesAnswer } from '../rpc/server.js';
|
|
30
30
|
import { ApiEventEmitter } from '../util/emitter.js';
|
|
31
|
-
import {
|
|
32
|
-
import { isWriteMethod } from '../
|
|
31
|
+
import { describeCallbackAddresses, staticNetwork, systemNetwork } from '../util/net.js';
|
|
32
|
+
import { RpcLog, isWriteMethod } from '../rpc/log.js';
|
|
33
|
+
import { currentOrigin, runWithOrigin } from '../rpc/origin.js';
|
|
33
34
|
import { ParamsetWriter } from '../write/paramset.js';
|
|
34
35
|
import { ConfigRepair } from '../write/repair.js';
|
|
35
36
|
import { WriteQueue } from '../write/queue.js';
|
|
@@ -56,7 +57,7 @@ export class Backend {
|
|
|
56
57
|
#options;
|
|
57
58
|
#config;
|
|
58
59
|
#queue;
|
|
59
|
-
#
|
|
60
|
+
#rpcLog;
|
|
60
61
|
#unknownMethodsSeen = new Set();
|
|
61
62
|
#writer;
|
|
62
63
|
#repair;
|
|
@@ -125,14 +126,14 @@ export class Backend {
|
|
|
125
126
|
this.#queue = new WriteQueue({
|
|
126
127
|
paceFor: (interfaceName) => writePaceFor(interfaceName, this.#config.connection.writePaceMs),
|
|
127
128
|
});
|
|
128
|
-
this.#
|
|
129
|
+
this.#rpcLog = new RpcLog({
|
|
129
130
|
file: config.cacheFile('write-log.json'),
|
|
130
131
|
rpcLogFolder: config.connection.rpcLogFolder,
|
|
131
132
|
onAppended: (entry) => {
|
|
132
|
-
this.events.emit('
|
|
133
|
+
this.events.emit('rpcLog.appended', entry);
|
|
133
134
|
},
|
|
134
135
|
onError: (error) => {
|
|
135
|
-
this.#notice('warn', `
|
|
136
|
+
this.#notice('warn', `RPC log: ${errorMessage(error)}`);
|
|
136
137
|
},
|
|
137
138
|
...(options.cacheWriteDelayMs === undefined ? {} : { writeDelayMs: options.cacheWriteDelayMs }),
|
|
138
139
|
});
|
|
@@ -166,7 +167,7 @@ export class Backend {
|
|
|
166
167
|
});
|
|
167
168
|
await caches.load();
|
|
168
169
|
const backend = new Backend(options, config, caches);
|
|
169
|
-
await backend.#
|
|
170
|
+
await backend.#rpcLog.load();
|
|
170
171
|
await backend.#linkTemplates.load();
|
|
171
172
|
return backend;
|
|
172
173
|
}
|
|
@@ -213,7 +214,7 @@ export class Backend {
|
|
|
213
214
|
await this.#meta?.stop();
|
|
214
215
|
this.#meta = undefined;
|
|
215
216
|
await this.#caches.flush();
|
|
216
|
-
await this.#
|
|
217
|
+
await this.#rpcLog.flush();
|
|
217
218
|
this.events.clear();
|
|
218
219
|
}
|
|
219
220
|
/**
|
|
@@ -236,7 +237,7 @@ export class Backend {
|
|
|
236
237
|
}
|
|
237
238
|
if (this.#sessions > 0) {
|
|
238
239
|
if (previous === 0) {
|
|
239
|
-
void this.#resubscribe();
|
|
240
|
+
void runWithOrigin('background', () => this.#resubscribe());
|
|
240
241
|
}
|
|
241
242
|
return;
|
|
242
243
|
}
|
|
@@ -246,7 +247,7 @@ export class Backend {
|
|
|
246
247
|
}
|
|
247
248
|
const timer = setTimeout(() => {
|
|
248
249
|
this.#idleTimer = undefined;
|
|
249
|
-
void this.#unsubscribeIdle();
|
|
250
|
+
void runWithOrigin('background', () => this.#unsubscribeIdle());
|
|
250
251
|
}, grace);
|
|
251
252
|
if (typeof timer.unref === 'function') {
|
|
252
253
|
timer.unref();
|
|
@@ -279,7 +280,9 @@ export class Backend {
|
|
|
279
280
|
*/
|
|
280
281
|
async request(method, ...params) {
|
|
281
282
|
try {
|
|
282
|
-
|
|
283
|
+
// Task 48: everything a request awaits is a UI action for the RPC log; the console's
|
|
284
|
+
// own call is the console's. Whatever runs outside a request is background work.
|
|
285
|
+
return await runWithOrigin(method === 'rpc.call' ? 'console' : 'ui', () => this.#dispatch(method, params));
|
|
283
286
|
}
|
|
284
287
|
catch (error) {
|
|
285
288
|
throw error instanceof BackendError ? error : internalError(errorMessage(error), error);
|
|
@@ -305,6 +308,8 @@ export class Backend {
|
|
|
305
308
|
return this.#discover();
|
|
306
309
|
case 'config.clearCaches':
|
|
307
310
|
return this.#clearCaches();
|
|
311
|
+
case 'config.callbackAddresses':
|
|
312
|
+
return this.#callbackAddresses(p[0]);
|
|
308
313
|
case 'interfaces.list':
|
|
309
314
|
return this.#manager?.states() ?? [];
|
|
310
315
|
case 'interfaces.reconnect':
|
|
@@ -468,10 +473,10 @@ export class Backend {
|
|
|
468
473
|
return this.#methods(p[0]);
|
|
469
474
|
case 'write.cancel':
|
|
470
475
|
return this.#queue.cancel(p[0]);
|
|
471
|
-
case '
|
|
472
|
-
return this.#
|
|
473
|
-
case '
|
|
474
|
-
this.#
|
|
476
|
+
case 'rpcLog.list':
|
|
477
|
+
return this.#rpcLog.list(p[0]);
|
|
478
|
+
case 'rpcLog.clear':
|
|
479
|
+
this.#rpcLog.clear();
|
|
475
480
|
return null;
|
|
476
481
|
case 'data.file':
|
|
477
482
|
return this.#files.read(p[0]);
|
|
@@ -493,7 +498,15 @@ export class Backend {
|
|
|
493
498
|
}
|
|
494
499
|
return this.#manager;
|
|
495
500
|
}
|
|
496
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Task 48: the connection is background work even when a `config.set` from the settings dialog
|
|
503
|
+
* asks for it - the watchdog, the callback servers and the polling timers it creates would
|
|
504
|
+
* otherwise inherit that request's origin for the rest of the session.
|
|
505
|
+
*/
|
|
506
|
+
#connect() {
|
|
507
|
+
return runWithOrigin('background', () => this.#connectNow());
|
|
508
|
+
}
|
|
509
|
+
async #connectNow() {
|
|
497
510
|
const connection = this.#config.connection;
|
|
498
511
|
this.#noServiceMessages.clear();
|
|
499
512
|
this.#serviceMessageFailures.clear();
|
|
@@ -508,10 +521,11 @@ export class Backend {
|
|
|
508
521
|
onNotice: (level, message, interfaceName) => {
|
|
509
522
|
this.#notice(level, message, interfaceName);
|
|
510
523
|
},
|
|
511
|
-
onConnected: (interfaceName) => this.#onInterfaceConnected(interfaceName),
|
|
524
|
+
onConnected: (interfaceName) => runWithOrigin('background', () => this.#onInterfaceConnected(interfaceName)),
|
|
512
525
|
onCall: (record) => {
|
|
513
526
|
this.#onCall(record);
|
|
514
527
|
},
|
|
528
|
+
originOf: currentOrigin,
|
|
515
529
|
...(this.#options.callbackHost === undefined ? {} : { callbackHost: this.#options.callbackHost }),
|
|
516
530
|
...(this.#options.defaultCallbackPorts === undefined
|
|
517
531
|
? {}
|
|
@@ -522,6 +536,8 @@ export class Backend {
|
|
|
522
536
|
? {}
|
|
523
537
|
: { watchdogIntervalMs: this.#options.watchdogIntervalMs }),
|
|
524
538
|
...(this.#options.localAddresses === undefined ? {} : { localAddresses: this.#options.localAddresses }),
|
|
539
|
+
...(this.#options.network === undefined ? {} : { network: this.#options.network }),
|
|
540
|
+
...(this.#keepsConfiguredCallbackIp() ? { keepConfiguredCallbackIp: true } : {}),
|
|
525
541
|
...(this.#options.now === undefined ? {} : { now: this.#options.now }),
|
|
526
542
|
...this.#options.interfaceManagerOptions,
|
|
527
543
|
});
|
|
@@ -833,7 +849,7 @@ export class Backend {
|
|
|
833
849
|
* RPC
|
|
834
850
|
*/
|
|
835
851
|
#onCall(record) {
|
|
836
|
-
this.#
|
|
852
|
+
this.#rpcLog.append(record);
|
|
837
853
|
}
|
|
838
854
|
/** A read: straight to the interface, never queued. */
|
|
839
855
|
async #read(interfaceName, method, params) {
|
|
@@ -842,7 +858,10 @@ export class Backend {
|
|
|
842
858
|
/** A write: through the paced queue of that interface. */
|
|
843
859
|
async #write(interfaceName, method, params) {
|
|
844
860
|
const client = this.#requireManager().client(interfaceName);
|
|
845
|
-
|
|
861
|
+
// Task 48: the queue runs a task from the timer that drained it, which is the *previous*
|
|
862
|
+
// task's context; the origin is the one of whoever enqueued, so it is taken here.
|
|
863
|
+
const origin = currentOrigin();
|
|
864
|
+
return this.#queue.enqueue(interfaceName, () => client.call(method, params, { origin }));
|
|
846
865
|
}
|
|
847
866
|
/*
|
|
848
867
|
* configuration
|
|
@@ -877,7 +896,7 @@ export class Backend {
|
|
|
877
896
|
: [];
|
|
878
897
|
await this.#disconnect();
|
|
879
898
|
const config = this.#withHostFacts(await this.#config.setConnection(connection));
|
|
880
|
-
this.#
|
|
899
|
+
this.#rpcLog.setRpcLogFolder(config.connection.rpcLogFolder);
|
|
881
900
|
if (config.connection.host !== previousHost) {
|
|
882
901
|
await this.#caches.flush();
|
|
883
902
|
this.#caches = new CacheStore({
|
|
@@ -942,6 +961,27 @@ export class Backend {
|
|
|
942
961
|
}
|
|
943
962
|
}
|
|
944
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* B-53: a callback address set at start (task 38) or inside a container is taken as it is - the
|
|
966
|
+
* Docker host's address is none of the container's, and it is still the right one.
|
|
967
|
+
*/
|
|
968
|
+
#keepsConfiguredCallbackIp() {
|
|
969
|
+
return this.#options.inContainer === true || this.#config.callbackPins?.ip === true;
|
|
970
|
+
}
|
|
971
|
+
#network() {
|
|
972
|
+
if (this.#options.network !== undefined) {
|
|
973
|
+
return this.#options.network;
|
|
974
|
+
}
|
|
975
|
+
const injected = this.#options.localAddresses;
|
|
976
|
+
return injected === undefined ? systemNetwork() : staticNetwork(injected);
|
|
977
|
+
}
|
|
978
|
+
/** B-53: `config.callbackAddresses` - for the host being typed, or the configured one. */
|
|
979
|
+
async #callbackAddresses(host) {
|
|
980
|
+
const { connection } = this.#config;
|
|
981
|
+
const target = typeof host === 'string' ? host.trim() : connection.host;
|
|
982
|
+
const info = await describeCallbackAddresses(target, this.#network(), connection.local === true);
|
|
983
|
+
return this.#keepsConfiguredCallbackIp() ? { ...info, keepsConfigured: true } : info;
|
|
984
|
+
}
|
|
945
985
|
async #discover() {
|
|
946
986
|
const discover = this.#options.discover ?? ((options) => discoverCcus(options));
|
|
947
987
|
const found = await discover({ tls: this.#config.connection.tls });
|
|
@@ -1282,7 +1322,7 @@ export class Backend {
|
|
|
1282
1322
|
return;
|
|
1283
1323
|
}
|
|
1284
1324
|
const timer = setInterval(() => {
|
|
1285
|
-
void this.pollServiceMessages();
|
|
1325
|
+
void runWithOrigin('background', () => this.pollServiceMessages());
|
|
1286
1326
|
}, interval);
|
|
1287
1327
|
if (typeof timer.unref === 'function') {
|
|
1288
1328
|
timer.unref();
|
|
@@ -1310,7 +1350,7 @@ export class Backend {
|
|
|
1310
1350
|
}
|
|
1311
1351
|
const timer = setTimeout(() => {
|
|
1312
1352
|
this.#hmipSweepTimer = undefined;
|
|
1313
|
-
void this.sweepHmip();
|
|
1353
|
+
void runWithOrigin('background', () => this.sweepHmip());
|
|
1314
1354
|
}, this.#options.hmipSweepDelayMs ?? HMIP_SWEEP_DELAY_MS);
|
|
1315
1355
|
if (typeof timer.unref === 'function') {
|
|
1316
1356
|
timer.unref();
|
|
@@ -1565,6 +1605,7 @@ export const API_METHOD_NAMES = [
|
|
|
1565
1605
|
'config.set',
|
|
1566
1606
|
'config.discover',
|
|
1567
1607
|
'config.clearCaches',
|
|
1608
|
+
'config.callbackAddresses',
|
|
1568
1609
|
'interfaces.list',
|
|
1569
1610
|
'interfaces.reconnect',
|
|
1570
1611
|
'rega.state',
|
|
@@ -1630,8 +1671,8 @@ export const API_METHOD_NAMES = [
|
|
|
1630
1671
|
'rpc.call',
|
|
1631
1672
|
'rpc.methods',
|
|
1632
1673
|
'write.cancel',
|
|
1633
|
-
'
|
|
1634
|
-
'
|
|
1674
|
+
'rpcLog.list',
|
|
1675
|
+
'rpcLog.clear',
|
|
1635
1676
|
'data.file',
|
|
1636
1677
|
'session.info',
|
|
1637
1678
|
];
|
|
@@ -21,6 +21,8 @@ export * from './config/legacyImport.js';
|
|
|
21
21
|
export * from './config/store.js';
|
|
22
22
|
export * from './config/linkTemplates.js';
|
|
23
23
|
export * from './rpc/client.js';
|
|
24
|
+
export * from './rpc/log.js';
|
|
25
|
+
export * from './rpc/origin.js';
|
|
24
26
|
export * from './rpc/server.js';
|
|
25
27
|
export * from './interfaces/manager.js';
|
|
26
28
|
export * from './cache/devices.js';
|
|
@@ -30,7 +32,6 @@ export * from './cache/unreach.js';
|
|
|
30
32
|
export * from './cache/store.js';
|
|
31
33
|
export * from './meta/index.js';
|
|
32
34
|
export * from './write/queue.js';
|
|
33
|
-
export * from './write/log.js';
|
|
34
35
|
export * from './write/paramset.js';
|
|
35
36
|
export * from './rega/auth.js';
|
|
36
37
|
export * from './rega/client.js';
|
|
@@ -24,6 +24,8 @@ export * from './config/store.js';
|
|
|
24
24
|
export * from './config/linkTemplates.js';
|
|
25
25
|
// the protocol layer
|
|
26
26
|
export * from './rpc/client.js';
|
|
27
|
+
export * from './rpc/log.js';
|
|
28
|
+
export * from './rpc/origin.js';
|
|
27
29
|
export * from './rpc/server.js';
|
|
28
30
|
export * from './interfaces/manager.js';
|
|
29
31
|
// state
|
|
@@ -36,7 +38,6 @@ export * from './cache/store.js';
|
|
|
36
38
|
export * from './meta/index.js';
|
|
37
39
|
// the write path (task 6)
|
|
38
40
|
export * from './write/queue.js';
|
|
39
|
-
export * from './write/log.js';
|
|
40
41
|
export * from './write/paramset.js';
|
|
41
42
|
// the optional and the peripheral
|
|
42
43
|
export * from './rega/auth.js';
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
* dialog. 2.x probed six ports with a 5 s timeout each before the window became usable, which is
|
|
20
20
|
* the root of the "endless loading" issues #121, #126, #128 and #134.
|
|
21
21
|
*/
|
|
22
|
-
import type { CallbackPins, ConnectionConfig, InterfaceState, ResolvedInterface } from '@homematic-manager/core';
|
|
22
|
+
import type { CallbackPins, CallbackWarning, ConnectionConfig, InterfaceState, ResolvedInterface } from '@homematic-manager/core';
|
|
23
23
|
import { type InterfaceTarget } from '../config/defaults.js';
|
|
24
24
|
import { RpcClient, type RpcCallRecord, type RpcClientOptions } from '../rpc/client.js';
|
|
25
25
|
import { type CallbackHandler, type CallbackServerSet } from '../rpc/server.js';
|
|
26
|
-
import { type PortProbe } from '../util/net.js';
|
|
26
|
+
import { type CallbackNetwork, type PortProbe } from '../util/net.js';
|
|
27
27
|
/** How often the watchdog looks at every interface. 2.x used the same 15 s. */
|
|
28
28
|
export declare const WATCHDOG_INTERVAL_MS = 15000;
|
|
29
29
|
/** Where an interface process on this very box calls back (#144). */
|
|
@@ -65,6 +65,8 @@ export interface InterfaceManagerOptions {
|
|
|
65
65
|
/** Called after a successful `init`; the backend fills its caches there. */
|
|
66
66
|
readonly onConnected?: (interfaceName: string) => void | Promise<void>;
|
|
67
67
|
readonly onCall?: (record: RpcCallRecord) => void;
|
|
68
|
+
/** Task 48: the origin of a call that names none, handed to every client (the backend's request context). */
|
|
69
|
+
readonly originOf?: RpcClientOptions['originOf'];
|
|
68
70
|
readonly now?: () => number;
|
|
69
71
|
readonly rpcTimeoutMs?: number;
|
|
70
72
|
readonly watchdogIntervalMs?: number;
|
|
@@ -93,8 +95,20 @@ export interface InterfaceManagerOptions {
|
|
|
93
95
|
readonly createClient?: (options: RpcClientOptions) => RpcClient;
|
|
94
96
|
readonly createCallbackServers?: (handler: CallbackHandler) => CallbackServerSet;
|
|
95
97
|
readonly probe?: (host: string, port: number) => Promise<PortProbe>;
|
|
96
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Injected for the callback address; defaults to this machine's IPv4 addresses. A list given
|
|
100
|
+
* here has no netmasks, so the automatic choice is its first entry (or the loopback for a CCU
|
|
101
|
+
* on the loopback) unless {@link network} is given as well.
|
|
102
|
+
*/
|
|
97
103
|
readonly localAddresses?: () => string[];
|
|
104
|
+
/** B-53: the interfaces, name lookup and route probe the automatic callback address is worked out from. */
|
|
105
|
+
readonly network?: CallbackNetwork;
|
|
106
|
+
/**
|
|
107
|
+
* B-53: take a set callback address as it is, even when it is no address of this machine - the
|
|
108
|
+
* host set it at start (task 38) or runs in a container, where the Docker host's address is the
|
|
109
|
+
* one that works. No fallback and no warning then.
|
|
110
|
+
*/
|
|
111
|
+
readonly keepConfiguredCallbackIp?: boolean;
|
|
98
112
|
/**
|
|
99
113
|
* Overrides the port of one interface, for a process that does not sit on the well-known one:
|
|
100
114
|
* the integration tests point at an hm-simulator on an ephemeral port, and an unusual proxy
|
|
@@ -132,8 +146,14 @@ export declare class InterfaceManager {
|
|
|
132
146
|
* one address that changes - a new lease, another network - while `init` registrations survive
|
|
133
147
|
* such a change in the interface process's handler list, and every other local subscriber on a
|
|
134
148
|
* CCU registers on the loopback, which is what a look at that list expects to see.
|
|
149
|
+
*
|
|
150
|
+
* B-53 (#162, #165): with no address set, the one in the CCU's network, else the one the route
|
|
151
|
+
* to the CCU leaves from, else the first one that is not link-local - worked out again at every
|
|
152
|
+
* `init`. Until the first `init` this is the same choice without the name lookup and the route.
|
|
135
153
|
*/
|
|
136
154
|
get callbackIp(): string;
|
|
155
|
+
/** B-53: the callback address set in the settings that does not fit, as of the last connect. */
|
|
156
|
+
get callbackWarning(): CallbackWarning | undefined;
|
|
137
157
|
/** D-31: are the subscriptions currently dropped because nobody is looking? */
|
|
138
158
|
get idle(): boolean;
|
|
139
159
|
/** The interfaces whose ports answered the last background probe. */
|