UNPKG

@project-chip/matter.js

Version:
516 lines (515 loc) 18 kB
/** * @license * Copyright 2022-2025 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { GeneralCommissioning } from "#clusters"; import { CachedClientNodeStore } from "#device/CachedClientNodeStore.js"; import { Bytes, ChannelType, Construction, Crypto, CRYPTO_SYMMETRIC_KEY_LENGTH, Environment, ImplementationError, Logger, MatterError, NetInterfaceSet, serverAddressToString, StorageBackendMemory, StorageManager } from "#general"; import { LegacyControllerStore } from "#LegacyControllerStore.js"; import { CertificateAuthority, ChannelManager, ClusterClient, CommissioningError, ControllerCommissioner, DEFAULT_ADMIN_VENDOR_ID, DEFAULT_FABRIC_ID, DeviceAdvertiser, ExchangeManager, Fabric, FabricBuilder, FabricManager, InteractionClientProvider, NodeDiscoveryType, PeerAddressStore, PeerSet, RetransmissionLimitReachedError, SecureChannelProtocol, SessionManager, SubscriptionClient } from "#protocol"; import { EndpointNumber, FabricId, FabricIndex, NodeId } from "#types"; import { MessageChannel } from "@matter/protocol"; const DEFAULT_FABRIC_INDEX = FabricIndex(1); const CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE = 3; const CONTROLLER_MAX_PATHS_PER_INVOKE = 10; const logger = Logger.get("MatterController"); class MatterController { static async create(options) { const { controllerStore, scanners, netInterfaces, sessionClosedCallback, adminVendorId, adminFabricId = FabricId(DEFAULT_FABRIC_ID), adminFabricIndex = FabricIndex(DEFAULT_FABRIC_INDEX), caseAuthenticatedTags, adminFabricLabel, rootNodeId, rootCertificateAuthority, rootFabric } = options; const crypto = options.crypto ?? Environment.default.get(Crypto); const ca = rootCertificateAuthority ?? await CertificateAuthority.create(crypto, controllerStore.caStorage); const fabricStorage = controllerStore.fabricStorage; let controller = void 0; if (rootFabric !== void 0 || await fabricStorage.has("fabric")) { const fabric = rootFabric ?? new Fabric(crypto, await fabricStorage.get("fabric")); if (Bytes.areEqual(fabric.rootCert, ca.rootCert)) { logger.info("Used existing fabric"); controller = new MatterController({ controllerStore, scanners, netInterfaces, certificateManager: ca, fabric, adminFabricLabel, sessionClosedCallback }); fabric.storage = fabricStorage; } else { if (rootFabric !== void 0) { throw new MatterError("Fabric CA certificate is not in sync with CA."); } logger.info("Fabric CA certificate changed ..."); if (await controllerStore.nodesStorage.has("commissionedNodes")) { throw new MatterError( "Fabric certificate changed, but commissioned nodes are still present. Please clear the storage." ); } } } if (controller === void 0) { logger.info("Creating new fabric"); const controllerNodeId = rootNodeId ?? NodeId.randomOperationalNodeId(crypto); const ipkValue = crypto.randomBytes(CRYPTO_SYMMETRIC_KEY_LENGTH); const fabricBuilder = await FabricBuilder.create(crypto); await fabricBuilder.setRootCert(ca.rootCert); fabricBuilder.setRootNodeId(controllerNodeId).setIdentityProtectionKey(ipkValue).setRootVendorId(adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID).setLabel(adminFabricLabel); await fabricBuilder.setOperationalCert( await ca.generateNoc(fabricBuilder.publicKey, adminFabricId, controllerNodeId, caseAuthenticatedTags) ); const fabric = await fabricBuilder.build(adminFabricIndex); fabric.storage = fabricStorage; controller = new MatterController({ controllerStore, scanners, netInterfaces, certificateManager: ca, fabric, adminFabricLabel, sessionClosedCallback }); } await controller.construction; return controller; } static async createAsPaseCommissioner(options) { const { certificateAuthorityConfig, rootCertificateAuthority, fabricConfig, adminFabricLabel, scanners, netInterfaces, sessionClosedCallback } = options; const crypto = options.crypto ?? Environment.default.get(Crypto); if (!netInterfaces.hasInterfaceFor(ChannelType.BLE)) { if (!scanners.hasScannerFor(ChannelType.UDP) || !netInterfaces.hasInterfaceFor(ChannelType.UDP, "::")) { throw new ImplementationError( "Ble must be initialized to create a Sub Commissioner without an IP network!" ); } logger.info("BLE is not enabled. Using only IP network for commissioning."); } if (rootCertificateAuthority === void 0 && certificateAuthorityConfig === void 0) { throw new ImplementationError("Either rootCertificateAuthority or certificateAuthorityConfig must be set."); } const certificateManager = rootCertificateAuthority ?? await CertificateAuthority.create(crypto, certificateAuthorityConfig); const storageManager = new StorageManager(new StorageBackendMemory()); await storageManager.initialize(); const fabric = new Fabric(crypto, fabricConfig); const controller = new MatterController({ controllerStore: new LegacyControllerStore(storageManager.createContext("Commissioner")), scanners, netInterfaces, certificateManager, fabric, adminFabricLabel, sessionClosedCallback }); await controller.construction; return controller; } sessionManager; netInterfaces = new NetInterfaceSet(); channelManager = new ChannelManager(CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE); exchangeManager; peers; clients; commissioner; #construction; #store; nodesStore; scanners; ca; fabric; sessionClosedCallback; #advertiser; get construction() { return this.#construction; } constructor(options) { const { controllerStore, scanners, netInterfaces, certificateManager, fabric, sessionClosedCallback, adminFabricLabel } = options; this.#store = controllerStore; this.scanners = scanners; this.netInterfaces = netInterfaces; this.ca = certificateManager; this.fabric = fabric; this.sessionClosedCallback = sessionClosedCallback; const fabricManager = new FabricManager(fabric.crypto); fabricManager.addFabric(fabric); fabric.persistCallback = async () => { await this.#store.fabricStorage.set("fabric", this.fabric.config); }; this.sessionManager = new SessionManager({ fabrics: fabricManager, storage: controllerStore.sessionStorage, parameters: { maxPathsPerInvoke: CONTROLLER_MAX_PATHS_PER_INVOKE } }); this.sessionManager.sessions.deleted.on(async (session) => { this.sessionClosedCallback?.(session.peerNodeId); }); const subscriptionClient = new SubscriptionClient(); this.exchangeManager = new ExchangeManager({ crypto: fabric.crypto, sessionManager: this.sessionManager, channelManager: this.channelManager, transportInterfaces: this.netInterfaces }); this.exchangeManager.addProtocolHandler(new SecureChannelProtocol(this.sessionManager, fabricManager)); this.exchangeManager.addProtocolHandler(subscriptionClient); this.nodesStore = new CommissionedNodeStore(controllerStore, fabric); this.nodesStore.peers = this.peers = new PeerSet({ sessions: this.sessionManager, channels: this.channelManager, exchanges: this.exchangeManager, subscriptionClient, scanners: this.scanners, netInterfaces: this.netInterfaces, store: this.nodesStore }); this.clients = new InteractionClientProvider(this.peers); this.commissioner = new ControllerCommissioner({ peers: this.peers, clients: this.clients, scanners: this.scanners, netInterfaces: this.netInterfaces, exchanges: this.exchangeManager, sessions: this.sessionManager, ca: this.ca }); this.#advertiser = new DeviceAdvertiser({ fabrics: fabricManager, sessions: this.sessionManager }); this.#construction = Construction(this, async () => { await this.peers.construction.ready; await this.sessionManager.construction.ready; if (this.fabric.label !== adminFabricLabel) { await fabric.setLabel(adminFabricLabel); } }); } get nodeId() { return this.fabric.rootNodeId; } get caConfig() { return this.ca.config; } get fabricConfig() { return this.fabric.config; } get sessions() { return this.sessionManager.sessions; } getFabrics() { return [this.fabric]; } hasBroadcaster(broadcaster) { return this.#advertiser.hasBroadcaster(broadcaster); } addBroadcaster(broadcaster) { this.#advertiser.addBroadcaster(broadcaster); } async deleteBroadcaster(broadcaster) { await this.#advertiser.deleteBroadcaster(broadcaster); } collectScanners(discoveryCapabilities = { onIpNetwork: true }) { return this.scanners.filter( (scanner) => scanner.type === ChannelType.UDP || discoveryCapabilities.ble && scanner.type === ChannelType.BLE ); } /** * Commission a device by its identifier and the Passcode. If a known address is provided this is tried first * before discovering devices in the network. If multiple addresses or devices are found, they are tried all after * each other. It returns the NodeId of the commissioned device. * If it throws an PairRetransmissionLimitReachedError that means that no found device responded to the pairing * request or the passode did not match to any discovered device/address. * * Use the connectNodeAfterCommissioning callback to implement an own logic to do the operative device discovery and * to complete the commissioning process. * Return true when the commissioning process is completed successfully, false on error. */ async commission(options, customizations) { const commissioningOptions = { ...options.commissioning, fabric: this.fabric, discovery: options.discovery, passcode: options.passcode }; const { completeCommissioningCallback, commissioningFlowImpl } = customizations ?? {}; if (completeCommissioningCallback) { commissioningOptions.finalizeCommissioning = async (peerAddress, discoveryData) => { const result = await completeCommissioningCallback(peerAddress.nodeId, discoveryData); if (!result) { throw new RetransmissionLimitReachedError("Device could not be discovered"); } }; } commissioningOptions.commissioningFlowImpl = commissioningFlowImpl; const address = await this.commissioner.commissionWithDiscovery(commissioningOptions); await this.fabric.persist(); return address.nodeId; } async disconnect(nodeId) { return this.peers.disconnect(this.fabric.addressOf(nodeId)); } async connectPaseChannel(options) { const { paseSecureChannel } = await this.commissioner.discoverAndEstablishPase({ ...options.commissioning, fabric: this.fabric, discovery: options.discovery, passcode: options.passcode }); logger.warn("PASE channel established", paseSecureChannel.session.name, paseSecureChannel.session.isSecure); return paseSecureChannel; } async removeNode(nodeId) { return this.peers.delete(this.fabric.addressOf(nodeId)); } /** * Method to complete the commissioning process to a node which was initialized with a PASE secure channel. */ async completeCommissioning(peerNodeId, discoveryData) { const interactionClient = await this.connect( peerNodeId, { discoveryType: NodeDiscoveryType.TimedDiscovery, timeoutSeconds: 120, discoveryData }, true ); const generalCommissioningClusterClient = ClusterClient( GeneralCommissioning.Cluster, EndpointNumber(0), interactionClient ); const { errorCode, debugText } = await generalCommissioningClusterClient.commissioningComplete(void 0, { useExtendedFailSafeMessageResponseTimeout: true }); if (errorCode !== GeneralCommissioning.CommissioningError.Ok) { await this.peers.delete(this.fabric.addressOf(peerNodeId)); throw new CommissioningError(`Commission error on commissioningComplete: ${errorCode}, ${debugText}`); } await this.fabric.persist(); } isCommissioned() { return this.peers.size > 0; } getCommissionedNodes() { return this.peers.map((peer) => peer.address.nodeId); } getCommissionedNodesDetails() { return this.peers.map((peer) => { const { address, operationalAddress, discoveryData, deviceData } = peer; return { nodeId: address.nodeId, operationalAddress: operationalAddress ? serverAddressToString(operationalAddress) : void 0, advertisedName: discoveryData?.DN, discoveryData, deviceData }; }); } getCommissionedNodeDetails(nodeId) { const nodeDetails = this.peers.get(this.fabric.addressOf(nodeId)); if (nodeDetails === void 0) { throw new Error(`Node ${nodeId} is not commissioned.`); } const { address, operationalAddress, discoveryData, deviceData } = nodeDetails; return { nodeId: address.nodeId, operationalAddress: operationalAddress ? serverAddressToString(operationalAddress) : void 0, advertisedName: discoveryData?.DN, discoveryData, deviceData }; } async enhanceCommissionedNodeDetails(nodeId, deviceData) { const nodeDetails = this.peers.get(this.fabric.addressOf(nodeId)); if (nodeDetails === void 0) { throw new Error(`Node ${nodeId} is not commissioned.`); } nodeDetails.deviceData = deviceData; await this.nodesStore.save(); } /** * Connect to the device by opening a channel and creating a new CASE session if necessary. * Returns a InteractionClient on success. */ async connect(peerNodeId, discoveryOptions, allowUnknownPeer) { return this.clients.connect(this.fabric.addressOf(peerNodeId), { discoveryOptions, allowUnknownPeer }); } createInteractionClient(peerNodeIdOrChannel, discoveryOptions) { if (peerNodeIdOrChannel instanceof MessageChannel) { return this.clients.getInteractionClientForChannel(peerNodeIdOrChannel); } return this.clients.getInteractionClient(this.fabric.addressOf(peerNodeIdOrChannel), discoveryOptions); } async getNextAvailableSessionId() { return this.sessionManager.getNextAvailableSessionId(); } getResumptionRecord(resumptionId) { return this.sessionManager.findResumptionRecordById(resumptionId); } findResumptionRecordByNodeId(nodeId) { return this.sessionManager.findResumptionRecordByAddress(this.fabric.addressOf(nodeId)); } async saveResumptionRecord(resumptionRecord) { return this.sessionManager.saveResumptionRecord(resumptionRecord); } announce() { return this.#advertiser.advertise(); } async close() { await this.peers.close(); await this.exchangeManager.close(); await this.sessionManager.close(); await this.channelManager.close(); await this.netInterfaces.close(); await this.#advertiser.close(); } getActiveSessionInformation() { return this.sessionManager.getActiveSessionInformation(); } async getStoredClusterDataVersions(nodeId, filterEndpointId, filterClusterId) { const peer = this.peers.get(this.fabric.addressOf(nodeId)); if (peer === void 0 || peer.dataStore === void 0) { return []; } await peer.dataStore.construction; return peer.dataStore.getClusterDataVersions(filterEndpointId, filterClusterId); } async retrieveStoredAttributes(nodeId, endpointId, clusterId) { const peer = this.peers.get(this.fabric.addressOf(nodeId)); if (peer === void 0 || peer.dataStore === void 0) { return []; } await peer.dataStore.construction; return peer.dataStore.retrieveAttributes(endpointId, clusterId); } async updateFabricLabel(label) { await this.fabric.setLabel(label); } } class CommissionedNodeStore extends PeerAddressStore { constructor(controllerStore, fabric) { super(); this.fabric = fabric; this.#controllerStore = controllerStore; } #controllerStore; async createNodeStore(address, load = true) { return new CachedClientNodeStore(await this.#controllerStore.clientNodeStore(address.nodeId.toString()), load); } async loadPeers() { if (!await this.#controllerStore.nodesStorage.has("commissionedNodes")) { return []; } const commissionedNodes = await this.#controllerStore.nodesStorage.get("commissionedNodes"); const nodes = new Array(); for (const [nodeId, { operationalServerAddress, discoveryData, deviceData }] of commissionedNodes) { const address = this.fabric.addressOf(nodeId); nodes.push({ address, operationalAddress: operationalServerAddress, discoveryData, deviceData, dataStore: await this.createNodeStore(address) }); } return nodes; } async updatePeer() { return this.save(); } async deletePeer(address) { await (await this.#controllerStore.clientNodeStore(address.nodeId.toString())).clearAll(); return this.save(); } async save() { await this.#controllerStore.nodesStorage.set( "commissionedNodes", this.peers.map((peer) => { const { address, operationalAddress: operationalServerAddress, discoveryData, deviceData } = peer; return [ address.nodeId, { operationalServerAddress, discoveryData, deviceData } ]; }) ); } } export { MatterController }; //# sourceMappingURL=MatterController.js.map