/* * Copyright (c) 2024 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 "CommandResponseSender.h" #include "InteractionModelEngine.h" #include "messaging/ExchangeContext.h" namespace chip { namespace app { using Status = Protocols::InteractionModel::Status; CHIP_ERROR CommandResponseSender::OnMessageReceived(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload) { CHIP_ERROR err = CHIP_NO_ERROR; Optional failureStatusToSend; if (mState == State::AwaitingStatusResponse && aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::StatusResponse)) { CHIP_ERROR statusError = CHIP_NO_ERROR; err = StatusResponse::ProcessStatusResponse(std::move(aPayload), statusError); VerifyOrExit(err == CHIP_NO_ERROR, failureStatusToSend.SetValue(Status::InvalidAction)); err = statusError; VerifyOrExit(err == CHIP_NO_ERROR, failureStatusToSend.SetValue(Status::InvalidAction)); err = SendCommandResponse(); // If SendCommandResponse() fails, we must close the exchange. We signal the failure to the // requester with a StatusResponse ('Failure'). Since we're in the middle of processing an // incoming message, we close the exchange by indicating that we don't expect a further response. VerifyOrExit(err == CHIP_NO_ERROR, failureStatusToSend.SetValue(Status::Failure)); bool moreToSend = !mChunks.IsNull(); if (!moreToSend) { // We are sending the final message and do not anticipate any further responses. We are // calling ExitNow() to immediately execute Close() and subsequently return from this function. ExitNow(); } return CHIP_NO_ERROR; } ChipLogDetail(DataManagement, "CommandResponseSender: Unexpected message type %d", aPayloadHeader.GetMessageType()); err = CHIP_ERROR_INVALID_MESSAGE_TYPE; if (mState != State::AllInvokeResponsesSent) { failureStatusToSend.SetValue(Status::Failure); ExitNow(); } StatusResponse::Send(Status::InvalidAction, mExchangeCtx.Get(), false /*aExpectResponse*/); return err; exit: if (failureStatusToSend.HasValue()) { StatusResponse::Send(failureStatusToSend.Value(), mExchangeCtx.Get(), false /*aExpectResponse*/); } Close(); return err; } void CommandResponseSender::OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) { ChipLogDetail(DataManagement, "CommandResponseSender: Timed out waiting for response from requester mState=[%10.10s]", GetStateStr()); Close(); } void CommandResponseSender::StartSendingCommandResponses() { VerifyOrDie(mState == State::ReadyForInvokeResponses); CHIP_ERROR err = SendCommandResponse(); if (err != CHIP_NO_ERROR) { ChipLogError(DataManagement, "Failed to send InvokeResponseMessage"); // TODO(#30453): It should be our responsibility to send a Failure StatusResponse to the requestor // if there is a SessionHandle, but legacy unit tests explicitly check the behavior where // we do not send any message. Changing this behavior should be done in a standalone // PR where only that specific change is made. Here is a possible solution that could // be used that fulfills our responsibility to send a Failure StatusResponse. This causes unit // tests to start failing. // ``` // if (mExchangeCtx && mExchangeCtx->HasSessionHandle()) // { // SendStatusResponse(Status::Failure); // } // ``` Close(); return; } if (HasMoreToSend()) { MoveToState(State::AwaitingStatusResponse); mExchangeCtx->SetDelegate(this); } else { Close(); } } void CommandResponseSender::OnDone(CommandHandlerImpl & apCommandObj) { if (mState == State::ErrorSentDelayCloseUntilOnDone) { // We have already sent a message to the client indicating that we are not expecting // a response. Close(); return; } StartSendingCommandResponses(); } void CommandResponseSender::DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) { VerifyOrReturn(mpCommandHandlerCallback); mpCommandHandlerCallback->DispatchCommand(apCommandObj, aCommandPath, apPayload); } Protocols::InteractionModel::Status CommandResponseSender::ValidateCommandCanBeDispatched(const DataModel::InvokeRequest & request) { VerifyOrReturnValue(mpCommandHandlerCallback, Protocols::InteractionModel::Status::Failure); return mpCommandHandlerCallback->ValidateCommandCanBeDispatched(request); } CHIP_ERROR CommandResponseSender::SendCommandResponse() { VerifyOrReturnError(HasMoreToSend(), CHIP_ERROR_INCORRECT_STATE); if (mChunks.IsNull()) { VerifyOrReturnError(mReportResponseDropped, CHIP_ERROR_INCORRECT_STATE); SendStatusResponse(Status::ResourceExhausted); mReportResponseDropped = false; return CHIP_NO_ERROR; } System::PacketBufferHandle commandResponsePayload = mChunks.PopHead(); Messaging::SendFlags sendFlag = Messaging::SendMessageFlags::kNone; if (HasMoreToSend()) { sendFlag = Messaging::SendMessageFlags::kExpectResponse; mExchangeCtx->UseSuggestedResponseTimeout(app::kExpectedIMProcessingTime); } ReturnErrorOnFailure(mExchangeCtx->SendMessage(Protocols::InteractionModel::MsgType::InvokeCommandResponse, std::move(commandResponsePayload), sendFlag)); return CHIP_NO_ERROR; } const char * CommandResponseSender::GetStateStr() const { #if CHIP_DETAIL_LOGGING switch (mState) { case State::ReadyForInvokeResponses: return "ReadyForInvokeResponses"; case State::AwaitingStatusResponse: return "AwaitingStatusResponse"; case State::AllInvokeResponsesSent: return "AllInvokeResponsesSent"; case State::ErrorSentDelayCloseUntilOnDone: return "ErrorSentDelayCloseUntilOnDone"; } #endif // CHIP_DETAIL_LOGGING return "N/A"; } void CommandResponseSender::MoveToState(const State aTargetState) { if (mState == aTargetState) { return; } mState = aTargetState; ChipLogDetail(DataManagement, "Command response sender moving to [%10.10s]", GetStateStr()); } void CommandResponseSender::Close() { MoveToState(State::AllInvokeResponsesSent); mpCallback->OnDone(*this); } void CommandResponseSender::OnInvokeCommandRequest(Messaging::ExchangeContext * ec, System::PacketBufferHandle && payload, bool isTimedInvoke) { VerifyOrDieWithMsg(ec != nullptr, DataManagement, "Incoming exchange context should not be null"); VerifyOrDieWithMsg(mState == State::ReadyForInvokeResponses, DataManagement, "state should be ReadyForInvokeResponses"); // NOTE: we already know this is an InvokeRequestMessage because we explicitly registered with the // Exchange Manager for unsolicited InvokeRequestMessages. mExchangeCtx.Grab(ec); mExchangeCtx->WillSendMessage(); // Grabbing Handle to prevent mCommandHandler from calling OnDone before OnInvokeCommandRequest returns. // This allows us to send a StatusResponse error instead of any potentially queued up InvokeResponseMessages. CommandHandler::Handle workHandle(&mCommandHandler); Status status = mCommandHandler.OnInvokeCommandRequest(*this, std::move(payload), isTimedInvoke); if (status != Status::Success) { VerifyOrDie(mState == State::ReadyForInvokeResponses); SendStatusResponse(status); // The API contract of OnInvokeCommandRequest requires the CommandResponder instance to outlive // the CommandHandler. Therefore, we cannot safely call Close() here, even though we have // finished sending data. Closing must be deferred until the CommandHandler::OnDone callback. MoveToState(State::ErrorSentDelayCloseUntilOnDone); } } size_t CommandResponseSender::GetCommandResponseMaxBufferSize() { if (!mExchangeCtx || !mExchangeCtx->HasSessionHandle()) { ChipLogError(DataManagement, "Session not available. Unable to infer session-specific buffer capacities."); return kMaxSecureSduLengthBytes; } if (mExchangeCtx->GetSessionHandle()->AllowsLargePayload()) { return kMaxLargeSecureSduLengthBytes; } return kMaxSecureSduLengthBytes; } #if CHIP_WITH_NLFAULTINJECTION void CommandResponseSender::TestOnlyInvokeCommandRequestWithFaultsInjected(Messaging::ExchangeContext * ec, System::PacketBufferHandle && payload, bool isTimedInvoke, CommandHandlerImpl::NlFaultInjectionType faultType) { VerifyOrDieWithMsg(ec != nullptr, DataManagement, "TH Failure: Incoming exchange context should not be null"); VerifyOrDieWithMsg(mState == State::ReadyForInvokeResponses, DataManagement, "TH Failure: state should be ReadyForInvokeResponses, issue with TH"); mExchangeCtx.Grab(ec); mExchangeCtx->WillSendMessage(); mCommandHandler.TestOnlyInvokeCommandRequestWithFaultsInjected(*this, std::move(payload), isTimedInvoke, faultType); } #endif // CHIP_WITH_NLFAULTINJECTION } // namespace app } // namespace chip