@matter/node 0.13.0 → 0.13.1-alpha.0-20250501-80c86b03e
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/dist/cjs/behavior/context/server/OnlineContext.d.ts.map +1 -1
- package/dist/cjs/behavior/context/server/OnlineContext.js +6 -3
- package/dist/cjs/behavior/context/server/OnlineContext.js.map +1 -1
- package/dist/cjs/behavior/system/controller/ControllerBehavior.js +2 -1
- package/dist/cjs/behavior/system/controller/ControllerBehavior.js.map +1 -1
- package/dist/cjs/behavior/system/network/ServerNetworkRuntime.d.ts.map +1 -1
- package/dist/cjs/behavior/system/network/ServerNetworkRuntime.js +7 -7
- package/dist/cjs/behavior/system/network/ServerNetworkRuntime.js.map +1 -1
- package/dist/cjs/behavior/system/subscription/SubscriptionBehavior.d.ts +2 -3
- package/dist/cjs/behavior/system/subscription/SubscriptionBehavior.d.ts.map +1 -1
- package/dist/cjs/behavior/system/subscription/SubscriptionBehavior.js.map +1 -1
- package/dist/cjs/node/server/InteractionServer.d.ts +84 -0
- package/dist/cjs/node/server/InteractionServer.d.ts.map +1 -0
- package/dist/cjs/node/server/InteractionServer.js +1326 -0
- package/dist/cjs/node/server/InteractionServer.js.map +6 -0
- package/dist/cjs/node/server/ProtocolService.d.ts.map +1 -1
- package/dist/cjs/node/server/ProtocolService.js +9 -8
- package/dist/cjs/node/server/ProtocolService.js.map +2 -2
- package/dist/cjs/node/server/index.d.ts +1 -1
- package/dist/cjs/node/server/index.d.ts.map +1 -1
- package/dist/cjs/node/server/index.js +1 -1
- package/dist/cjs/node/server/index.js.map +1 -1
- package/dist/esm/behavior/context/server/OnlineContext.d.ts.map +1 -1
- package/dist/esm/behavior/context/server/OnlineContext.js +6 -3
- package/dist/esm/behavior/context/server/OnlineContext.js.map +1 -1
- package/dist/esm/behavior/system/controller/ControllerBehavior.js +1 -1
- package/dist/esm/behavior/system/controller/ControllerBehavior.js.map +1 -1
- package/dist/esm/behavior/system/network/ServerNetworkRuntime.d.ts.map +1 -1
- package/dist/esm/behavior/system/network/ServerNetworkRuntime.js +2 -3
- package/dist/esm/behavior/system/network/ServerNetworkRuntime.js.map +1 -1
- package/dist/esm/behavior/system/subscription/SubscriptionBehavior.d.ts +2 -3
- package/dist/esm/behavior/system/subscription/SubscriptionBehavior.d.ts.map +1 -1
- package/dist/esm/behavior/system/subscription/SubscriptionBehavior.js.map +1 -1
- package/dist/esm/node/server/InteractionServer.d.ts +84 -0
- package/dist/esm/node/server/InteractionServer.d.ts.map +1 -0
- package/dist/esm/node/server/InteractionServer.js +1348 -0
- package/dist/esm/node/server/InteractionServer.js.map +6 -0
- package/dist/esm/node/server/ProtocolService.d.ts.map +1 -1
- package/dist/esm/node/server/ProtocolService.js +9 -8
- package/dist/esm/node/server/ProtocolService.js.map +1 -1
- package/dist/esm/node/server/index.d.ts +1 -1
- package/dist/esm/node/server/index.d.ts.map +1 -1
- package/dist/esm/node/server/index.js +1 -1
- package/package.json +7 -7
- package/src/behavior/context/server/OnlineContext.ts +9 -4
- package/src/behavior/system/controller/ControllerBehavior.ts +1 -1
- package/src/behavior/system/network/ServerNetworkRuntime.ts +4 -7
- package/src/behavior/system/subscription/SubscriptionBehavior.ts +2 -3
- package/src/node/server/InteractionServer.ts +1757 -0
- package/src/node/server/ProtocolService.ts +10 -8
- package/src/node/server/index.ts +1 -1
- package/dist/cjs/node/server/TransactionalInteractionServer.d.ts +0 -57
- package/dist/cjs/node/server/TransactionalInteractionServer.d.ts.map +0 -1
- package/dist/cjs/node/server/TransactionalInteractionServer.js +0 -334
- package/dist/cjs/node/server/TransactionalInteractionServer.js.map +0 -6
- package/dist/esm/node/server/TransactionalInteractionServer.d.ts +0 -57
- package/dist/esm/node/server/TransactionalInteractionServer.d.ts.map +0 -1
- package/dist/esm/node/server/TransactionalInteractionServer.js +0 -322
- package/dist/esm/node/server/TransactionalInteractionServer.js.map +0 -6
- package/src/node/server/TransactionalInteractionServer.ts +0 -413
|
@@ -0,0 +1,1757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2025 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ActionContext } from "#behavior/context/ActionContext.js";
|
|
8
|
+
import { ActionTracer } from "#behavior/context/ActionTracer.js";
|
|
9
|
+
import { NodeActivity } from "#behavior/context/NodeActivity.js";
|
|
10
|
+
import { OfflineContext } from "#behavior/context/server/OfflineContext.js";
|
|
11
|
+
import { OnlineContext } from "#behavior/context/server/OnlineContext.js";
|
|
12
|
+
import { AccessControlServer } from "#behaviors/access-control";
|
|
13
|
+
import { AccessControlCluster } from "#clusters/access-control";
|
|
14
|
+
import { Endpoint } from "#endpoint/Endpoint.js";
|
|
15
|
+
import { EndpointLifecycle } from "#endpoint/properties/EndpointLifecycle.js";
|
|
16
|
+
import { EndpointServer } from "#endpoint/server/EndpointServer.js";
|
|
17
|
+
import {
|
|
18
|
+
Crypto,
|
|
19
|
+
Diagnostic,
|
|
20
|
+
InternalError,
|
|
21
|
+
Logger,
|
|
22
|
+
MatterError,
|
|
23
|
+
MaybePromise,
|
|
24
|
+
Observable,
|
|
25
|
+
ServerAddressIp,
|
|
26
|
+
} from "#general";
|
|
27
|
+
import { ClusterModel, CommandModel, GLOBAL_IDS, MatterModel, Specification } from "#model";
|
|
28
|
+
import {
|
|
29
|
+
AccessControl,
|
|
30
|
+
AccessDeniedError,
|
|
31
|
+
AnyAttributeServer,
|
|
32
|
+
AnyEventServer,
|
|
33
|
+
assertSecureSession,
|
|
34
|
+
AttributePath,
|
|
35
|
+
attributePathToId,
|
|
36
|
+
AttributeServer,
|
|
37
|
+
clusterPathToId,
|
|
38
|
+
CommandPath,
|
|
39
|
+
commandPathToId,
|
|
40
|
+
CommandServer,
|
|
41
|
+
DataReport,
|
|
42
|
+
DataReportPayloadIterator,
|
|
43
|
+
decodeAttributeValueWithSchema,
|
|
44
|
+
decodeListAttributeValueWithSchema,
|
|
45
|
+
EndpointInterface,
|
|
46
|
+
EventPath,
|
|
47
|
+
EventReportPayload,
|
|
48
|
+
ExchangeManager,
|
|
49
|
+
expandPathsInAttributeData,
|
|
50
|
+
FabricScopedAttributeServer,
|
|
51
|
+
InteractionEndpointStructure,
|
|
52
|
+
InteractionRecipient,
|
|
53
|
+
InteractionServerMessenger,
|
|
54
|
+
InvokeRequest,
|
|
55
|
+
Message,
|
|
56
|
+
MessageExchange,
|
|
57
|
+
MessageType,
|
|
58
|
+
PeerAddress,
|
|
59
|
+
ProtocolHandler,
|
|
60
|
+
ReadRequest,
|
|
61
|
+
SecureSession,
|
|
62
|
+
ServerSubscription,
|
|
63
|
+
ServerSubscriptionConfig,
|
|
64
|
+
ServerSubscriptionContext,
|
|
65
|
+
SessionManager,
|
|
66
|
+
SessionType,
|
|
67
|
+
SubscribeRequest,
|
|
68
|
+
TimedRequest,
|
|
69
|
+
WriteRequest,
|
|
70
|
+
WriteResponse,
|
|
71
|
+
} from "#protocol";
|
|
72
|
+
import {
|
|
73
|
+
ArraySchema,
|
|
74
|
+
ClusterId,
|
|
75
|
+
CommandId,
|
|
76
|
+
DEFAULT_MAX_PATHS_PER_INVOKE,
|
|
77
|
+
EventNumber,
|
|
78
|
+
INTERACTION_PROTOCOL_ID,
|
|
79
|
+
ReceivedStatusResponseError,
|
|
80
|
+
StatusCode,
|
|
81
|
+
StatusResponseError,
|
|
82
|
+
TlvAny,
|
|
83
|
+
TlvAttributePath,
|
|
84
|
+
TlvCommandPath,
|
|
85
|
+
TlvEventFilter,
|
|
86
|
+
TlvEventPath,
|
|
87
|
+
TlvInvokeResponseData,
|
|
88
|
+
TlvInvokeResponseForSend,
|
|
89
|
+
TlvNoArguments,
|
|
90
|
+
TlvNoResponse,
|
|
91
|
+
TlvSubscribeResponse,
|
|
92
|
+
TypeFromSchema,
|
|
93
|
+
ValidationError,
|
|
94
|
+
} from "#types";
|
|
95
|
+
import { AttributeReportPayload, ServerInteraction } from "@matter/protocol";
|
|
96
|
+
import { ServerNode } from "../ServerNode.js";
|
|
97
|
+
|
|
98
|
+
const logger = Logger.get("InteractionServer");
|
|
99
|
+
|
|
100
|
+
const activityKey = Symbol("activity");
|
|
101
|
+
|
|
102
|
+
interface WithActivity {
|
|
103
|
+
[activityKey]?: NodeActivity.Activity;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const AclClusterId = AccessControlCluster.id;
|
|
107
|
+
const AclAttributeId = AccessControlCluster.attributes.acl.id;
|
|
108
|
+
|
|
109
|
+
export interface PeerSubscription {
|
|
110
|
+
subscriptionId: number;
|
|
111
|
+
peerAddress: PeerAddress;
|
|
112
|
+
minIntervalFloorSeconds: number;
|
|
113
|
+
maxIntervalCeilingSeconds: number;
|
|
114
|
+
attributeRequests?: TypeFromSchema<typeof TlvAttributePath>[];
|
|
115
|
+
eventRequests?: TypeFromSchema<typeof TlvEventPath>[];
|
|
116
|
+
isFabricFiltered: boolean;
|
|
117
|
+
maxInterval: number;
|
|
118
|
+
sendInterval: number;
|
|
119
|
+
operationalAddress?: ServerAddressIp;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isConcreteAttributePath(
|
|
123
|
+
path: TypeFromSchema<typeof TlvAttributePath>,
|
|
124
|
+
): path is TypeFromSchema<typeof TlvAttributePath> & AttributePath {
|
|
125
|
+
const { endpointId, clusterId, attributeId } = path;
|
|
126
|
+
return endpointId !== undefined && clusterId !== undefined && attributeId !== undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function validateReadAttributesPath(path: TypeFromSchema<typeof TlvAttributePath>, isGroupSession = false) {
|
|
130
|
+
if (isGroupSession) {
|
|
131
|
+
throw new StatusResponseError("Illegal read request with group session", StatusCode.InvalidAction);
|
|
132
|
+
}
|
|
133
|
+
const { clusterId, attributeId } = path;
|
|
134
|
+
if (clusterId === undefined && attributeId !== undefined) {
|
|
135
|
+
if (!GLOBAL_IDS.has(attributeId)) {
|
|
136
|
+
throw new StatusResponseError(
|
|
137
|
+
`Illegal read request for wildcard cluster and non global attribute ${attributeId}`,
|
|
138
|
+
StatusCode.InvalidAction,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateWriteAttributesPath(path: TypeFromSchema<typeof TlvAttributePath>, isGroupSession = false) {
|
|
145
|
+
const { endpointId, clusterId, attributeId } = path;
|
|
146
|
+
if (clusterId === undefined || attributeId === undefined) {
|
|
147
|
+
throw new StatusResponseError(
|
|
148
|
+
"Illegal write request with wildcard cluster or attribute ID",
|
|
149
|
+
StatusCode.InvalidAction,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (isGroupSession && endpointId !== undefined) {
|
|
153
|
+
throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isConcreteEventPath(
|
|
158
|
+
path: TypeFromSchema<typeof TlvEventPath>,
|
|
159
|
+
): path is TypeFromSchema<typeof TlvEventPath> & EventPath {
|
|
160
|
+
const { endpointId, clusterId, eventId } = path;
|
|
161
|
+
return endpointId !== undefined && clusterId !== undefined && eventId !== undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateReadEventPath(path: TypeFromSchema<typeof TlvEventPath>, isGroupSession = false) {
|
|
165
|
+
const { clusterId, eventId } = path;
|
|
166
|
+
if (clusterId === undefined && eventId !== undefined) {
|
|
167
|
+
throw new StatusResponseError("Illegal read request with wildcard cluster ID", StatusCode.InvalidAction);
|
|
168
|
+
}
|
|
169
|
+
if (isGroupSession) {
|
|
170
|
+
throw new StatusResponseError("Illegal read request with group session", StatusCode.InvalidAction);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isConcreteCommandPath(
|
|
175
|
+
path: TypeFromSchema<typeof TlvCommandPath>,
|
|
176
|
+
): path is TypeFromSchema<typeof TlvCommandPath> & CommandPath {
|
|
177
|
+
const { endpointId, clusterId, commandId } = path;
|
|
178
|
+
return endpointId !== undefined && clusterId !== undefined && commandId !== undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validateCommandPath(path: TypeFromSchema<typeof TlvCommandPath>, isGroupSession = false) {
|
|
182
|
+
const { endpointId, clusterId, commandId } = path;
|
|
183
|
+
if (clusterId === undefined || commandId === undefined) {
|
|
184
|
+
throw new StatusResponseError(
|
|
185
|
+
"Illegal write request with wildcard cluster or attribute ID",
|
|
186
|
+
StatusCode.InvalidAction,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (isGroupSession && endpointId !== undefined) {
|
|
190
|
+
throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getMatterModelCluster(clusterId: ClusterId) {
|
|
195
|
+
return MatterModel.standard.get(ClusterModel, clusterId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getMatterModelClusterCommand(clusterId: ClusterId, commandId: CommandId) {
|
|
199
|
+
return getMatterModelCluster(clusterId)?.get(CommandModel, commandId);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Interfaces {@link InteractionServer} with other components.
|
|
204
|
+
*/
|
|
205
|
+
export interface InteractionContext {
|
|
206
|
+
readonly sessions: SessionManager;
|
|
207
|
+
readonly exchangeManager: ExchangeManager;
|
|
208
|
+
readonly structure: InteractionEndpointStructure; // Remove later
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Translates interactions from the Matter protocol to matter.js APIs.
|
|
213
|
+
*/
|
|
214
|
+
export class InteractionServer implements ProtocolHandler, InteractionRecipient {
|
|
215
|
+
readonly id = INTERACTION_PROTOCOL_ID;
|
|
216
|
+
readonly requiresSecureSession = true;
|
|
217
|
+
#context: InteractionContext;
|
|
218
|
+
#nextSubscriptionId = Crypto.getRandomUInt32();
|
|
219
|
+
#isClosing = false;
|
|
220
|
+
#clientHandler?: ProtocolHandler;
|
|
221
|
+
readonly #subscriptionConfig: ServerSubscriptionConfig;
|
|
222
|
+
readonly #maxPathsPerInvoke;
|
|
223
|
+
readonly #subscriptionEstablishmentStarted = Observable<[peerAddress: PeerAddress]>();
|
|
224
|
+
#changeListener: (type: EndpointLifecycle.Change, endpoint: Endpoint) => void;
|
|
225
|
+
#node: ServerNode;
|
|
226
|
+
#activity: NodeActivity;
|
|
227
|
+
#newActivityBlocked = false;
|
|
228
|
+
#aclServer?: AccessControlServer;
|
|
229
|
+
#aclUpdateIsDelayedInExchange = new Set<MessageExchange>();
|
|
230
|
+
#serverInteraction: ServerInteraction;
|
|
231
|
+
|
|
232
|
+
constructor(node: ServerNode, sessions: SessionManager) {
|
|
233
|
+
this.#context = {
|
|
234
|
+
sessions,
|
|
235
|
+
exchangeManager: node.env.get(ExchangeManager),
|
|
236
|
+
structure: new InteractionEndpointStructure(),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
this.#subscriptionConfig = ServerSubscriptionConfig.of(node.state.network.subscriptionOptions);
|
|
240
|
+
this.#maxPathsPerInvoke = node.state.basicInformation.maxPathsPerInvoke ?? DEFAULT_MAX_PATHS_PER_INVOKE;
|
|
241
|
+
|
|
242
|
+
this.#context.structure.change.on(async () => {
|
|
243
|
+
this.#context.sessions.updateAllSubscriptions();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
this.#activity = node.env.get(NodeActivity);
|
|
247
|
+
|
|
248
|
+
this.#node = node;
|
|
249
|
+
|
|
250
|
+
// ServerInteraction is the "new way" and will replace most logic here over time and especially
|
|
251
|
+
// the InteractionEndpointStructure, which is currently a duplication of the node protocol
|
|
252
|
+
this.#serverInteraction = new ServerInteraction(node.protocol);
|
|
253
|
+
|
|
254
|
+
// TODO - rewrite element lookup so we don't need to build the secondary endpoint structure cache
|
|
255
|
+
this.#updateStructure();
|
|
256
|
+
this.#changeListener = (type, endpoint) => {
|
|
257
|
+
switch (type) {
|
|
258
|
+
case EndpointLifecycle.Change.ServersChanged:
|
|
259
|
+
EndpointServer.forEndpoint(endpoint).updateServers();
|
|
260
|
+
this.#updateStructure();
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
case EndpointLifecycle.Change.PartsReady:
|
|
264
|
+
case EndpointLifecycle.Change.ClientsChanged:
|
|
265
|
+
case EndpointLifecycle.Change.Destroyed:
|
|
266
|
+
this.#updateStructure();
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
node.lifecycle.changed.on(this.#changeListener);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async [Symbol.asyncDispose]() {
|
|
275
|
+
this.#node.lifecycle.changed.off(this.#changeListener);
|
|
276
|
+
await this.close();
|
|
277
|
+
this.#context.structure.close();
|
|
278
|
+
await EndpointServer.forEndpoint(this.#node)[Symbol.asyncDispose]();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
blockNewActivity() {
|
|
282
|
+
this.#newActivityBlocked = true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
protected get isClosing() {
|
|
286
|
+
return this.#isClosing;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
get maxPathsPerInvoke() {
|
|
290
|
+
return this.#maxPathsPerInvoke;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
get subscriptionEstablishmentStarted() {
|
|
294
|
+
return this.#subscriptionEstablishmentStarted;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async onNewExchange(exchange: MessageExchange, message: Message) {
|
|
298
|
+
// When closing, ignore anything newly incoming
|
|
299
|
+
if (this.#newActivityBlocked || this.isClosing) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// An incoming data report as the first message is not a valid server operation. We instead delegate to a
|
|
304
|
+
// client implementation if available
|
|
305
|
+
if (message.payloadHeader.messageType === MessageType.ReportData && this.clientHandler) {
|
|
306
|
+
return this.clientHandler.onNewExchange(exchange, message);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Activity tracking. This provides diagnostic information and prevents the server from shutting down whilst
|
|
310
|
+
// the exchange is active
|
|
311
|
+
using activity = this.#activity.begin(`session#${exchange.session.id.toString(16)}`);
|
|
312
|
+
(exchange as WithActivity)[activityKey] = activity;
|
|
313
|
+
|
|
314
|
+
// Delegate to InteractionServerMessenger
|
|
315
|
+
return new InteractionServerMessenger(exchange)
|
|
316
|
+
.handleRequest(this)
|
|
317
|
+
.finally(() => delete (exchange as WithActivity)[activityKey]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
get aclServer() {
|
|
321
|
+
if (this.#aclServer !== undefined) {
|
|
322
|
+
return this.#aclServer;
|
|
323
|
+
}
|
|
324
|
+
const aclServer = this.#node.act(agent => agent.get(AccessControlServer));
|
|
325
|
+
if (MaybePromise.is(aclServer)) {
|
|
326
|
+
throw new InternalError("AccessControlServer should already be initialized.");
|
|
327
|
+
}
|
|
328
|
+
return (this.#aclServer = aclServer);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
get clientHandler(): ProtocolHandler | undefined {
|
|
332
|
+
return this.#clientHandler;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
set clientHandler(clientHandler: ProtocolHandler) {
|
|
336
|
+
this.#clientHandler = clientHandler;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async #collectEventDataForRead(
|
|
340
|
+
{ eventRequests, eventFilters, isFabricFiltered }: ReadRequest,
|
|
341
|
+
exchange: MessageExchange,
|
|
342
|
+
message: Message,
|
|
343
|
+
) {
|
|
344
|
+
let eventReportsPayload: undefined | EventReportPayload[];
|
|
345
|
+
if (eventRequests) {
|
|
346
|
+
eventReportsPayload = [];
|
|
347
|
+
for (const requestPath of eventRequests) {
|
|
348
|
+
validateReadEventPath(requestPath);
|
|
349
|
+
|
|
350
|
+
const events = this.#context.structure.getEvents([requestPath]);
|
|
351
|
+
|
|
352
|
+
// Requested event path not found in any cluster server on any endpoint
|
|
353
|
+
if (events.length === 0) {
|
|
354
|
+
if (isConcreteEventPath(requestPath)) {
|
|
355
|
+
const { endpointId, clusterId, eventId } = requestPath;
|
|
356
|
+
try {
|
|
357
|
+
this.#context.structure.validateConcreteEventPath(endpointId, clusterId, eventId);
|
|
358
|
+
throw new InternalError(
|
|
359
|
+
"validateConcreteEventPath should throw StatusResponseError but did not.",
|
|
360
|
+
);
|
|
361
|
+
} catch (e) {
|
|
362
|
+
StatusResponseError.accept(e);
|
|
363
|
+
|
|
364
|
+
logger.debug(
|
|
365
|
+
`Read event from ${
|
|
366
|
+
exchange.channel.name
|
|
367
|
+
}: ${this.#context.structure.resolveEventName(requestPath)}: unsupported path: Status=${
|
|
368
|
+
e.code
|
|
369
|
+
}`,
|
|
370
|
+
);
|
|
371
|
+
eventReportsPayload?.push({
|
|
372
|
+
hasFabricSensitiveData: false,
|
|
373
|
+
eventStatus: { path: requestPath, status: { status: e.code } },
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Wildcard path: Just leave out values
|
|
378
|
+
logger.debug(
|
|
379
|
+
`Read event from ${exchange.channel.name}: ${this.#context.structure.resolveEventName(
|
|
380
|
+
requestPath,
|
|
381
|
+
)}: ignore non-existing event`,
|
|
382
|
+
);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const reportsForPath = new Array<EventReportPayload>();
|
|
387
|
+
for (const { path, event } of events) {
|
|
388
|
+
try {
|
|
389
|
+
const matchingEvents = await this.readEvent(
|
|
390
|
+
path,
|
|
391
|
+
eventFilters,
|
|
392
|
+
event,
|
|
393
|
+
exchange,
|
|
394
|
+
isFabricFiltered,
|
|
395
|
+
message,
|
|
396
|
+
);
|
|
397
|
+
logger.debug(
|
|
398
|
+
`Read event from ${exchange.channel.name}: ${this.#context.structure.resolveEventName(
|
|
399
|
+
path,
|
|
400
|
+
)}=${Diagnostic.json(matchingEvents)}`,
|
|
401
|
+
);
|
|
402
|
+
const { schema } = event;
|
|
403
|
+
reportsForPath.push(
|
|
404
|
+
...matchingEvents.map(({ number, priority, epochTimestamp, payload }) => ({
|
|
405
|
+
hasFabricSensitiveData: event.hasFabricSensitiveData,
|
|
406
|
+
eventData: {
|
|
407
|
+
path,
|
|
408
|
+
eventNumber: number,
|
|
409
|
+
priority,
|
|
410
|
+
epochTimestamp,
|
|
411
|
+
payload,
|
|
412
|
+
schema,
|
|
413
|
+
},
|
|
414
|
+
})),
|
|
415
|
+
);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
logger.error(
|
|
418
|
+
`Error while reading event from ${
|
|
419
|
+
exchange.channel.name
|
|
420
|
+
} to ${this.#context.structure.resolveEventName(path)}:`,
|
|
421
|
+
error,
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
StatusResponseError.accept(error);
|
|
425
|
+
|
|
426
|
+
// Add StatusResponseErrors, but only when the initial path was concrete, else error are ignored
|
|
427
|
+
if (isConcreteEventPath(requestPath)) {
|
|
428
|
+
eventReportsPayload?.push({
|
|
429
|
+
hasFabricSensitiveData: false,
|
|
430
|
+
eventStatus: { path, status: { status: error.code } },
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
eventReportsPayload.push(
|
|
436
|
+
...reportsForPath.sort((a, b) => {
|
|
437
|
+
const eventNumberA = a.eventData?.eventNumber ?? EventNumber(0);
|
|
438
|
+
const eventNumberB = b.eventData?.eventNumber ?? EventNumber(0);
|
|
439
|
+
if (eventNumberA > eventNumberB) {
|
|
440
|
+
return 1;
|
|
441
|
+
} else if (eventNumberA < eventNumberB) {
|
|
442
|
+
return -1;
|
|
443
|
+
} else {
|
|
444
|
+
return 0;
|
|
445
|
+
}
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return eventReportsPayload;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Returns an iterator that yields the data reports and events data for the given read request.
|
|
455
|
+
*/
|
|
456
|
+
async *#iterateReadAttributesPaths(
|
|
457
|
+
readRequest: ReadRequest,
|
|
458
|
+
eventReportsPayload: EventReportPayload[] | undefined,
|
|
459
|
+
exchange: MessageExchange,
|
|
460
|
+
message: Message,
|
|
461
|
+
) {
|
|
462
|
+
const { isFabricFiltered } = readRequest;
|
|
463
|
+
|
|
464
|
+
const context = OnlineContext({
|
|
465
|
+
activity: (exchange as WithActivity)[activityKey],
|
|
466
|
+
fabricFiltered: isFabricFiltered,
|
|
467
|
+
message,
|
|
468
|
+
exchange,
|
|
469
|
+
tracer: this.#tracer,
|
|
470
|
+
actionType: ActionTracer.ActionType.Read,
|
|
471
|
+
node: this.#node,
|
|
472
|
+
}).beginReadOnly();
|
|
473
|
+
|
|
474
|
+
for await (const chunk of this.#serverInteraction.read(readRequest, context)) {
|
|
475
|
+
for (const report of chunk) {
|
|
476
|
+
// TODO Centralize this conversion and at the end move into encoder
|
|
477
|
+
switch (report.kind) {
|
|
478
|
+
case "attr-value": {
|
|
479
|
+
const { path, value: payload, version: dataVersion, tlv: schema } = report;
|
|
480
|
+
if (schema === undefined) {
|
|
481
|
+
throw new InternalError(`Attribute ${path.clusterId}/${path.attributeId} not found`);
|
|
482
|
+
}
|
|
483
|
+
const data: AttributeReportPayload = {
|
|
484
|
+
attributeData: {
|
|
485
|
+
path,
|
|
486
|
+
payload,
|
|
487
|
+
schema,
|
|
488
|
+
dataVersion,
|
|
489
|
+
},
|
|
490
|
+
hasFabricSensitiveData: true, // With this we disable the validation for missing data in encoding, we trust behavior logic
|
|
491
|
+
};
|
|
492
|
+
yield data;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case "attr-status": {
|
|
496
|
+
const { path, status } = report;
|
|
497
|
+
const statusReport: AttributeReportPayload = {
|
|
498
|
+
attributeStatus: {
|
|
499
|
+
path,
|
|
500
|
+
status: { status },
|
|
501
|
+
},
|
|
502
|
+
hasFabricSensitiveData: false,
|
|
503
|
+
};
|
|
504
|
+
yield statusReport;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
context[Symbol.dispose]();
|
|
511
|
+
|
|
512
|
+
if (eventReportsPayload !== undefined) {
|
|
513
|
+
for (const eventReport of eventReportsPayload) {
|
|
514
|
+
yield eventReport;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async handleReadRequest(
|
|
520
|
+
exchange: MessageExchange,
|
|
521
|
+
readRequest: ReadRequest,
|
|
522
|
+
message: Message,
|
|
523
|
+
): Promise<{ dataReport: DataReport; payload?: DataReportPayloadIterator }> {
|
|
524
|
+
const { attributeRequests, eventRequests, isFabricFiltered, interactionModelRevision } = readRequest;
|
|
525
|
+
logger.debug(
|
|
526
|
+
`Received read request from ${exchange.channel.name}: attributes:${
|
|
527
|
+
attributeRequests?.map(path => this.#context.structure.resolveAttributeName(path)).join(", ") ?? "none"
|
|
528
|
+
}, events:${
|
|
529
|
+
eventRequests?.map(path => this.#context.structure.resolveEventName(path)).join(", ") ?? "none"
|
|
530
|
+
} isFabricFiltered=${isFabricFiltered}`,
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
534
|
+
logger.debug(
|
|
535
|
+
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (attributeRequests === undefined && eventRequests === undefined) {
|
|
539
|
+
return {
|
|
540
|
+
dataReport: {
|
|
541
|
+
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
542
|
+
suppressResponse: true,
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (message.packetHeader.sessionType !== SessionType.Unicast) {
|
|
548
|
+
throw new StatusResponseError(
|
|
549
|
+
"Subscriptions are only allowed on unicast sessions",
|
|
550
|
+
StatusCode.InvalidAction,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
dataReport: {
|
|
556
|
+
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
557
|
+
suppressResponse: true,
|
|
558
|
+
},
|
|
559
|
+
payload: this.#iterateReadAttributesPaths(
|
|
560
|
+
readRequest,
|
|
561
|
+
await this.#collectEventDataForRead(readRequest, exchange, message),
|
|
562
|
+
exchange,
|
|
563
|
+
message,
|
|
564
|
+
),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
protected readAttribute(
|
|
569
|
+
path: AttributePath,
|
|
570
|
+
attribute: AnyAttributeServer<any>,
|
|
571
|
+
exchange: MessageExchange,
|
|
572
|
+
fabricFiltered: boolean,
|
|
573
|
+
message: Message,
|
|
574
|
+
offline = false,
|
|
575
|
+
) {
|
|
576
|
+
const readAttribute = () =>
|
|
577
|
+
attribute.getWithVersion(exchange.session, fabricFiltered, offline ? undefined : message);
|
|
578
|
+
|
|
579
|
+
const endpoint = this.#context.structure.getEndpoint(path.endpointId);
|
|
580
|
+
if (!endpoint) {
|
|
581
|
+
throw new InternalError("Endpoint not found for ACL check. This should never happen.");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const result = offline
|
|
585
|
+
? OfflineContext.act("offline-read", this.#activity, readAttribute)
|
|
586
|
+
: OnlineContext({
|
|
587
|
+
activity: (exchange as WithActivity)[activityKey],
|
|
588
|
+
fabricFiltered,
|
|
589
|
+
message,
|
|
590
|
+
exchange,
|
|
591
|
+
tracer: this.#tracer,
|
|
592
|
+
actionType: ActionTracer.ActionType.Read,
|
|
593
|
+
node: this.#node,
|
|
594
|
+
}).act(readAttribute);
|
|
595
|
+
|
|
596
|
+
if (MaybePromise.is(result)) {
|
|
597
|
+
throw new InternalError("Reads should not return a promise.");
|
|
598
|
+
}
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Reads the attributes for the given endpoint.
|
|
604
|
+
* This can currently only be used for subscriptions because errors are ignored!
|
|
605
|
+
*/
|
|
606
|
+
protected readEndpointAttributesForSubscription(
|
|
607
|
+
attributes: { path: AttributePath; attribute: AnyAttributeServer<any> }[],
|
|
608
|
+
exchange: MessageExchange,
|
|
609
|
+
fabricFiltered: boolean,
|
|
610
|
+
message: Message,
|
|
611
|
+
offline = false,
|
|
612
|
+
) {
|
|
613
|
+
const readAttributes = () => {
|
|
614
|
+
const result = new Array<{
|
|
615
|
+
path: AttributePath;
|
|
616
|
+
attribute: AnyAttributeServer<unknown>;
|
|
617
|
+
value: any;
|
|
618
|
+
version: number;
|
|
619
|
+
}>();
|
|
620
|
+
for (const { path, attribute } of attributes) {
|
|
621
|
+
try {
|
|
622
|
+
const value = attribute.getWithVersion(
|
|
623
|
+
exchange.session,
|
|
624
|
+
fabricFiltered,
|
|
625
|
+
offline ? undefined : message,
|
|
626
|
+
);
|
|
627
|
+
result.push({ path, attribute, value: value.value, version: value.version });
|
|
628
|
+
} catch (error) {
|
|
629
|
+
if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
|
|
630
|
+
logger.warn(
|
|
631
|
+
`Permission denied reading attribute ${this.#context.structure.resolveAttributeName(path)}`,
|
|
632
|
+
);
|
|
633
|
+
} else {
|
|
634
|
+
logger.warn(
|
|
635
|
+
`Error reading attribute ${this.#context.structure.resolveAttributeName(path)}:`,
|
|
636
|
+
error,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return result;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const result = offline
|
|
645
|
+
? OfflineContext.act("offline-read", this.#activity, readAttributes)
|
|
646
|
+
: OnlineContext({
|
|
647
|
+
activity: (exchange as WithActivity)[activityKey],
|
|
648
|
+
fabricFiltered,
|
|
649
|
+
message,
|
|
650
|
+
exchange,
|
|
651
|
+
tracer: this.#tracer,
|
|
652
|
+
actionType: ActionTracer.ActionType.Read,
|
|
653
|
+
node: this.#node,
|
|
654
|
+
}).act(readAttributes);
|
|
655
|
+
if (MaybePromise.is(result)) {
|
|
656
|
+
throw new InternalError("Online read should not return a promise.");
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
protected async readEvent(
|
|
662
|
+
path: EventPath,
|
|
663
|
+
eventFilters: TypeFromSchema<typeof TlvEventFilter>[] | undefined,
|
|
664
|
+
event: AnyEventServer<any, any>,
|
|
665
|
+
exchange: MessageExchange,
|
|
666
|
+
fabricFiltered: boolean,
|
|
667
|
+
message: Message,
|
|
668
|
+
) {
|
|
669
|
+
const readEvent = (context: ActionContext) => {
|
|
670
|
+
if (
|
|
671
|
+
context.authorityAt(event.readAcl, {
|
|
672
|
+
endpoint: path.endpointId,
|
|
673
|
+
cluster: path.clusterId,
|
|
674
|
+
} as AccessControl.Location) !== AccessControl.Authority.Granted
|
|
675
|
+
) {
|
|
676
|
+
throw new AccessDeniedError(
|
|
677
|
+
`Access to ${path.endpointId}/${Diagnostic.hex(path.clusterId)} denied on ${exchange.session.name}.`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return event.get(exchange.session, fabricFiltered, message, eventFilters);
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
return OnlineContext({
|
|
684
|
+
activity: (exchange as WithActivity)[activityKey],
|
|
685
|
+
fabricFiltered,
|
|
686
|
+
message,
|
|
687
|
+
exchange,
|
|
688
|
+
tracer: this.#tracer,
|
|
689
|
+
actionType: ActionTracer.ActionType.Read,
|
|
690
|
+
node: this.#node,
|
|
691
|
+
}).act(readEvent);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async handleWriteRequest(
|
|
695
|
+
exchange: MessageExchange,
|
|
696
|
+
writeRequest: WriteRequest,
|
|
697
|
+
message: Message,
|
|
698
|
+
): Promise<WriteResponse> {
|
|
699
|
+
let result: WriteResponse;
|
|
700
|
+
try {
|
|
701
|
+
result = await this.#handleWriteRequestLogic(exchange, writeRequest, message);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
if (this.#aclUpdateIsDelayedInExchange.has(exchange)) {
|
|
704
|
+
// Unlikely to get there at all, but make sure we handle it best we can for now
|
|
705
|
+
this.#aclUpdateIsDelayedInExchange.delete(exchange);
|
|
706
|
+
|
|
707
|
+
if (this.#aclUpdateIsDelayedInExchange.size === 0) {
|
|
708
|
+
// only that one ACl change in flight, so we can reset the delayed ACL
|
|
709
|
+
this.aclServer.resetDelayedAccessControlList();
|
|
710
|
+
} else {
|
|
711
|
+
// TODO: we should restore the delayed data just for this errored fabric?
|
|
712
|
+
logger.error("One of multiple concurrent ACL writes failed, unhandled case for now.");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
throw error;
|
|
716
|
+
}
|
|
717
|
+
// We delayed the ACL update during this write transaction, so we need to update it now that anything is written
|
|
718
|
+
if (this.#aclUpdateIsDelayedInExchange.has(exchange)) {
|
|
719
|
+
this.#aclUpdateIsDelayedInExchange.delete(exchange);
|
|
720
|
+
|
|
721
|
+
if (this.#aclUpdateIsDelayedInExchange.size === 0) {
|
|
722
|
+
// Committing the ACL changes in case of an unhandled exception might be dangerous, but we do it anyway
|
|
723
|
+
this.aclServer.aclUpdateDelayed = false;
|
|
724
|
+
} else {
|
|
725
|
+
logger.info("Multiple concurrent ACL writes, waiting for all to finish.");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async #handleWriteRequestLogic(
|
|
733
|
+
exchange: MessageExchange,
|
|
734
|
+
{ suppressResponse, timedRequest, writeRequests, interactionModelRevision, moreChunkedMessages }: WriteRequest,
|
|
735
|
+
message: Message,
|
|
736
|
+
): Promise<WriteResponse> {
|
|
737
|
+
const sessionType = message.packetHeader.sessionType;
|
|
738
|
+
logger.debug(
|
|
739
|
+
`Received write request from ${exchange.channel.name}: ${writeRequests
|
|
740
|
+
.map(req => this.#context.structure.resolveAttributeName(req.path))
|
|
741
|
+
.join(", ")}, suppressResponse=${suppressResponse}, moreChunkedMessages=${moreChunkedMessages}`,
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
if (moreChunkedMessages && suppressResponse) {
|
|
745
|
+
throw new StatusResponseError(
|
|
746
|
+
"MoreChunkedMessages and SuppressResponse cannot be used together in write messages",
|
|
747
|
+
StatusCode.InvalidAction,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
752
|
+
logger.debug(
|
|
753
|
+
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
|
|
758
|
+
|
|
759
|
+
if (receivedWithinTimedInteraction && moreChunkedMessages) {
|
|
760
|
+
throw new StatusResponseError(
|
|
761
|
+
"Write Request action that is part of a Timed Write Interaction SHALL NOT be chunked.",
|
|
762
|
+
StatusCode.InvalidAction,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (exchange.hasExpiredTimedInteraction()) {
|
|
767
|
+
exchange.clearTimedInteraction(); // ??
|
|
768
|
+
throw new StatusResponseError(`Timed request window expired. Decline write request.`, StatusCode.Timeout);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (timedRequest !== exchange.hasTimedInteraction()) {
|
|
772
|
+
throw new StatusResponseError(
|
|
773
|
+
`timedRequest flag of write interaction (${timedRequest}) mismatch with expected timed interaction (${receivedWithinTimedInteraction}).`,
|
|
774
|
+
StatusCode.TimedRequestMismatch,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (receivedWithinTimedInteraction) {
|
|
779
|
+
logger.debug(
|
|
780
|
+
`Write request from ${exchange.channel.name} successfully received while timed interaction is running.`,
|
|
781
|
+
);
|
|
782
|
+
exchange.clearTimedInteraction();
|
|
783
|
+
if (sessionType !== SessionType.Unicast) {
|
|
784
|
+
throw new StatusResponseError(
|
|
785
|
+
"Write requests are only allowed on unicast sessions when a timed interaction is running.",
|
|
786
|
+
StatusCode.InvalidAction,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (sessionType === SessionType.Group && !suppressResponse) {
|
|
792
|
+
throw new StatusResponseError(
|
|
793
|
+
"Write requests are only allowed as group casts when suppressResponse=true.",
|
|
794
|
+
StatusCode.InvalidAction,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const writeData = expandPathsInAttributeData(writeRequests, true);
|
|
799
|
+
|
|
800
|
+
const writeResults = new Array<{
|
|
801
|
+
path: TypeFromSchema<typeof TlvAttributePath>;
|
|
802
|
+
statusCode: StatusCode;
|
|
803
|
+
clusterStatusCode?: number;
|
|
804
|
+
}>();
|
|
805
|
+
const attributeListWrites = new Set<AttributeServer<any>>();
|
|
806
|
+
const clusterDataVersionInfo = new Map<string, number>();
|
|
807
|
+
const inaccessiblePaths = new Set<string>();
|
|
808
|
+
|
|
809
|
+
// TODO Add handling for moreChunkedMessages here when adopting for Matter 1.4
|
|
810
|
+
|
|
811
|
+
for (const writeRequest of writeData) {
|
|
812
|
+
const { path: writePath, dataVersion } = writeRequest;
|
|
813
|
+
|
|
814
|
+
validateWriteAttributesPath(writePath);
|
|
815
|
+
|
|
816
|
+
const attributes = this.#context.structure.getAttributes([writePath], true);
|
|
817
|
+
|
|
818
|
+
// No existing attribute matches the given path and is writable
|
|
819
|
+
if (attributes.length === 0) {
|
|
820
|
+
if (isConcreteAttributePath(writePath)) {
|
|
821
|
+
const { endpointId, clusterId, attributeId } = writePath;
|
|
822
|
+
|
|
823
|
+
// was a concrete path
|
|
824
|
+
try {
|
|
825
|
+
this.#context.structure.validateConcreteAttributePath(endpointId, clusterId, attributeId);
|
|
826
|
+
|
|
827
|
+
// Ok it is a valid concrete path, so it is not writable
|
|
828
|
+
throw new StatusResponseError(
|
|
829
|
+
`Attribute ${attributeId} is not writable.`,
|
|
830
|
+
StatusCode.UnsupportedWrite,
|
|
831
|
+
);
|
|
832
|
+
} catch (e) {
|
|
833
|
+
StatusResponseError.accept(e);
|
|
834
|
+
|
|
835
|
+
logger.debug(
|
|
836
|
+
`Write from ${exchange.channel.name}: ${this.#context.structure.resolveAttributeName(
|
|
837
|
+
writePath,
|
|
838
|
+
)} not allowed: Status=${e.code}`,
|
|
839
|
+
);
|
|
840
|
+
writeResults.push({ path: writePath, statusCode: e.code });
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
// Wildcard path: Just ignore
|
|
844
|
+
logger.debug(
|
|
845
|
+
`Write from ${exchange.channel.name}: ${this.#context.structure.resolveAttributeName(
|
|
846
|
+
writePath,
|
|
847
|
+
)}: ignore non-existing (wildcard) attribute`,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Concrete path and found and writable
|
|
854
|
+
if (attributes.length === 1 && isConcreteAttributePath(writePath)) {
|
|
855
|
+
const { endpointId, clusterId } = writePath;
|
|
856
|
+
const { attribute } = attributes[0];
|
|
857
|
+
|
|
858
|
+
if (attribute.requiresTimedInteraction && !receivedWithinTimedInteraction) {
|
|
859
|
+
logger.debug(`This write requires a timed interaction which is not initialized.`);
|
|
860
|
+
writeResults.push({ path: writePath, statusCode: StatusCode.NeedsTimedInteraction });
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (
|
|
865
|
+
attribute instanceof FabricScopedAttributeServer &&
|
|
866
|
+
(!exchange.session.isSecure || !(exchange.session as SecureSession).fabric)
|
|
867
|
+
) {
|
|
868
|
+
logger.debug(`This write requires a secure session with a fabric assigned which is missing.`);
|
|
869
|
+
writeResults.push({ path: writePath, statusCode: StatusCode.UnsupportedAccess });
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Check the provided dataVersion with the dataVersion of the cluster
|
|
874
|
+
// And remember this initial dataVersion for all checks inside this write transaction to allow proper
|
|
875
|
+
// processing of chunked lists
|
|
876
|
+
if (dataVersion !== undefined) {
|
|
877
|
+
const datasource = this.#context.structure.getClusterServer(endpointId, clusterId)?.datasource;
|
|
878
|
+
const { nodeId } = writePath;
|
|
879
|
+
const clusterKey = clusterPathToId({ nodeId, endpointId, clusterId });
|
|
880
|
+
const currentDataVersion = clusterDataVersionInfo.get(clusterKey) ?? datasource?.version;
|
|
881
|
+
|
|
882
|
+
if (currentDataVersion !== undefined) {
|
|
883
|
+
if (dataVersion !== currentDataVersion) {
|
|
884
|
+
logger.debug(
|
|
885
|
+
`This write requires a specific data version (${dataVersion}) which do not match the current cluster data version (${currentDataVersion}).`,
|
|
886
|
+
);
|
|
887
|
+
writeResults.push({ path: writePath, statusCode: StatusCode.DataVersionMismatch });
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
clusterDataVersionInfo.set(clusterKey, currentDataVersion);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
for (const { path, attribute } of attributes) {
|
|
896
|
+
const { schema, defaultValue } = attribute;
|
|
897
|
+
const pathId = attributePathToId(path);
|
|
898
|
+
|
|
899
|
+
try {
|
|
900
|
+
if (
|
|
901
|
+
!(attribute instanceof AttributeServer) &&
|
|
902
|
+
!(attribute instanceof FabricScopedAttributeServer)
|
|
903
|
+
) {
|
|
904
|
+
throw new StatusResponseError(
|
|
905
|
+
"Fixed attributes cannot be written",
|
|
906
|
+
StatusCode.UnsupportedWrite,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (inaccessiblePaths.has(pathId)) {
|
|
911
|
+
logger.debug(`This write is not allowed due to previous access denied.`);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const { endpointId } = path;
|
|
916
|
+
const { listIndex } = writePath;
|
|
917
|
+
const value =
|
|
918
|
+
listIndex === undefined
|
|
919
|
+
? decodeAttributeValueWithSchema(schema, [writeRequest], defaultValue)
|
|
920
|
+
: decodeListAttributeValueWithSchema(
|
|
921
|
+
schema as ArraySchema<any>,
|
|
922
|
+
[writeRequest],
|
|
923
|
+
this.readAttribute(path, attribute, exchange, true, message).value ?? defaultValue,
|
|
924
|
+
);
|
|
925
|
+
logger.debug(
|
|
926
|
+
`Handle write request from ${
|
|
927
|
+
exchange.channel.name
|
|
928
|
+
} resolved to: ${this.#context.structure.resolveAttributeName(path)}=${Diagnostic.json(
|
|
929
|
+
value,
|
|
930
|
+
)} (listIndex=${listIndex}, for-version=${dataVersion})`,
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
if (attribute.requiresTimedInteraction && !receivedWithinTimedInteraction) {
|
|
934
|
+
logger.debug(`This write requires a timed interaction which is not initialized.`);
|
|
935
|
+
throw new StatusResponseError(
|
|
936
|
+
"This write requires a timed interaction which is not initialized.",
|
|
937
|
+
StatusCode.NeedsTimedInteraction,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
await this.writeAttribute(
|
|
942
|
+
path,
|
|
943
|
+
attribute,
|
|
944
|
+
value,
|
|
945
|
+
exchange,
|
|
946
|
+
message,
|
|
947
|
+
this.#context.structure.getEndpoint(endpointId)!,
|
|
948
|
+
receivedWithinTimedInteraction,
|
|
949
|
+
schema instanceof ArraySchema,
|
|
950
|
+
);
|
|
951
|
+
if (schema instanceof ArraySchema && !attributeListWrites.has(attribute)) {
|
|
952
|
+
attributeListWrites.add(attribute);
|
|
953
|
+
}
|
|
954
|
+
} catch (error: any) {
|
|
955
|
+
if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
|
|
956
|
+
inaccessiblePaths.add(pathId);
|
|
957
|
+
}
|
|
958
|
+
if (attributes.length === 1 && isConcreteAttributePath(writePath)) {
|
|
959
|
+
// For Multi-Attribute-Writes we ignore errors
|
|
960
|
+
logger.error(
|
|
961
|
+
`Error while handling write request from ${
|
|
962
|
+
exchange.channel.name
|
|
963
|
+
} to ${this.#context.structure.resolveAttributeName(path)}:`,
|
|
964
|
+
error instanceof StatusResponseError ? error.message : error,
|
|
965
|
+
);
|
|
966
|
+
if (error instanceof StatusResponseError) {
|
|
967
|
+
writeResults.push({ path, statusCode: error.code, clusterStatusCode: error.clusterCode });
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
writeResults.push({ path, statusCode: StatusCode.ConstraintError });
|
|
971
|
+
continue;
|
|
972
|
+
} else {
|
|
973
|
+
logger.debug(
|
|
974
|
+
`While handling write request from ${
|
|
975
|
+
exchange.channel.name
|
|
976
|
+
} to ${this.#context.structure.resolveAttributeName(path)} ignored: ${error.message}`,
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
// TODO - This behavior may be wrong.
|
|
980
|
+
//
|
|
981
|
+
// If a wildcard write fails we should either:
|
|
982
|
+
//
|
|
983
|
+
// 1. Ignore entirely (add nothing to write results), or
|
|
984
|
+
// 2. Add an error response for the concrete attribute that failed.
|
|
985
|
+
//
|
|
986
|
+
// Spec is a little ambiguous. After request path expansion, in core 1.2 8.7.3.2 it states:
|
|
987
|
+
//
|
|
988
|
+
// "If the path indicates attribute data that is not writable, then the path SHALL be
|
|
989
|
+
// discarded"
|
|
990
|
+
//
|
|
991
|
+
// So is this "not writable" -- so it should be #1 -- or is this "writable" but with invalid
|
|
992
|
+
// data? The latter is error case #2 but spec doesn't make clear what the status code would
|
|
993
|
+
// be... We could fall back to CONSTRAINT_ERROR like we do above though
|
|
994
|
+
//
|
|
995
|
+
// Currently what we do is add a success response for every concrete path that fails.
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
writeResults.push({ path, statusCode: StatusCode.Success });
|
|
999
|
+
}
|
|
1000
|
+
//.filter(({ statusCode }) => statusCode !== StatusCode.Success); // see https://github.com/project-chip/connectedhomeip/issues/26198
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const errorResults = writeResults.filter(({ statusCode }) => statusCode !== StatusCode.Success);
|
|
1004
|
+
logger.debug(
|
|
1005
|
+
`Write request from ${exchange.channel.name} done ${
|
|
1006
|
+
errorResults.length
|
|
1007
|
+
? `with following errors: ${errorResults
|
|
1008
|
+
.map(
|
|
1009
|
+
({ path, statusCode }) =>
|
|
1010
|
+
`${this.#context.structure.resolveAttributeName(path)}=${Diagnostic.json(statusCode)}`,
|
|
1011
|
+
)
|
|
1012
|
+
.join(", ")}`
|
|
1013
|
+
: "without errors"
|
|
1014
|
+
}`,
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
const response = {
|
|
1018
|
+
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
1019
|
+
writeResponses: writeResults.map(({ path, statusCode, clusterStatusCode }) => ({
|
|
1020
|
+
path,
|
|
1021
|
+
status: { status: statusCode, clusterStatus: clusterStatusCode },
|
|
1022
|
+
})),
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// Trigger attribute events for delayed list writes
|
|
1026
|
+
for (const attribute of attributeListWrites.values()) {
|
|
1027
|
+
try {
|
|
1028
|
+
attribute.triggerDelayedChangeEvents();
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
logger.error(
|
|
1031
|
+
`Ignored Error while writing attribute from ${exchange.channel.name} to ${attribute.name}:`,
|
|
1032
|
+
error,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return response;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
protected async writeAttribute(
|
|
1041
|
+
path: AttributePath,
|
|
1042
|
+
attribute: AttributeServer<any>,
|
|
1043
|
+
value: any,
|
|
1044
|
+
exchange: MessageExchange,
|
|
1045
|
+
message: Message,
|
|
1046
|
+
_endpoint: EndpointInterface,
|
|
1047
|
+
timed?: boolean,
|
|
1048
|
+
isListWrite = false,
|
|
1049
|
+
) {
|
|
1050
|
+
const writeAttribute = () => attribute.set(value, exchange.session, message, isListWrite);
|
|
1051
|
+
|
|
1052
|
+
if (path.endpointId === 0 && path.clusterId === AclClusterId && path.attributeId === AclAttributeId) {
|
|
1053
|
+
// This is a hack to prevent the ACL from updating while we are in the middle of a write transaction
|
|
1054
|
+
// and is needed because Acl should not become effective during writing of the ACL itself.
|
|
1055
|
+
this.aclServer.aclUpdateDelayed = true;
|
|
1056
|
+
this.#aclUpdateIsDelayedInExchange.add(exchange);
|
|
1057
|
+
} else {
|
|
1058
|
+
// Ok it seems that acl was written, but we now write another path, so we can update Acl attribute now
|
|
1059
|
+
if (this.#aclUpdateIsDelayedInExchange.has(exchange)) {
|
|
1060
|
+
this.#aclUpdateIsDelayedInExchange.delete(exchange);
|
|
1061
|
+
|
|
1062
|
+
if (this.#aclUpdateIsDelayedInExchange.size === 0) {
|
|
1063
|
+
this.aclServer.aclUpdateDelayed = false;
|
|
1064
|
+
} else {
|
|
1065
|
+
logger.info("Multiple concurrent ACL writes, waiting for all to finish.");
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return OnlineContext({
|
|
1071
|
+
activity: (exchange as WithActivity)[activityKey],
|
|
1072
|
+
timed,
|
|
1073
|
+
message,
|
|
1074
|
+
exchange,
|
|
1075
|
+
fabricFiltered: true,
|
|
1076
|
+
tracer: this.#tracer,
|
|
1077
|
+
actionType: ActionTracer.ActionType.Write,
|
|
1078
|
+
node: this.#node,
|
|
1079
|
+
}).act(writeAttribute);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
async handleSubscribeRequest(
|
|
1083
|
+
exchange: MessageExchange,
|
|
1084
|
+
request: SubscribeRequest,
|
|
1085
|
+
messenger: InteractionServerMessenger,
|
|
1086
|
+
message: Message,
|
|
1087
|
+
): Promise<void> {
|
|
1088
|
+
const {
|
|
1089
|
+
minIntervalFloorSeconds,
|
|
1090
|
+
maxIntervalCeilingSeconds,
|
|
1091
|
+
attributeRequests,
|
|
1092
|
+
dataVersionFilters,
|
|
1093
|
+
eventRequests,
|
|
1094
|
+
eventFilters,
|
|
1095
|
+
keepSubscriptions,
|
|
1096
|
+
isFabricFiltered,
|
|
1097
|
+
interactionModelRevision,
|
|
1098
|
+
} = request;
|
|
1099
|
+
logger.debug(
|
|
1100
|
+
`Received subscribe request from ${exchange.channel.name} (keepSubscriptions=${keepSubscriptions}, isFabricFiltered=${isFabricFiltered})`,
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
1104
|
+
logger.debug(
|
|
1105
|
+
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (message.packetHeader.sessionType !== SessionType.Unicast) {
|
|
1110
|
+
throw new StatusResponseError(
|
|
1111
|
+
"Subscriptions are only allowed on unicast sessions",
|
|
1112
|
+
StatusCode.InvalidAction,
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
assertSecureSession(exchange.session, "Subscriptions are only implemented on secure sessions");
|
|
1117
|
+
const session = exchange.session;
|
|
1118
|
+
const fabric = session.fabric;
|
|
1119
|
+
if (fabric === undefined)
|
|
1120
|
+
throw new StatusResponseError(
|
|
1121
|
+
"Subscriptions are only implemented after a fabric has been assigned",
|
|
1122
|
+
StatusCode.InvalidAction,
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
if (!keepSubscriptions) {
|
|
1126
|
+
const clearedCount = await this.#context.sessions.clearSubscriptionsForNode(
|
|
1127
|
+
fabric.addressOf(session.peerNodeId),
|
|
1128
|
+
true,
|
|
1129
|
+
);
|
|
1130
|
+
if (clearedCount > 0) {
|
|
1131
|
+
logger.debug(
|
|
1132
|
+
`Cleared ${clearedCount} subscriptions for Subscriber node ${session.peerNodeId} because keepSubscriptions=false`,
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (
|
|
1138
|
+
(!Array.isArray(attributeRequests) || attributeRequests.length === 0) &&
|
|
1139
|
+
(!Array.isArray(eventRequests) || eventRequests.length === 0)
|
|
1140
|
+
) {
|
|
1141
|
+
throw new StatusResponseError("No attributes or events requested", StatusCode.InvalidAction);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
logger.debug(
|
|
1145
|
+
`Subscribe to attributes:${
|
|
1146
|
+
attributeRequests?.map(path => this.#context.structure.resolveAttributeName(path)).join(", ") ?? "none"
|
|
1147
|
+
}, events:${
|
|
1148
|
+
eventRequests?.map(path => this.#context.structure.resolveEventName(path)).join(", ") ?? "none"
|
|
1149
|
+
}`,
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
if (dataVersionFilters !== undefined && dataVersionFilters.length > 0) {
|
|
1153
|
+
logger.debug(
|
|
1154
|
+
`DataVersionFilters: ${dataVersionFilters
|
|
1155
|
+
.map(
|
|
1156
|
+
({ path: { nodeId, endpointId, clusterId }, dataVersion }) =>
|
|
1157
|
+
`${clusterPathToId({ nodeId, endpointId, clusterId })}=${dataVersion}`,
|
|
1158
|
+
)
|
|
1159
|
+
.join(", ")}`,
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
if (eventFilters !== undefined && eventFilters.length > 0)
|
|
1163
|
+
logger.debug(
|
|
1164
|
+
`Event filters: ${eventFilters.map(filter => `${filter.nodeId}/${filter.eventMin}`).join(", ")}`,
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
// Validate of the paths before proceeding
|
|
1168
|
+
attributeRequests?.forEach(path => validateReadAttributesPath(path));
|
|
1169
|
+
eventRequests?.forEach(path => validateReadEventPath(path));
|
|
1170
|
+
|
|
1171
|
+
if (minIntervalFloorSeconds < 0) {
|
|
1172
|
+
throw new StatusResponseError(
|
|
1173
|
+
"minIntervalFloorSeconds should be greater or equal to 0",
|
|
1174
|
+
StatusCode.InvalidAction,
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
if (maxIntervalCeilingSeconds < 0) {
|
|
1178
|
+
throw new StatusResponseError(
|
|
1179
|
+
"maxIntervalCeilingSeconds should be greater or equal to 1",
|
|
1180
|
+
StatusCode.InvalidAction,
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
if (maxIntervalCeilingSeconds < minIntervalFloorSeconds) {
|
|
1184
|
+
throw new StatusResponseError(
|
|
1185
|
+
"maxIntervalCeilingSeconds should be greater or equal to minIntervalFloorSeconds",
|
|
1186
|
+
StatusCode.InvalidAction,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (this.#nextSubscriptionId === 0xffffffff) this.#nextSubscriptionId = 0;
|
|
1191
|
+
const subscriptionId = this.#nextSubscriptionId++;
|
|
1192
|
+
|
|
1193
|
+
this.#subscriptionEstablishmentStarted.emit(session.peerAddress);
|
|
1194
|
+
let subscription: ServerSubscription;
|
|
1195
|
+
try {
|
|
1196
|
+
subscription = await this.#establishSubscription(
|
|
1197
|
+
subscriptionId,
|
|
1198
|
+
request,
|
|
1199
|
+
messenger,
|
|
1200
|
+
session,
|
|
1201
|
+
exchange,
|
|
1202
|
+
message,
|
|
1203
|
+
);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
logger.error(
|
|
1206
|
+
`Subscription ${subscriptionId} for Session ${session.id}: Error while sending initial data reports`,
|
|
1207
|
+
error instanceof MatterError ? error.message : error,
|
|
1208
|
+
);
|
|
1209
|
+
if (error instanceof StatusResponseError && !(error instanceof ReceivedStatusResponseError)) {
|
|
1210
|
+
logger.info(`Sending status response ${error.code} for interaction error: ${error.message}`);
|
|
1211
|
+
await messenger.sendStatus(error.code, {
|
|
1212
|
+
logContext: {
|
|
1213
|
+
for: "I/SubscriptionSeed-Status",
|
|
1214
|
+
},
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
await messenger.close();
|
|
1218
|
+
return; // Make sure to not bubble up the exception
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const maxInterval = subscription.maxInterval;
|
|
1222
|
+
// Then send the subscription response
|
|
1223
|
+
await messenger.send(
|
|
1224
|
+
MessageType.SubscribeResponse,
|
|
1225
|
+
TlvSubscribeResponse.encode({
|
|
1226
|
+
subscriptionId,
|
|
1227
|
+
maxInterval,
|
|
1228
|
+
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
1229
|
+
}),
|
|
1230
|
+
{
|
|
1231
|
+
logContext: {
|
|
1232
|
+
subId: subscriptionId,
|
|
1233
|
+
maxInterval,
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
// When an error occurs while sending the response, the subscription is not yet active and will be cleaned up by GC
|
|
1239
|
+
subscription.activate();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async #establishSubscription(
|
|
1243
|
+
id: number,
|
|
1244
|
+
{
|
|
1245
|
+
minIntervalFloorSeconds,
|
|
1246
|
+
maxIntervalCeilingSeconds,
|
|
1247
|
+
attributeRequests,
|
|
1248
|
+
dataVersionFilters,
|
|
1249
|
+
eventRequests,
|
|
1250
|
+
eventFilters,
|
|
1251
|
+
isFabricFiltered,
|
|
1252
|
+
}: SubscribeRequest,
|
|
1253
|
+
messenger: InteractionServerMessenger,
|
|
1254
|
+
session: SecureSession,
|
|
1255
|
+
exchange: MessageExchange,
|
|
1256
|
+
message: Message,
|
|
1257
|
+
) {
|
|
1258
|
+
const context: ServerSubscriptionContext = {
|
|
1259
|
+
session,
|
|
1260
|
+
structure: this.#context.structure,
|
|
1261
|
+
|
|
1262
|
+
readAttribute: (path, attribute, offline) =>
|
|
1263
|
+
this.readAttribute(path, attribute, exchange, isFabricFiltered, message, offline),
|
|
1264
|
+
|
|
1265
|
+
readEndpointAttributesForSubscription: attributes =>
|
|
1266
|
+
this.readEndpointAttributesForSubscription(attributes, exchange, isFabricFiltered, message),
|
|
1267
|
+
|
|
1268
|
+
readEvent: (path, event, eventFilters) =>
|
|
1269
|
+
this.readEvent(path, eventFilters, event, exchange, isFabricFiltered, message),
|
|
1270
|
+
|
|
1271
|
+
initiateExchange: (address: PeerAddress, protocolId) =>
|
|
1272
|
+
this.#context.exchangeManager.initiateExchange(address, protocolId),
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
const subscription = new ServerSubscription({
|
|
1276
|
+
id,
|
|
1277
|
+
context,
|
|
1278
|
+
criteria: {
|
|
1279
|
+
attributeRequests,
|
|
1280
|
+
dataVersionFilters,
|
|
1281
|
+
eventRequests,
|
|
1282
|
+
eventFilters,
|
|
1283
|
+
isFabricFiltered,
|
|
1284
|
+
},
|
|
1285
|
+
minIntervalFloorSeconds,
|
|
1286
|
+
maxIntervalCeilingSeconds,
|
|
1287
|
+
subscriptionOptions: this.#subscriptionConfig,
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
try {
|
|
1291
|
+
// Send initial data report to prime the subscription with initial data
|
|
1292
|
+
await subscription.sendInitialReport(messenger);
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
await subscription.close(); // Cleanup
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
logger.info(
|
|
1299
|
+
`Successfully created subscription ${id} for Session ${
|
|
1300
|
+
session.id
|
|
1301
|
+
} to ${session.peerAddress}. Updates: ${minIntervalFloorSeconds} - ${maxIntervalCeilingSeconds} => ${subscription.maxInterval} seconds (sendInterval = ${subscription.sendInterval} seconds)`,
|
|
1302
|
+
);
|
|
1303
|
+
return subscription;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async establishFormerSubscription(
|
|
1307
|
+
{
|
|
1308
|
+
subscriptionId,
|
|
1309
|
+
attributeRequests,
|
|
1310
|
+
eventRequests,
|
|
1311
|
+
isFabricFiltered,
|
|
1312
|
+
minIntervalFloorSeconds,
|
|
1313
|
+
maxIntervalCeilingSeconds,
|
|
1314
|
+
maxInterval,
|
|
1315
|
+
sendInterval,
|
|
1316
|
+
}: PeerSubscription,
|
|
1317
|
+
session: SecureSession,
|
|
1318
|
+
) {
|
|
1319
|
+
const exchange = this.#context.exchangeManager.initiateExchange(session.peerAddress, INTERACTION_PROTOCOL_ID);
|
|
1320
|
+
const message = {} as Message;
|
|
1321
|
+
logger.debug(
|
|
1322
|
+
`Send DataReports to re-establish subscription ${subscriptionId} to `,
|
|
1323
|
+
Diagnostic.dict({ isFabricFiltered, maxInterval, sendInterval }),
|
|
1324
|
+
);
|
|
1325
|
+
const context: ServerSubscriptionContext = {
|
|
1326
|
+
session,
|
|
1327
|
+
structure: this.#context.structure,
|
|
1328
|
+
|
|
1329
|
+
readAttribute: (path, attribute, offline) =>
|
|
1330
|
+
this.readAttribute(path, attribute, exchange, isFabricFiltered, message, offline),
|
|
1331
|
+
|
|
1332
|
+
readEndpointAttributesForSubscription: attributes =>
|
|
1333
|
+
this.readEndpointAttributesForSubscription(attributes, exchange, isFabricFiltered, message),
|
|
1334
|
+
|
|
1335
|
+
readEvent: (path, event, eventFilters) =>
|
|
1336
|
+
this.readEvent(path, eventFilters, event, exchange, isFabricFiltered, message),
|
|
1337
|
+
|
|
1338
|
+
initiateExchange: (address: PeerAddress, protocolId) =>
|
|
1339
|
+
this.#context.exchangeManager.initiateExchange(address, protocolId),
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
const subscription = new ServerSubscription({
|
|
1343
|
+
id: subscriptionId,
|
|
1344
|
+
context,
|
|
1345
|
+
minIntervalFloorSeconds,
|
|
1346
|
+
maxIntervalCeilingSeconds,
|
|
1347
|
+
criteria: {
|
|
1348
|
+
attributeRequests,
|
|
1349
|
+
eventRequests,
|
|
1350
|
+
isFabricFiltered,
|
|
1351
|
+
},
|
|
1352
|
+
subscriptionOptions: this.#subscriptionConfig,
|
|
1353
|
+
useAsMaxInterval: maxInterval,
|
|
1354
|
+
useAsSendInterval: sendInterval,
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
try {
|
|
1358
|
+
// Send initial data report to prime the subscription with initial data
|
|
1359
|
+
await subscription.sendInitialReport(new InteractionServerMessenger(exchange));
|
|
1360
|
+
subscription.activate();
|
|
1361
|
+
logger.info(
|
|
1362
|
+
`Successfully re-established subscription ${subscriptionId} for Session ${
|
|
1363
|
+
session.id
|
|
1364
|
+
} to ${session.peerAddress}. Updates: ${minIntervalFloorSeconds} - ${maxIntervalCeilingSeconds} => ${subscription.maxInterval} seconds (sendInterval = ${subscription.sendInterval} seconds)`,
|
|
1365
|
+
);
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
await subscription.close(); // Cleanup
|
|
1368
|
+
throw error;
|
|
1369
|
+
}
|
|
1370
|
+
return subscription;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async handleInvokeRequest(
|
|
1374
|
+
exchange: MessageExchange,
|
|
1375
|
+
{ invokeRequests, timedRequest, suppressResponse, interactionModelRevision }: InvokeRequest,
|
|
1376
|
+
messenger: InteractionServerMessenger,
|
|
1377
|
+
message: Message,
|
|
1378
|
+
): Promise<void> {
|
|
1379
|
+
logger.debug(
|
|
1380
|
+
`Received invoke request from ${exchange.channel.name}${invokeRequests.length > 0 ? ` with ${invokeRequests.length} commands` : ""}: ${invokeRequests
|
|
1381
|
+
.map(({ commandPath: { endpointId, clusterId, commandId } }) =>
|
|
1382
|
+
this.#context.structure.resolveCommandName({ endpointId, clusterId, commandId }),
|
|
1383
|
+
)
|
|
1384
|
+
.join(", ")}, suppressResponse=${suppressResponse}`,
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1387
|
+
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
1388
|
+
logger.debug(
|
|
1389
|
+
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
|
|
1394
|
+
if (exchange.hasExpiredTimedInteraction()) {
|
|
1395
|
+
exchange.clearTimedInteraction(); // ??
|
|
1396
|
+
throw new StatusResponseError(`Timed request window expired. Decline invoke request.`, StatusCode.Timeout);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if (timedRequest !== exchange.hasTimedInteraction()) {
|
|
1400
|
+
throw new StatusResponseError(
|
|
1401
|
+
`timedRequest flag of invoke interaction (${timedRequest}) mismatch with expected timed interaction (${receivedWithinTimedInteraction}).`,
|
|
1402
|
+
StatusCode.TimedRequestMismatch,
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if (receivedWithinTimedInteraction) {
|
|
1407
|
+
logger.debug(`Invoke request from ${exchange.channel.name} received while timed interaction is running.`);
|
|
1408
|
+
exchange.clearTimedInteraction();
|
|
1409
|
+
if (message.packetHeader.sessionType !== SessionType.Unicast) {
|
|
1410
|
+
throw new StatusResponseError(
|
|
1411
|
+
"Invoke requests are only allowed on unicast sessions when a timed interaction is running.",
|
|
1412
|
+
StatusCode.InvalidAction,
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (invokeRequests.length > this.#maxPathsPerInvoke) {
|
|
1418
|
+
throw new StatusResponseError(
|
|
1419
|
+
`Only ${this.#maxPathsPerInvoke} invoke requests are supported in one message. This message contains ${invokeRequests.length}`,
|
|
1420
|
+
StatusCode.InvalidAction,
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Validate all commandPaths before proceeding to make sure not to have executed partial commands
|
|
1425
|
+
invokeRequests.forEach(({ commandPath }) => validateCommandPath(commandPath));
|
|
1426
|
+
|
|
1427
|
+
// Perform additional cross-command validation required for batch invoke
|
|
1428
|
+
if (invokeRequests.length > 1) {
|
|
1429
|
+
const pathsUsed = new Set<string>();
|
|
1430
|
+
const commandRefsUsed = new Set<number>();
|
|
1431
|
+
invokeRequests.forEach(({ commandPath, commandRef }) => {
|
|
1432
|
+
if (!isConcreteCommandPath(commandPath)) {
|
|
1433
|
+
throw new StatusResponseError("Illegal wildcard path in batch invoke", StatusCode.InvalidAction);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const commandPathId = commandPathToId(commandPath);
|
|
1437
|
+
if (pathsUsed.has(commandPathId)) {
|
|
1438
|
+
throw new StatusResponseError(
|
|
1439
|
+
`Duplicate command path (${commandPathId}) in batch invoke`,
|
|
1440
|
+
StatusCode.InvalidAction,
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (commandRef === undefined) {
|
|
1445
|
+
throw new StatusResponseError(
|
|
1446
|
+
`Command reference missing in batch invoke of ${commandPathId}`,
|
|
1447
|
+
StatusCode.InvalidAction,
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (commandRefsUsed.has(commandRef)) {
|
|
1452
|
+
throw new StatusResponseError(
|
|
1453
|
+
`Duplicate command reference ${commandRef} in invoke of ${commandPathId}`,
|
|
1454
|
+
StatusCode.InvalidAction,
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
pathsUsed.add(commandPathId);
|
|
1459
|
+
commandRefsUsed.add(commandRef);
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const isGroupSession = message.packetHeader.sessionType === SessionType.Group;
|
|
1464
|
+
const invokeResponseMessage: TypeFromSchema<typeof TlvInvokeResponseForSend> = {
|
|
1465
|
+
suppressResponse: false, // Deprecated but must be present
|
|
1466
|
+
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
1467
|
+
invokeResponses: [],
|
|
1468
|
+
moreChunkedMessages: invokeRequests.length > 1, // Assume for now we have multiple responses when having multiple invokes
|
|
1469
|
+
};
|
|
1470
|
+
const emptyInvokeResponseBytes = TlvInvokeResponseForSend.encode(invokeResponseMessage);
|
|
1471
|
+
let messageSize = emptyInvokeResponseBytes.length;
|
|
1472
|
+
let invokeResultsProcessed = 0;
|
|
1473
|
+
|
|
1474
|
+
// To lower potential latency when we would process all invoke messages and just send responses at the end we
|
|
1475
|
+
// assemble response on the fly locally here and send when message becomes too big
|
|
1476
|
+
const processResponseResult = async (
|
|
1477
|
+
invokeResponse: TypeFromSchema<typeof TlvInvokeResponseData>,
|
|
1478
|
+
): Promise<void> => {
|
|
1479
|
+
invokeResultsProcessed++;
|
|
1480
|
+
|
|
1481
|
+
if (isGroupSession) {
|
|
1482
|
+
// We send no responses at all for group sessions
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const encodedInvokeResponse = TlvInvokeResponseData.encodeTlv(invokeResponse);
|
|
1486
|
+
const invokeResponseBytes = TlvAny.getEncodedByteLength(encodedInvokeResponse);
|
|
1487
|
+
|
|
1488
|
+
if (
|
|
1489
|
+
messageSize + invokeResponseBytes > exchange.maxPayloadSize ||
|
|
1490
|
+
invokeResultsProcessed === invokeRequests.length
|
|
1491
|
+
) {
|
|
1492
|
+
let lastMessageProcessed = false;
|
|
1493
|
+
if (messageSize + invokeResponseBytes <= exchange.maxPayloadSize) {
|
|
1494
|
+
// last invoke response and matches in the message
|
|
1495
|
+
invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
|
|
1496
|
+
lastMessageProcessed = true;
|
|
1497
|
+
}
|
|
1498
|
+
// Send the response when the message is full or when all responses are processed
|
|
1499
|
+
if (invokeResponseMessage.invokeResponses.length > 0) {
|
|
1500
|
+
if (invokeRequests.length > 1) {
|
|
1501
|
+
logger.debug(
|
|
1502
|
+
`Send ${lastMessageProcessed ? "final " : ""}invoke response for ${invokeResponseMessage.invokeResponses} commands`,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
const moreChunkedMessages = lastMessageProcessed ? undefined : true;
|
|
1506
|
+
await messenger.send(
|
|
1507
|
+
MessageType.InvokeResponse,
|
|
1508
|
+
TlvInvokeResponseForSend.encode({
|
|
1509
|
+
...invokeResponseMessage,
|
|
1510
|
+
moreChunkedMessages,
|
|
1511
|
+
}),
|
|
1512
|
+
{
|
|
1513
|
+
logContext: {
|
|
1514
|
+
invokeMsgFlags: Diagnostic.asFlags({
|
|
1515
|
+
suppressResponse,
|
|
1516
|
+
moreChunkedMessages,
|
|
1517
|
+
}),
|
|
1518
|
+
},
|
|
1519
|
+
},
|
|
1520
|
+
);
|
|
1521
|
+
invokeResponseMessage.invokeResponses = [];
|
|
1522
|
+
messageSize = emptyInvokeResponseBytes.length;
|
|
1523
|
+
}
|
|
1524
|
+
if (!lastMessageProcessed) {
|
|
1525
|
+
invokeResultsProcessed--; // Correct counter again because we recall the method
|
|
1526
|
+
return processResponseResult(invokeResponse);
|
|
1527
|
+
}
|
|
1528
|
+
} else {
|
|
1529
|
+
invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
|
|
1530
|
+
messageSize += invokeResponseBytes;
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
// We could do more fancy parallel command processing, but it makes no sense for now, so lets simply process
|
|
1535
|
+
// invoked commands one by one sequentially
|
|
1536
|
+
for (const { commandPath, commandFields, commandRef } of invokeRequests) {
|
|
1537
|
+
const commands = this.#context.structure.getCommands([commandPath]);
|
|
1538
|
+
|
|
1539
|
+
if (commands.length === 0) {
|
|
1540
|
+
if (isConcreteCommandPath(commandPath)) {
|
|
1541
|
+
const { endpointId, clusterId, commandId } = commandPath;
|
|
1542
|
+
|
|
1543
|
+
let result;
|
|
1544
|
+
|
|
1545
|
+
try {
|
|
1546
|
+
this.#context.structure.validateConcreteCommandPath(endpointId, clusterId, commandId);
|
|
1547
|
+
throw new InternalError(
|
|
1548
|
+
"validateConcreteCommandPath should throw StatusResponseError but did not.",
|
|
1549
|
+
);
|
|
1550
|
+
} catch (e) {
|
|
1551
|
+
StatusResponseError.accept(e);
|
|
1552
|
+
|
|
1553
|
+
logger.debug(
|
|
1554
|
+
`Invoke from ${exchange.channel.name}: ${this.#context.structure.resolveCommandName(
|
|
1555
|
+
commandPath,
|
|
1556
|
+
)} unsupported path: Status=${e.code}`,
|
|
1557
|
+
);
|
|
1558
|
+
result = { status: { commandPath, status: { status: e.code }, commandRef } };
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
await processResponseResult(result);
|
|
1562
|
+
} else {
|
|
1563
|
+
// Wildcard path: Just ignore
|
|
1564
|
+
logger.debug(
|
|
1565
|
+
`Invoke from ${exchange.channel.name}: ${this.#context.structure.resolveCommandName(
|
|
1566
|
+
commandPath,
|
|
1567
|
+
)} ignore non-existing command`,
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const isConcretePath = isConcreteCommandPath(commandPath);
|
|
1574
|
+
for (const { command, path } of commands) {
|
|
1575
|
+
const { endpointId, clusterId, commandId } = path;
|
|
1576
|
+
if (endpointId === undefined) {
|
|
1577
|
+
// Should never happen
|
|
1578
|
+
logger.error(
|
|
1579
|
+
`Invoke from ${exchange.channel.name}: ${this.#context.structure.resolveCommandName(
|
|
1580
|
+
path,
|
|
1581
|
+
)} invalid path because empty endpoint!`,
|
|
1582
|
+
);
|
|
1583
|
+
if (isConcretePath) {
|
|
1584
|
+
await processResponseResult({
|
|
1585
|
+
status: {
|
|
1586
|
+
commandPath: path,
|
|
1587
|
+
status: { status: StatusCode.UnsupportedEndpoint },
|
|
1588
|
+
commandRef,
|
|
1589
|
+
},
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const endpoint = this.#context.structure.getEndpoint(endpointId);
|
|
1595
|
+
if (endpoint === undefined) {
|
|
1596
|
+
// Should never happen
|
|
1597
|
+
logger.error(
|
|
1598
|
+
`Invoke from ${exchange.channel.name}: ${this.#context.structure.resolveCommandName(
|
|
1599
|
+
path,
|
|
1600
|
+
)} invalid path because endpoint not found!`,
|
|
1601
|
+
);
|
|
1602
|
+
if (isConcretePath) {
|
|
1603
|
+
await processResponseResult({
|
|
1604
|
+
status: {
|
|
1605
|
+
commandPath: path,
|
|
1606
|
+
status: { status: StatusCode.UnsupportedEndpoint },
|
|
1607
|
+
commandRef,
|
|
1608
|
+
},
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (command.requiresTimedInteraction && !receivedWithinTimedInteraction) {
|
|
1614
|
+
logger.debug(`This invoke requires a timed interaction which is not initialized.`);
|
|
1615
|
+
if (isConcretePath) {
|
|
1616
|
+
await processResponseResult({
|
|
1617
|
+
status: {
|
|
1618
|
+
commandPath: path,
|
|
1619
|
+
status: { status: StatusCode.NeedsTimedInteraction },
|
|
1620
|
+
commandRef,
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
if (
|
|
1627
|
+
getMatterModelClusterCommand(clusterId, commandId)?.fabricScoped &&
|
|
1628
|
+
(!exchange.session.isSecure || !(exchange.session as SecureSession).fabric)
|
|
1629
|
+
) {
|
|
1630
|
+
logger.debug(`This invoke requires a secure session with a fabric assigned which is missing.`);
|
|
1631
|
+
if (isConcretePath) {
|
|
1632
|
+
await processResponseResult({
|
|
1633
|
+
status: { commandPath: path, status: { status: StatusCode.UnsupportedAccess }, commandRef },
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
let result;
|
|
1640
|
+
try {
|
|
1641
|
+
result = await this.invokeCommand(
|
|
1642
|
+
path,
|
|
1643
|
+
command,
|
|
1644
|
+
exchange,
|
|
1645
|
+
commandFields ?? TlvNoArguments.encodeTlv(commandFields),
|
|
1646
|
+
message,
|
|
1647
|
+
endpoint,
|
|
1648
|
+
receivedWithinTimedInteraction,
|
|
1649
|
+
);
|
|
1650
|
+
} catch (e) {
|
|
1651
|
+
StatusResponseError.accept(e);
|
|
1652
|
+
|
|
1653
|
+
let errorCode = e.code;
|
|
1654
|
+
const errorLogText = `Error ${Diagnostic.hex(errorCode)}${
|
|
1655
|
+
e.clusterCode !== undefined ? `/${Diagnostic.hex(e.clusterCode)}` : ""
|
|
1656
|
+
} while invoking command: ${e.message}`;
|
|
1657
|
+
|
|
1658
|
+
if (e instanceof ValidationError) {
|
|
1659
|
+
logger.info(
|
|
1660
|
+
`Validation-${errorLogText}${e.fieldName !== undefined ? ` in field ${e.fieldName}` : ""}`,
|
|
1661
|
+
);
|
|
1662
|
+
if (errorCode === StatusCode.InvalidAction) {
|
|
1663
|
+
errorCode = StatusCode.InvalidCommand;
|
|
1664
|
+
}
|
|
1665
|
+
} else {
|
|
1666
|
+
logger.info(errorLogText);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
result = {
|
|
1670
|
+
code: errorCode,
|
|
1671
|
+
clusterCode: e.clusterCode,
|
|
1672
|
+
responseId: command.responseId,
|
|
1673
|
+
response: TlvNoResponse.encodeTlv(),
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
const { code, clusterCode, responseId, response } = result;
|
|
1677
|
+
if (response.length === 0) {
|
|
1678
|
+
await processResponseResult({
|
|
1679
|
+
status: { commandPath: path, status: { status: code, clusterStatus: clusterCode }, commandRef },
|
|
1680
|
+
});
|
|
1681
|
+
} else {
|
|
1682
|
+
await processResponseResult({
|
|
1683
|
+
command: {
|
|
1684
|
+
commandPath: { ...path, commandId: responseId },
|
|
1685
|
+
commandFields: response,
|
|
1686
|
+
commandRef,
|
|
1687
|
+
},
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
protected async invokeCommand(
|
|
1695
|
+
path: CommandPath,
|
|
1696
|
+
command: CommandServer<any, any>,
|
|
1697
|
+
exchange: MessageExchange,
|
|
1698
|
+
commandFields: any,
|
|
1699
|
+
message: Message,
|
|
1700
|
+
endpoint: EndpointInterface,
|
|
1701
|
+
timed = false,
|
|
1702
|
+
) {
|
|
1703
|
+
const invokeCommand = (context: ActionContext) => {
|
|
1704
|
+
if (
|
|
1705
|
+
context.authorityAt(command.invokeAcl, {
|
|
1706
|
+
endpoint: endpoint.number,
|
|
1707
|
+
cluster: path.clusterId,
|
|
1708
|
+
} as AccessControl.Location) !== AccessControl.Authority.Granted
|
|
1709
|
+
) {
|
|
1710
|
+
throw new AccessDeniedError(
|
|
1711
|
+
`Access to ${endpoint.number}/${Diagnostic.hex(path.clusterId)} denied on ${exchange.session.name}.`,
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
return command.invoke(exchange.session, commandFields, message, endpoint);
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
return OnlineContext({
|
|
1718
|
+
activity: (exchange as WithActivity)[activityKey],
|
|
1719
|
+
command: true,
|
|
1720
|
+
timed,
|
|
1721
|
+
message,
|
|
1722
|
+
exchange,
|
|
1723
|
+
tracer: this.#tracer,
|
|
1724
|
+
actionType: ActionTracer.ActionType.Invoke,
|
|
1725
|
+
node: this.#node,
|
|
1726
|
+
}).act(invokeCommand);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
handleTimedRequest(exchange: MessageExchange, { timeout, interactionModelRevision }: TimedRequest) {
|
|
1730
|
+
logger.debug(`Received timed request (${timeout}ms) from ${exchange.channel.name}`);
|
|
1731
|
+
|
|
1732
|
+
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
1733
|
+
logger.debug(
|
|
1734
|
+
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
exchange.startTimedInteraction(timeout);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
async close() {
|
|
1742
|
+
this.#isClosing = true;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
get #tracer() {
|
|
1746
|
+
if (this.#node.env.has(ActionTracer)) {
|
|
1747
|
+
return this.#node.env.get(ActionTracer);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
#updateStructure() {
|
|
1752
|
+
if (this.#node.lifecycle.isPartsReady) {
|
|
1753
|
+
const server = EndpointServer.forEndpoint(this.#node);
|
|
1754
|
+
this.#context.structure.initializeFromEndpoint(server);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|