| /* | |
| * | |
| * Copyright (c) 2026 Project CHIP Authors | |
| * All rights reserved. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include <app/clusters/commissioning-proxy-server/CommissioningProxySessionManager.h> | |
| #include <clusters/CommissioningProxy/Commands.h> | |
| #include <lib/support/logging/CHIPLogging.h> | |
| #include <messaging/ExchangeContext.h> | |
| #include <system/SystemClock.h> | |
| #include <transport/raw/MessageHeader.h> | |
| #include <utility> | |
| namespace chip { | |
| namespace app { | |
| namespace Clusters { | |
| namespace CommissioningProxy { | |
| using Status = Protocols::InteractionModel::Status; | |
| namespace { | |
| // Head-room so the IM exchange outlives our own response timer and can still carry | |
| // the Status::Timeout back to the commissioner. | |
| constexpr uint16_t kResponseTimeoutMarginSecs = 5; | |
| // Smallest payload header: exchange flags, opcode, exchange ID and protocol ID. | |
| constexpr size_t kMinPayloadHeaderLength = 6; | |
| } // namespace | |
| CommissioningProxySessionManager::SessionSlot * CommissioningProxySessionManager::FindSlot(uint16_t sessionId) | |
| { | |
| for (auto & slot : mSessions) | |
| { | |
| if (slot.inUse && slot.sessionId == sessionId) | |
| { | |
| return &slot; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| const CommissioningProxySessionManager::SessionSlot * CommissioningProxySessionManager::FindSlot(uint16_t sessionId) const | |
| { | |
| return const_cast<CommissioningProxySessionManager *>(this)->FindSlot(sessionId); | |
| } | |
| uint8_t CommissioningProxySessionManager::ActiveCount() const | |
| { | |
| uint8_t count = 0; | |
| for (const auto & slot : mSessions) | |
| { | |
| count = static_cast<uint8_t>(count + (slot.inUse ? 1 : 0)); | |
| } | |
| return count; | |
| } | |
| uint16_t CommissioningProxySessionManager::AllocSessionId() | |
| { | |
| uint16_t id; | |
| do | |
| { | |
| if (mNextSessionId == 0) | |
| { | |
| mNextSessionId = 1; | |
| } | |
| id = mNextSessionId++; | |
| } while (FindSlot(id) != nullptr); | |
| return id; | |
| } | |
| void CommissioningProxySessionManager::RegisterSession(uint16_t sessionId, CapabilitiesBitmap transport, FabricIndex fabricIndex) | |
| { | |
| SessionSlot * slot = FindSlot(sessionId); | |
| if (slot == nullptr) | |
| { | |
| for (auto & candidate : mSessions) | |
| { | |
| if (!candidate.inUse) | |
| { | |
| slot = &candidate; | |
| break; | |
| } | |
| } | |
| } | |
| // The cluster gates on MaxSessions before connecting, so the pool cannot be full here. | |
| VerifyOrDie(slot != nullptr); | |
| slot->inUse = true; | |
| slot->sessionId = sessionId; | |
| slot->info = SessionInfo{ transport, fabricIndex }; | |
| } | |
| void CommissioningProxySessionManager::RemoveSession(uint16_t sessionId) | |
| { | |
| AbortPending(sessionId); | |
| if (SessionSlot * slot = FindSlot(sessionId)) | |
| { | |
| slot->inUse = false; | |
| } | |
| } | |
| std::optional<CommissioningProxySessionManager::SessionInfo> CommissioningProxySessionManager::FindSession(uint16_t sessionId) const | |
| { | |
| const SessionSlot * slot = FindSlot(sessionId); | |
| if (slot == nullptr) | |
| { | |
| return std::nullopt; | |
| } | |
| return slot->info; | |
| } | |
| std::optional<uint16_t> CommissioningProxySessionManager::FindAnySessionIdOnFabric(FabricIndex fabricIndex) const | |
| { | |
| for (const auto & slot : mSessions) | |
| { | |
| if (slot.inUse && slot.info.fabricIndex == fabricIndex) | |
| { | |
| return slot.sessionId; | |
| } | |
| } | |
| return std::nullopt; | |
| } | |
| void CommissioningProxySessionManager::OnResponseTimeout(PendingMessage * pm) | |
| { | |
| SessionSlot * slot = FindSlot(pm->sessionId); | |
| if (slot == nullptr || slot->pending != pm) | |
| { | |
| return; // Already resolved (reply arrived or session closed). | |
| } | |
| slot->pending = nullptr; | |
| ChipLogProgress(Zcl, "CommissioningProxy: ProxyMessageRequest responseTimeout expired for session %u", pm->sessionId); | |
| // Per spec, an expired ProxyMessageRequest ResponseTimeout SHALL return TIMEOUT. | |
| if (app::CommandHandler * cmd = pm->handle.Get()) | |
| { | |
| cmd->AddStatus(pm->path, Status::Timeout); | |
| } | |
| mPendingPool.ReleaseObject(pm); | |
| } | |
| Status CommissioningProxySessionManager::BeginMessage(uint16_t sessionId, app::CommandHandler * commandObj, | |
| const DataModel::InvokeRequest & request, uint8_t responseTimeoutSeconds) | |
| { | |
| SessionSlot * slot = FindSlot(sessionId); | |
| VerifyOrReturnError(slot != nullptr, Status::NotFound); | |
| // Reject if another request for this session is still live; clean up an already | |
| // expired one rather than blocking the session forever. | |
| if (slot->pending != nullptr) | |
| { | |
| if (slot->pending->handle.Get() != nullptr) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: session %u already has a pending ProxyMessageRequest (BUSY)", sessionId); | |
| return Status::Busy; | |
| } | |
| mTimerDelegate.CancelTimer(slot->pending); | |
| mPendingPool.ReleaseObject(slot->pending); | |
| slot->pending = nullptr; | |
| } | |
| auto * pm = mPendingPool.CreateObject(this, app::CommandHandler::Handle(commandObj), request.path, sessionId); | |
| VerifyOrReturnError(pm != nullptr, Status::ResourceExhausted); | |
| commandObj->FlushAcksRightAwayOnSlowCommand(); | |
| slot->pending = pm; | |
| if (auto * exchange = commandObj->GetExchangeContext()) | |
| { | |
| // The exchange must outlive our own timer, otherwise it expires first and the | |
| // Status::Timeout that OnResponseTimeout adds never reaches the commissioner. | |
| exchange->SetResponseTimeout( | |
| System::Clock::Seconds16(static_cast<uint16_t>(responseTimeoutSeconds + kResponseTimeoutMarginSecs))); | |
| } | |
| CHIP_ERROR err = mTimerDelegate.StartTimer(pm, System::Clock::Seconds16(responseTimeoutSeconds)); | |
| if (err != CHIP_NO_ERROR) | |
| { | |
| // Nothing else would ever resolve this request: the session would stay Busy for | |
| // every later ProxyMessageRequest and the commissioner would get no response. | |
| ChipLogError(Zcl, "CommissioningProxy: failed to start ProxyMessage response timer: %" CHIP_ERROR_FORMAT, err.Format()); | |
| AbortPending(sessionId); | |
| return Status::Failure; | |
| } | |
| return Status::Success; | |
| } | |
| void CommissioningProxySessionManager::AbortPending(uint16_t sessionId) | |
| { | |
| SessionSlot * slot = FindSlot(sessionId); | |
| if (slot == nullptr || slot->pending == nullptr) | |
| { | |
| return; | |
| } | |
| PendingMessage * pm = slot->pending; | |
| slot->pending = nullptr; | |
| mTimerDelegate.CancelTimer(pm); | |
| mPendingPool.ReleaseObject(pm); | |
| } | |
| CHIP_ERROR CommissioningProxySessionManager::ValidateCommissioneeMessage(ByteSpan message) | |
| { | |
| VerifyOrReturnError(!message.empty(), CHIP_ERROR_INVALID_MESSAGE_LENGTH); | |
| VerifyOrReturnError(message.size() <= kMaxProxyMessageLength, CHIP_ERROR_MESSAGE_TOO_LONG); | |
| PacketHeader packetHeader; | |
| uint16_t packetHeaderLength = 0; | |
| ReturnErrorOnFailure(packetHeader.Decode(message.data(), message.size(), &packetHeaderLength)); | |
| ByteSpan payload = message.SubSpan(packetHeaderLength); | |
| // An encrypted payload header cannot be decoded here, so require room for the | |
| // smallest one and the MIC. | |
| if (packetHeader.IsEncrypted()) | |
| { | |
| VerifyOrReturnError(payload.size() >= kMinPayloadHeaderLength + packetHeader.MICTagLength(), | |
| CHIP_ERROR_INVALID_MESSAGE_LENGTH); | |
| return CHIP_NO_ERROR; | |
| } | |
| // An unsecured message has no privacy, and carries the ephemeral initiator node ID as | |
| // exactly one of the Source and Destination Node IDs, as SessionManager requires. | |
| VerifyOrReturnError(!packetHeader.HasPrivacyFlag(), CHIP_ERROR_INVALID_ARGUMENT); | |
| VerifyOrReturnError(packetHeader.HasSourceNodeId() != packetHeader.HasDestinationNodeId(), CHIP_ERROR_WRONG_NODE_ID); | |
| PayloadHeader payloadHeader; | |
| uint16_t payloadHeaderLength = 0; | |
| return payloadHeader.Decode(payload.data(), payload.size(), &payloadHeaderLength); | |
| } | |
| void CommissioningProxySessionManager::DispatchMessageResponse(uint16_t sessionId, const uint8_t * data, size_t length) | |
| { | |
| CHIP_ERROR err = ValidateCommissioneeMessage(ByteSpan(data, length)); | |
| if (err != CHIP_NO_ERROR) | |
| { | |
| ChipLogError(Zcl, "CommissioningProxy: session %u commissionee data not forwarded: %" CHIP_ERROR_FORMAT, sessionId, | |
| err.Format()); | |
| return; | |
| } | |
| SessionSlot * slot = FindSlot(sessionId); | |
| if (slot == nullptr || slot->pending == nullptr) | |
| { | |
| ChipLogDetail(Zcl, "CommissioningProxy: no pending request for session %u — dropping commissionee data", sessionId); | |
| return; | |
| } | |
| PendingMessage * pm = slot->pending; | |
| slot->pending = nullptr; | |
| mTimerDelegate.CancelTimer(pm); | |
| if (app::CommandHandler * cmd = pm->handle.Get()) | |
| { | |
| Commands::ProxyMessageResponse::Type response; | |
| response.sessionID = sessionId; | |
| response.message.SetNonNull(ByteSpan(data, length)); | |
| cmd->AddResponse(pm->path, response); | |
| } | |
| mPendingPool.ReleaseObject(pm); | |
| } | |
| void CommissioningProxySessionManager::DispatchMessageFailure(uint16_t sessionId, Status status) | |
| { | |
| SessionSlot * slot = FindSlot(sessionId); | |
| if (slot == nullptr || slot->pending == nullptr) | |
| { | |
| return; | |
| } | |
| PendingMessage * pm = slot->pending; | |
| slot->pending = nullptr; | |
| mTimerDelegate.CancelTimer(pm); | |
| if (app::CommandHandler * cmd = pm->handle.Get()) | |
| { | |
| cmd->AddStatus(pm->path, status); | |
| } | |
| mPendingPool.ReleaseObject(pm); | |
| } | |
| void CommissioningProxySessionManager::Shutdown() | |
| { | |
| for (auto & slot : mSessions) | |
| { | |
| if (slot.pending != nullptr) | |
| { | |
| mTimerDelegate.CancelTimer(slot.pending); | |
| mPendingPool.ReleaseObject(slot.pending); | |
| slot.pending = nullptr; | |
| } | |
| slot.inUse = false; | |
| } | |
| } | |
| } // namespace CommissioningProxy | |
| } // namespace Clusters | |
| } // namespace app | |
| } // namespace chip |