From e80451c0d2c6a27ca56ec6d4dd74a82ff3413372 Mon Sep 17 00:00:00 2001 From: Vivien Nicolas Date: Wed, 29 Jan 2025 18:45:33 +0100 Subject: [PATCH] [chip-tool] Add Enhanced Commissioning Support (T&C Flow via Local DCL) to chip-tool (#37049) * [chip-tool] Add chip-tool dcl fake cluster commands * [chip-tool] Add a fake local dcl server script for testing/developement purposes * [chip-tool] Add chip-tool dcl tc-display and tc-display-by-payload commands * [General Commissioning Server] Dynamically encode the feature map 'GeneralCommissioning::Feature::kTermsAndConditions' if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED is set * [Examples/platform/linux] Set default TermsAndConditions if requested from the command line * [chip-tool] Add TermsAndConditions support to chip-tool pairing code command --- examples/chip-tool/BUILD.gn | 15 + examples/chip-tool/commands/dcl/Commands.h | 37 ++ examples/chip-tool/commands/dcl/DCLClient.cpp | 241 +++++++++++++ examples/chip-tool/commands/dcl/DCLClient.h | 100 ++++++ examples/chip-tool/commands/dcl/DCLCommands.h | 202 +++++++++++ .../dcl/DisplayTermsAndConditions.cpp | 230 ++++++++++++ .../commands/dcl/DisplayTermsAndConditions.h | 44 +++ .../chip-tool/commands/dcl/HTTPSRequest.cpp | 339 ++++++++++++++++++ .../chip-tool/commands/dcl/HTTPSRequest.h | 39 ++ .../commands/dcl/JsonSchemaMacros.cpp | 64 ++++ .../chip-tool/commands/dcl/JsonSchemaMacros.h | 41 +++ .../chip-tool/commands/dcl/test_dcl_server.py | 245 +++++++++++++ .../commands/pairing/PairingCommand.cpp | 27 ++ .../commands/pairing/PairingCommand.h | 8 + examples/chip-tool/main.cpp | 2 + examples/platform/linux/AppMain.cpp | 14 + examples/platform/linux/Options.cpp | 28 ++ examples/platform/linux/Options.h | 5 + .../general-commissioning-server.cpp | 8 + third_party/boringssl/repo/BUILD.gn | 16 + 20 files changed, 1705 insertions(+) create mode 100644 examples/chip-tool/commands/dcl/Commands.h create mode 100644 examples/chip-tool/commands/dcl/DCLClient.cpp create mode 100644 examples/chip-tool/commands/dcl/DCLClient.h create mode 100644 examples/chip-tool/commands/dcl/DCLCommands.h create mode 100644 examples/chip-tool/commands/dcl/DisplayTermsAndConditions.cpp create mode 100644 examples/chip-tool/commands/dcl/DisplayTermsAndConditions.h create mode 100644 examples/chip-tool/commands/dcl/HTTPSRequest.cpp create mode 100644 examples/chip-tool/commands/dcl/HTTPSRequest.h create mode 100644 examples/chip-tool/commands/dcl/JsonSchemaMacros.cpp create mode 100644 examples/chip-tool/commands/dcl/JsonSchemaMacros.h create mode 100755 examples/chip-tool/commands/dcl/test_dcl_server.py diff --git a/examples/chip-tool/BUILD.gn b/examples/chip-tool/BUILD.gn index acacbc70e8bfad..992fbd53e4ecf5 100644 --- a/examples/chip-tool/BUILD.gn +++ b/examples/chip-tool/BUILD.gn @@ -23,6 +23,9 @@ if (config_use_interactive_mode) { import("//build_overrides/editline.gni") } +import("${chip_root}/build_overrides/boringssl.gni") +import("${chip_root}/src/crypto/crypto.gni") + assert(chip_build_tools) config("config") { @@ -67,6 +70,14 @@ static_library("chip-tool-utils") { "commands/common/HexConversion.h", "commands/common/RemoteDataModelLogger.cpp", "commands/common/RemoteDataModelLogger.h", + "commands/dcl/DCLClient.cpp", + "commands/dcl/DCLClient.h", + "commands/dcl/DisplayTermsAndConditions.cpp", + "commands/dcl/DisplayTermsAndConditions.h", + "commands/dcl/HTTPSRequest.cpp", + "commands/dcl/HTTPSRequest.h", + "commands/dcl/JsonSchemaMacros.cpp", + "commands/dcl/JsonSchemaMacros.h", "commands/delay/SleepCommand.cpp", "commands/delay/WaitForCommissioneeCommand.cpp", "commands/discover/DiscoverCommand.cpp", @@ -102,6 +113,10 @@ static_library("chip-tool-utils") { sources += [ "commands/common/DeviceScanner.cpp" ] } + if (chip_device_platform == "darwin" || chip_crypto == "boringssl") { + deps += [ "${boringssl_root}:boringssl_with_ssl_sources" ] + } + public_deps = [ "${chip_root}/examples/common/tracing:commandline", "${chip_root}/src/app/icd/client:handler", diff --git a/examples/chip-tool/commands/dcl/Commands.h b/examples/chip-tool/commands/dcl/Commands.h new file mode 100644 index 00000000000000..05220771fcda09 --- /dev/null +++ b/examples/chip-tool/commands/dcl/Commands.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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. + * + */ + +#pragma once + +#include "commands/common/Commands.h" +#include "commands/dcl/DCLCommands.h" + +void registerCommandsDCL(Commands & commands) +{ + const char * clusterName = "DCL"; + commands_list clusterCommands = { + make_unique(), // + make_unique(), // + make_unique(), // + make_unique(), // + make_unique(), // + make_unique(), // + }; + + commands.RegisterCommandSet(clusterName, clusterCommands, "Commands to interact with the DCL."); +} diff --git a/examples/chip-tool/commands/dcl/DCLClient.cpp b/examples/chip-tool/commands/dcl/DCLClient.cpp new file mode 100644 index 00000000000000..ada5d8113d7d05 --- /dev/null +++ b/examples/chip-tool/commands/dcl/DCLClient.cpp @@ -0,0 +1,241 @@ +/* + * 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 "DCLClient.h" + +#include +#include +#include +#include + +#include "HTTPSRequest.h" +#include "JsonSchemaMacros.h" + +namespace { +constexpr const char * kDefaultDCLHostName = "on.dcl.csa-iot.org"; +constexpr const char * kErrorSchemaValidation = "Model schema validation failed for response content: "; +constexpr const char * kErrorVendorIdIsZero = "Invalid argument: Vendor ID should not be 0"; +constexpr const char * kErrorProductIdIsZero = "Invalid argument: Product ID should not be 0"; +constexpr const char * kErrorOrdinalValueTooLarge = "Ordinal value exceeds the maximum allowable bits: "; +constexpr const char * kRequestModelVendorProductPath = "/dcl/model/models/%u/%u"; +constexpr uint8_t kRequestPathBufferSize = 64; +constexpr uint16_t kTermsAndConditionSchemaVersion = 1; +} // namespace + +namespace chip { +namespace tool { +namespace dcl { + +namespace { +CHIP_ERROR ValidateModelSchema(const Json::Value & json) +{ + CHECK_REQUIRED_TYPE(json, model, Object) + auto model = json["model"]; + + CHECK_REQUIRED_TYPE(model, commissioningCustomFlow, UInt); + + // The "enhancedSetupFlowOptions" field is theoretically required by the schema. + // However, the current DCL implementation does not include it. + // To handle this gracefully, we inject the field and set its value to 0 if it is missing. + if (!model.isMember("enhancedSetupFlowOptions")) + { + model["enhancedSetupFlowOptions"] = 0; + } + + CHECK_REQUIRED_TYPE(model, enhancedSetupFlowOptions, UInt) + + // Check if enhancedSetupFlowOptions has bit 0 set. + // Bit 0 indicates that enhanced setup flow is enabled. + auto enhancedSetupFlowOptions = model["enhancedSetupFlowOptions"]; + VerifyOrReturnError((enhancedSetupFlowOptions.asUInt() & 0x01) != 0, CHIP_NO_ERROR); + + // List of required keys in the "model" object if enhancedSetupFlowOptions has bit 0 set. + CHECK_REQUIRED_TYPE(model, enhancedSetupFlowTCUrl, String) + CHECK_REQUIRED_TYPE(model, enhancedSetupFlowTCDigest, String) + CHECK_REQUIRED_TYPE(model, enhancedSetupFlowTCFileSize, UInt) + CHECK_REQUIRED_TYPE(model, enhancedSetupFlowTCRevision, UInt) + CHECK_REQUIRED_TYPE(model, enhancedSetupFlowMaintenanceUrl, String) + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ValidateModelCustomFlow(const Json::Value & json, CommissioningFlow payloadCommissioningFlow) +{ + auto model = json["model"]; + CHECK_REQUIRED_VALUE(model, commissioningCustomFlow, to_underlying(payloadCommissioningFlow)) + return CHIP_NO_ERROR; +} + +CHIP_ERROR ValidateTCLanguageEntries(const Json::Value & languageEntries) +{ + for (Json::Value::const_iterator it = languageEntries.begin(); it != languageEntries.end(); it++) + { + const Json::Value & languageArray = *it; + + CHECK_TYPE(languageArray, languageArray, Array); + + for (Json::ArrayIndex i = 0; i < languageArray.size(); i++) + { + const Json::Value & term = languageArray[i]; + CHECK_REQUIRED_TYPE(term, title, String); + CHECK_REQUIRED_TYPE(term, text, String); + CHECK_REQUIRED_TYPE(term, required, Bool); + CHECK_REQUIRED_TYPE(term, ordinal, UInt); + + auto ordinal = term["ordinal"].asUInt(); + VerifyOrReturnError(ordinal < 16, CHIP_ERROR_INVALID_ARGUMENT, + ChipLogError(chipTool, "%s%u", kErrorOrdinalValueTooLarge, ordinal)); + } + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ValidateTCCountryEntries(const Json::Value & countryEntries) +{ + for (Json::Value::const_iterator it = countryEntries.begin(); it != countryEntries.end(); it++) + { + const Json::Value & countryEntry = *it; + + CHECK_REQUIRED_TYPE(countryEntry, defaultLanguage, String); + CHECK_REQUIRED_TYPE(countryEntry, languageEntries, Object); + + ReturnErrorOnFailure(ValidateTCLanguageEntries(countryEntry["languageEntries"])); + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ValidateTermsAndConditionsSchema(const Json::Value & tc, unsigned int expectedEnhancedSetupFlowTCRevision) +{ + CHECK_REQUIRED_VALUE(tc, schemaVersion, kTermsAndConditionSchemaVersion) + CHECK_REQUIRED_TYPE(tc, esfRevision, UInt) + CHECK_REQUIRED_TYPE(tc, defaultCountry, String) + CHECK_REQUIRED_TYPE(tc, countryEntries, Object) + CHECK_REQUIRED_VALUE(tc, esfRevision, expectedEnhancedSetupFlowTCRevision) + return ValidateTCCountryEntries(tc["countryEntries"]); +} + +CHIP_ERROR RequestTermsAndConditions(const Json::Value & json, Json::Value & tc) +{ + auto & model = json["model"]; + if ((model["enhancedSetupFlowOptions"].asUInt() & 0x01) == 0) + { + ChipLogProgress(chipTool, + "Enhanced setup flow is not enabled for this model (bit 0 of enhancedSetupFlowOptions is not set). No " + "Terms and Conditions are required for this configuration."); + tc = Json::nullValue; + return CHIP_NO_ERROR; + } + + auto & enhancedSetupFlowTCUrl = model["enhancedSetupFlowTCUrl"]; + auto & enhancedSetupFlowTCFileSize = model["enhancedSetupFlowTCFileSize"]; + auto & enhancedSetupFlowTCDigest = model["enhancedSetupFlowTCDigest"]; + auto & enhancedSetupFlowTCRevision = model["enhancedSetupFlowTCRevision"]; + + auto * tcUrl = enhancedSetupFlowTCUrl.asCString(); + const auto optionalFileSize = MakeOptional(static_cast(enhancedSetupFlowTCFileSize.asUInt())); + const auto optionalDigest = MakeOptional(enhancedSetupFlowTCDigest.asCString()); + ReturnErrorOnFailure(https::Request(tcUrl, tc, optionalFileSize, optionalDigest)); + ReturnErrorOnFailure(ValidateTermsAndConditionsSchema(tc, enhancedSetupFlowTCRevision.asUInt())); + + return CHIP_NO_ERROR; +} + +} // namespace + +DCLClient::DCLClient(Optional hostname, Optional port) +{ + mHostName = hostname.ValueOr(kDefaultDCLHostName); + mPort = port.ValueOr(0); +} + +CHIP_ERROR DCLClient::Model(const char * onboardingPayload, Json::Value & outModel) +{ + SetupPayload payload; + bool isQRCode = strncmp(onboardingPayload, kQRCodePrefix, strlen(kQRCodePrefix)) == 0; + if (isQRCode) + { + ReturnErrorOnFailure(QRCodeSetupPayloadParser(onboardingPayload).populatePayload(payload)); + VerifyOrReturnError(payload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT); + } + else + { + ReturnErrorOnFailure(ManualSetupPayloadParser(onboardingPayload).populatePayload(payload)); + VerifyOrReturnError(payload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT); + } + + auto vendorId = static_cast(payload.vendorID); + auto productId = payload.productID; + + // If both vendorId and productId are zero, return a null model without error. + if (vendorId == 0 && productId == 0) + { + ChipLogProgress(chipTool, "Vendor ID and Product ID not found in the provided payload. DCL lookup will not be used."); + outModel = Json::nullValue; + return CHIP_NO_ERROR; + } + + ReturnErrorOnFailure(Model(vendorId, productId, outModel)); + + auto commissioningFlow = payload.commissioningFlow; + CHIP_ERROR error = ValidateModelCustomFlow(outModel, commissioningFlow); + VerifyOrReturnError(CHIP_NO_ERROR == error, error, + ChipLogError(chipTool, "%s%s", kErrorSchemaValidation, outModel.toStyledString().c_str())); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR DCLClient::Model(const chip::VendorId vendorId, const uint16_t productId, Json::Value & outModel) +{ + VerifyOrReturnError(0 != vendorId, CHIP_ERROR_INVALID_ARGUMENT, ChipLogError(chipTool, "%s", kErrorVendorIdIsZero)); + VerifyOrReturnError(0 != productId, CHIP_ERROR_INVALID_ARGUMENT, ChipLogError(chipTool, "%s", kErrorProductIdIsZero)); + + char path[kRequestPathBufferSize]; + VerifyOrReturnError(snprintf(path, sizeof(path), kRequestModelVendorProductPath, to_underlying(vendorId), productId) >= 0, + CHIP_ERROR_INVALID_ARGUMENT); + ReturnErrorOnFailure(https::Request(mHostName, mPort, path, outModel)); + + CHIP_ERROR error = ValidateModelSchema(outModel); + VerifyOrReturnError(CHIP_NO_ERROR == error, error, + ChipLogError(chipTool, "%s%s", kErrorSchemaValidation, outModel.toStyledString().c_str())); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR DCLClient::TermsAndConditions(const char * onboardingPayload, Json::Value & outTc) +{ + Json::Value json; + ReturnErrorOnFailure(Model(onboardingPayload, json)); + VerifyOrReturnError(Json::nullValue != json.type(), CHIP_NO_ERROR, outTc = Json::nullValue); + ReturnErrorOnFailure(RequestTermsAndConditions(json, outTc)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR DCLClient::TermsAndConditions(const chip::VendorId vendorId, const uint16_t productId, Json::Value & outTc) +{ + Json::Value json; + ReturnErrorOnFailure(Model(vendorId, productId, json)); + VerifyOrReturnError(Json::nullValue != json.type(), CHIP_NO_ERROR, outTc = Json::nullValue); + ReturnErrorOnFailure(RequestTermsAndConditions(json, outTc)); + return CHIP_NO_ERROR; +} + +} // namespace dcl +} // namespace tool +} // namespace chip diff --git a/examples/chip-tool/commands/dcl/DCLClient.h b/examples/chip-tool/commands/dcl/DCLClient.h new file mode 100644 index 00000000000000..d4ce72c38f76f8 --- /dev/null +++ b/examples/chip-tool/commands/dcl/DCLClient.h @@ -0,0 +1,100 @@ +/* + * 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 +#include +#include + +#include +#include + +namespace chip { +namespace tool { +namespace dcl { +class DCLClient +{ +public: + DCLClient(Optional hostname, Optional port); + + /** + * @brief Retrieves the model information from the DCL based on the onboarding payload. + * + * This function uses the onboarding payload (a QR Code or Manual Code) to fetch the model information. + * It constructs an HTTPS request to retrieve the model data associated with the specified vendor ID and product ID from the + * payload. + * + * @param[in] onboardingPayload A null-terminated string containing the onboarding payload. + * This can either start with a QR Code prefix or be a Manual Code. + * @param[out] outModel A Json::Value object to store the retrieved model information. + * If the vendor and product IDs are missing, this will be set to null. + * + * @return CHIP_ERROR CHIP_NO_ERROR on success, error code otherwise. + */ + CHIP_ERROR Model(const char * onboardingPayload, Json::Value & outModel); + + /** + * @brief Retrieves the model information from the DCL using vendor ID and product ID. + * + * This function constructs an HTTPS request to retrieve the model data associated with the specified vendor ID and product ID. + * + * @param[in] vendorId The vendor ID of the model (must not be 0). + * @param[in] productId The product ID of the model (must not be 0). + * @param[out] outModel A Json::Value object to store the retrieved model information. + * + * @return CHIP_ERROR CHIP_NO_ERROR on success, error code otherwise. + */ + CHIP_ERROR Model(const VendorId vendorId, const uint16_t productId, Json::Value & outModel); + + /** + * @brief Retrieves the Terms and Conditions from the DCL based on the onboarding payload. + * + * This function uses the onboarding payload (a QR Code or Manual Code) to fetch the model information. + * If the model includes enhanced setup flow options, it requests and validates the associated Terms + * and Conditions data. If enhanced setup flow is not enabled, the output `tc` is set to null. + * + * @param[in] onboardingPayload A null-terminated string containing the onboarding payload. + * This can either start with a QR Code prefix or be a Manual Code. + * @param[out] outTc A Json::Value object to store the retrieved Terms and Conditions data. + * If enhanced setup flow options are not enabled, this will be set to null. + * + * @return CHIP_ERROR CHIP_NO_ERROR on success, error code otherwise. + */ + CHIP_ERROR TermsAndConditions(const char * onboardingPayload, Json::Value & outTc); + + /** + * @brief Retrieves the Terms and Conditions from the DCL using vendor ID and product ID. + * + * This function first retrieves the model information using the specified vendor ID and product ID. + * If the model includes enhanced setup flow options, it fetches the Terms and Conditions, validates the data, and returns it. + * + * @param[in] vendorId The vendor ID of the model (must not be 0). + * @param[in] productId The product ID of the model (must not be 0). + * @param[out] outTc A Json::Value object to store the retrieved Terms and Conditions data. + * If enhanced setup flow options are not enabled, this will be set to null. + * + * @return CHIP_ERROR CHIP_NO_ERROR on success, error code otherwise. + */ + CHIP_ERROR TermsAndConditions(const chip::VendorId vendorId, const uint16_t productId, Json::Value & outTc); + +private: + std::string mHostName; + uint16_t mPort; +}; +} // namespace dcl +} // namespace tool +} // namespace chip diff --git a/examples/chip-tool/commands/dcl/DCLCommands.h b/examples/chip-tool/commands/dcl/DCLCommands.h new file mode 100644 index 00000000000000..7fc910aa727952 --- /dev/null +++ b/examples/chip-tool/commands/dcl/DCLCommands.h @@ -0,0 +1,202 @@ +/* + * 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 "../common/Command.h" + +#include "DCLClient.h" +#include "DisplayTermsAndConditions.h" + +class DCLCommandBase : public Command +{ +public: + DCLCommandBase(const char * name) : Command(name) {} + + void AddArguments() + { + AddArgument("hostname", &mHostName, + "Hostname of the DCL server to fetch information from. Defaults to 'on.dcl.csa-iot.org'."); + AddArgument("port", 0, UINT16_MAX, &mPort, "Port number for connecting to the DCL server. Defaults to '443'."); + } + + CHIP_ERROR Run() + { + auto client = chip::tool::dcl::DCLClient(mHostName, mPort); + return RunCommand(client); + + return CHIP_NO_ERROR; + } + + virtual CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client) = 0; + +private: + chip::Optional mHostName; + chip::Optional mPort; +}; + +class DCLPayloadCommandBase : public DCLCommandBase +{ +public: + DCLPayloadCommandBase(const char * name) : DCLCommandBase(name) + { + AddArgument("payload", &mPayload); + DCLCommandBase::AddArguments(); + } + +protected: + char * mPayload; +}; + +class DCLIdsCommandBase : public DCLCommandBase +{ +public: + DCLIdsCommandBase(const char * name) : DCLCommandBase(name) + { + AddArgument("vendor-id", 0, UINT16_MAX, &mVendorId); + AddArgument("product-id", 0, UINT16_MAX, &mProductId); + DCLCommandBase::AddArguments(); + } + +protected: + uint16_t mVendorId; + uint16_t mProductId; +}; + +class DCLModelByPayloadCommand : public DCLPayloadCommandBase +{ +public: + DCLModelByPayloadCommand() : DCLPayloadCommandBase("model-by-payload") {} + + CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client) + { + Json::Value model; + ReturnErrorOnFailure(client.Model(mPayload, model)); + VerifyOrReturnError(model != Json::nullValue, CHIP_NO_ERROR); + + ChipLogProgress(chipTool, "%s", model.toStyledString().c_str()); + return CHIP_NO_ERROR; + } +}; + +class DCLModelCommand : public DCLIdsCommandBase +{ +public: + DCLModelCommand() : DCLIdsCommandBase("model") {} + + CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client) + { + Json::Value model; + ReturnErrorOnFailure(client.Model(static_cast(mVendorId), mProductId, model)); + VerifyOrReturnError(model != Json::nullValue, CHIP_NO_ERROR); + + ChipLogProgress(chipTool, "%s", model.toStyledString().c_str()); + return CHIP_NO_ERROR; + } +}; + +class DCLTCByPayloadCommand : public DCLPayloadCommandBase +{ +public: + DCLTCByPayloadCommand() : DCLPayloadCommandBase("tc-by-payload") {} + + CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client) + { + Json::Value tc; + ReturnErrorOnFailure(client.TermsAndConditions(mPayload, tc)); + VerifyOrReturnError(tc != Json::nullValue, CHIP_NO_ERROR); + + ChipLogProgress(chipTool, "%s", tc.toStyledString().c_str()); + return CHIP_NO_ERROR; + } +}; + +class DCLTCCommand : public DCLIdsCommandBase +{ +public: + DCLTCCommand() : DCLIdsCommandBase("tc") {} + + CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client) + { + Json::Value tc; + ReturnErrorOnFailure(client.TermsAndConditions(static_cast(mVendorId), mProductId, tc)); + VerifyOrReturnError(tc != Json::nullValue, CHIP_NO_ERROR); + + ChipLogProgress(chipTool, "%s", tc.toStyledString().c_str()); + return CHIP_NO_ERROR; + } +}; + +class DCLTCDisplayByPayloadCommand : public DCLPayloadCommandBase +{ +public: + DCLTCDisplayByPayloadCommand() : DCLPayloadCommandBase("tc-display-by-payload") + { + AddArgument("country-code", &mCountryCode, + "The country code to retrieve terms and conditions for. Defaults to the country configured in the DCL."); + AddArgument("language-code", &mLanguageCode, + "The language code to retrieve terms and conditions for. Defaults to the language configured for the chosen " + "country in the DCL."); + } + + CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client) + { + Json::Value tc; + ReturnErrorOnFailure(client.TermsAndConditions(mPayload, tc)); + VerifyOrReturnError(tc != Json::nullValue, CHIP_NO_ERROR); + + uint16_t version = 0; + uint16_t userResponse = 0; + ReturnErrorOnFailure(chip::tool::dcl::DisplayTermsAndConditions(tc, version, userResponse, mCountryCode, mLanguageCode)); + + ChipLogProgress(chipTool, "\nTerms and conditions\n\tRevision : %u\n\tUserResponse: %u", version, userResponse); + return CHIP_NO_ERROR; + } + +private: + chip::Optional mCountryCode; + chip::Optional mLanguageCode; +}; + +class DCLTCDisplayCommand : public DCLIdsCommandBase +{ +public: + DCLTCDisplayCommand() : DCLIdsCommandBase("tc-display") + { + AddArgument("country-code", &mCountryCode, + "The country code to retrieve terms and conditions for. Defaults to the country configured in the DCL."); + AddArgument("language-code", &mLanguageCode, + "The language code to retrieve terms and conditions for. Defaults to the language configured for the chosen " + "country in the DCL."); + } + CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client) + { + Json::Value tc; + ReturnErrorOnFailure(client.TermsAndConditions(static_cast(mVendorId), mProductId, tc)); + VerifyOrReturnError(tc != Json::nullValue, CHIP_NO_ERROR); + + uint16_t version = 0; + uint16_t userResponse = 0; + ReturnErrorOnFailure(chip::tool::dcl::DisplayTermsAndConditions(tc, version, userResponse, mCountryCode, mLanguageCode)); + + ChipLogProgress(chipTool, "\nTerms and conditions\n\tRevision : %u\n\tUserResponse: %u", version, userResponse); + return CHIP_NO_ERROR; + } + +private: + chip::Optional mCountryCode; + chip::Optional mLanguageCode; +}; diff --git a/examples/chip-tool/commands/dcl/DisplayTermsAndConditions.cpp b/examples/chip-tool/commands/dcl/DisplayTermsAndConditions.cpp new file mode 100644 index 00000000000000..8745db90c6dff4 --- /dev/null +++ b/examples/chip-tool/commands/dcl/DisplayTermsAndConditions.cpp @@ -0,0 +1,230 @@ +/* + * 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 "DisplayTermsAndConditions.h" + +#include +#include + +#include +#include +#include + +namespace chip { +namespace tool { +namespace dcl { +namespace { +constexpr const char * kAcceptTerms = "Do you accept these terms? [Y/n]: "; +constexpr const char * kRequiredTerms = "Required"; +constexpr const char * kOptionalTerms = "Optional"; +constexpr const char * kTitleAllowedTags = R"(<(/?)(b|em|i|small|strong|u)>)"; +constexpr const char * kTextAllowedTags = R"(<(/?)(b|br|em|h1|h2|h3|h4|h5|h6|hr|i|li|ol|p|small|strong|u|ul)>)"; +constexpr const char * kAnsiCodeReset = "\033[0m"; +constexpr const char * kAnsiCodeBold = "\033[1m"; +constexpr const char * kAnsiCodeFaint = "\033[2m"; +constexpr const char * kAnsiCodeItalics = "\033[3m"; +constexpr const char * kAnsiCodeUnderline = "\033[4m"; +constexpr const char * kLineBreak = "\n"; +constexpr const char * kListItem = " - "; +constexpr const char * kHorizontalLine = "\n==========================================\n"; +constexpr const char * kErrorInvalidInput = "Invalid input. Please enter 'Y' (yes) or 'N' (no). Default is 'Y'."; + +// Fields names for the ESF JSON schema +constexpr const char * kFieldCountryEntries = "countryEntries"; +constexpr const char * kFieldDefaultCountry = "defaultCountry"; +constexpr const char * kFieldLanguageEntries = "languageEntries"; +constexpr const char * kFieldDefaultLanguage = "defaultLanguage"; +constexpr const char * kFieldOrdinal = "ordinal"; +constexpr const char * kFieldRequired = "required"; +constexpr const char * kFieldSchemaVersion = "schemaVersion"; +constexpr const char * kFieldText = "text"; +constexpr const char * kFieldTitle = "title"; + +const std::unordered_map kHtmlToAnsiCodes = { + { "b", kAnsiCodeBold }, // + { "br", kLineBreak }, // + { "em", kAnsiCodeItalics }, // + { "h1", kAnsiCodeBold }, // + { "h2", kAnsiCodeBold }, // + { "h3", kAnsiCodeBold }, // + { "h4", kAnsiCodeBold }, // + { "h5", kAnsiCodeBold }, // + { "h6", kAnsiCodeBold }, // + { "hr", kHorizontalLine }, // + { "i", kAnsiCodeItalics }, // + { "li", kListItem }, // + { "ol", kLineBreak }, // + { "p", kLineBreak }, // + { "small", kAnsiCodeFaint }, // + { "strong", kAnsiCodeBold }, // + { "u", kAnsiCodeUnderline }, // + { "ul", kLineBreak }, // +}; + +std::string ToUpperCase(const std::string & input) +{ + std::string output = input; + std::transform(output.begin(), output.end(), output.begin(), [](unsigned char c) { return std::toupper(c); }); + return output; +} + +std::string ToLowerCase(const std::string & input) +{ + std::string output = input; + std::transform(output.begin(), output.end(), output.begin(), [](unsigned char c) { return std::tolower(c); }); + return output; +} + +std::string Center(const std::string & text) +{ + size_t lineWidth = strlen(kHorizontalLine) - 1; + if (text.length() >= lineWidth) + { + return text; // No padding if the text is longer than the width + } + + size_t totalPadding = lineWidth - text.length(); + size_t paddingLeft = totalPadding / 2; + size_t paddingRight = totalPadding - paddingLeft; + + return std::string(paddingLeft, ' ') + text + std::string(paddingRight, ' '); +} + +std::string HTMLTagToAnsiCode(const std::smatch & match) +{ + if (match[1] == "/") + { + return kAnsiCodeReset; + } + + std::string tag = match[2]; + auto ansiCode = kHtmlToAnsiCodes.find(ToLowerCase(tag)); + if (ansiCode == kHtmlToAnsiCodes.end()) + { + return "<" + tag + ">"; + } + + return ansiCode->second; +} + +std::string renderHTMLInTerminal(const std::string & html, const std::string & allowedTags = kTextAllowedTags) +{ + std::string formattedText; + std::string::const_iterator current = html.cbegin(); + + std::regex regex(allowedTags, std::regex_constants::icase); + for (std::sregex_iterator it(html.cbegin(), html.cend(), regex), end; it != end; ++it) + { + const auto & match = *it; + + formattedText += std::string(current, html.cbegin() + match.position()); + formattedText += HTMLTagToAnsiCode(match); + + current = html.cbegin() + match.position() + match.length(); + } + + formattedText += std::string(current, html.cend()); + formattedText += kAnsiCodeReset; + return formattedText; +} + +const char * ResolveValueOrDefault(const Json::Value & entries, const Optional & userValue, const char * defaultValue, + const char * valueType) +{ + const char * resolvedValue = userValue.ValueOr(defaultValue); + + if (userValue.HasValue() && !entries.isMember(resolvedValue)) + { + ChipLogProgress(chipTool, "User-chosen %s ('%s') not found. Defaulting to '%s'", valueType, userValue.Value(), + defaultValue); + resolvedValue = defaultValue; + } + + return resolvedValue; +} + +const Json::Value & GetTexts(const Json::Value & tc, Optional optionalCountryCode, + Optional optionalLanguageCode) +{ + const char * defaultCountry = tc[kFieldDefaultCountry].asCString(); + const char * chosenCountry = ResolveValueOrDefault(tc[kFieldCountryEntries], optionalCountryCode, defaultCountry, "country"); + auto & countryEntry = tc[kFieldCountryEntries][chosenCountry]; + + const char * defaultLanguage = countryEntry[kFieldDefaultLanguage].asCString(); + const char * chosenLanguage = + ResolveValueOrDefault(countryEntry[kFieldLanguageEntries], optionalLanguageCode, defaultLanguage, "language"); + auto & languageEntry = countryEntry[kFieldLanguageEntries][chosenLanguage]; + + return languageEntry; +} + +void PrintText(const Json::Value & json) +{ + auto title = renderHTMLInTerminal(Center(ToUpperCase(json[kFieldTitle].asCString())), kTitleAllowedTags); + auto text = renderHTMLInTerminal(json[kFieldText].asCString()); + auto userQuestion = renderHTMLInTerminal(kAcceptTerms); + auto required = json[kFieldRequired].asBool() ? kRequiredTerms : kOptionalTerms; + + printf("%s", kHorizontalLine); + printf("%s", title.c_str()); + printf("%s", kHorizontalLine); + printf("%s", text.c_str()); + printf("%s", kHorizontalLine); + printf("[%s] %s", required, userQuestion.c_str()); +} + +bool AcknowledgeText() +{ + while (true) + { + std::string userInput; + std::getline(std::cin, userInput); + + VerifyOrReturnValue(!userInput.empty() && userInput != "Y" && userInput != "y", true); + VerifyOrReturnValue(userInput != "N" && userInput != "n", false); + + ChipLogError(chipTool, "%s", kErrorInvalidInput); + } +} + +} // namespace + +CHIP_ERROR DisplayTermsAndConditions(const Json::Value & tc, uint16_t & outVersion, uint16_t & outUserResponse, + Optional countryCode, Optional languageCode) +{ + VerifyOrReturnError(CanCastTo(tc[kFieldSchemaVersion].asUInt()), CHIP_ERROR_INVALID_ARGUMENT); + outVersion = static_cast(tc[kFieldSchemaVersion].asUInt()); + + auto texts = GetTexts(tc, countryCode, languageCode); + for (const auto & text : texts) + { + PrintText(text); + + if (AcknowledgeText()) + { + auto ordinal = text[kFieldOrdinal].asUInt(); + VerifyOrReturnError(ordinal < 16, CHIP_ERROR_INVALID_ARGUMENT); // Only 16 bits are available for user response + uint16_t shiftedValue = static_cast((1U << (ordinal & 0x0F)) & 0xFFFF); + outUserResponse |= shiftedValue; + } + } + return CHIP_NO_ERROR; +} +} // namespace dcl +} // namespace tool +} // namespace chip diff --git a/examples/chip-tool/commands/dcl/DisplayTermsAndConditions.h b/examples/chip-tool/commands/dcl/DisplayTermsAndConditions.h new file mode 100644 index 00000000000000..49a68e3a6e1781 --- /dev/null +++ b/examples/chip-tool/commands/dcl/DisplayTermsAndConditions.h @@ -0,0 +1,44 @@ +/* + * 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 +#include + +#include + +namespace chip { +namespace tool { +namespace dcl { +/** + * Display the terms and conditions to the user and prompt for acceptance. + * + * @param[in] tc The terms and conditions JSON object. + * @param[out] outVersion The schema version of the terms and conditions. + * @param[out] outUserResponse The user response as a bitfield where each bit corresponds to the ordinal of the text. + * @param[in] countryCode The country code to use for the terms and conditions. If not provided, the default country will be used. + * @param[in] languageCode The language code to use for the terms and conditions. If not provided, the default language will be + * used. + * + * @return CHIP_NO_ERROR on success, error code otherwise. + */ +CHIP_ERROR DisplayTermsAndConditions(const Json::Value & tc, uint16_t & outVersion, uint16_t & outUserResponse, + Optional countryCode = NullOptional, + Optional languageCode = NullOptional); +} // namespace dcl +} // namespace tool +} // namespace chip diff --git a/examples/chip-tool/commands/dcl/HTTPSRequest.cpp b/examples/chip-tool/commands/dcl/HTTPSRequest.cpp new file mode 100644 index 00000000000000..29b0ed8f2726db --- /dev/null +++ b/examples/chip-tool/commands/dcl/HTTPSRequest.cpp @@ -0,0 +1,339 @@ +/* + * 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 "HTTPSRequest.h" + +#include +#include +#include +#include +#include +#include + +#if (CHIP_CRYPTO_OPENSSL || CHIP_CRYPTO_BORINGSSL) +#include +#include +#include +#ifdef SHA256_DIGEST_LENGTH +#define USE_CHIP_CRYPTO 1 +#endif +#endif //(CHIP_CRYPTO_OPENSSL || CHIP_CRYPTO_BORINGSSL) + +namespace { +constexpr const char * kHttpsPrefix = "https://"; +constexpr uint16_t kHttpsPort = 443; +constexpr const char * kErrorJsonParse = "Failed to parse JSON: "; +constexpr const char * kErrorHTTPSPrefix = "URL must start with 'https://': "; +constexpr const char * kErrorHTTPSPort = "Invalid port: 0"; +constexpr const char * kErrorHTTPSHostName = "Invalid hostname: empty"; +constexpr const char * kErrorBase64Decode = "Error while decoding base64 data"; +constexpr const char * kErrorSizeMismatch = "The response size does not match the expected size: "; +} // namespace + +namespace chip { +namespace tool { +namespace https { +namespace { +#ifndef USE_CHIP_CRYPTO +/** + * @brief Stub implementation of HTTPSSessionHolder when neither OpenSSL nor BoringSSL is enabled. + * + * This class provides placeholder methods that log errors indicating the lack of SSL library support + * and encourages contributions for new implementations. + */ +class HTTPSSessionHolder +{ +public: + CHIP_ERROR Init(std::string & hostname, uint16_t port) { return LogNotImplementedError(); } + + CHIP_ERROR SendRequest(std::string & request) { return LogNotImplementedError(); } + + CHIP_ERROR ReceiveResponse(std::string & response) { return LogNotImplementedError(); } + +private: + CHIP_ERROR LogNotImplementedError() const + { + ChipLogError(chipTool, + "HTTPS requests are not available because neither OpenSSL nor BoringSSL is enabled. Contributions for " + "alternative implementations are welcome!"); + return CHIP_ERROR_NOT_IMPLEMENTED; + } +}; +#else // USE_CHIP_CRYPTO +constexpr uint16_t kResponseBufferSize = 4096; +constexpr const char * kErrorSendHTTPRequest = "Failed to send HTTP request"; +constexpr const char * kErrorReceiveHTTPResponse = "Failed to read HTTP response"; +constexpr const char * kErrorConnection = "Failed to connect to: "; +constexpr const char * kErrorSSLContextCreate = "Failed to create SSL context"; +constexpr const char * kErrorSSLObjectCreate = "Failed to create SSL object"; +constexpr const char * kErrorSSLHandshake = "SSL handshake failed"; +constexpr const char * kErrorDigestMismatch = "The response digest does not match the expected digest"; +class HTTPSSessionHolder +{ +public: + HTTPSSessionHolder(){}; + + ~HTTPSSessionHolder() + { + VerifyOrReturn(nullptr != mContext); + SSL_free(mSSL); + SSL_CTX_free(mContext); + close(mSock); + +#if !defined(OPENSSL_IS_BORINGSSL) + EVP_cleanup(); +#endif + } + + CHIP_ERROR Init(std::string & hostname, uint16_t port) + { + int sock; + ReturnErrorOnFailure(InitSocket(hostname, port, sock)); + ReturnErrorOnFailure(InitSSL(sock)); + return CHIP_NO_ERROR; + } + + CHIP_ERROR SendRequest(std::string & request) + { + int written = SSL_write(mSSL, request.c_str(), (int) request.size()); + VerifyOrReturnError(written > 0, CHIP_ERROR_BAD_REQUEST, ChipLogError(chipTool, "%s", kErrorSendHTTPRequest)); + return CHIP_NO_ERROR; + } + + CHIP_ERROR ReceiveResponse(std::string & response) + { + char buffer[kResponseBufferSize]; + + ssize_t n = -1; + while ((n = SSL_read(mSSL, buffer, sizeof(buffer))) > 0) + { + VerifyOrReturnError(CanCastTo(n), CHIP_ERROR_INVALID_ARGUMENT); + response.append(buffer, static_cast(n)); + } + + VerifyOrReturnError(n >= 0, CHIP_ERROR_INTERNAL, ChipLogError(chipTool, "%s", kErrorReceiveHTTPResponse)); + + return CHIP_NO_ERROR; + } + +private: + CHIP_ERROR InitSocket(std::string & hostname, uint16_t port, int & sock) + { + auto * server = gethostbyname(hostname.c_str()); + VerifyOrReturnError(nullptr != server, CHIP_ERROR_NOT_CONNECTED); + + sock = socket(AF_INET, SOCK_STREAM, 0); + VerifyOrReturnError(sock >= 0, CHIP_ERROR_NOT_CONNECTED); + + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(port); + memcpy(&server_addr.sin_addr.s_addr, server->h_addr, (size_t) server->h_length); + + int rv = connect(sock, (struct sockaddr *) &server_addr, sizeof(server_addr)); + VerifyOrReturnError(rv >= 0, CHIP_ERROR_POSIX(errno), + ChipLogError(chipTool, "%s%s:%u", kErrorConnection, hostname.c_str(), port)); + + return CHIP_NO_ERROR; + } + + CHIP_ERROR InitSSL(int sock) + { + SSL_load_error_strings(); + OpenSSL_add_ssl_algorithms(); + + auto * context = SSL_CTX_new(TLS_client_method()); + VerifyOrReturnError(nullptr != context, CHIP_ERROR_NOT_CONNECTED, ChipLogError(chipTool, "%s", kErrorSSLContextCreate)); + + auto * ssl = SSL_new(context); + VerifyOrReturnError(nullptr != ssl, CHIP_ERROR_NOT_CONNECTED, ChipLogError(chipTool, "%s", kErrorSSLObjectCreate)); + + SSL_set_fd(ssl, sock); + VerifyOrReturnError(SSL_connect(ssl) > 0, CHIP_ERROR_NOT_CONNECTED, ChipLogError(chipTool, "%s", kErrorSSLHandshake)); + + mContext = context; + mSSL = ssl; + mSock = sock; + return CHIP_NO_ERROR; + } + + SSL_CTX * mContext = nullptr; + SSL * mSSL = nullptr; + int mSock = -1; +}; +#endif // USE_CHIP_CRYPTO + +std::string BuildRequest(std::string & hostname, std::string & path) +{ + return "GET " + path + " HTTP/1.1\r\n" + // + "Host: " + hostname + "\r\n" + // + "Accept: application/json\r\n" + // + "Connection: close\r\n\r\n"; // +} + +CHIP_ERROR RemoveHeader(std::string & response) +{ + size_t headerEnd = response.find("\r\n\r\n"); + VerifyOrReturnError(std::string::npos != headerEnd, CHIP_ERROR_INVALID_ARGUMENT); + + auto body = response.substr(headerEnd + 4); + response = body; + + return CHIP_NO_ERROR; +} + +CHIP_ERROR MaybeCheckResponseSize(const std::string & response, const chip::Optional & optionalExpectedSize) +{ + VerifyOrReturnError(optionalExpectedSize.HasValue(), CHIP_NO_ERROR); + VerifyOrReturnError(chip::CanCastTo(response.size()), CHIP_ERROR_INVALID_ARGUMENT); + + uint32_t responseSize = static_cast(response.size()); + uint32_t expectedSize = optionalExpectedSize.Value(); + VerifyOrReturnError(expectedSize == responseSize, CHIP_ERROR_INVALID_ARGUMENT, + ChipLogError(chipTool, "%s%u != %u", kErrorSizeMismatch, responseSize, expectedSize)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR MaybeCheckResponseDigest(const std::string & response, const chip::Optional & optionalExpectedDigest) +{ + VerifyOrReturnError(optionalExpectedDigest.HasValue(), CHIP_NO_ERROR); + VerifyOrReturnError(CanCastTo(strlen(optionalExpectedDigest.Value())), CHIP_ERROR_INVALID_ARGUMENT); + + const char * encodedData = optionalExpectedDigest.Value(); + uint16_t encodedDataSize = static_cast(strlen(encodedData)); + + size_t expectedMaxDecodedSize = BASE64_MAX_DECODED_LEN(encodedDataSize); + chip::Platform::ScopedMemoryBuffer decodedData; + VerifyOrReturnError(decodedData.Calloc(expectedMaxDecodedSize + 1 /* for null */), CHIP_ERROR_INVALID_ARGUMENT); + + size_t decodedDataSize = chip::Base64Decode(encodedData, encodedDataSize, decodedData.Get()); + VerifyOrReturnError(0 != decodedDataSize, CHIP_ERROR_INVALID_ARGUMENT, ChipLogError(chipTool, "%s", kErrorBase64Decode)); + +#ifdef USE_CHIP_CRYPTO + // Compute the SHA-256 hash of the response + unsigned char responseDigest[SHA256_DIGEST_LENGTH]; + SHA256(reinterpret_cast(response.c_str()), response.size(), responseDigest); + + VerifyOrReturnError(memcmp(responseDigest, decodedData.Get(), SHA256_DIGEST_LENGTH) == 0, CHIP_ERROR_INVALID_ARGUMENT, + ChipLogError(chipTool, "%s", kErrorDigestMismatch)); +#else + return CHIP_ERROR_NOT_IMPLEMENTED; +#endif // USE_CHIP_CRYPTO + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ConvertResponseToJSON(std::string & body, Json::Value & jsonResponse) +{ + + Json::CharReaderBuilder readerBuilder; + std::string errors; + std::istringstream jsonStream(body); + bool success = Json::parseFromStream(readerBuilder, jsonStream, &jsonResponse, &errors); + VerifyOrReturnError(success, CHIP_ERROR_INTERNAL, ChipLogError(chipTool, "%s%s", kErrorJsonParse, errors.c_str())); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ExtractHostAndPath(const std::string & url, std::string & hostAndPort, std::string & outPath) +{ + VerifyOrReturnError(url.compare(0, strlen(kHttpsPrefix), kHttpsPrefix) == 0, CHIP_ERROR_INVALID_ARGUMENT, + ChipLogError(chipTool, "%s%s", kErrorHTTPSPrefix, url.c_str())); + + auto strippedUrl = url.substr(strlen(kHttpsPrefix)); + VerifyOrReturnError("" != strippedUrl, CHIP_ERROR_INVALID_ARGUMENT, ChipLogError(chipTool, "%s", kErrorHTTPSHostName)); + + size_t position = strippedUrl.find('/'); + if (position == std::string::npos) + { + hostAndPort = strippedUrl; + outPath = "/"; + } + else + { + hostAndPort = strippedUrl.substr(0, position); + outPath = strippedUrl.substr(position); + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ExtractHostAndPort(const std::string & hostAndPort, std::string & outHostName, uint16_t & outPort) +{ + size_t position = hostAndPort.find(':'); + if (position == std::string::npos) + { + outHostName = hostAndPort; + outPort = kHttpsPort; + } + else + { + outHostName = hostAndPort.substr(0, position); + auto portString = hostAndPort.substr(position + 1); + outPort = static_cast(std::atoi(portString.c_str())); + VerifyOrReturnError(0 != outPort, CHIP_ERROR_INVALID_ARGUMENT, ChipLogError(chipTool, "%s", kErrorHTTPSPort)); + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ExtractHostNamePortPath(std::string url, std::string & outHostName, uint16_t & outPort, std::string & outPath) +{ + std::string hostAndPort; + ReturnErrorOnFailure(ExtractHostAndPath(url, hostAndPort, outPath)); + ReturnErrorOnFailure(ExtractHostAndPort(hostAndPort, outHostName, outPort)); + + return CHIP_NO_ERROR; +} +} // namespace + +CHIP_ERROR Request(std::string url, Json::Value & jsonResponse, const Optional & optionalExpectedSize, + const Optional & optionalExpectedDigest) +{ + std::string hostname; + uint16_t port; + std::string path; + ReturnErrorOnFailure(ExtractHostNamePortPath(url, hostname, port, path)); + return Request(hostname, port, path, jsonResponse, optionalExpectedSize, optionalExpectedDigest); +} + +CHIP_ERROR Request(std::string hostname, uint16_t port, std::string path, Json::Value & jsonResponse, + const Optional & optionalExpectedSize, const Optional & optionalExpectedDigest) +{ + VerifyOrDo(port != 0, port = kHttpsPort); + + ChipLogDetail(chipTool, "HTTPS request to %s:%u%s", hostname.c_str(), port, path.c_str()); + + std::string request = BuildRequest(hostname, path); + std::string response; + + HTTPSSessionHolder session; + ReturnErrorOnFailure(session.Init(hostname, port)); + ReturnErrorOnFailure(session.SendRequest(request)); + ReturnErrorOnFailure(session.ReceiveResponse(response)); + ReturnErrorOnFailure(RemoveHeader(response)); + ReturnErrorOnFailure(MaybeCheckResponseSize(response, optionalExpectedSize)); + ReturnErrorOnFailure(MaybeCheckResponseDigest(response, optionalExpectedDigest)); + ReturnErrorOnFailure(ConvertResponseToJSON(response, jsonResponse)); + return CHIP_NO_ERROR; +} + +} // namespace https +} // namespace tool +} // namespace chip diff --git a/examples/chip-tool/commands/dcl/HTTPSRequest.h b/examples/chip-tool/commands/dcl/HTTPSRequest.h new file mode 100644 index 00000000000000..f11e3b33d354bb --- /dev/null +++ b/examples/chip-tool/commands/dcl/HTTPSRequest.h @@ -0,0 +1,39 @@ +/* + * 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 +#include + +#include +#include + +namespace chip { +namespace tool { +namespace https { + +CHIP_ERROR Request(std::string url, Json::Value & jsonResponse, + const chip::Optional & optionalExpectedSize = chip::NullOptional, + const chip::Optional & optionalExpectedDigest = chip::NullOptional); + +CHIP_ERROR Request(std::string hostname, uint16_t port, std::string path, Json::Value & jsonResponse, + const chip::Optional & optionalExpectedSize = chip::NullOptional, + const chip::Optional & optionalExpectedDigest = chip::NullOptional); + +} // namespace https +} // namespace tool +} // namespace chip diff --git a/examples/chip-tool/commands/dcl/JsonSchemaMacros.cpp b/examples/chip-tool/commands/dcl/JsonSchemaMacros.cpp new file mode 100644 index 00000000000000..f73fb4dac9d116 --- /dev/null +++ b/examples/chip-tool/commands/dcl/JsonSchemaMacros.cpp @@ -0,0 +1,64 @@ +/* + * 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 "JsonSchemaMacros.h" + +namespace { +constexpr const char * kJsonTypeNull = "Null"; +constexpr const char * kJsonTypeInt = "Int"; +constexpr const char * kJsonTypeUInt = "UInt"; +constexpr const char * kJsonTypeReal = "Real"; +constexpr const char * kJsonTypeString = "String"; +constexpr const char * kJsonTypeBool = "Bool"; +constexpr const char * kJsonTypeArray = "Array"; +constexpr const char * kJsonTypeObject = "Object"; +constexpr const char * kJsonTypeUnknown = "Unknown"; +} // namespace + +namespace chip { +namespace json { +const char * GetTypeName(const Json::Value & value) +{ + const char * type = kJsonTypeUnknown; + + switch (value.type()) + { + case Json::nullValue: + return kJsonTypeNull; + case Json::intValue: + return kJsonTypeInt; + case Json::uintValue: + return kJsonTypeUInt; + case Json::realValue: + return kJsonTypeReal; + case Json::stringValue: + return kJsonTypeString; + case Json::booleanValue: + return kJsonTypeBool; + case Json::arrayValue: + return kJsonTypeArray; + case Json::objectValue: + return kJsonTypeObject; + default: + return kJsonTypeUnknown; + } + + return type; +} +} // namespace json +} // namespace chip diff --git a/examples/chip-tool/commands/dcl/JsonSchemaMacros.h b/examples/chip-tool/commands/dcl/JsonSchemaMacros.h new file mode 100644 index 00000000000000..812cf516787c98 --- /dev/null +++ b/examples/chip-tool/commands/dcl/JsonSchemaMacros.h @@ -0,0 +1,41 @@ +/* + * 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 + +#define CHECK_TYPE(source, fieldName, fieldType) \ + VerifyOrReturnError(source.is##fieldType(), CHIP_ERROR_SCHEMA_MISMATCH, \ + ChipLogError(chipTool, "Type mismatch for field '%s': expected '%s', got '%s'", #fieldName, #fieldType, \ + chip::json::GetTypeName(source))); + +#define CHECK_REQUIRED_TYPE(source, fieldName, fieldType) \ + VerifyOrReturnError(source.isMember(#fieldName), CHIP_ERROR_SCHEMA_MISMATCH, \ + ChipLogError(chipTool, "Missing required field: '%s'", #fieldName)); \ + CHECK_TYPE(source[#fieldName], fieldName, fieldType) + +#define CHECK_REQUIRED_VALUE(source, fieldName, expectedValue) \ + CHECK_REQUIRED_TYPE(source, fieldName, UInt); \ + VerifyOrReturnError(source[#fieldName].asUInt() == expectedValue, CHIP_ERROR_INCORRECT_STATE, \ + ChipLogError(chipTool, "Value mismatch for '%s': expected '%u', got '%u'", #fieldName, expectedValue, \ + source[#fieldName].asUInt())); + +namespace chip { +namespace json { +const char * GetTypeName(const Json::Value & value); +} // namespace json +} // namespace chip diff --git a/examples/chip-tool/commands/dcl/test_dcl_server.py b/examples/chip-tool/commands/dcl/test_dcl_server.py new file mode 100755 index 00000000000000..f22f2d2a8e9390 --- /dev/null +++ b/examples/chip-tool/commands/dcl/test_dcl_server.py @@ -0,0 +1,245 @@ +#!/usr/bin/env -S python3 -B + +# Copyright (c) 2025 Project CHIP Authors +# +# 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. + +import base64 +import hashlib +import http.server +import json +import os +import re +import ssl + +DEFAULT_HOSTNAME = "localhost" +DEFAULT_PORT = 4443 + + +TC = { + 0XFFF1: { + 0x8001: { + "schemaVersion": 1, + "esfRevision": 1, + "defaultCountry": "US", + "countryEntries": { + "US": { + "defaultLanguage": "en", + "languageEntries": { + "en": [ + { + "ordinal": 0, + "required": True, + "title": "Terms and Conditions", + "text": "

Feature 1 Text

Please accept these.

" + }, + { + "ordinal": 1, + "required": False, + "title": "Privacy Policy", + "text": "

Feature 2 Text

" + } + ], + "es": [ + { + "ordinal": 0, + "required": True, + "title": "Términos y condiciones", + "text": "

Característica 1 Texto

Por favor acéptelos.

" + }, + { + "ordinal": 1, + "required": False, + "title": "Política de privacidad", + "text": "

Característica 2 Texto

" + } + ] + } + }, + "MX": { + "defaultLanguage": "es", + "languageEntries": { + "es": [ + { + "ordinal": 0, + "required": True, + "title": "Términos y condiciones", + "text": "

Característica 1 Texto

Por favor acéptelos.

" + } + ] + } + }, + "CN": { + "defaultLanguage": "zh", + "languageEntries": { + "zh": [ + { + "ordinal": 0, + "required": True, + "title": "条款和条件", + "text": "

产品1文字

" + }, + { + "ordinal": 1, + "required": False, + "title": "隐私条款", + "text": "

产品2文字

" + } + ] + } + }, + "RU": { + "defaultLanguage": "ru", + "languageEntries": { + "ru": [ + { + "ordinal": 0, + "required": True, + "title": "Условия и положения", + "text": "

Текст функции 1

Пожалуйста, примите эти условия пользования.

" + }, + { + "ordinal": 1, + "required": False, + "title": "Положение о конфиденциальности", + "text": "

Текст функции 2

" + } + ] + } + } + } + } + } +} + +MODELS = { + 0XFFF1: { + 0x8001: { + "model": + { + "vid": 65521, + "pid": 32769, + "deviceTypeId": 65535, + "productName": "TEST_PRODUCT", + "productLabel": "All Clusters App", + "partNumber": "", + "commissioningCustomFlow": 2, + "commissioningCustomFlowUrl": "", + "commissioningModeInitialStepsHint": 0, + "commissioningModeInitialStepsInstruction": "", + "commissioningModeSecondaryStepsHint": 0, + "commissioningModeSecondaryStepsInstruction": "", + "creator": "chip project", + "lsfRevision": 0, + "lsfUrl": "", + "productUrl": "https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app", + "supportUrl": "https://github.com/project-chip/connectedhomeip/", + "userManualUrl": "", + "enhancedSetupFlowOptions": 1, + "enhancedSetupFlowTCUrl": f"https://{DEFAULT_HOSTNAME}:{DEFAULT_PORT}/tc/65521/32769", + "enhancedSetupFlowTCRevision": 1, + "enhancedSetupFlowTCDigest": "", + "enhancedSetupFlowTCFileSize": 0, + "enhancedSetupFlowMaintenanceUrl": "" + } + } + } +} + + +class RESTRequestHandler(http.server.BaseHTTPRequestHandler): + def __init__(self, *args, **kwargs): + self.routes = { + r"/dcl/model/models/(\d+)/(\d+)": self.handle_model_request, + r"/tc/(\d+)/(\d+)": self.handle_tc_request, + } + super().__init__(*args, **kwargs) + + def do_GET(self): + for pattern, handler in self.routes.items(): + match = re.match(pattern, self.path) + if match: + response = handler(*match.groups()) + if response: + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(response).encode("utf-8")) + return + + # Handle 404 for unmatched paths + self.send_response(404) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"error": "Not found"}).encode("utf-8")) + + def handle_model_request(self, vendor_id, product_id): + vendor_id = int(vendor_id) + product_id = int(product_id) + if vendor_id in MODELS and product_id in MODELS[vendor_id]: + model = MODELS[int(vendor_id)][int(product_id)] + # We will return a model that contains the file size and the digest of the TC. + # Instead of manually setting them, it is calculated on the fly. + tc = TC[int(vendor_id)][int(product_id)] + tc_encoded = json.dumps(tc).encode("utf-8") + sha256_hash = hashlib.sha256(tc_encoded).digest() + model['model']['enhancedSetupFlowTCFileSize'] = len(tc_encoded) + model['model']['enhancedSetupFlowTCDigest'] = base64.b64encode( + sha256_hash).decode("utf-8") + + return model + + return None + + def handle_tc_request(self, vendor_id, product_id): + vendor_id = int(vendor_id) + product_id = int(product_id) + if vendor_id in TC and product_id in TC[vendor_id]: + return TC[int(vendor_id)][int(product_id)] + + return None + + +def run_https_server(cert_file="cert.pem", key_file="key.pem"): + httpd = http.server.HTTPServer( + (DEFAULT_HOSTNAME, DEFAULT_PORT), RESTRequestHandler) + + httpd.socket = ssl.wrap_socket( + httpd.socket, + server_side=True, + certfile=cert_file, + keyfile=key_file, + ssl_version=ssl.PROTOCOL_TLS, + ) + + print(f"Serving on https://{DEFAULT_HOSTNAME}:{DEFAULT_PORT}") + httpd.serve_forever() + + +# Generate self-signed certificates if needed +def generate_self_signed_cert(cert_file="cert.pem", key_file="key.pem"): + from subprocess import run + run([ + "openssl", "req", "-x509", "-nodes", "-days", "365", "-newkey", "rsa:2048", + "-keyout", key_file, "-out", cert_file, + "-subj", f"/C=US/ST=Test/L=Test/O=Test/OU=Test/CN={DEFAULT_HOSTNAME}" + ]) + + +# Check if certificates exist; if not, generate them +if not os.path.exists("cert.pem") or not os.path.exists("key.pem"): + print("Generating self-signed certificates...") + generate_self_signed_cert() + +# Run the server +run_https_server() diff --git a/examples/chip-tool/commands/pairing/PairingCommand.cpp b/examples/chip-tool/commands/pairing/PairingCommand.cpp index 96ccd6965d395c..7ebf74433ade7e 100644 --- a/examples/chip-tool/commands/pairing/PairingCommand.cpp +++ b/examples/chip-tool/commands/pairing/PairingCommand.cpp @@ -28,6 +28,9 @@ #include #include +#include "../dcl/DCLClient.h" +#include "../dcl/DisplayTermsAndConditions.h" + #include using namespace ::chip; @@ -232,6 +235,7 @@ CHIP_ERROR PairingCommand::PairWithCode(NodeId remoteId) discoveryType = DiscoveryType::kDiscoveryNetworkOnlyWithoutPASEAutoRetry; } + ReturnErrorOnFailure(MaybeDisplayTermsAndConditions(commissioningParams)); return CurrentCommissioner().PairDevice(remoteId, mOnboardingPayload, commissioningParams, discoveryType); } @@ -585,3 +589,26 @@ void PairingCommand::OnDeviceAttestationCompleted(Controller::DeviceCommissioner SetCommandExitStatus(err); } } + +CHIP_ERROR PairingCommand::MaybeDisplayTermsAndConditions(CommissioningParameters & params) +{ + VerifyOrReturnError(mUseDCL.ValueOr(false), CHIP_NO_ERROR); + + Json::Value tc; + auto client = tool::dcl::DCLClient(mDCLHostName, mDCLPort); + ReturnErrorOnFailure(client.TermsAndConditions(mOnboardingPayload, tc)); + if (tc != Json::nullValue) + { + uint16_t version = 0; + uint16_t userResponse = 0; + ReturnErrorOnFailure(tool::dcl::DisplayTermsAndConditions(tc, version, userResponse, mCountryCode)); + + TermsAndConditionsAcknowledgement termsAndConditionsAcknowledgement = { + .acceptedTermsAndConditions = userResponse, + .acceptedTermsAndConditionsVersion = version, + }; + params.SetTermsAndConditionsAcknowledgement(termsAndConditionsAcknowledgement); + } + + return CHIP_NO_ERROR; +} diff --git a/examples/chip-tool/commands/pairing/PairingCommand.h b/examples/chip-tool/commands/pairing/PairingCommand.h index 5572a724e918dd..3b0f58936cfac2 100644 --- a/examples/chip-tool/commands/pairing/PairingCommand.h +++ b/examples/chip-tool/commands/pairing/PairingCommand.h @@ -106,6 +106,10 @@ class PairingCommand : public CHIPCommand, break; case PairingMode::Code: AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete); + AddArgument("dcl-hostname", &mDCLHostName, + "Hostname of the DCL server to fetch information from. Defaults to 'on.dcl.csa-iot.org'."); + AddArgument("dcl-port", 0, UINT16_MAX, &mDCLPort, "Port number for connecting to the DCL server. Defaults to '443'."); + AddArgument("use-dcl", 0, 1, &mUseDCL, "Use DCL to fetch onboarding information"); FALLTHROUGH; case PairingMode::CodePaseOnly: AddArgument("payload", &mOnboardingPayload); @@ -247,6 +251,7 @@ class PairingCommand : public CHIPCommand, CHIP_ERROR PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uint16_t index); CHIP_ERROR Unpair(NodeId remoteId); chip::Controller::CommissioningParameters GetCommissioningParameters(); + CHIP_ERROR MaybeDisplayTermsAndConditions(chip::Controller::CommissioningParameters & params); const PairingMode mPairingMode; const PairingNetworkType mNetworkType; @@ -269,6 +274,9 @@ class PairingCommand : public CHIPCommand, chip::Optional mICDStayActiveDurationMsec; chip::Optional mTCAcknowledgements; chip::Optional mTCAcknowledgementVersion; + chip::Optional mDCLHostName; + chip::Optional mDCLPort; + chip::Optional mUseDCL; chip::app::DataModel::List mTimeZoneList; TypedComplexArgument> mComplex_TimeZones; diff --git a/examples/chip-tool/main.cpp b/examples/chip-tool/main.cpp index 6a52941e8b8d9c..cb296f3f31bdcf 100644 --- a/examples/chip-tool/main.cpp +++ b/examples/chip-tool/main.cpp @@ -20,6 +20,7 @@ #include "commands/example/ExampleCredentialIssuerCommands.h" #include "commands/clusters/SubscriptionsCommands.h" +#include "commands/dcl/Commands.h" #include "commands/delay/Commands.h" #include "commands/discover/Commands.h" #include "commands/group/Commands.h" @@ -39,6 +40,7 @@ int main(int argc, char * argv[]) { ExampleCredentialIssuerCommands credIssuerCommands; Commands commands; + registerCommandsDCL(commands); registerCommandsDelay(commands, &credIssuerCommands); registerCommandsDiscover(commands, &credIssuerCommands); registerCommandsICD(commands, &credIssuerCommands); diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp index 07ac781f3509b3..b4fb0cb13b7027 100644 --- a/examples/platform/linux/AppMain.cpp +++ b/examples/platform/linux/AppMain.cpp @@ -111,6 +111,10 @@ #include "ExampleAccessRestrictionProvider.h" #endif +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED +#include // nogncheck +#endif + #if CHIP_DEVICE_LAYER_TARGET_DARWIN #include #if CHIP_DEVICE_CONFIG_ENABLE_WIFI @@ -542,6 +546,16 @@ void ChipLinuxAppMainLoop(AppMainLoopImplementation * impl) VerifyOrDie(initParams.InitializeStaticResourcesBeforeServerInit() == CHIP_NO_ERROR); initParams.dataModelProvider = app::CodegenDataModelProviderInstance(initParams.persistentStorageDelegate); +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + if (LinuxDeviceOptions::GetInstance().tcVersion.HasValue() && LinuxDeviceOptions::GetInstance().tcRequired.HasValue()) + { + uint16_t version = LinuxDeviceOptions::GetInstance().tcVersion.Value(); + uint16_t required = LinuxDeviceOptions::GetInstance().tcRequired.Value(); + Optional requiredAcknowledgements(app::TermsAndConditions(required, version)); + app::TermsAndConditionsManager::GetInstance()->Init(initParams.persistentStorageDelegate, requiredAcknowledgements); + } +#endif // CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + #if defined(ENABLE_CHIP_SHELL) Engine::Root().Init(); Shell::RegisterCommissioneeCommands(); diff --git a/examples/platform/linux/Options.cpp b/examples/platform/linux/Options.cpp index e3b7ba42675f41..fa56b62feac4c1 100644 --- a/examples/platform/linux/Options.cpp +++ b/examples/platform/linux/Options.cpp @@ -128,6 +128,10 @@ enum kDeviceOption_WiFi_PAF, #endif kDeviceOption_DacProvider, +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + kDeviceOption_TermsAndConditions_Version, + kDeviceOption_TermsAndConditions_Required, +#endif }; constexpr unsigned kAppUsageLength = 64; @@ -204,6 +208,10 @@ OptionDef sDeviceOptionDefs[] = { { "faults", kArgumentRequired, kDeviceOption_FaultInjection }, #endif { "dac_provider", kArgumentRequired, kDeviceOption_DacProvider }, +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + { "tc-version", kArgumentRequired, kDeviceOption_TermsAndConditions_Version }, + { "tc-required", kArgumentRequired, kDeviceOption_TermsAndConditions_Required }, +#endif {} }; @@ -362,6 +370,15 @@ const char * sDeviceOptionHelp = " Specifies the time after which the device transitions from active to idle.\n" "\n" #endif +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + " --tc-version\n" + " Sets the minimum required version of the Terms and Conditions\n" + "\n" + " --tc-required\n" + " Sets the required acknowledgements for the Terms and Conditions as a 16-bit enumeration.\n" + " Each bit represents an ordinal corresponding to a specific acknowledgment requirement.\n" + "\n" +#endif #if CHIP_WITH_NLFAULTINJECTION " --faults \n" " Inject specified fault(s) at runtime.\n" @@ -747,6 +764,17 @@ bool HandleOption(const char * aProgram, OptionSet * aOptions, int aIdentifier, LinuxDeviceOptions::GetInstance().dacProvider = &testDacProvider; break; } +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + case kDeviceOption_TermsAndConditions_Version: { + LinuxDeviceOptions::GetInstance().tcVersion.SetValue(static_cast(atoi(aValue))); + break; + } + + case kDeviceOption_TermsAndConditions_Required: { + LinuxDeviceOptions::GetInstance().tcRequired.SetValue(static_cast(atoi(aValue))); + break; + } +#endif default: PrintArgError("%s: INTERNAL ERROR: Unhandled option: %s\n", aProgram, aName); retval = false; diff --git a/examples/platform/linux/Options.h b/examples/platform/linux/Options.h index d9b1716bd39e95..6cad0469e02270 100644 --- a/examples/platform/linux/Options.h +++ b/examples/platform/linux/Options.h @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -91,6 +92,10 @@ struct LinuxDeviceOptions #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS chip::Optional> commissioningArlEntries; chip::Optional> arlEntries; +#endif +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + chip::Optional tcVersion; + chip::Optional tcRequired; #endif static LinuxDeviceOptions & GetInstance(); }; diff --git a/src/app/clusters/general-commissioning-server/general-commissioning-server.cpp b/src/app/clusters/general-commissioning-server/general-commissioning-server.cpp index 4defc79e911c2f..04f7e4e5df15e4 100644 --- a/src/app/clusters/general-commissioning-server/general-commissioning-server.cpp +++ b/src/app/clusters/general-commissioning-server/general-commissioning-server.cpp @@ -107,6 +107,14 @@ CHIP_ERROR GeneralCommissioningGlobalInstance::Read(const ConcreteReadAttributeP switch (aPath.mAttributeId) { + case FeatureMap::Id: { + BitFlags features; +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + features.Set(GeneralCommissioning::Feature::kTermsAndConditions); +#endif // CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + return aEncoder.Encode(features); + } + case RegulatoryConfig::Id: { return ReadIfSupported(&ConfigurationManager::GetRegulatoryLocation, aEncoder); } diff --git a/third_party/boringssl/repo/BUILD.gn b/third_party/boringssl/repo/BUILD.gn index a57807f52b4762..bb315cde989d60 100644 --- a/third_party/boringssl/repo/BUILD.gn +++ b/third_party/boringssl/repo/BUILD.gn @@ -41,6 +41,7 @@ all_sources = crypto_sources all_headers = crypto_headers +# Core BoringSSL library used by the SDK static_library("boringssl") { cflags = [ "-O2" ] @@ -55,3 +56,18 @@ static_library("boringssl") { # on boringssl, not just boringssl itself. configs += [ ":boringssl_config_disable_warnings" ] } + +# Extended version of BoringSSL with additional SSL sources (for optional tools) +static_library("boringssl_with_ssl_sources") { + cflags = [ "-O2" ] + + public = crypto_headers + ssl_headers + sources = crypto_sources + ssl_sources + + public_configs = [ ":boringssl_config" ] + + # The disable-warnings config should not be a public config, since + # that would make it apply to compilations of anything that depends + # on boringssl, not just boringssl itself. + configs += [ ":boringssl_config_disable_warnings" ] +}