| /* |
| * |
| * Copyright (c) 2021 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 <ota-provider-common/OTAProviderExample.h> |
| |
| #include <algorithm> |
| #include <app-common/zap-generated/cluster-objects.h> |
| #include <app/clusters/ota-provider/ota-provider-delegate.h> |
| #include <app/server/Server.h> |
| #include <credentials/FabricTable.h> |
| #include <crypto/RandUtils.h> |
| #include <lib/core/TLV.h> |
| #include <lib/support/CHIPMemString.h> |
| #include <protocols/bdx/BdxUri.h> |
| |
| #include <fstream> |
| #include <string.h> |
| |
| using chip::BitFlags; |
| using chip::ByteSpan; |
| using chip::CharSpan; |
| using chip::FabricIndex; |
| using chip::FabricInfo; |
| using chip::MutableCharSpan; |
| using chip::NodeId; |
| using chip::Optional; |
| using chip::Server; |
| using chip::Span; |
| using chip::app::Clusters::OTAProviderDelegate; |
| using chip::bdx::TransferControlFlags; |
| using chip::Protocols::InteractionModel::Status; |
| using namespace chip; |
| using namespace chip::ota; |
| using namespace chip::app::Clusters::OtaSoftwareUpdateProvider; |
| using namespace chip::app::Clusters::OtaSoftwareUpdateProvider::Commands; |
| |
| constexpr uint8_t kUpdateTokenLen = 32; // must be between 8 and 32 |
| constexpr uint8_t kUpdateTokenStrLen = kUpdateTokenLen * 2 + 1; // Hex string needs 2 hex chars for every byte |
| constexpr size_t kOtaHeaderMaxSize = 1024; |
| |
| // Arbitrary BDX Transfer Params |
| constexpr uint32_t kMaxBdxBlockSize = 1024; |
| constexpr chip::System::Clock::Timeout kBdxTimeout = chip::System::Clock::Seconds16(5 * 60); // OTA Spec mandates >= 5 minutes |
| constexpr uint32_t kBdxServerPollIntervalMillis = 50; // poll every 50ms by default |
| |
| void GetUpdateTokenString(const chip::ByteSpan & token, char * buf, size_t bufSize) |
| { |
| const uint8_t * tokenData = static_cast<const uint8_t *>(token.data()); |
| size_t minLength = std::min(token.size(), bufSize); |
| for (size_t i = 0; i < (minLength / 2) - 1; ++i) |
| { |
| snprintf(&buf[i * 2], bufSize, "%02X", tokenData[i]); |
| } |
| } |
| |
| void GenerateUpdateToken(uint8_t * buf, size_t bufSize) |
| { |
| for (size_t i = 0; i < bufSize; ++i) |
| { |
| buf[i] = chip::Crypto::GetRandU8(); |
| } |
| } |
| |
| OTAProviderExample::OTAProviderExample() |
| { |
| memset(mOTAFilePath, 0, sizeof(mOTAFilePath)); |
| memset(mImageUri, 0, sizeof(mImageUri)); |
| mIgnoreQueryImageCount = 0; |
| mIgnoreApplyUpdateCount = 0; |
| mQueryImageStatus = OTAQueryStatus::kNotAvailable; |
| mUpdateAction = OTAApplyUpdateAction::kDiscontinue; |
| mDelayedQueryActionTimeSec = 0; |
| mDelayedApplyActionTimeSec = 0; |
| mUserConsentDelegate = nullptr; |
| mUserConsentNeeded = false; |
| mPollInterval = kBdxServerPollIntervalMillis; |
| mCandidates.clear(); |
| } |
| |
| void OTAProviderExample::SetOTAFilePath(const char * path) |
| { |
| if (path != nullptr) |
| { |
| chip::Platform::CopyString(mOTAFilePath, path); |
| } |
| else |
| { |
| memset(mOTAFilePath, 0, sizeof(mOTAFilePath)); |
| } |
| } |
| |
| void OTAProviderExample::SetImageUri(const char * imageUri) |
| { |
| if (imageUri != nullptr) |
| { |
| chip::Platform::CopyString(mImageUri, imageUri); |
| } |
| else |
| { |
| memset(mImageUri, 0, sizeof(mImageUri)); |
| } |
| } |
| |
| void OTAProviderExample::SetOTACandidates(std::vector<OTAProviderExample::DeviceSoftwareVersionModel> candidates) |
| { |
| mCandidates = std::move(candidates); |
| |
| // Validate that each candidate matches the info in the image header |
| for (auto candidate : mCandidates) |
| { |
| OTAImageHeaderParser parser; |
| OTAImageHeader header; |
| ParseOTAHeader(parser, candidate.otaURL, header); |
| |
| ChipLogDetail(SoftwareUpdate, "Validating image list candidate %s: ", candidate.otaURL); |
| VerifyOrDie(candidate.vendorId == header.mVendorId); |
| VerifyOrDie(candidate.productId == header.mProductId); |
| VerifyOrDie(candidate.softwareVersion == header.mSoftwareVersion); |
| VerifyOrDie(strlen(candidate.softwareVersionString) == header.mSoftwareVersionString.size()); |
| VerifyOrDie(memcmp(candidate.softwareVersionString, header.mSoftwareVersionString.data(), |
| header.mSoftwareVersionString.size()) == 0); |
| if (header.mMinApplicableVersion.HasValue()) |
| { |
| VerifyOrDie(candidate.minApplicableSoftwareVersion == header.mMinApplicableVersion.Value()); |
| } |
| if (header.mMaxApplicableVersion.HasValue()) |
| { |
| VerifyOrDie(candidate.maxApplicableSoftwareVersion == header.mMaxApplicableVersion.Value()); |
| } |
| parser.Clear(); |
| } |
| } |
| |
| static bool CompareSoftwareVersions(const OTAProviderExample::DeviceSoftwareVersionModel & a, |
| const OTAProviderExample::DeviceSoftwareVersionModel & b) |
| { |
| return (a.softwareVersion < b.softwareVersion); |
| } |
| |
| bool OTAProviderExample::SelectOTACandidate(const uint16_t requestorVendorID, const uint16_t requestorProductID, |
| const uint32_t requestorSoftwareVersion, |
| OTAProviderExample::DeviceSoftwareVersionModel & finalCandidate) |
| { |
| bool candidateFound = false; |
| std::sort(mCandidates.begin(), mCandidates.end(), CompareSoftwareVersions); |
| for (auto candidate : mCandidates) |
| { |
| // VendorID and ProductID will be the primary key when querying |
| // the DCL servers. If not we can add the vendor/product ID checks here. |
| if (candidate.softwareVersionValid && requestorSoftwareVersion < candidate.softwareVersion && |
| requestorSoftwareVersion >= candidate.minApplicableSoftwareVersion && |
| requestorSoftwareVersion <= candidate.maxApplicableSoftwareVersion) |
| { |
| candidateFound = true; |
| finalCandidate = candidate; |
| } |
| } |
| return candidateFound; |
| } |
| |
| UserConsentSubject OTAProviderExample::GetUserConsentSubject(const app::CommandHandler * commandObj, |
| const app::ConcreteCommandPath & commandPath, |
| const QueryImage::DecodableType & commandData, uint32_t targetVersion) |
| { |
| UserConsentSubject subject; |
| subject.fabricIndex = commandObj->GetSubjectDescriptor().fabricIndex; |
| subject.requestorNodeId = commandObj->GetSubjectDescriptor().subject; |
| subject.providerEndpointId = commandPath.mEndpointId; |
| subject.requestorVendorId = commandData.vendorID; |
| subject.requestorProductId = commandData.productID; |
| subject.requestorCurrentVersion = commandData.softwareVersion; |
| subject.requestorTargetVersion = targetVersion; |
| if (commandData.metadataForProvider.HasValue()) |
| { |
| subject.metadata = commandData.metadataForProvider.Value(); |
| } |
| return subject; |
| } |
| |
| bool OTAProviderExample::ParseOTAHeader(OTAImageHeaderParser & parser, const char * otaFilePath, OTAImageHeader & header) |
| { |
| uint8_t otaFileContent[kOtaHeaderMaxSize]; |
| ByteSpan buffer(otaFileContent); |
| |
| std::ifstream otaFile(otaFilePath, std::ifstream::in); |
| if (!otaFile.is_open() || !otaFile.good()) |
| { |
| ChipLogError(SoftwareUpdate, "Error opening OTA image file: %s", otaFilePath); |
| return false; |
| } |
| |
| otaFile.read(reinterpret_cast<char *>(otaFileContent), kOtaHeaderMaxSize); |
| if (otaFile.bad()) |
| { |
| ChipLogError(SoftwareUpdate, "Error reading OTA image file: %s", otaFilePath); |
| return false; |
| } |
| |
| parser.Init(); |
| if (!parser.IsInitialized()) |
| { |
| return false; |
| } |
| |
| CHIP_ERROR error = parser.AccumulateAndDecode(buffer, header); |
| if (error != CHIP_NO_ERROR) |
| { |
| ChipLogError(SoftwareUpdate, "Error parsing OTA image header: %" CHIP_ERROR_FORMAT, error.Format()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void OTAProviderExample::SendQueryImageResponse(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, |
| const QueryImage::DecodableType & commandData) |
| { |
| VerifyOrReturn(commandObj != nullptr, ChipLogError(SoftwareUpdate, "Invalid commandObj, cannot send QueryImageResponse")); |
| |
| QueryImageResponse::Type response; |
| bool requestorCanConsent = commandData.requestorCanConsent.ValueOr(false); |
| uint8_t updateToken[kUpdateTokenLen] = { 0 }; |
| char strBuf[kUpdateTokenStrLen] = { 0 }; |
| |
| // Set fields specific for an available status response |
| if (mQueryImageStatus == OTAQueryStatus::kUpdateAvailable) |
| { |
| GenerateUpdateToken(updateToken, kUpdateTokenLen); |
| GetUpdateTokenString(ByteSpan(updateToken), strBuf, kUpdateTokenStrLen); |
| ChipLogDetail(SoftwareUpdate, "Generated updateToken: %s", strBuf); |
| |
| // TODO: This uses the current node as the provider to supply the OTA image. This can be configurable such that the |
| // provider supplying the response is not the provider supplying the OTA image. |
| FabricIndex fabricIndex = commandObj->GetAccessingFabricIndex(); |
| const FabricInfo * fabricInfo = Server::GetInstance().GetFabricTable().FindFabricWithIndex(fabricIndex); |
| NodeId nodeId = fabricInfo->GetPeerId().GetNodeId(); |
| |
| // Generate the ImageURI if one is not already preset |
| if (strlen(mImageUri) == 0) |
| { |
| // Only supporting BDX protocol for now |
| MutableCharSpan uri(mImageUri); |
| CHIP_ERROR error = chip::bdx::MakeURI(nodeId, CharSpan::fromCharString(mOTAFilePath), uri); |
| if (error != CHIP_NO_ERROR) |
| { |
| ChipLogError(SoftwareUpdate, "Cannot generate URI"); |
| memset(mImageUri, 0, sizeof(mImageUri)); |
| } |
| else |
| { |
| ChipLogDetail(SoftwareUpdate, "Generated URI: %s", mImageUri); |
| } |
| } |
| |
| // Initialize the transfer session in prepartion for a BDX transfer |
| BitFlags<TransferControlFlags> bdxFlags; |
| bdxFlags.Set(TransferControlFlags::kReceiverDrive); |
| if (mBdxOtaSender.InitializeTransfer(commandObj->GetSubjectDescriptor().fabricIndex, |
| commandObj->GetSubjectDescriptor().subject) == CHIP_NO_ERROR) |
| { |
| CHIP_ERROR error = |
| mBdxOtaSender.PrepareForTransfer(&chip::DeviceLayer::SystemLayer(), chip::bdx::TransferRole::kSender, bdxFlags, |
| kMaxBdxBlockSize, kBdxTimeout, chip::System::Clock::Milliseconds32(mPollInterval)); |
| if (error != CHIP_NO_ERROR) |
| { |
| ChipLogError(SoftwareUpdate, "Cannot prepare for transfer: %" CHIP_ERROR_FORMAT, error.Format()); |
| commandObj->AddStatus(commandPath, Status::Failure); |
| return; |
| } |
| |
| response.imageURI.Emplace(chip::CharSpan::fromCharString(mImageUri)); |
| response.softwareVersion.Emplace(mSoftwareVersion); |
| response.softwareVersionString.Emplace(chip::CharSpan::fromCharString(mSoftwareVersionString)); |
| response.updateToken.Emplace(chip::ByteSpan(updateToken)); |
| } |
| else |
| { |
| // Another BDX transfer in progress |
| mQueryImageStatus = OTAQueryStatus::kBusy; |
| } |
| } |
| |
| // Delay action time is only applicable when the provider is busy |
| if (mQueryImageStatus == OTAQueryStatus::kBusy) |
| { |
| response.delayedActionTime.Emplace(mDelayedQueryActionTimeSec); |
| } |
| |
| // Set remaining fields common to all status types |
| response.status = mQueryImageStatus; |
| if (mUserConsentNeeded && requestorCanConsent) |
| { |
| response.userConsentNeeded.Emplace(true); |
| } |
| else |
| { |
| response.userConsentNeeded.Emplace(false); |
| } |
| // For test coverage, sending empty metadata when (requestorNodeId % 2) == 0 and not sending otherwise. |
| if (commandObj->GetSubjectDescriptor().subject % 2 == 0) |
| { |
| response.metadataForRequestor.Emplace(chip::ByteSpan()); |
| } |
| |
| // Either sends the response or an error status |
| commandObj->AddResponse(commandPath, response); |
| } |
| |
| void OTAProviderExample::HandleQueryImage(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, |
| const QueryImage::DecodableType & commandData) |
| { |
| bool requestorCanConsent = commandData.requestorCanConsent.ValueOr(false); |
| |
| if (mIgnoreQueryImageCount > 0) |
| { |
| ChipLogDetail(SoftwareUpdate, "Skip sending QueryImageResponse, ignore count: %" PRIu32, mIgnoreQueryImageCount); |
| mIgnoreQueryImageCount--; |
| return; |
| } |
| |
| if (mQueryImageStatus == OTAQueryStatus::kUpdateAvailable) |
| { |
| memset(mSoftwareVersionString, 0, sizeof(mSoftwareVersionString)); |
| |
| if (!mCandidates.empty()) // If list of OTA candidates is supplied |
| { |
| OTAProviderExample::DeviceSoftwareVersionModel candidate; |
| if (SelectOTACandidate(commandData.vendorID, commandData.productID, commandData.softwareVersion, candidate)) |
| { |
| VerifyOrDie(sizeof(mSoftwareVersionString) > strlen(candidate.softwareVersionString)); |
| |
| // This assumes all candidates have passed verification so the values are safe to use |
| mSoftwareVersion = candidate.softwareVersion; |
| memcpy(mSoftwareVersionString, candidate.softwareVersionString, strlen(candidate.softwareVersionString)); |
| SetOTAFilePath(candidate.otaURL); |
| } |
| } |
| else if (strlen(mOTAFilePath) > 0) // If OTA file is directly provided |
| { |
| // Parse the header and set version info based on the header |
| OTAImageHeaderParser parser; |
| OTAImageHeader header; |
| VerifyOrDie(ParseOTAHeader(parser, mOTAFilePath, header) == true); |
| VerifyOrDie(sizeof(mSoftwareVersionString) > header.mSoftwareVersionString.size()); |
| mSoftwareVersion = header.mSoftwareVersion; |
| memcpy(mSoftwareVersionString, header.mSoftwareVersionString.data(), header.mSoftwareVersionString.size()); |
| parser.Clear(); |
| } |
| |
| // If mUserConsentNeeded (set by the CLI) is true and requestor is capable of taking user consent |
| // then delegate obtaining user consent to the requestor |
| if (mUserConsentDelegate && (requestorCanConsent && mUserConsentNeeded) == false) |
| { |
| UserConsentState state = mUserConsentDelegate->GetUserConsentState( |
| GetUserConsentSubject(commandObj, commandPath, commandData, mSoftwareVersion)); |
| ChipLogProgress(SoftwareUpdate, "User Consent state: %s", mUserConsentDelegate->UserConsentStateToString(state)); |
| switch (state) |
| { |
| case UserConsentState::kGranted: |
| mQueryImageStatus = OTAQueryStatus::kUpdateAvailable; |
| break; |
| |
| case UserConsentState::kObtaining: |
| mQueryImageStatus = OTAQueryStatus::kBusy; |
| break; |
| |
| case UserConsentState::kDenied: |
| case UserConsentState::kUnknown: |
| mQueryImageStatus = OTAQueryStatus::kNotAvailable; |
| break; |
| } |
| } |
| } |
| |
| // Guarantees that either a response or an error status is sent |
| SendQueryImageResponse(commandObj, commandPath, commandData); |
| |
| // After the first response is sent, default to these values for subsequent queries |
| mQueryImageStatus = OTAQueryStatus::kUpdateAvailable; |
| mDelayedQueryActionTimeSec = 0; |
| } |
| |
| void OTAProviderExample::HandleApplyUpdateRequest(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, |
| const ApplyUpdateRequest::DecodableType & commandData) |
| { |
| VerifyOrReturn(commandObj != nullptr, ChipLogError(SoftwareUpdate, "Invalid commandObj, cannot handle ApplyUpdateRequest")); |
| |
| if (mIgnoreApplyUpdateCount > 0) |
| { |
| ChipLogDetail(SoftwareUpdate, "Skip sending ApplyUpdateResponse, ignore count %" PRIu32, mIgnoreApplyUpdateCount); |
| mIgnoreApplyUpdateCount--; |
| return; |
| } |
| |
| // TODO: handle multiple transfers by tracking updateTokens |
| char tokenBuf[kUpdateTokenStrLen] = { 0 }; |
| |
| GetUpdateTokenString(commandData.updateToken, tokenBuf, kUpdateTokenStrLen); |
| ChipLogDetail(SoftwareUpdate, "%s: token: %s, version: %" PRIu32, __FUNCTION__, tokenBuf, commandData.newVersion); |
| |
| ApplyUpdateResponse::Type response; |
| response.action = mUpdateAction; |
| response.delayedActionTime = mDelayedApplyActionTimeSec; |
| |
| // Reset delay back to 0 for subsequent uses |
| mDelayedApplyActionTimeSec = 0; |
| // Reset back to success case for subsequent uses |
| mUpdateAction = OTAApplyUpdateAction::kProceed; |
| |
| // Either sends the response or an error status |
| commandObj->AddResponse(commandPath, response); |
| } |
| |
| void OTAProviderExample::HandleNotifyUpdateApplied(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, |
| const NotifyUpdateApplied::DecodableType & commandData) |
| { |
| VerifyOrReturn(commandObj != nullptr, ChipLogError(SoftwareUpdate, "Invalid commandObj, cannot handle NotifyUpdateApplied")); |
| |
| char tokenBuf[kUpdateTokenStrLen] = { 0 }; |
| |
| GetUpdateTokenString(commandData.updateToken, tokenBuf, kUpdateTokenStrLen); |
| ChipLogDetail(SoftwareUpdate, "%s: token: %s, version: %" PRIu32, __FUNCTION__, tokenBuf, commandData.softwareVersion); |
| |
| commandObj->AddStatus(commandPath, Status::Success); |
| } |