diff --git a/examples/fabric-admin/.gn b/examples/fabric-admin/.gn new file mode 100644 index 00000000000000..3b11e2ba2e62ee --- /dev/null +++ b/examples/fabric-admin/.gn @@ -0,0 +1,25 @@ +# Copyright (c) 2024 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("//build_overrides/build.gni") + +# The location of the build configuration file. +buildconfig = "${build_root}/config/BUILDCONFIG.gn" + +# CHIP uses angle bracket includes. +check_system_includes = true + +default_args = { + import("//args.gni") +} diff --git a/examples/fabric-admin/BUILD.gn b/examples/fabric-admin/BUILD.gn new file mode 100644 index 00000000000000..79e175fb4a70c1 --- /dev/null +++ b/examples/fabric-admin/BUILD.gn @@ -0,0 +1,120 @@ +# Copyright (c) 2024 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("//build_overrides/build.gni") +import("//build_overrides/chip.gni") + +import("//build_overrides/editline.gni") +import("${chip_root}/build/chip/tools.gni") +import("${chip_root}/examples/fabric-admin/fabric-admin.gni") +import("${chip_root}/src/lib/core/core.gni") + +assert(chip_build_tools) + +config("config") { + include_dirs = [ + ".", + "${chip_root}/examples/common", + "${chip_root}/zzz_generated/app-common/app-common", + "${chip_root}/zzz_generated/chip-tool", + "${chip_root}/src/lib", + ] + + defines = [ "CONFIG_USE_SEPARATE_EVENTLOOP=${config_use_separate_eventloop}" ] + + # Note: CONFIG_USE_LOCAL_STORAGE is tested for via #ifdef, not #if. + if (config_use_local_storage) { + defines += [ "CONFIG_USE_LOCAL_STORAGE" ] + } + + cflags = [ "-Wconversion" ] +} + +static_library("fabric-admin-utils") { + sources = [ + "${chip_root}/src/controller/ExamplePersistentStorage.cpp", + "${chip_root}/src/controller/ExamplePersistentStorage.h", + "${chip_root}/zzz_generated/chip-tool/zap-generated/cluster/ComplexArgumentParser.cpp", + "${chip_root}/zzz_generated/chip-tool/zap-generated/cluster/logging/DataModelLogger.cpp", + "commands/clusters/ModelCommand.cpp", + "commands/clusters/ModelCommand.h", + "commands/common/CHIPCommand.cpp", + "commands/common/CHIPCommand.h", + "commands/common/Command.cpp", + "commands/common/Command.h", + "commands/common/Commands.cpp", + "commands/common/Commands.h", + "commands/common/CredentialIssuerCommands.h", + "commands/common/HexConversion.h", + "commands/common/RemoteDataModelLogger.cpp", + "commands/common/RemoteDataModelLogger.h", + "commands/pairing/OpenCommissioningWindowCommand.cpp", + "commands/pairing/OpenCommissioningWindowCommand.h", + "commands/pairing/PairingCommand.cpp", + "commands/pairing/ToTLVCert.cpp", + ] + + deps = [ "${chip_root}/src/app:events" ] + + sources += [ "commands/interactive/InteractiveCommands.cpp" ] + deps += [ + "${chip_root}/examples/common/websocket-server", + "${chip_root}/src/platform/logging:headers", + "${editline_root}:editline", + ] + + if (chip_device_platform == "darwin") { + sources += [ "commands/common/DeviceScanner.cpp" ] + } + + public_deps = [ + "${chip_root}/examples/common/tracing:commandline", + "${chip_root}/src/app/icd/client:handler", + "${chip_root}/src/app/icd/client:manager", + "${chip_root}/src/app/server", + "${chip_root}/src/app/tests/suites/commands/interaction_model", + "${chip_root}/src/controller/data_model", + "${chip_root}/src/credentials:file_attestation_trust_store", + "${chip_root}/src/lib", + "${chip_root}/src/lib/core:types", + "${chip_root}/src/lib/support/jsontlv", + "${chip_root}/src/platform", + "${chip_root}/third_party/inipp", + "${chip_root}/third_party/jsoncpp", + ] + + public_configs = [ ":config" ] + + if (chip_enable_transport_trace) { + public_deps += + [ "${chip_root}/examples/common/tracing:trace_handlers_decoder" ] + } + + output_dir = root_out_dir +} + +executable("fabric-admin") { + sources = [ "main.cpp" ] + + deps = [ + ":fabric-admin-utils", + "${chip_root}/src/platform/logging:force_stdio", + ] + + output_dir = root_out_dir +} + +group("default") { + deps = [ ":fabric-admin" ] +} diff --git a/examples/fabric-admin/args.gni b/examples/fabric-admin/args.gni new file mode 100644 index 00000000000000..83300d797ed08a --- /dev/null +++ b/examples/fabric-admin/args.gni @@ -0,0 +1,34 @@ +# Copyright (c) 2024 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("//build_overrides/chip.gni") + +import("${chip_root}/config/standalone/args.gni") + +chip_device_project_config_include = "" +chip_project_config_include = "" +chip_system_project_config_include = "" + +chip_project_config_include_dirs = + [ "${chip_root}/examples/fabric-admin/include" ] +chip_project_config_include_dirs += [ "${chip_root}/config/standalone" ] + +matter_enable_tracing_support = true + +matter_log_json_payload_hex = true +matter_log_json_payload_decode_full = true + +# make fabric-admin very strict by default +chip_tlv_validate_char_string_on_read = true +chip_tlv_validate_char_string_on_write = true diff --git a/examples/fabric-admin/build_overrides b/examples/fabric-admin/build_overrides new file mode 120000 index 00000000000000..b430cf6a2e6391 --- /dev/null +++ b/examples/fabric-admin/build_overrides @@ -0,0 +1 @@ +../build_overrides \ No newline at end of file diff --git a/examples/fabric-admin/commands/clusters/ClusterCommand.h b/examples/fabric-admin/commands/clusters/ClusterCommand.h new file mode 100644 index 00000000000000..4865f056e135f4 --- /dev/null +++ b/examples/fabric-admin/commands/clusters/ClusterCommand.h @@ -0,0 +1,196 @@ +/* + * 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. + * + */ + +#pragma once + +#include + +#include "DataModelLogger.h" +#include "ModelCommand.h" + +class ClusterCommand : public InteractionModelCommands, public ModelCommand, public chip::app::CommandSender::Callback +{ +public: + ClusterCommand(CredentialIssuerCommands * credsIssuerConfig) : + InteractionModelCommands(this), ModelCommand("command-by-id", credsIssuerConfig) + { + AddArgument("cluster-id", 0, UINT32_MAX, &mClusterId); + AddByIdArguments(); + AddArguments(); + } + + ClusterCommand(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) : + InteractionModelCommands(this), ModelCommand("command-by-id", credsIssuerConfig), mClusterId(clusterId) + { + AddByIdArguments(); + AddArguments(); + } + + ~ClusterCommand() {} + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + return InteractionModelCommands::SendCommand(device, endpointIds.at(0), mClusterId, mCommandId, mPayload); + } + + template + CHIP_ERROR SendCommand(chip::DeviceProxy * device, chip::EndpointId endpointId, chip::ClusterId clusterId, + chip::CommandId commandId, const T & value) + { + return InteractionModelCommands::SendCommand(device, endpointId, clusterId, commandId, value); + } + + CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) override + { + return InteractionModelCommands::SendGroupCommand(groupId, fabricIndex, mClusterId, mCommandId, mPayload); + } + + template + CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex, chip::ClusterId clusterId, + chip::CommandId commandId, const T & value) + { + return InteractionModelCommands::SendGroupCommand(groupId, fabricIndex, clusterId, commandId, value); + } + + /////////// CommandSender Callback Interface ///////// + virtual void OnResponse(chip::app::CommandSender * client, const chip::app::ConcreteCommandPath & path, + const chip::app::StatusIB & status, chip::TLV::TLVReader * data) override + { + CHIP_ERROR error = status.ToChipError(); + if (CHIP_NO_ERROR != error) + { + LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status)); + + ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error)); + mError = error; + return; + } + + if (data != nullptr) + { + LogErrorOnFailure(RemoteDataModelLogger::LogCommandAsJSON(path, data)); + + error = DataModelLogger::LogCommand(path, data); + if (CHIP_NO_ERROR != error) + { + ChipLogError(NotSpecified, "Response Failure: Can not decode Data"); + mError = error; + return; + } + } + } + + virtual void OnError(const chip::app::CommandSender * client, CHIP_ERROR error) override + { + LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error)); + + ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error)); + mError = error; + } + + virtual void OnDone(chip::app::CommandSender * client) override + { + if (mCommandSender.size()) + { + mCommandSender.front().reset(); + mCommandSender.erase(mCommandSender.begin()); + } + + // If the command is repeated N times, wait for all the responses to comes in + // before exiting. + bool shouldStop = true; + if (mRepeatCount.HasValue()) + { + mRepeatCount.SetValue(static_cast(mRepeatCount.Value() - 1)); + shouldStop = mRepeatCount.Value() == 0; + } + + if (shouldStop) + { + SetCommandExitStatus(mError); + } + } + + void Shutdown() override + { + mError = CHIP_NO_ERROR; + ModelCommand::Shutdown(); + } + +protected: + ClusterCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) : + InteractionModelCommands(this), ModelCommand(commandName, credsIssuerConfig) + { + // Subclasses are responsible for calling AddArguments. + } + + void AddByIdArguments() + { + AddArgument("command-id", 0, UINT32_MAX, &mCommandId); + AddArgument("payload", &mPayload, + "The command payload. This should be a JSON-encoded object, with string representations of field ids as keys. " + " The values for the keys are represented as follows, depending on the type:\n" + " * struct: a JSON-encoded object, with field ids as keys.\n" + " * list: a JSON-encoded array of values.\n" + " * null: A literal null.\n" + " * boolean: A literal true or false.\n" + " * unsigned integer: One of:\n" + " a) The number directly, as decimal.\n" + " b) A string starting with \"u:\" followed by decimal digits\n" + " * signed integer: One of:\n" + " a) The number directly, if it's negative.\n" + " b) A string starting with \"s:\" followed by decimal digits\n" + " * single-precision float: A string starting with \"f:\" followed by the number.\n" + " * double-precision float: One of:\n" + " a) The number directly, if it's not an integer.\n" + " b) A string starting with \"d:\" followed by the number.\n" + " * octet string: A string starting with \"hex:\" followed by the hex encoding of the bytes.\n" + " * string: A string with the characters.\n" + "\n" + " An example payload may look like this: '{ \"0x0\": { \"0\": null, \"1\": false }, \"1\": [17, \"u:17\"], " + "\"0x2\": [ -17, \"s:17\", \"s:-17\" ], \"0x3\": \"f:2\", \"0x4\": [ \"d:3\", 4.5 ], \"0x5\": \"hex:ab12\", " + "\"0x6\": \"ab12\" }' and represents:\n" + " Field 0: a struct with two fields, one with value null and one with value false.\n" + " Field 1: A list of unsigned integers.\n" + " Field 2: A list of signed integers.\n" + " Field 3: A single-precision float.\n" + " Field 4: A list of double-precision floats.\n" + " Field 5: A 2-byte octet string.\n" + " Field 6: A 4-char character string."); + } + + void AddArguments() + { + AddArgument("timedInteractionTimeoutMs", 0, UINT16_MAX, &mTimedInteractionTimeoutMs, + "If provided, do a timed invoke with the given timed interaction timeout. See \"7.6.10. Timed Interaction\" in " + "the Matter specification."); + AddArgument("busyWaitForMs", 0, UINT16_MAX, &mBusyWaitForMs, + "If provided, block the main thread processing for the given time right after sending a command."); + AddArgument("suppressResponse", 0, 1, &mSuppressResponse); + AddArgument("repeat-count", 1, UINT16_MAX, &mRepeatCount); + AddArgument("repeat-delay-ms", 0, UINT16_MAX, &mRepeatDelayInMs); + ModelCommand::AddArguments(); + } + +private: + chip::ClusterId mClusterId; + chip::CommandId mCommandId; + + CHIP_ERROR mError = CHIP_NO_ERROR; + CustomArgument mPayload; +}; diff --git a/examples/fabric-admin/commands/clusters/ComplexArgument.h b/examples/fabric-admin/commands/clusters/ComplexArgument.h new file mode 100644 index 00000000000000..954ea1db352b97 --- /dev/null +++ b/examples/fabric-admin/commands/clusters/ComplexArgument.h @@ -0,0 +1,422 @@ +/* + * 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. + * + */ + +/** + * This file allocate/free memory using the chip platform abstractions + * (Platform::MemoryCalloc and Platform::MemoryFree) for hosting a subset of the + * data model internal types until they are consumed by the DataModel::Encode machinery: + * - chip::app:DataModel::List + * - chip::ByteSpan + * - chip::CharSpan + * + * Memory allocation happens during the 'Setup' phase, while memory deallocation happens + * during the 'Finalize' phase. + * + * The 'Finalize' phase during the destructor phase, and if needed, 'Finalize' will call + * the 'Finalize' phase of its descendant. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "JsonParser.h" + +inline constexpr uint8_t kMaxLabelLength = UINT8_MAX; +inline constexpr char kNullString[] = "null"; + +class ComplexArgumentParser +{ +public: + ComplexArgumentParser() {} + + template ::value && !std::is_signed::value && + !std::is_same>, bool>::value, + int> = 0> + static CHIP_ERROR Setup(const char * label, T & request, Json::Value value) + { + if (value.isNumeric()) + { + if (chip::CanCastTo(value.asLargestUInt())) + { + request = static_cast(value.asLargestUInt()); + return CHIP_NO_ERROR; + } + } + else if (value.isString()) + { + // Check for a hex number; JSON does not support those as numbers, + // so they have to be done as strings. And we might as well support + // string-encoded unsigned numbers in general if we're doing that. + bool isHexNotation = strncmp(value.asCString(), "0x", 2) == 0 || strncmp(value.asCString(), "0X", 2) == 0; + + std::stringstream str; + isHexNotation ? str << std::hex << value.asCString() : str << value.asCString(); + uint64_t val; + str >> val; + if (!str.fail() && str.eof() && chip::CanCastTo(val)) + { + request = static_cast(val); + return CHIP_NO_ERROR; + } + } + + ChipLogError(NotSpecified, "Error while encoding %s as an unsigned integer.", label); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + template ::value, bool> = true> + static CHIP_ERROR Setup(const char * label, T & request, Json::Value value) + { + if (!value.isNumeric() || !chip::CanCastTo(value.asLargestInt())) + { + ChipLogError(NotSpecified, "Error while encoding %s as an unsigned integer.", label); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + request = static_cast(value.asLargestInt()); + return CHIP_NO_ERROR; + } + + template ::value, int> = 0> + static CHIP_ERROR Setup(const char * label, T & request, Json::Value value) + { + std::underlying_type_t requestValue; + ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value)); + + request = static_cast(requestValue); + return CHIP_NO_ERROR; + } + + template + static CHIP_ERROR Setup(const char * label, chip::BitFlags & request, Json::Value & value) + { + T requestValue; + ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value)); + + request = chip::BitFlags(requestValue); + return CHIP_NO_ERROR; + } + + template + static CHIP_ERROR Setup(const char * label, chip::BitMask & request, Json::Value & value) + { + T requestValue; + ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value)); + + request = chip::BitMask(requestValue); + return CHIP_NO_ERROR; + } + + template + static CHIP_ERROR Setup(const char * label, chip::Optional & request, Json::Value & value) + { + T requestValue; + ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value)); + + request = chip::Optional(requestValue); + return CHIP_NO_ERROR; + } + + template + static CHIP_ERROR Setup(const char * label, chip::app::DataModel::Nullable & request, Json::Value & value) + { + if (value.isNull()) + { + request.SetNull(); + return CHIP_NO_ERROR; + } + + T requestValue; + ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value)); + + request = chip::app::DataModel::Nullable(requestValue); + return CHIP_NO_ERROR; + } + + template + static CHIP_ERROR Setup(const char * label, chip::app::DataModel::List & request, Json::Value & value) + { + if (!value.isArray()) + { + ChipLogError(NotSpecified, "Error while encoding %s as an array.", label); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + auto content = static_cast::type *>(chip::Platform::MemoryCalloc(value.size(), sizeof(T))); + VerifyOrReturnError(content != nullptr, CHIP_ERROR_NO_MEMORY); + + Json::ArrayIndex size = value.size(); + for (Json::ArrayIndex i = 0; i < size; i++) + { + char labelWithIndex[kMaxLabelLength]; + // GCC 7.0.1 has introduced some new warnings for snprintf (-Werror=format-truncation) by default. + // This is not particularly useful when using snprintf and especially in this context, so in order + // to disable the warning the %s is constrained to be of max length: (254 - 11 - 2) where: + // - 254 is kMaxLabelLength - 1 (for null) + // - 11 is the maximum length of a %d (-2147483648, 2147483647) + // - 2 is the length for the "[" and "]" characters. + snprintf(labelWithIndex, sizeof(labelWithIndex), "%.241s[%d]", label, i); + ReturnErrorOnFailure(ComplexArgumentParser::Setup(labelWithIndex, content[i], value[i])); + } + + request = chip::app::DataModel::List(content, value.size()); + return CHIP_NO_ERROR; + } + + static CHIP_ERROR Setup(const char * label, chip::ByteSpan & request, Json::Value & value) + { + if (!value.isString()) + { + ChipLogError(NotSpecified, "Error while encoding %s as an octet string: Not a string.", label); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + auto str = value.asString(); + auto size = str.size(); + uint8_t * buffer = nullptr; + + if (IsStrString(str.c_str())) + { + // Skip the prefix + str.erase(0, kStrStringPrefixLen); + size = str.size(); + + buffer = static_cast(chip::Platform::MemoryCalloc(size, sizeof(uint8_t))); + VerifyOrReturnError(buffer != nullptr, CHIP_ERROR_NO_MEMORY); + + memcpy(buffer, str.c_str(), size); + } + else + { + if (IsHexString(str.c_str())) + { + // Skip the prefix + str.erase(0, kHexStringPrefixLen); + size = str.size(); + } + + CHIP_ERROR err = HexToBytes( + chip::CharSpan(str.c_str(), size), + [&buffer](size_t allocSize) { + buffer = static_cast(chip::Platform::MemoryCalloc(allocSize, sizeof(uint8_t))); + return buffer; + }, + &size); + + if (err != CHIP_NO_ERROR) + { + if (buffer != nullptr) + { + chip::Platform::MemoryFree(buffer); + } + + return err; + } + } + + request = chip::ByteSpan(buffer, size); + return CHIP_NO_ERROR; + } + + static CHIP_ERROR Setup(const char * label, chip::CharSpan & request, Json::Value & value) + { + if (!value.isString()) + { + ChipLogError(NotSpecified, "Error while encoding %s as a string: Not a string.", label); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + size_t size = strlen(value.asCString()); + auto buffer = static_cast(chip::Platform::MemoryCalloc(size, sizeof(char))); + VerifyOrReturnError(buffer != nullptr, CHIP_ERROR_NO_MEMORY); + + memcpy(buffer, value.asCString(), size); + + request = chip::CharSpan(buffer, size); + return CHIP_NO_ERROR; + } + + static CHIP_ERROR Setup(const char * label, float & request, Json::Value & value) + { + if (!value.isNumeric()) + { + ChipLogError(NotSpecified, "Error while encoding %s as a float: Not a number.", label); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + request = static_cast(value.asFloat()); + return CHIP_NO_ERROR; + } + + static CHIP_ERROR Setup(const char * label, double & request, Json::Value & value) + { + if (!value.isNumeric()) + { + ChipLogError(NotSpecified, "Error while encoding %s as a double: Not a number.", label); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + request = static_cast(value.asDouble()); + return CHIP_NO_ERROR; + } + + static CHIP_ERROR Setup(const char * label, bool & request, Json::Value & value) + { + if (!value.isBool()) + { + ChipLogError(NotSpecified, "Error while encoding %s as a boolean: Not a boolean.", label); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + request = value.asBool(); + return CHIP_NO_ERROR; + } + + static CHIP_ERROR EnsureMemberExist(const char * label, const char * memberName, bool hasMember) + { + if (hasMember) + { + return CHIP_NO_ERROR; + } + + ChipLogError(NotSpecified, "%s is required. Should be provided as {\"%s\": value}", label, memberName); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + static CHIP_ERROR EnsureNoMembersRemaining(const char * label, const Json::Value & value) + { + auto remainingFields = value.getMemberNames(); + if (remainingFields.size() == 0) + { + return CHIP_NO_ERROR; + } +#if CHIP_ERROR_LOGGING + for (auto & field : remainingFields) + { + ChipLogError(NotSpecified, "Unexpected field name: '%s.%s'", label, field.c_str()); + } +#endif // CHIP_ERROR_LOGGING + return CHIP_ERROR_INVALID_ARGUMENT; + } + + template + static void Finalize(T & request) + { + // Nothing to do + } + + template + static void Finalize(chip::Optional & request) + { + VerifyOrReturn(request.HasValue()); + ComplexArgumentParser::Finalize(request.Value()); + } + + template + static void Finalize(chip::app::DataModel::Nullable & request) + { + VerifyOrReturn(!request.IsNull()); + ComplexArgumentParser::Finalize(request.Value()); + } + + static void Finalize(chip::ByteSpan & request) + { + VerifyOrReturn(request.data() != nullptr); + chip::Platform::MemoryFree(reinterpret_cast(const_cast(request.data()))); + } + + static void Finalize(chip::CharSpan & request) + { + VerifyOrReturn(request.data() != nullptr); + chip::Platform::MemoryFree(reinterpret_cast(const_cast(request.data()))); + } + + template + static void Finalize(chip::app::DataModel::List & request) + { + VerifyOrReturn(request.data() != nullptr); + + size_t size = request.size(); + auto data = const_cast::type *>(request.data()); + for (size_t i = 0; i < size; i++) + { + Finalize(data[i]); + } + + chip::Platform::MemoryFree(reinterpret_cast(data)); + } + +#include +}; + +class ComplexArgument +{ +public: + virtual ~ComplexArgument() {} + + virtual CHIP_ERROR Parse(const char * label, const char * json) = 0; + + virtual void Reset() = 0; +}; + +template +class TypedComplexArgument : public ComplexArgument +{ +public: + TypedComplexArgument() {} + TypedComplexArgument(T * request) : mRequest(request) {} + ~TypedComplexArgument() + { + if (mRequest != nullptr) + { + ComplexArgumentParser::Finalize(*mRequest); + } + } + + void SetArgument(T * request) { mRequest = request; }; + + CHIP_ERROR Parse(const char * label, const char * json) + { + Json::Value value; + if (strcmp(kNullString, json) == 0) + { + value = Json::nullValue; + } + else if (!JsonParser::ParseComplexArgument(label, json, value)) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + return ComplexArgumentParser::Setup(label, *mRequest, value); + } + + void Reset() { *mRequest = T(); } + +private: + T * mRequest; +}; diff --git a/examples/fabric-admin/commands/clusters/CustomArgument.h b/examples/fabric-admin/commands/clusters/CustomArgument.h new file mode 100644 index 00000000000000..3769c0052236cd --- /dev/null +++ b/examples/fabric-admin/commands/clusters/CustomArgument.h @@ -0,0 +1,297 @@ +/* + * 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. + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "JsonParser.h" + +namespace { +static constexpr char kPayloadHexPrefix[] = "hex:"; +static constexpr char kPayloadSignedPrefix[] = "s:"; +static constexpr char kPayloadUnsignedPrefix[] = "u:"; +static constexpr char kPayloadFloatPrefix[] = "f:"; +static constexpr char kPayloadDoublePrefix[] = "d:"; +static constexpr size_t kPayloadHexPrefixLen = ArraySize(kPayloadHexPrefix) - 1; // ignore null character +static constexpr size_t kPayloadSignedPrefixLen = ArraySize(kPayloadSignedPrefix) - 1; // ignore null character +static constexpr size_t kPayloadUnsignedPrefixLen = ArraySize(kPayloadUnsignedPrefix) - 1; // ignore null character +static constexpr size_t kPayloadFloatPrefixLen = ArraySize(kPayloadFloatPrefix) - 1; // ignore null character +static constexpr size_t kPayloadDoublePrefixLen = ArraySize(kPayloadDoublePrefix) - 1; // ignore null character +} // namespace + +class CustomArgumentParser +{ +public: + static CHIP_ERROR Put(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + if (value.isObject()) + { + return CustomArgumentParser::PutObject(writer, tag, value); + } + + if (value.isArray()) + { + return CustomArgumentParser::PutArray(writer, tag, value); + } + + if (value.isString()) + { + if (IsOctetString(value)) + { + return CustomArgumentParser::PutOctetString(writer, tag, value); + } + if (IsUnsignedNumberPrefix(value)) + { + return CustomArgumentParser::PutUnsignedFromString(writer, tag, value); + } + if (IsSignedNumberPrefix(value)) + { + return CustomArgumentParser::PutSignedFromString(writer, tag, value); + } + if (IsFloatNumberPrefix(value)) + { + return CustomArgumentParser::PutFloatFromString(writer, tag, value); + } + if (IsDoubleNumberPrefix(value)) + { + return CustomArgumentParser::PutDoubleFromString(writer, tag, value); + } + + return CustomArgumentParser::PutCharString(writer, tag, value); + } + + if (value.isNull()) + { + return chip::app::DataModel::Encode(*writer, tag, chip::app::DataModel::Nullable()); + } + + if (value.isBool()) + { + return chip::app::DataModel::Encode(*writer, tag, value.asBool()); + } + + if (value.isUInt()) + { + return chip::app::DataModel::Encode(*writer, tag, value.asLargestUInt()); + } + + if (value.isInt()) + { + return chip::app::DataModel::Encode(*writer, tag, value.asLargestInt()); + } + + if (value.isNumeric()) + { + return chip::app::DataModel::Encode(*writer, tag, value.asDouble()); + } + + return CHIP_ERROR_NOT_IMPLEMENTED; + } + +private: + static CHIP_ERROR PutArray(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + chip::TLV::TLVType outer; + ReturnErrorOnFailure(writer->StartContainer(tag, chip::TLV::kTLVType_Array, outer)); + + Json::ArrayIndex size = value.size(); + + for (Json::ArrayIndex i = 0; i < size; i++) + { + ReturnErrorOnFailure(CustomArgumentParser::Put(writer, chip::TLV::AnonymousTag(), value[i])); + } + + return writer->EndContainer(outer); + } + + static CHIP_ERROR PutObject(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + chip::TLV::TLVType outer; + ReturnErrorOnFailure(writer->StartContainer(tag, chip::TLV::kTLVType_Structure, outer)); + + for (auto const & id : value.getMemberNames()) + { + auto index = std::stoul(id, nullptr, 0); + VerifyOrReturnError(chip::CanCastTo(index), CHIP_ERROR_INVALID_ARGUMENT); + ReturnErrorOnFailure(CustomArgumentParser::Put(writer, chip::TLV::ContextTag(static_cast(index)), value[id])); + } + + return writer->EndContainer(outer); + } + + static CHIP_ERROR PutOctetString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + const char * hexData = value.asCString() + kPayloadHexPrefixLen; + size_t hexDataLen = strlen(hexData); + chip::Platform::ScopedMemoryBuffer buffer; + + size_t octetCount; + ReturnErrorOnFailure(HexToBytes( + chip::CharSpan(hexData, hexDataLen), + [&buffer](size_t allocSize) { + buffer.Calloc(allocSize); + return buffer.Get(); + }, + &octetCount)); + + return chip::app::DataModel::Encode(*writer, tag, chip::ByteSpan(buffer.Get(), octetCount)); + } + + static CHIP_ERROR PutCharString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + size_t size = strlen(value.asCString()); + return chip::app::DataModel::Encode(*writer, tag, chip::CharSpan(value.asCString(), size)); + } + + static CHIP_ERROR PutUnsignedFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + char numberAsString[21]; + chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadUnsignedPrefixLen); + + auto number = std::stoull(numberAsString, nullptr, 0); + return chip::app::DataModel::Encode(*writer, tag, static_cast(number)); + } + + static CHIP_ERROR PutSignedFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + char numberAsString[21]; + chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadSignedPrefixLen); + + auto number = std::stoll(numberAsString, nullptr, 0); + return chip::app::DataModel::Encode(*writer, tag, static_cast(number)); + } + + static CHIP_ERROR PutFloatFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + char numberAsString[21]; + chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadFloatPrefixLen); + + auto number = std::stof(numberAsString); + return chip::app::DataModel::Encode(*writer, tag, number); + } + + static CHIP_ERROR PutDoubleFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value) + { + char numberAsString[21]; + chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadDoublePrefixLen); + + auto number = std::stod(numberAsString); + return chip::app::DataModel::Encode(*writer, tag, number); + } + + static bool IsOctetString(Json::Value & value) + { + return (strncmp(value.asCString(), kPayloadHexPrefix, kPayloadHexPrefixLen) == 0); + } + + static bool IsUnsignedNumberPrefix(Json::Value & value) + { + return (strncmp(value.asCString(), kPayloadUnsignedPrefix, kPayloadUnsignedPrefixLen) == 0); + } + + static bool IsSignedNumberPrefix(Json::Value & value) + { + return (strncmp(value.asCString(), kPayloadSignedPrefix, kPayloadSignedPrefixLen) == 0); + } + + static bool IsFloatNumberPrefix(Json::Value & value) + { + return (strncmp(value.asCString(), kPayloadFloatPrefix, kPayloadFloatPrefixLen) == 0); + } + + static bool IsDoubleNumberPrefix(Json::Value & value) + { + return (strncmp(value.asCString(), kPayloadDoublePrefix, kPayloadDoublePrefixLen) == 0); + } +}; + +class CustomArgument +{ +public: + ~CustomArgument() + { + if (mData != nullptr) + { + chip::Platform::MemoryFree(mData); + } + } + + CHIP_ERROR Parse(const char * label, const char * json) + { + Json::Value value; + static constexpr char kHexNumPrefix[] = "0x"; + constexpr size_t kHexNumPrefixLen = ArraySize(kHexNumPrefix) - 1; + if (strncmp(json, kPayloadHexPrefix, kPayloadHexPrefixLen) == 0 || + strncmp(json, kPayloadSignedPrefix, kPayloadSignedPrefixLen) == 0 || + strncmp(json, kPayloadUnsignedPrefix, kPayloadUnsignedPrefixLen) == 0 || + strncmp(json, kPayloadFloatPrefix, kPayloadFloatPrefixLen) == 0 || + strncmp(json, kPayloadDoublePrefix, kPayloadDoublePrefixLen) == 0) + { + value = Json::Value(json); + } + else if (strncmp(json, kHexNumPrefix, kHexNumPrefixLen) == 0) + { + // Assume that hex numbers are unsigned. Prepend + // kPayloadUnsignedPrefix and then let the rest of the logic handle + // things. + std::string str(kPayloadUnsignedPrefix); + str += json; + value = Json::Value(str); + } + else if (!JsonParser::ParseCustomArgument(label, json, value)) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + mData = static_cast(chip::Platform::MemoryCalloc(sizeof(uint8_t), mDataMaxLen)); + VerifyOrReturnError(mData != nullptr, CHIP_ERROR_NO_MEMORY); + + chip::TLV::TLVWriter writer; + writer.Init(mData, mDataMaxLen); + + ReturnErrorOnFailure(CustomArgumentParser::Put(&writer, chip::TLV::AnonymousTag(), value)); + + mDataLen = writer.GetLengthWritten(); + return writer.Finalize(); + } + + CHIP_ERROR Encode(chip::TLV::TLVWriter & writer, chip::TLV::Tag tag) const + { + chip::TLV::TLVReader reader; + reader.Init(mData, mDataLen); + ReturnErrorOnFailure(reader.Next()); + + return writer.CopyElement(tag, reader); + } + + // We trust our consumers to do the encoding of our data correctly, so don't + // need to know whether we are being encoded for a write. + static constexpr bool kIsFabricScoped = false; + +private: + uint8_t * mData = nullptr; + uint32_t mDataLen = 0; + static constexpr uint32_t mDataMaxLen = 4096; +}; diff --git a/examples/fabric-admin/commands/clusters/DataModelLogger.h b/examples/fabric-admin/commands/clusters/DataModelLogger.h new file mode 100644 index 00000000000000..ee649755014906 --- /dev/null +++ b/examples/fabric-admin/commands/clusters/DataModelLogger.h @@ -0,0 +1,190 @@ +/* + * 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. + * + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class DataModelLogger +{ +public: + static CHIP_ERROR LogAttribute(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data); + static CHIP_ERROR LogCommand(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data); + static CHIP_ERROR LogEvent(const chip::app::EventHeader & header, chip::TLV::TLVReader * data); + +private: + static CHIP_ERROR LogValue(const char * label, size_t indent, bool value) + { + DataModelLogger::LogString(label, indent, value ? "TRUE" : "FALSE"); + return CHIP_NO_ERROR; + } + + static CHIP_ERROR LogValue(const char * label, size_t indent, chip::CharSpan value) + { + DataModelLogger::LogString(label, indent, std::string(value.data(), value.size())); + return CHIP_NO_ERROR; + } + + static CHIP_ERROR LogValue(const char * label, size_t indent, chip::ByteSpan value) + { + // CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE includes various prefixes we don't + // control (timestamps, process ids, etc). Let's assume (hope?) that + // those prefixes use up no more than half the total available space. + // Right now it looks like the prefixes are 45 chars out of a 255 char + // buffer. + char buffer[CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE / 2]; + size_t prefixSize = ComputePrefixSize(label, indent); + if (prefixSize > ArraySize(buffer)) + { + DataModelLogger::LogString("", 0, "Prefix is too long to fit in buffer"); + return CHIP_ERROR_INTERNAL; + } + + const size_t availableSize = ArraySize(buffer) - prefixSize; + // Each byte ends up as two hex characters. + const size_t bytesPerLogCall = availableSize / 2; + std::string labelStr(label); + while (value.size() > bytesPerLogCall) + { + ReturnErrorOnFailure( + chip::Encoding::BytesToUppercaseHexString(value.data(), bytesPerLogCall, &buffer[0], ArraySize(buffer))); + LogString(labelStr, indent, buffer); + value = value.SubSpan(bytesPerLogCall); + // For the second and following lines, make it clear that they are + // continuation lines by replacing the label with "....". + labelStr.replace(labelStr.begin(), labelStr.end(), labelStr.size(), '.'); + } + ReturnErrorOnFailure(chip::Encoding::BytesToUppercaseHexString(value.data(), value.size(), &buffer[0], ArraySize(buffer))); + LogString(labelStr, indent, buffer); + + return CHIP_NO_ERROR; + } + + template ::value && !std::is_same>, bool>::value, int> = 0> + static CHIP_ERROR LogValue(const char * label, size_t indent, X value) + { + DataModelLogger::LogString(label, indent, std::to_string(value)); + return CHIP_NO_ERROR; + } + + template ::value, int> = 0> + static CHIP_ERROR LogValue(const char * label, size_t indent, X value) + { + DataModelLogger::LogString(label, indent, std::to_string(value)); + return CHIP_NO_ERROR; + } + + template ::value, int> = 0> + static CHIP_ERROR LogValue(const char * label, size_t indent, X value) + { + return DataModelLogger::LogValue(label, indent, chip::to_underlying(value)); + } + + template + static CHIP_ERROR LogValue(const char * label, size_t indent, chip::BitFlags value) + { + return DataModelLogger::LogValue(label, indent, value.Raw()); + } + + template + static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::app::DataModel::DecodableList & value) + { + size_t count = 0; + ReturnErrorOnFailure(value.ComputeSize(&count)); + DataModelLogger::LogString(label, indent, std::to_string(count) + " entries"); + + auto iter = value.begin(); + size_t i = 0; + while (iter.Next()) + { + ++i; + std::string itemLabel = std::string("[") + std::to_string(i) + "]"; + ReturnErrorOnFailure(DataModelLogger::LogValue(itemLabel.c_str(), indent + 1, iter.GetValue())); + } + if (iter.GetStatus() != CHIP_NO_ERROR) + { + DataModelLogger::LogString(indent + 1, "List truncated due to invalid value"); + } + return iter.GetStatus(); + } + + template + static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::app::DataModel::Nullable & value) + { + if (value.IsNull()) + { + DataModelLogger::LogString(label, indent, "null"); + return CHIP_NO_ERROR; + } + + return DataModelLogger::LogValue(label, indent, value.Value()); + } + + template + static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::Optional & value) + { + if (value.HasValue()) + { + return DataModelLogger::LogValue(label, indent, value.Value()); + } + + return CHIP_NO_ERROR; + } + +#include + + static void LogString(size_t indent, const std::string string) { LogString("", indent, string); } + + static void LogString(const std::string label, size_t indent, const std::string string) + { + std::string prefix = ComputePrefix(label, indent); + + ChipLogProgress(NotSpecified, "%s%s", prefix.c_str(), string.c_str()); + } + +private: + static std::string ComputePrefix(const std::string label, size_t indent) + { + std::string prefix; + for (size_t i = 0; i < indent; ++i) + { + prefix.append(" "); + } + if (label.size() > 0) + { + prefix.append(label); + prefix.append(":"); + } + prefix.append(" "); + + return prefix; + } + + static size_t ComputePrefixSize(const std::string label, size_t indent) { return ComputePrefix(label, indent).size(); } +}; diff --git a/examples/fabric-admin/commands/clusters/JsonParser.h b/examples/fabric-admin/commands/clusters/JsonParser.h new file mode 100644 index 00000000000000..0871e767c21bd6 --- /dev/null +++ b/examples/fabric-admin/commands/clusters/JsonParser.h @@ -0,0 +1,166 @@ +/* + * 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. + * + */ + +#pragma once + +#include "../common/CustomStringPrefix.h" + +#include +#include + +#include +#include +#include +#include + +class JsonParser +{ +public: + // Returns whether the parse succeeded. + static bool ParseComplexArgument(const char * label, const char * json, Json::Value & value) + { + return Parse(label, json, /* strictRoot = */ true, value); + } + + // Returns whether the parse succeeded. + static bool ParseCustomArgument(const char * label, const char * json, Json::Value & value) + { + return Parse(label, json, /* strictRoot = */ false, value); + } + +private: + static bool Parse(const char * label, const char * json, bool strictRoot, Json::Value & value) + { + Json::CharReaderBuilder readerBuilder; + readerBuilder.settings_["strictRoot"] = strictRoot; + readerBuilder.settings_["allowSingleQuotes"] = true; + readerBuilder.settings_["failIfExtra"] = true; + readerBuilder.settings_["rejectDupKeys"] = true; + + auto reader = std::unique_ptr(readerBuilder.newCharReader()); + std::string errors; + if (reader->parse(json, json + strlen(json), &value, &errors)) + { + return true; + } + + // The CharReader API allows us to set failIfExtra, unlike Reader, but does + // not allow us to get structured errors. We get to try to manually undo + // the work it did to create a string from the structured errors it had. + ChipLogError(NotSpecified, "Error parsing JSON for %s:", label); + + // For each error "errors" has the following: + // + // 1) A line starting with "* " that has line/column info + // 2) A line with the error message. + // 3) An optional line with some extra info. + // + // We keep track of the last error column, in case the error message + // reporting needs it. + std::istringstream stream(errors); + std::string error; + chip::Optional errorColumn; + while (getline(stream, error)) + { + if (error.rfind("* ", 0) == 0) + { + // Flush out any pending error location. + LogErrorLocation(errorColumn, json); + + // The format of this line is: + // + // * Line N, Column M + // + // Unfortunately it does not indicate end of error, so we can only + // show its start. + unsigned errorLine; // ignored in practice + if (sscanf(error.c_str(), "* Line %u, Column %u", &errorLine, &errorColumn.Emplace()) != 2) + { + ChipLogError(NotSpecified, "Unexpected location string: %s\n", error.c_str()); + // We don't know how to make sense of this thing anymore. + break; + } + if (errorColumn.Value() == 0) + { + ChipLogError(NotSpecified, "Expected error column to be at least 1"); + // We don't know how to make sense of this thing anymore. + break; + } + // We are using our column numbers as offsets, so want them to be + // 0-based. + --errorColumn.Value(); + } + else + { + ChipLogError(NotSpecified, " %s", error.c_str()); + if (error == " Missing ',' or '}' in object declaration" && errorColumn.HasValue() && errorColumn.Value() > 0 && + json[errorColumn.Value() - 1] == '0' && (json[errorColumn.Value()] == 'x' || json[errorColumn.Value()] == 'X')) + { + // Log the error location marker before showing the NOTE + // message. + LogErrorLocation(errorColumn, json); + ChipLogError(NotSpecified, + "NOTE: JSON does not allow hex syntax beginning with 0x for numbers. Try putting the hex number " + "in quotes (like {\"name\": \"0x100\"})."); + } + } + } + + // Write out the marker for our last error. + LogErrorLocation(errorColumn, json); + + return false; + } + +private: + static void LogErrorLocation(chip::Optional & errorColumn, const char * json) + { +#if CHIP_ERROR_LOGGING + if (!errorColumn.HasValue()) + { + return; + } + + const char * sourceText = json; + unsigned error_start = errorColumn.Value(); + // The whole JSON string might be too long to fit in our log + // messages. Just include 30 chars before the error. + constexpr ptrdiff_t kMaxContext = 30; + std::string errorMarker; + if (error_start > kMaxContext) + { + sourceText += (error_start - kMaxContext); + error_start = kMaxContext; + ChipLogError(NotSpecified, "... %s", sourceText); + // Add markers corresponding to the "... " above. + errorMarker += "----"; + } + else + { + ChipLogError(NotSpecified, "%s", sourceText); + } + for (unsigned i = 0; i < error_start; ++i) + { + errorMarker += "-"; + } + errorMarker += "^"; + ChipLogError(NotSpecified, "%s", errorMarker.c_str()); + errorColumn.ClearValue(); +#endif // CHIP_ERROR_LOGGING + } +}; diff --git a/examples/fabric-admin/commands/clusters/ModelCommand.cpp b/examples/fabric-admin/commands/clusters/ModelCommand.cpp new file mode 100644 index 00000000000000..8f379dbcc4ee06 --- /dev/null +++ b/examples/fabric-admin/commands/clusters/ModelCommand.cpp @@ -0,0 +1,105 @@ +/* + * 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 "ModelCommand.h" + +#include +#include +#include + +using namespace ::chip; + +CHIP_ERROR ModelCommand::RunCommand() +{ + + if (IsGroupId(mDestinationId)) + { + FabricIndex fabricIndex = CurrentCommissioner().GetFabricIndex(); + ChipLogProgress(chipTool, "Sending command to group 0x%x", GroupIdFromNodeId(mDestinationId)); + + return SendGroupCommand(GroupIdFromNodeId(mDestinationId), fabricIndex); + } + + ChipLogProgress(NotSpecified, "Sending command to node " ChipLogFormatX64, ChipLogValueX64(mDestinationId)); + CheckPeerICDType(); + + CommissioneeDeviceProxy * commissioneeDeviceProxy = nullptr; + if (CHIP_NO_ERROR == CurrentCommissioner().GetDeviceBeingCommissioned(mDestinationId, &commissioneeDeviceProxy)) + { + return SendCommand(commissioneeDeviceProxy, mEndPointId); + } + + return CurrentCommissioner().GetConnectedDevice(mDestinationId, &mOnDeviceConnectedCallback, + &mOnDeviceConnectionFailureCallback); +} + +void ModelCommand::OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr, + const chip::SessionHandle & sessionHandle) +{ + ModelCommand * command = reinterpret_cast(context); + VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnDeviceConnectedFn: context is null")); + + chip::OperationalDeviceProxy device(&exchangeMgr, sessionHandle); + CHIP_ERROR err = command->SendCommand(&device, command->mEndPointId); + VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err)); +} + +void ModelCommand::OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR err) +{ + LogErrorOnFailure(err); + + ModelCommand * command = reinterpret_cast(context); + VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnDeviceConnectionFailureFn: context is null")); + command->SetCommandExitStatus(err); +} + +void ModelCommand::Shutdown() +{ + mOnDeviceConnectedCallback.Cancel(); + mOnDeviceConnectionFailureCallback.Cancel(); + + CHIPCommand::Shutdown(); +} + +void ModelCommand::CheckPeerICDType() +{ + if (mIsPeerLIT.HasValue()) + { + ChipLogProgress(NotSpecified, "Peer ICD type is set to %s", mIsPeerLIT.Value() == 1 ? "LIT-ICD" : "non LIT-ICD"); + return; + } + + app::ICDClientInfo info; + auto destinationPeerId = chip::ScopedNodeId(mDestinationId, CurrentCommissioner().GetFabricIndex()); + auto iter = CHIPCommand::sICDClientStorage.IterateICDClientInfo(); + if (iter == nullptr) + { + return; + } + app::DefaultICDClientStorage::ICDClientInfoIteratorWrapper clientInfoIteratorWrapper(iter); + + while (iter->Next(info)) + { + if (ScopedNodeId(info.peer_node.GetNodeId(), info.peer_node.GetFabricIndex()) == destinationPeerId) + { + ChipLogProgress(NotSpecified, "Peer is a registered LIT ICD."); + mIsPeerLIT.SetValue(true); + return; + } + } +} diff --git a/examples/fabric-admin/commands/clusters/ModelCommand.h b/examples/fabric-admin/commands/clusters/ModelCommand.h new file mode 100644 index 00000000000000..c14d3c9952f3fc --- /dev/null +++ b/examples/fabric-admin/commands/clusters/ModelCommand.h @@ -0,0 +1,91 @@ +/* + * 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. + * + */ + +#pragma once + +#ifdef CONFIG_USE_LOCAL_STORAGE +#include +#endif // CONFIG_USE_LOCAL_STORAGE + +#include "../common/CHIPCommand.h" +#include + +class ModelCommand : public CHIPCommand +{ +public: + ModelCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig, bool supportsMultipleEndpoints = false) : + CHIPCommand(commandName, credsIssuerConfig), mOnDeviceConnectedCallback(OnDeviceConnectedFn, this), + mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureFn, this), mSupportsMultipleEndpoints(supportsMultipleEndpoints) + {} + + void AddArguments(bool skipEndpoints = false) + { + AddArgument( + "destination-id", 0, UINT64_MAX, &mDestinationId, + "64-bit node or group identifier.\n Group identifiers are detected by being in the 0xFFFF'FFFF'FFFF'xxxx range."); + if (skipEndpoints == false) + { + if (mSupportsMultipleEndpoints) + { + AddArgument("endpoint-ids", 0, UINT16_MAX, &mEndPointId, + "Comma-separated list of endpoint ids (e.g. \"1\" or \"1,2,3\").\n Allowed to be 0xFFFF to indicate a " + "wildcard endpoint."); + } + else + { + AddArgument("endpoint-id-ignored-for-group-commands", 0, UINT16_MAX, &mEndPointId, + "Endpoint the command is targeted at."); + } + } + AddArgument( + "lit-icd-peer", 0, 1, &mIsPeerLIT, + "Whether to treat the peer as a LIT ICD. false: Always no, true: Always yes, (not set): Yes if the peer is registered " + "to this controller."); + AddArgument("timeout", 0, UINT16_MAX, &mTimeout); + } + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override; + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(20)); } + + virtual CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endPointIds) = 0; + + virtual CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) { return CHIP_ERROR_BAD_REQUEST; }; + + void Shutdown() override; + +protected: + bool IsPeerLIT() { return mIsPeerLIT.ValueOr(false); } + + chip::Optional mTimeout; + +private: + chip::NodeId mDestinationId; + std::vector mEndPointId; + chip::Optional mIsPeerLIT; + + void CheckPeerICDType(); + + static void OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr, + const chip::SessionHandle & sessionHandle); + static void OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR error); + + chip::Callback::Callback mOnDeviceConnectedCallback; + chip::Callback::Callback mOnDeviceConnectionFailureCallback; + const bool mSupportsMultipleEndpoints; +}; diff --git a/examples/fabric-admin/commands/clusters/ReportCommand.h b/examples/fabric-admin/commands/clusters/ReportCommand.h new file mode 100644 index 00000000000000..4e9dbd0f043931 --- /dev/null +++ b/examples/fabric-admin/commands/clusters/ReportCommand.h @@ -0,0 +1,551 @@ +/* + * 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. + * + */ + +#pragma once + +#include + +#include "DataModelLogger.h" +#include "ModelCommand.h" + +class ReportCommand : public InteractionModelReports, public ModelCommand, public chip::app::ReadClient::Callback +{ +public: + ReportCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) : + InteractionModelReports(this), ModelCommand(commandName, credsIssuerConfig, /* supportsMultipleEndpoints = */ true) + {} + + /////////// ReadClient Callback Interface ///////// + void OnAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data, + const chip::app::StatusIB & status) override + { + CHIP_ERROR error = status.ToChipError(); + if (CHIP_NO_ERROR != error) + { + LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status)); + + ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error)); + mError = error; + return; + } + + if (data == nullptr) + { + ChipLogError(NotSpecified, "Response Failure: No Data"); + mError = CHIP_ERROR_INTERNAL; + return; + } + + LogErrorOnFailure(RemoteDataModelLogger::LogAttributeAsJSON(path, data)); + + error = DataModelLogger::LogAttribute(path, data); + if (CHIP_NO_ERROR != error) + { + ChipLogError(NotSpecified, "Response Failure: Can not decode Data"); + mError = error; + return; + } + } + + void OnEventData(const chip::app::EventHeader & eventHeader, chip::TLV::TLVReader * data, + const chip::app::StatusIB * status) override + { + if (status != nullptr) + { + CHIP_ERROR error = status->ToChipError(); + if (CHIP_NO_ERROR != error) + { + LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(eventHeader, *status)); + + ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error)); + mError = error; + return; + } + } + + if (data == nullptr) + { + ChipLogError(NotSpecified, "Response Failure: No Data"); + mError = CHIP_ERROR_INTERNAL; + return; + } + + LogErrorOnFailure(RemoteDataModelLogger::LogEventAsJSON(eventHeader, data)); + + CHIP_ERROR error = DataModelLogger::LogEvent(eventHeader, data); + if (CHIP_NO_ERROR != error) + { + ChipLogError(NotSpecified, "Response Failure: Can not decode Data"); + mError = error; + return; + } + } + + void OnError(CHIP_ERROR error) override + { + LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error)); + + ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error)); + mError = error; + } + + void OnDeallocatePaths(chip::app::ReadPrepareParams && aReadPrepareParams) override + { + InteractionModelReports::OnDeallocatePaths(std::move(aReadPrepareParams)); + } + + void Shutdown() override + { + // We don't shut down InteractionModelReports here; we leave it for + // Cleanup to handle. + mError = CHIP_NO_ERROR; + ModelCommand::Shutdown(); + } + + void Cleanup() override { InteractionModelReports::Shutdown(); } + +protected: + // Use a 3x-longer-than-default timeout because wildcard reads can take a + // while. + chip::System::Clock::Timeout GetWaitDuration() const override + { + return mTimeout.HasValue() ? chip::System::Clock::Seconds16(mTimeout.Value()) : (ModelCommand::GetWaitDuration() * 3); + } + + CHIP_ERROR mError = CHIP_NO_ERROR; +}; + +class ReadCommand : public ReportCommand +{ +protected: + ReadCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) : + ReportCommand(commandName, credsIssuerConfig) + {} + + void OnDone(chip::app::ReadClient * aReadClient) override + { + InteractionModelReports::CleanupReadClient(aReadClient); + SetCommandExitStatus(mError); + } +}; + +class SubscribeCommand : public ReportCommand +{ +protected: + SubscribeCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) : + ReportCommand(commandName, credsIssuerConfig) + {} + + void OnSubscriptionEstablished(chip::SubscriptionId subscriptionId) override + { + mSubscriptionEstablished = true; + SetCommandExitStatus(CHIP_NO_ERROR); + } + + void OnDone(chip::app::ReadClient * aReadClient) override + { + InteractionModelReports::CleanupReadClient(aReadClient); + + if (!mSubscriptionEstablished) + { + SetCommandExitStatus(mError); + } + // else we must be getting here from Cleanup(), which means we have + // already done our exit status thing. + } + + void Shutdown() override + { + mSubscriptionEstablished = false; + ReportCommand::Shutdown(); + } + + // For subscriptions we always defer interactive cleanup. Either our + // ReadClients will terminate themselves (in which case they will be removed + // from our list anyway), or they should hang around until shutdown. + bool DeferInteractiveCleanup() override { return true; } + +private: + bool mSubscriptionEstablished = false; +}; + +class ReadAttribute : public ReadCommand +{ +public: + ReadAttribute(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-by-id", credsIssuerConfig) + { + AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds, + "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n Allowed to be 0xFFFFFFFF to " + "indicate a wildcard cluster."); + AddAttributeIdArgument(); + AddCommonArguments(); + ReadCommand::AddArguments(); + } + + ReadAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) : + ReadCommand("read-by-id", credsIssuerConfig), mClusterIds(1, clusterId) + { + AddAttributeIdArgument(); + AddCommonArguments(); + ReadCommand::AddArguments(); + } + + ReadAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId, + CredentialIssuerCommands * credsIssuerConfig) : + ReadCommand("read", credsIssuerConfig), + mClusterIds(1, clusterId), mAttributeIds(1, attributeId) + { + AddArgument("attr-name", attributeName); + AddCommonArguments(); + ReadCommand::AddArguments(); + } + + ~ReadAttribute() {} + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + return ReadCommand::ReadAttribute(device, endpointIds, mClusterIds, mAttributeIds); + } + +private: + void AddAttributeIdArgument() + { + AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds, + "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n Allowed to be " + "0xFFFFFFFF to indicate a wildcard attribute."); + } + + void AddCommonArguments() + { + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered, + "Boolean indicating whether to do a fabric-filtered read. Defaults to true."); + AddArgument("data-version", 0, UINT32_MAX, &mDataVersions, + "Comma-separated list of data versions for the clusters being read."); + } + + std::vector mClusterIds; + std::vector mAttributeIds; +}; + +class SubscribeAttribute : public SubscribeCommand +{ +public: + SubscribeAttribute(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-by-id", credsIssuerConfig) + { + AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds, + "Comma-separated list of cluster ids to subscribe to (e.g. \"6\" or \"8,0x201\").\n Allowed to be 0xFFFFFFFF " + "to indicate a wildcard cluster."); + AddAttributeIdArgument(); + AddCommonArguments(); + SubscribeCommand::AddArguments(); + } + + SubscribeAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) : + SubscribeCommand("subscribe-by-id", credsIssuerConfig), mClusterIds(1, clusterId) + { + AddAttributeIdArgument(); + AddCommonArguments(); + SubscribeCommand::AddArguments(); + } + + SubscribeAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId, + CredentialIssuerCommands * credsIssuerConfig) : + SubscribeCommand("subscribe", credsIssuerConfig), + mClusterIds(1, clusterId), mAttributeIds(1, attributeId) + { + AddArgument("attr-name", attributeName); + AddCommonArguments(); + SubscribeCommand::AddArguments(); + } + + ~SubscribeAttribute() {} + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + SubscribeCommand::SetPeerLIT(IsPeerLIT()); + return SubscribeCommand::SubscribeAttribute(device, endpointIds, mClusterIds, mAttributeIds); + } + +private: + void AddAttributeIdArgument() + { + AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds, + "Comma-separated list of attribute ids to subscribe to (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n Allowed to be " + "0xFFFFFFFF to indicate a wildcard attribute."); + } + + void AddCommonArguments() + { + AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval, + "Server should not send a new report if less than this number of seconds has elapsed since the last report."); + AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval, + "Server must send a report if this number of seconds has elapsed since the last report."); + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered, + "Boolean indicating whether to do a fabric-filtered subscription. Defaults to true."); + AddArgument("data-version", 0, UINT32_MAX, &mDataVersions, + "Comma-separated list of data versions for the clusters being subscribed to."); + AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions, + "Boolean indicating whether to keep existing subscriptions when creating the new one. Defaults to false."); + AddArgument("auto-resubscribe", 0, 1, &mAutoResubscribe, + "Boolean indicating whether the subscription should auto-resubscribe. Defaults to false."); + } + + std::vector mClusterIds; + std::vector mAttributeIds; +}; + +class ReadEvent : public ReadCommand +{ +public: + ReadEvent(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-event-by-id", credsIssuerConfig) + { + AddArgument("cluster-id", 0, UINT32_MAX, &mClusterIds); + AddArgument("event-id", 0, UINT32_MAX, &mEventIds); + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered); + AddArgument("event-min", 0, UINT64_MAX, &mEventNumber); + ReadCommand::AddArguments(); + } + + ReadEvent(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) : + ReadCommand("read-event-by-id", credsIssuerConfig), mClusterIds(1, clusterId) + { + AddArgument("event-id", 0, UINT32_MAX, &mEventIds); + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered); + AddArgument("event-min", 0, UINT64_MAX, &mEventNumber); + ReadCommand::AddArguments(); + } + + ReadEvent(chip::ClusterId clusterId, const char * eventName, chip::EventId eventId, + CredentialIssuerCommands * credsIssuerConfig) : + ReadCommand("read-event", credsIssuerConfig), + mClusterIds(1, clusterId), mEventIds(1, eventId) + { + AddArgument("event-name", eventName); + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered); + AddArgument("event-min", 0, UINT64_MAX, &mEventNumber); + ReadCommand::AddArguments(); + } + + ~ReadEvent() {} + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + return ReadCommand::ReadEvent(device, endpointIds, mClusterIds, mEventIds); + } + +private: + std::vector mClusterIds; + std::vector mEventIds; +}; + +class SubscribeEvent : public SubscribeCommand +{ +public: + SubscribeEvent(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-event-by-id", credsIssuerConfig) + { + AddArgument("cluster-id", 0, UINT32_MAX, &mClusterIds); + AddArgument("event-id", 0, UINT32_MAX, &mEventIds); + AddCommonArguments(); + SubscribeCommand::AddArguments(); + } + + SubscribeEvent(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) : + SubscribeCommand("subscribe-event-by-id", credsIssuerConfig), mClusterIds(1, clusterId) + { + AddArgument("event-id", 0, UINT32_MAX, &mEventIds); + AddCommonArguments(); + SubscribeCommand::AddArguments(); + } + + SubscribeEvent(chip::ClusterId clusterId, const char * eventName, chip::EventId eventId, + CredentialIssuerCommands * credsIssuerConfig) : + SubscribeCommand("subscribe-event", credsIssuerConfig), + mClusterIds(1, clusterId), mEventIds(1, eventId) + { + AddArgument("event-name", eventName, "Event name."); + AddCommonArguments(); + SubscribeCommand::AddArguments(); + } + + void AddCommonArguments() + { + AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval, + "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request."); + AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval, + "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request."); + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered); + AddArgument("event-min", 0, UINT64_MAX, &mEventNumber); + AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions, + "false - Terminate existing subscriptions from initiator.\n true - Leave existing subscriptions in place."); + AddArgument( + "is-urgent", 0, 1, &mIsUrgents, + "Sets isUrgent in the Subscribe Request.\n" + " The queueing of any urgent event SHALL force an immediate generation of reports containing all events queued " + "leading up to (and including) the urgent event in question.\n" + " This argument takes a comma separated list of true/false values.\n" + " If the number of paths exceeds the number of entries provided to is-urgent, then isUrgent will be false for the " + "extra paths."); + AddArgument("auto-resubscribe", 0, 1, &mAutoResubscribe, + "Boolean indicating whether the subscription should auto-resubscribe. Defaults to false."); + } + + ~SubscribeEvent() {} + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + SubscribeCommand::SetPeerLIT(IsPeerLIT()); + return SubscribeCommand::SubscribeEvent(device, endpointIds, mClusterIds, mEventIds); + } + +private: + std::vector mClusterIds; + std::vector mEventIds; +}; + +class ReadNone : public ReadCommand +{ +public: + ReadNone(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-none", credsIssuerConfig) + { + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered, + "Boolean indicating whether to do a fabric-filtered read. Defaults to true."); + AddArgument("data-versions", 0, UINT32_MAX, &mDataVersions, + "Comma-separated list of data versions for the clusters being read."); + AddArgument("event-min", 0, UINT64_MAX, &mEventNumber); + ReadCommand::AddArguments(true /* skipEndpoints */); + } + + ~ReadNone() {} + + void OnDone(chip::app::ReadClient * aReadClient) override + { + InteractionModelReports::CleanupReadClient(aReadClient); + SetCommandExitStatus(mError); + } + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + return ReadCommand::ReadNone(device); + } +}; + +class ReadAll : public ReadCommand +{ +public: + ReadAll(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-all", credsIssuerConfig) + { + AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds, + "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n Allowed to be 0xFFFFFFFF to " + "indicate a wildcard cluster."); + AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds, + "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n Allowed to be " + "0xFFFFFFFF to indicate a wildcard attribute."); + AddArgument("event-ids", 0, UINT32_MAX, &mEventIds, + "Comma-separated list of event ids to read (e.g. \"0\" or \"1,2,3\").\n Allowed to be " + "0xFFFFFFFF to indicate a wildcard event."); + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered, + "Boolean indicating whether to do a fabric-filtered read. Defaults to true."); + AddArgument("data-versions", 0, UINT32_MAX, &mDataVersions, + "Comma-separated list of data versions for the clusters being read."); + AddArgument("event-min", 0, UINT64_MAX, &mEventNumber); + ReadCommand::AddArguments(); + } + + ~ReadAll() {} + + void OnDone(chip::app::ReadClient * aReadClient) override + { + InteractionModelReports::CleanupReadClient(aReadClient); + SetCommandExitStatus(mError); + } + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + return ReadCommand::ReadAll(device, endpointIds, mClusterIds, mAttributeIds, mEventIds); + } + +private: + std::vector mClusterIds; + std::vector mAttributeIds; + std::vector mEventIds; +}; + +class SubscribeNone : public SubscribeCommand +{ +public: + SubscribeNone(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-none", credsIssuerConfig) + { + AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval, + "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request."); + AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval, + "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request."); + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered, + "Boolean indicating whether to do a fabric-filtered read. Defaults to true."); + AddArgument("event-min", 0, UINT64_MAX, &mEventNumber); + AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions, + "false - Terminate existing subscriptions from initiator.\n true - Leave existing subscriptions in place."); + SubscribeCommand::AddArguments(true /* skipEndpoints */); + } + + ~SubscribeNone() {} + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + return SubscribeCommand::SubscribeNone(device); + } +}; + +class SubscribeAll : public SubscribeCommand +{ +public: + SubscribeAll(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-all", credsIssuerConfig) + { + AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds, + "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n Allowed to be 0xFFFFFFFF to " + "indicate a wildcard cluster."); + AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds, + "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n Allowed to be " + "0xFFFFFFFF to indicate a wildcard attribute."); + AddArgument("event-ids", 0, UINT32_MAX, &mEventIds, + "Comma-separated list of event ids to read (e.g. \"0\" or \"1,2,3\").\n Allowed to be " + "0xFFFFFFFF to indicate a wildcard event."); + AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval, + "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request."); + AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval, + "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request."); + AddArgument("fabric-filtered", 0, 1, &mFabricFiltered, + "Boolean indicating whether to do a fabric-filtered read. Defaults to true."); + AddArgument("event-min", 0, UINT64_MAX, &mEventNumber); + AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions, + "false - Terminate existing subscriptions from initiator.\n true - Leave existing subscriptions in place."); + SubscribeCommand::AddArguments(); + } + + ~SubscribeAll() {} + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + SubscribeCommand::SetPeerLIT(IsPeerLIT()); + return SubscribeCommand::SubscribeAll(device, endpointIds, mClusterIds, mAttributeIds, mEventIds); + } + +private: + std::vector mClusterIds; + std::vector mAttributeIds; + std::vector mEventIds; +}; diff --git a/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h b/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h new file mode 100644 index 00000000000000..625e5a73245507 --- /dev/null +++ b/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h @@ -0,0 +1,108 @@ +/* + * 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. + * + */ + +#pragma once + +#include + +#include +#include + +class ShutdownSubscription : public CHIPCommand +{ +public: + ShutdownSubscription(CredentialIssuerCommands * credsIssuerConfig) : + CHIPCommand("shutdown-one", credsIssuerConfig, + "Shut down a single subscription, identified by its subscription id and target node id.") + { + AddArgument("subscription-id", 0, UINT32_MAX, &mSubscriptionId); + AddArgument("node-id", 0, UINT64_MAX, &mNodeId, + "The node id, scoped to the commissioner name the command is running under."); + } + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override + { + CHIP_ERROR err = chip::app::InteractionModelEngine::GetInstance()->ShutdownSubscription( + chip::ScopedNodeId(mNodeId, CurrentCommissioner().GetFabricIndex()), mSubscriptionId); + SetCommandExitStatus(err); + return CHIP_NO_ERROR; + } + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); } + +private: + chip::SubscriptionId mSubscriptionId; + chip::NodeId mNodeId; +}; + +class ShutdownSubscriptionsForNode : public CHIPCommand +{ +public: + ShutdownSubscriptionsForNode(CredentialIssuerCommands * credsIssuerConfig) : + CHIPCommand("shutdown-all-for-node", credsIssuerConfig, "Shut down all subscriptions targeting a given node.") + { + AddArgument("node-id", 0, UINT64_MAX, &mNodeId, + "The node id, scoped to the commissioner name the command is running under."); + } + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override + { + chip::app::InteractionModelEngine::GetInstance()->ShutdownSubscriptions(CurrentCommissioner().GetFabricIndex(), mNodeId); + + SetCommandExitStatus(CHIP_NO_ERROR); + return CHIP_NO_ERROR; + } + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); } + +private: + chip::NodeId mNodeId; +}; + +class ShutdownAllSubscriptions : public CHIPCommand +{ +public: + ShutdownAllSubscriptions(CredentialIssuerCommands * credsIssuerConfig) : + CHIPCommand("shutdown-all", credsIssuerConfig, "Shut down all subscriptions to all nodes.") + {} + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override + { + chip::app::InteractionModelEngine::GetInstance()->ShutdownAllSubscriptions(); + + SetCommandExitStatus(CHIP_NO_ERROR); + return CHIP_NO_ERROR; + } + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); } + +private: +}; + +void registerCommandsSubscriptions(Commands & commands, CredentialIssuerCommands * credsIssuerConfig) +{ + const char * clusterName = "Subscriptions"; + + commands_list clusterCommands = { + make_unique(credsIssuerConfig), // + make_unique(credsIssuerConfig), // + make_unique(credsIssuerConfig), // + }; + + commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for shutting down subscriptions."); +} diff --git a/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h b/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h new file mode 100644 index 00000000000000..8424e95020f92e --- /dev/null +++ b/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h @@ -0,0 +1,286 @@ +/* + * 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. + * + */ + +#pragma once + +#include + +#include "DataModelLogger.h" +#include "ModelCommand.h" + +inline constexpr char kWriteCommandKey[] = "write"; +inline constexpr char kWriteByIdCommandKey[] = "write-by-id"; +inline constexpr char kForceWriteCommandKey[] = "force-write"; + +enum class WriteCommandType +{ + kWrite, // regular, writable attributes + kForceWrite, // forced writes, send a write command on something expected to fail +}; + +template > +class WriteAttribute : public InteractionModelWriter, public ModelCommand, public chip::app::WriteClient::Callback +{ +public: + WriteAttribute(CredentialIssuerCommands * credsIssuerConfig) : + InteractionModelWriter(this), ModelCommand(kWriteByIdCommandKey, credsIssuerConfig) + { + AddArgumentClusterIds(); + AddArgumentAttributeIds(); + AddArgumentAttributeValues(); + AddArguments(); + } + + WriteAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) : + InteractionModelWriter(this), ModelCommand(kWriteByIdCommandKey, credsIssuerConfig), mClusterIds(1, clusterId) + { + AddArgumentAttributeIds(); + AddArgumentAttributeValues(); + AddArguments(); + } + + template + WriteAttribute(chip::ClusterId clusterId, const char * attributeName, minType minValue, maxType maxValue, + chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) : + WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig) + { + AddArgumentAttributeName(attributeName); + AddArgumentAttributeValues(static_cast(minValue), static_cast(maxValue)); + AddArguments(); + } + + WriteAttribute(chip::ClusterId clusterId, const char * attributeName, float minValue, float maxValue, + chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) : + WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig) + { + AddArgumentAttributeName(attributeName); + AddArgumentAttributeValues(minValue, maxValue); + AddArguments(); + } + + WriteAttribute(chip::ClusterId clusterId, const char * attributeName, double minValue, double maxValue, + chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) : + WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig) + { + AddArgumentAttributeName(attributeName); + AddArgumentAttributeValues(minValue, maxValue); + AddArguments(); + } + + WriteAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId, + WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) : + WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig) + { + AddArgumentAttributeName(attributeName); + AddArgumentAttributeValues(); + AddArguments(); + } + + WriteAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId, + TypedComplexArgument & attributeParser, WriteCommandType commandType, + CredentialIssuerCommands * credsIssuerConfig) : + WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig) + { + AddArgumentAttributeName(attributeName); + AddArgumentAttributeValues(attributeParser); + AddArguments(); + } + + ~WriteAttribute() {} + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds) override + { + return WriteAttribute::SendCommand(device, endpointIds, mClusterIds, mAttributeIds, mAttributeValues); + } + + CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) override + { + return WriteAttribute::SendGroupCommand(groupId, fabricIndex, mClusterIds, mAttributeIds, mAttributeValues); + } + + /////////// WriteClient Callback Interface ///////// + void OnResponse(const chip::app::WriteClient * client, const chip::app::ConcreteDataAttributePath & path, + chip::app::StatusIB status) override + { + CHIP_ERROR error = status.ToChipError(); + if (CHIP_NO_ERROR != error) + { + LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status)); + + ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error)); + mError = error; + } + } + + void OnError(const chip::app::WriteClient * client, CHIP_ERROR error) override + { + LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error)); + + ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error)); + mError = error; + } + + void OnDone(chip::app::WriteClient * client) override + { + InteractionModelWriter::Shutdown(); + SetCommandExitStatus(mError); + } + + CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector endpointIds, + std::vector clusterIds, std::vector attributeIds, const T & values) + { + return InteractionModelWriter::WriteAttribute(device, endpointIds, clusterIds, attributeIds, values); + } + + CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex, std::vector clusterIds, + std::vector attributeIds, const T & value) + { + ChipLogDetail(NotSpecified, "Sending Write Attribute to Group %u, on Fabric %x, for cluster %u with attributeId %u", + groupId, fabricIndex, clusterIds.at(0), attributeIds.at(0)); + chip::Optional dataVersion = chip::NullOptional; + if (mDataVersions.HasValue()) + { + dataVersion.SetValue(mDataVersions.Value().at(0)); + } + + return InteractionModelWriter::WriteGroupAttribute(groupId, fabricIndex, clusterIds.at(0), attributeIds.at(0), value, + dataVersion); + } + + void Shutdown() override + { + mError = CHIP_NO_ERROR; + ModelCommand::Shutdown(); + } + +protected: + WriteAttribute(const char * attributeName, CredentialIssuerCommands * credsIssuerConfig) : + InteractionModelWriter(this), ModelCommand(kWriteCommandKey, credsIssuerConfig) + { + // Subclasses are responsible for calling AddArguments. + } + + void AddArgumentClusterIds() + { + AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds, + "Comma-separated list of cluster ids to write to (e.g. \"6\" or \"6,0x201\")."); + } + + void AddArgumentAttributeIds() + { + AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds, + "Comma-separated list of attribute ids to write (e.g. \"16385\" or \"16385,0x4002\")."); + } + + void AddArgumentAttributeName(const char * attributeName) + { + AddArgument("attribute-name", attributeName, "The attribute name to write."); + } + + template >::value, int> = 0> + static const char * GetAttributeValuesDescription() + { + return "Semicolon-separated list of attribute values to write. Each value is represented as follows, depending on the " + "type:\n" + " * struct: a JSON-encoded object, with field ids as keys.\n" + " * list: a JSON-encoded array of values.\n" + " * null: A literal null.\n" + " * boolean: A literal true or false.\n" + " * unsigned integer: One of:\n" + " a) The number directly, as decimal.\n" + " b) The number directly, as 0x followed by hex digits. (Only for the toplevel value, not inside structs or " + "lists.)\n" + " c) A string starting with \"u:\" followed by decimal digits\n" + " * signed integer: One of:\n" + " a) The number directly, if it's negative.\n" + " c) A string starting with \"s:\" followed by decimal digits\n" + " * single-precision float: A string starting with \"f:\" followed by the number.\n" + " * double-precision float: One of:\n" + " a) The number directly, if it's not an integer.\n" + " b) A string starting with \"d:\" followed by the number.\n" + " * octet string: A string starting with \"hex:\" followed by the hex encoding of the bytes.\n" + " * string: A string with the characters.\n" + "\n" + " Example values: '10;20', '10;\"u:20\"', '\"hex:aabbcc\";\"hello\"'."; + } + + static const char * GetTypedAttributeValuesDescription() { return "Comma-separated list of attribute values to write."; } + + template >::value, int> = 0> + static const char * GetAttributeValuesDescription() + { + return GetTypedAttributeValuesDescription(); + } + + template + void AddArgumentAttributeValues(minType minValue, maxType maxValue) + { + AddArgument("attribute-values", minValue, maxValue, &mAttributeValues, GetTypedAttributeValuesDescription()); + } + + void AddArgumentAttributeValues() { AddArgument("attribute-values", &mAttributeValues, GetAttributeValuesDescription()); } + + void AddArgumentAttributeValues(TypedComplexArgument & attributeParser) + { + attributeParser.SetArgument(&mAttributeValues); + AddArgument("attribute-values", &attributeParser, GetTypedAttributeValuesDescription()); + } + + void AddArguments() + { + AddArgument("timedInteractionTimeoutMs", 0, UINT16_MAX, &mTimedInteractionTimeoutMs, + "If provided, do a timed write with the given timed interaction timeout. See \"7.6.10. Timed Interaction\" in " + "the Matter specification."); + AddArgument("busyWaitForMs", 0, UINT16_MAX, &mBusyWaitForMs, + "If provided, block the main thread processing for the given time right after sending a command."); + AddArgument("data-version", 0, UINT32_MAX, &mDataVersions, + "Comma-separated list of data versions for the clusters being written."); + AddArgument("suppressResponse", 0, 1, &mSuppressResponse); + AddArgument("repeat-count", 1, UINT16_MAX, &mRepeatCount); + AddArgument("repeat-delay-ms", 0, UINT16_MAX, &mRepeatDelayInMs); + ModelCommand::AddArguments(); + } + +private: + // This constructor is private as it is not intended to be used from outside the class. + WriteAttribute(chip::ClusterId clusterId, chip::AttributeId attributeId, WriteCommandType commandType, + CredentialIssuerCommands * credsIssuerConfig) : + InteractionModelWriter(this), + ModelCommand(commandType == WriteCommandType::kWrite ? kWriteCommandKey : kForceWriteCommandKey, credsIssuerConfig), + mClusterIds(1, clusterId), mAttributeIds(1, attributeId) + {} + + std::vector mClusterIds; + std::vector mAttributeIds; + + CHIP_ERROR mError = CHIP_NO_ERROR; + T mAttributeValues; +}; + +template +class WriteAttributeAsComplex : public WriteAttribute +{ +public: + WriteAttributeAsComplex(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId, + WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) : + WriteAttribute(clusterId, attributeName, attributeId, mAttributeParser, commandType, credsIssuerConfig) + {} + +private: + TypedComplexArgument mAttributeParser; +}; diff --git a/examples/fabric-admin/commands/common/CHIPCommand.cpp b/examples/fabric-admin/commands/common/CHIPCommand.cpp new file mode 100644 index 00000000000000..982c857d206136 --- /dev/null +++ b/examples/fabric-admin/commands/common/CHIPCommand.cpp @@ -0,0 +1,651 @@ +/* + * 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 "CHIPCommand.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED +#include "TraceDecoder.h" +#include "TraceHandlers.h" +#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED + +std::map> CHIPCommand::mCommissioners; +std::set CHIPCommand::sDeferredCleanups; + +using DeviceControllerFactory = chip::Controller::DeviceControllerFactory; + +constexpr chip::FabricId kIdentityNullFabricId = chip::kUndefinedFabricId; +constexpr chip::FabricId kIdentityAlphaFabricId = 1; +constexpr chip::FabricId kIdentityBetaFabricId = 2; +constexpr chip::FabricId kIdentityGammaFabricId = 3; +constexpr chip::FabricId kIdentityOtherFabricId = 4; +constexpr char kPAATrustStorePathVariable[] = "FABRICSYNC_PAA_TRUST_STORE_PATH"; +constexpr char kCDTrustStorePathVariable[] = "FABRICSYNC_CD_TRUST_STORE_PATH"; + +const chip::Credentials::AttestationTrustStore * CHIPCommand::sTrustStore = nullptr; +chip::Credentials::GroupDataProviderImpl CHIPCommand::sGroupDataProvider{ kMaxGroupsPerFabric, kMaxGroupKeysPerFabric }; +// All fabrics share the same ICD client storage. +chip::app::DefaultICDClientStorage CHIPCommand::sICDClientStorage; +chip::Crypto::RawKeySessionKeystore CHIPCommand::sSessionKeystore; +chip::app::DefaultCheckInDelegate CHIPCommand::sCheckInDelegate; +chip::app::CheckInHandler CHIPCommand::sCheckInHandler; + +namespace { + +CHIP_ERROR GetAttestationTrustStore(const char * paaTrustStorePath, const chip::Credentials::AttestationTrustStore ** trustStore) +{ + if (paaTrustStorePath == nullptr) + { + paaTrustStorePath = getenv(kPAATrustStorePathVariable); + } + + if (paaTrustStorePath == nullptr) + { + *trustStore = chip::Credentials::GetTestAttestationTrustStore(); + return CHIP_NO_ERROR; + } + + static chip::Credentials::FileAttestationTrustStore attestationTrustStore{ paaTrustStorePath }; + + if (paaTrustStorePath != nullptr && attestationTrustStore.paaCount() == 0) + { + ChipLogError(NotSpecified, "No PAAs found in path: %s", paaTrustStorePath); + ChipLogError(NotSpecified, + "Please specify a valid path containing trusted PAA certificates using " + "the argument [--paa-trust-store-path paa/file/path] " + "or environment variable [%s=paa/file/path]", + kPAATrustStorePathVariable); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + *trustStore = &attestationTrustStore; + return CHIP_NO_ERROR; +} + +} // namespace + +CHIP_ERROR CHIPCommand::MaybeSetUpStack() +{ + if (IsInteractive()) + { + return CHIP_NO_ERROR; + } + + StartTracing(); + +#if (CHIP_DEVICE_LAYER_TARGET_LINUX || CHIP_DEVICE_LAYER_TARGET_TIZEN) && CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE + // By default, Linux device is configured as a BLE peripheral while the controller needs a BLE central. + ReturnLogErrorOnFailure(chip::DeviceLayer::Internal::BLEMgrImpl().ConfigureBle(mBleAdapterId.ValueOr(0), true)); +#endif + + ReturnLogErrorOnFailure(mDefaultStorage.Init(nullptr, GetStorageDirectory().ValueOr(nullptr))); + ReturnLogErrorOnFailure(mOperationalKeystore.Init(&mDefaultStorage)); + ReturnLogErrorOnFailure(mOpCertStore.Init(&mDefaultStorage)); + + // fabric-admin uses a non-persistent keystore. + // ICD storage lifetime is currently tied to the fabric-admin's lifetime. Since fabric-admin interactive mode is currently used + // for ICD commissioning and check-in validation, this temporary storage meets the test requirements. + // TODO: Implement persistent ICD storage for the fabric-admin. + ReturnLogErrorOnFailure(sICDClientStorage.Init(&mDefaultStorage, &sSessionKeystore)); + + chip::Controller::FactoryInitParams factoryInitParams; + + factoryInitParams.fabricIndependentStorage = &mDefaultStorage; + factoryInitParams.operationalKeystore = &mOperationalKeystore; + factoryInitParams.opCertStore = &mOpCertStore; + factoryInitParams.enableServerInteractions = NeedsOperationalAdvertising(); + factoryInitParams.sessionKeystore = &sSessionKeystore; + + // Init group data provider that will be used for all group keys and IPKs for the + // fabric-admin-configured fabrics. This is OK to do once since the fabric tables + // and the DeviceControllerFactory all "share" in the same underlying data. + // Different commissioner implementations may want to use alternate implementations + // of GroupDataProvider for injection through factoryInitParams. + sGroupDataProvider.SetStorageDelegate(&mDefaultStorage); + sGroupDataProvider.SetSessionKeystore(factoryInitParams.sessionKeystore); + ReturnLogErrorOnFailure(sGroupDataProvider.Init()); + chip::Credentials::SetGroupDataProvider(&sGroupDataProvider); + factoryInitParams.groupDataProvider = &sGroupDataProvider; + + uint16_t port = mDefaultStorage.GetListenPort(); + if (port != 0) + { + // Make sure different commissioners run on different ports. + port = static_cast(port + CurrentCommissionerId()); + } + factoryInitParams.listenPort = port; + ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().Init(factoryInitParams)); + + auto systemState = chip::Controller::DeviceControllerFactory::GetInstance().GetSystemState(); + VerifyOrReturnError(nullptr != systemState, CHIP_ERROR_INCORRECT_STATE); + + ReturnErrorOnFailure(GetAttestationTrustStore(mPaaTrustStorePath.ValueOr(nullptr), &sTrustStore)); + + auto engine = chip::app::InteractionModelEngine::GetInstance(); + VerifyOrReturnError(engine != nullptr, CHIP_ERROR_INCORRECT_STATE); + ReturnLogErrorOnFailure(sCheckInDelegate.Init(&sICDClientStorage, engine)); + ReturnLogErrorOnFailure(sCheckInHandler.Init(DeviceControllerFactory::GetInstance().GetSystemState()->ExchangeMgr(), + &sICDClientStorage, &sCheckInDelegate, engine)); + + CommissionerIdentity nullIdentity{ kIdentityNull, chip::kUndefinedNodeId }; + ReturnLogErrorOnFailure(InitializeCommissioner(nullIdentity, kIdentityNullFabricId)); + + // After initializing first commissioner, add the additional CD certs once + { + const char * cdTrustStorePath = mCDTrustStorePath.ValueOr(nullptr); + if (cdTrustStorePath == nullptr) + { + cdTrustStorePath = getenv(kCDTrustStorePathVariable); + } + + auto additionalCdCerts = + chip::Credentials::LoadAllX509DerCerts(cdTrustStorePath, chip::Credentials::CertificateValidationMode::kPublicKeyOnly); + if (cdTrustStorePath != nullptr && additionalCdCerts.size() == 0) + { + ChipLogError(NotSpecified, "Warning: no CD signing certs found in path: %s, only defaults will be used", + cdTrustStorePath); + ChipLogError(NotSpecified, + "Please specify a path containing trusted CD verifying key certificates using " + "the argument [--cd-trust-store-path cd/file/path] " + "or environment variable [%s=cd/file/path]", + kCDTrustStorePathVariable); + } + ReturnErrorOnFailure(mCredIssuerCmds->AddAdditionalCDVerifyingCerts(additionalCdCerts)); + } + bool allowTestCdSigningKey = !mOnlyAllowTrustedCdKeys.ValueOr(false); + mCredIssuerCmds->SetCredentialIssuerOption(CredentialIssuerCommands::CredentialIssuerOptions::kAllowTestCdSigningKey, + allowTestCdSigningKey); + + return CHIP_NO_ERROR; +} + +void CHIPCommand::MaybeTearDownStack() +{ + if (IsInteractive()) + { + return; + } + + // + // We can call DeviceController::Shutdown() safely without grabbing the stack lock + // since the CHIP thread and event queue have been stopped, preventing any thread + // races. + // + for (auto & commissioner : mCommissioners) + { + ShutdownCommissioner(commissioner.first); + } + + StopTracing(); +} + +CHIP_ERROR CHIPCommand::EnsureCommissionerForIdentity(std::string identity) +{ + chip::NodeId nodeId; + ReturnErrorOnFailure(GetIdentityNodeId(identity, &nodeId)); + CommissionerIdentity lookupKey{ identity, nodeId }; + if (mCommissioners.find(lookupKey) != mCommissioners.end()) + { + return CHIP_NO_ERROR; + } + + // Need to initialize the commissioner. + chip::FabricId fabricId; + if (identity == kIdentityAlpha) + { + fabricId = kIdentityAlphaFabricId; + } + else if (identity == kIdentityBeta) + { + fabricId = kIdentityBetaFabricId; + } + else if (identity == kIdentityGamma) + { + fabricId = kIdentityGammaFabricId; + } + else + { + fabricId = strtoull(identity.c_str(), nullptr, 0); + if (fabricId < kIdentityOtherFabricId) + { + ChipLogError(NotSpecified, "Invalid identity: %s", identity.c_str()); + return CHIP_ERROR_INVALID_ARGUMENT; + } + } + + return InitializeCommissioner(lookupKey, fabricId); +} + +CHIP_ERROR CHIPCommand::Run() +{ + ReturnErrorOnFailure(MaybeSetUpStack()); + + CHIP_ERROR err = StartWaiting(GetWaitDuration()); + + if (IsInteractive()) + { + bool timedOut; + // Give it 2 hours to run our cleanup; that should never get hit in practice. + CHIP_ERROR cleanupErr = RunOnMatterQueue(RunCommandCleanup, chip::System::Clock::Seconds16(7200), &timedOut); + VerifyOrDie(cleanupErr == CHIP_NO_ERROR); + VerifyOrDie(!timedOut); + } + else + { + CleanupAfterRun(); + } + + MaybeTearDownStack(); + + return err; +} + +void CHIPCommand::StartTracing() +{ + if (mTraceTo.HasValue()) + { + for (const auto & destination : mTraceTo.Value()) + { + mTracingSetup.EnableTracingFor(destination.c_str()); + } + } + +#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED + chip::trace::InitTrace(); + + if (mTraceFile.HasValue()) + { + chip::trace::AddTraceStream(new chip::trace::TraceStreamFile(mTraceFile.Value())); + } + else if (mTraceLog.HasValue() && mTraceLog.Value()) + { + chip::trace::AddTraceStream(new chip::trace::TraceStreamLog()); + } + + if (mTraceDecode.HasValue() && mTraceDecode.Value()) + { + chip::trace::TraceDecoderOptions options; + // The interaction model protocol is already logged, so just disable logging those. + options.mEnableProtocolInteractionModelResponse = false; + chip::trace::TraceDecoder * decoder = new chip::trace::TraceDecoder(); + decoder->SetOptions(options); + chip::trace::AddTraceStream(decoder); + } +#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED +} + +void CHIPCommand::StopTracing() +{ + mTracingSetup.StopTracing(); + +#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED + chip::trace::DeInitTrace(); +#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED +} + +void CHIPCommand::SetIdentity(const char * identity) +{ + std::string name = std::string(identity); + if (name.compare(kIdentityAlpha) != 0 && name.compare(kIdentityBeta) != 0 && name.compare(kIdentityGamma) != 0 && + name.compare(kIdentityNull) != 0 && strtoull(name.c_str(), nullptr, 0) < kIdentityOtherFabricId) + { + ChipLogError(NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]", name.c_str(), + kIdentityAlpha, kIdentityBeta, kIdentityGamma); + chipDie(); + } + + mCommissionerName.SetValue(const_cast(identity)); +} + +std::string CHIPCommand::GetIdentity() +{ + std::string name = mCommissionerName.HasValue() ? mCommissionerName.Value() : kIdentityAlpha; + if (name.compare(kIdentityAlpha) != 0 && name.compare(kIdentityBeta) != 0 && name.compare(kIdentityGamma) != 0 && + name.compare(kIdentityNull) != 0) + { + chip::FabricId fabricId = strtoull(name.c_str(), nullptr, 0); + if (fabricId >= kIdentityOtherFabricId) + { + // normalize name since it is used in persistent storage + + char s[24]; + sprintf(s, "%lx", fabricId); + + name = s; + } + else + { + ChipLogError(NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]", name.c_str(), + kIdentityAlpha, kIdentityBeta, kIdentityGamma); + chipDie(); + } + } + + return name; +} + +CHIP_ERROR CHIPCommand::GetIdentityNodeId(std::string identity, chip::NodeId * nodeId) +{ + if (mCommissionerNodeId.HasValue()) + { + *nodeId = mCommissionerNodeId.Value(); + return CHIP_NO_ERROR; + } + + if (identity == kIdentityNull) + { + *nodeId = chip::kUndefinedNodeId; + return CHIP_NO_ERROR; + } + + ReturnLogErrorOnFailure(mCommissionerStorage.Init(identity.c_str(), GetStorageDirectory().ValueOr(nullptr))); + + *nodeId = mCommissionerStorage.GetLocalNodeId(); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR CHIPCommand::GetIdentityRootCertificate(std::string identity, chip::ByteSpan & span) +{ + if (identity == kIdentityNull) + { + return CHIP_ERROR_NOT_FOUND; + } + + chip::NodeId nodeId; + VerifyOrDie(GetIdentityNodeId(identity, &nodeId) == CHIP_NO_ERROR); + CommissionerIdentity lookupKey{ identity, nodeId }; + auto item = mCommissioners.find(lookupKey); + + span = chip::ByteSpan(item->first.mRCAC, item->first.mRCACLen); + return CHIP_NO_ERROR; +} + +chip::FabricId CHIPCommand::CurrentCommissionerId() +{ + chip::FabricId id; + + std::string name = GetIdentity(); + if (name.compare(kIdentityAlpha) == 0) + { + id = kIdentityAlphaFabricId; + } + else if (name.compare(kIdentityBeta) == 0) + { + id = kIdentityBetaFabricId; + } + else if (name.compare(kIdentityGamma) == 0) + { + id = kIdentityGammaFabricId; + } + else if (name.compare(kIdentityNull) == 0) + { + id = kIdentityNullFabricId; + } + else if ((id = strtoull(name.c_str(), nullptr, 0)) < kIdentityOtherFabricId) + { + VerifyOrDieWithMsg(false, NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]", + name.c_str(), kIdentityAlpha, kIdentityBeta, kIdentityGamma); + } + + return id; +} + +chip::Controller::DeviceCommissioner & CHIPCommand::CurrentCommissioner() +{ + return GetCommissioner(GetIdentity()); +} + +chip::Controller::DeviceCommissioner & CHIPCommand::GetCommissioner(std::string identity) +{ + // We don't have a great way to handle commissioner setup failures here. + // This only matters for commands (like TestCommand) that involve multiple + // identities. + VerifyOrDie(EnsureCommissionerForIdentity(identity) == CHIP_NO_ERROR); + + chip::NodeId nodeId; + VerifyOrDie(GetIdentityNodeId(identity, &nodeId) == CHIP_NO_ERROR); + CommissionerIdentity lookupKey{ identity, nodeId }; + auto item = mCommissioners.find(lookupKey); + VerifyOrDie(item != mCommissioners.end()); + return *item->second; +} + +void CHIPCommand::ShutdownCommissioner(const CommissionerIdentity & key) +{ + mCommissioners[key].get()->Shutdown(); +} + +CHIP_ERROR CHIPCommand::InitializeCommissioner(CommissionerIdentity & identity, chip::FabricId fabricId) +{ + std::unique_ptr commissioner = std::make_unique(); + chip::Controller::SetupParams commissionerParams; + + ReturnLogErrorOnFailure(mCredIssuerCmds->SetupDeviceAttestation(commissionerParams, sTrustStore)); + + chip::Crypto::P256Keypair ephemeralKey; + + if (fabricId != chip::kUndefinedFabricId) + { + + // TODO - OpCreds should only be generated for pairing command + // store the credentials in persistent storage, and + // generate when not available in the storage. + ReturnLogErrorOnFailure(mCommissionerStorage.Init(identity.mName.c_str(), GetStorageDirectory().ValueOr(nullptr))); + if (mUseMaxSizedCerts.HasValue()) + { + auto option = CredentialIssuerCommands::CredentialIssuerOptions::kMaximizeCertificateSizes; + mCredIssuerCmds->SetCredentialIssuerOption(option, mUseMaxSizedCerts.Value()); + } + + ReturnLogErrorOnFailure(mCredIssuerCmds->InitializeCredentialsIssuer(mCommissionerStorage)); + + chip::MutableByteSpan nocSpan(identity.mNOC); + chip::MutableByteSpan icacSpan(identity.mICAC); + chip::MutableByteSpan rcacSpan(identity.mRCAC); + + ReturnLogErrorOnFailure(ephemeralKey.Initialize(chip::Crypto::ECPKeyTarget::ECDSA)); + + ReturnLogErrorOnFailure(mCredIssuerCmds->GenerateControllerNOCChain(identity.mLocalNodeId, fabricId, + mCommissionerStorage.GetCommissionerCATs(), + ephemeralKey, rcacSpan, icacSpan, nocSpan)); + + identity.mRCACLen = rcacSpan.size(); + identity.mICACLen = icacSpan.size(); + identity.mNOCLen = nocSpan.size(); + + commissionerParams.operationalKeypair = &ephemeralKey; + commissionerParams.controllerRCAC = rcacSpan; + commissionerParams.controllerICAC = icacSpan; + commissionerParams.controllerNOC = nocSpan; + commissionerParams.permitMultiControllerFabrics = true; + commissionerParams.enableServerInteractions = NeedsOperationalAdvertising(); + } + + // TODO: Initialize IPK epoch key in ExampleOperationalCredentials issuer rather than relying on DefaultIpkValue + commissionerParams.operationalCredentialsDelegate = mCredIssuerCmds->GetCredentialIssuer(); + commissionerParams.controllerVendorId = mCommissionerVendorId.ValueOr(chip::VendorId::TestVendor1); + + ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().SetupCommissioner(commissionerParams, *(commissioner.get()))); + + if (identity.mName != kIdentityNull) + { + // Initialize Group Data, including IPK + chip::FabricIndex fabricIndex = commissioner->GetFabricIndex(); + uint8_t compressed_fabric_id[sizeof(uint64_t)]; + chip::MutableByteSpan compressed_fabric_id_span(compressed_fabric_id); + ReturnLogErrorOnFailure(commissioner->GetCompressedFabricIdBytes(compressed_fabric_id_span)); + + ReturnLogErrorOnFailure(chip::GroupTesting::InitData(&sGroupDataProvider, fabricIndex, compressed_fabric_id_span)); + + // Configure the default IPK for all fabrics used by CHIP-tool. The epoch + // key is the same, but the derived keys will be different for each fabric. + chip::ByteSpan defaultIpk = chip::GroupTesting::DefaultIpkValue::GetDefaultIpk(); + ReturnLogErrorOnFailure( + chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, fabricIndex, defaultIpk, compressed_fabric_id_span)); + } + + CHIPCommand::sICDClientStorage.UpdateFabricList(commissioner->GetFabricIndex()); + + mCommissioners[identity] = std::move(commissioner); + + return CHIP_NO_ERROR; +} + +void CHIPCommand::RunQueuedCommand(intptr_t commandArg) +{ + auto * command = reinterpret_cast(commandArg); + CHIP_ERROR err = command->EnsureCommissionerForIdentity(command->GetIdentity()); + if (err == CHIP_NO_ERROR) + { + err = command->RunCommand(); + } + + if (err != CHIP_NO_ERROR) + { + command->SetCommandExitStatus(err); + } +} + +void CHIPCommand::RunCommandCleanup(intptr_t commandArg) +{ + auto * command = reinterpret_cast(commandArg); + command->CleanupAfterRun(); + command->StopWaiting(); +} + +void CHIPCommand::CleanupAfterRun() +{ + assertChipStackLockedByCurrentThread(); + bool deferCleanup = (IsInteractive() && DeferInteractiveCleanup()); + + Shutdown(); + + if (deferCleanup) + { + sDeferredCleanups.insert(this); + } + else + { + Cleanup(); + } +} + +CHIP_ERROR CHIPCommand::RunOnMatterQueue(MatterWorkCallback callback, chip::System::Clock::Timeout timeout, bool * timedOut) +{ + { + std::lock_guard lk(cvWaitingForResponseMutex); + mWaitingForResponse = true; + } + + auto err = chip::DeviceLayer::PlatformMgr().ScheduleWork(callback, reinterpret_cast(this)); + if (CHIP_NO_ERROR != err) + { + { + std::lock_guard lk(cvWaitingForResponseMutex); + mWaitingForResponse = false; + } + return err; + } + + auto waitingUntil = std::chrono::system_clock::now() + std::chrono::duration_cast(timeout); + { + std::unique_lock lk(cvWaitingForResponseMutex); + *timedOut = !cvWaitingForResponse.wait_until(lk, waitingUntil, [this]() { return !this->mWaitingForResponse; }); + } + + return CHIP_NO_ERROR; +} + +#if !CONFIG_USE_SEPARATE_EVENTLOOP +static void OnResponseTimeout(chip::System::Layer *, void * appState) +{ + (reinterpret_cast(appState))->SetCommandExitStatus(CHIP_ERROR_TIMEOUT); +} +#endif // !CONFIG_USE_SEPARATE_EVENTLOOP + +CHIP_ERROR CHIPCommand::StartWaiting(chip::System::Clock::Timeout duration) +{ +#if CONFIG_USE_SEPARATE_EVENTLOOP + // ServiceEvents() calls StartEventLoopTask(), which is paired with the StopEventLoopTask() below. + if (!IsInteractive()) + { + ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().ServiceEvents()); + } + + if (duration.count() == 0) + { + mCommandExitStatus = RunCommand(); + } + else + { + bool timedOut; + CHIP_ERROR err = RunOnMatterQueue(RunQueuedCommand, duration, &timedOut); + if (CHIP_NO_ERROR != err) + { + return err; + } + if (timedOut) + { + mCommandExitStatus = CHIP_ERROR_TIMEOUT; + } + } + if (!IsInteractive()) + { + LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().StopEventLoopTask()); + } +#else + chip::DeviceLayer::PlatformMgr().ScheduleWork(RunQueuedCommand, reinterpret_cast(this)); + ReturnLogErrorOnFailure(chip::DeviceLayer::SystemLayer().StartTimer(duration, OnResponseTimeout, this)); + chip::DeviceLayer::PlatformMgr().RunEventLoop(); +#endif // CONFIG_USE_SEPARATE_EVENTLOOP + + return mCommandExitStatus; +} + +void CHIPCommand::StopWaiting() +{ +#if CONFIG_USE_SEPARATE_EVENTLOOP + { + std::lock_guard lk(cvWaitingForResponseMutex); + mWaitingForResponse = false; + } + cvWaitingForResponse.notify_all(); +#else // CONFIG_USE_SEPARATE_EVENTLOOP + LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().StopEventLoopTask()); +#endif // CONFIG_USE_SEPARATE_EVENTLOOP +} + +void CHIPCommand::ExecuteDeferredCleanups(intptr_t ignored) +{ + for (auto * cmd : sDeferredCleanups) + { + cmd->Cleanup(); + } + sDeferredCleanups.clear(); +} diff --git a/examples/fabric-admin/commands/common/CHIPCommand.h b/examples/fabric-admin/commands/common/CHIPCommand.h new file mode 100644 index 00000000000000..856a4daa48753d --- /dev/null +++ b/examples/fabric-admin/commands/common/CHIPCommand.h @@ -0,0 +1,264 @@ +/* + * 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. + * + */ + +#pragma once + +#ifdef CONFIG_USE_LOCAL_STORAGE +#include +#endif // CONFIG_USE_LOCAL_STORAGE + +#include "Command.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +inline constexpr char kIdentityAlpha[] = "alpha"; +inline constexpr char kIdentityBeta[] = "beta"; +inline constexpr char kIdentityGamma[] = "gamma"; +// The null fabric commissioner is a commissioner that isn't on a fabric. +// This is a legal configuration in which the commissioner delegates +// operational communication and invocation of the commssioning complete +// command to a separate on-fabric administrator node. +// +// The null-fabric-commissioner identity is provided here to demonstrate the +// commissioner portion of such an architecture. The null-fabric-commissioner +// can carry a commissioning flow up until the point of operational channel +// (CASE) communcation. +inline constexpr char kIdentityNull[] = "null-fabric-commissioner"; + +class CHIPCommand : public Command +{ +public: + using ChipDeviceCommissioner = ::chip::Controller::DeviceCommissioner; + using ChipDeviceController = ::chip::Controller::DeviceController; + using IPAddress = ::chip::Inet::IPAddress; + using NodeId = ::chip::NodeId; + using PeerId = ::chip::PeerId; + using PeerAddress = ::chip::Transport::PeerAddress; + + static constexpr uint16_t kMaxGroupsPerFabric = 50; + static constexpr uint16_t kMaxGroupKeysPerFabric = 25; + + CHIPCommand(const char * commandName, CredentialIssuerCommands * credIssuerCmds, const char * helpText = nullptr) : + Command(commandName, helpText), mCredIssuerCmds(credIssuerCmds) + { + AddArgument("paa-trust-store-path", &mPaaTrustStorePath, + "Path to directory holding PAA certificate information. Can be absolute or relative to the current working " + "directory."); + AddArgument("cd-trust-store-path", &mCDTrustStorePath, + "Path to directory holding CD certificate information. Can be absolute or relative to the current working " + "directory."); + AddArgument("commissioner-name", &mCommissionerName, + "Name of fabric to use. Valid values are \"alpha\", \"beta\", \"gamma\", and integers greater than or equal to " + "4. The default if not specified is \"alpha\"."); + AddArgument("commissioner-nodeid", 0, UINT64_MAX, &mCommissionerNodeId, + "The node id to use for fabric-admin. If not provided, kTestControllerNodeId (112233, 0x1B669) will be used."); + AddArgument("use-max-sized-certs", 0, 1, &mUseMaxSizedCerts, + "Maximize the size of operational certificates. If not provided or 0 (\"false\"), normally sized operational " + "certificates are generated."); + AddArgument("only-allow-trusted-cd-keys", 0, 1, &mOnlyAllowTrustedCdKeys, + "Only allow trusted CD verifying keys (disallow test keys). If not provided or 0 (\"false\"), untrusted CD " + "verifying keys are allowed. If 1 (\"true\"), test keys are disallowed."); +#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED + AddArgument("trace_file", &mTraceFile); + AddArgument("trace_log", 0, 1, &mTraceLog); + AddArgument("trace_decode", 0, 1, &mTraceDecode); +#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED + AddArgument("trace-to", &mTraceTo, "Trace destinations, comma-separated (" SUPPORTED_COMMAND_LINE_TRACING_TARGETS ")"); + AddArgument("ble-adapter", 0, UINT16_MAX, &mBleAdapterId); + AddArgument("storage-directory", &mStorageDirectory, + "Directory to place fabric-admin's storage files in. Defaults to $TMPDIR, with fallback to /tmp"); + AddArgument( + "commissioner-vendor-id", 0, UINT16_MAX, &mCommissionerVendorId, + "The vendor id to use for fabric-admin. If not provided, chip::VendorId::TestVendor1 (65521, 0xFFF1) will be used."); + } + + /////////// Command Interface ///////// + CHIP_ERROR Run() override; + + void SetCommandExitStatus(CHIP_ERROR status) + { + mCommandExitStatus = status; + // In interactive mode the stack is not shut down once a command is ended. + // That means calling `ErrorStr(err)` from the main thread when command + // completion is signaled may race since `ErrorStr` uses a static sErrorStr + // buffer for computing the error string. Call it here instead. + if (IsInteractive() && CHIP_NO_ERROR != status) + { + ChipLogError(NotSpecified, "Run command failure: %s", chip::ErrorStr(status)); + } + StopWaiting(); + } + +protected: + // Will be called in a setting in which it's safe to touch the CHIP + // stack. The rules for Run() are as follows: + // + // 1) If error is returned, Run() must not call SetCommandExitStatus. + // 2) If success is returned Run() must either have called + // SetCommandExitStatus() or scheduled async work that will do that. + virtual CHIP_ERROR RunCommand() = 0; + + // Get the wait duration, in seconds, before the command times out. + virtual chip::System::Clock::Timeout GetWaitDuration() const = 0; + + // Shut down the command. After a Shutdown call the command object is ready + // to be used for another command invocation. + virtual void Shutdown() { ResetArguments(); } + + // Clean up any resources allocated by the command. Some commands may hold + // on to resources after Shutdown(), but Cleanup() will guarantee those are + // cleaned up. + virtual void Cleanup() {} + + // If true, skip calling Cleanup() when in interactive mode, so the command + // can keep doing work as needed. Cleanup() will be called when quitting + // interactive mode. This method will be called before Shutdown, so it can + // use member values that Shutdown will normally reset. + virtual bool DeferInteractiveCleanup() { return false; } + + // If true, the controller will be created with server capabilities enabled, + // such as advertising operational nodes over DNS-SD and accepting incoming + // CASE sessions. + virtual bool NeedsOperationalAdvertising() { return mAdvertiseOperational; } + + // Execute any deferred cleanups. Used when exiting interactive mode. + static void ExecuteDeferredCleanups(intptr_t ignored); + +#ifdef CONFIG_USE_LOCAL_STORAGE + PersistentStorage mDefaultStorage; + // TODO: It's pretty weird that we re-init mCommissionerStorage for every + // identity without shutting it down or something in between... + PersistentStorage mCommissionerStorage; +#endif // CONFIG_USE_LOCAL_STORAGE + chip::PersistentStorageOperationalKeystore mOperationalKeystore; + chip::Credentials::PersistentStorageOpCertStore mOpCertStore; + static chip::Crypto::RawKeySessionKeystore sSessionKeystore; + + static chip::Credentials::GroupDataProviderImpl sGroupDataProvider; + static chip::app::DefaultICDClientStorage sICDClientStorage; + static chip::app::DefaultCheckInDelegate sCheckInDelegate; + static chip::app::CheckInHandler sCheckInHandler; + CredentialIssuerCommands * mCredIssuerCmds; + + std::string GetIdentity(); + CHIP_ERROR GetIdentityNodeId(std::string identity, chip::NodeId * nodeId); + CHIP_ERROR GetIdentityRootCertificate(std::string identity, chip::ByteSpan & span); + void SetIdentity(const char * name); + + // This method returns the commissioner instance to be used for running the command. + // The default commissioner instance name is "alpha", but it can be overridden by passing + // --identity "instance name" when running a command. + ChipDeviceCommissioner & CurrentCommissioner(); + + ChipDeviceCommissioner & GetCommissioner(std::string identity); + +private: + CHIP_ERROR MaybeSetUpStack(); + void MaybeTearDownStack(); + + CHIP_ERROR EnsureCommissionerForIdentity(std::string identity); + + // Commissioners are keyed by name and local node id. + struct CommissionerIdentity + { + bool operator<(const CommissionerIdentity & other) const + { + return mName < other.mName || (mName == other.mName && mLocalNodeId < other.mLocalNodeId); + } + std::string mName; + chip::NodeId mLocalNodeId; + uint8_t mRCAC[chip::Controller::kMaxCHIPDERCertLength] = {}; + uint8_t mICAC[chip::Controller::kMaxCHIPDERCertLength] = {}; + uint8_t mNOC[chip::Controller::kMaxCHIPDERCertLength] = {}; + + size_t mRCACLen; + size_t mICACLen; + size_t mNOCLen; + }; + + // InitializeCommissioner uses various members, so can't be static. This is + // obviously a little odd, since the commissioners are then shared across + // multiple commands in interactive mode... + CHIP_ERROR InitializeCommissioner(CommissionerIdentity & identity, chip::FabricId fabricId); + void ShutdownCommissioner(const CommissionerIdentity & key); + chip::FabricId CurrentCommissionerId(); + + static std::map> mCommissioners; + static std::set sDeferredCleanups; + + chip::Optional mCommissionerName; + chip::Optional mCommissionerNodeId; + chip::Optional mCommissionerVendorId; + chip::Optional mBleAdapterId; + chip::Optional mPaaTrustStorePath; + chip::Optional mCDTrustStorePath; + chip::Optional mUseMaxSizedCerts; + chip::Optional mOnlyAllowTrustedCdKeys; + + // Cached trust store so commands other than the original startup command + // can spin up commissioners as needed. + static const chip::Credentials::AttestationTrustStore * sTrustStore; + + static void RunQueuedCommand(intptr_t commandArg); + typedef decltype(RunQueuedCommand) MatterWorkCallback; + static void RunCommandCleanup(intptr_t commandArg); + + // Do cleanup after a commmand is done running. Must happen with the + // Matter stack locked. + void CleanupAfterRun(); + + // Run the given callback on the Matter thread. Return whether we managed + // to successfully dispatch it to the Matter thread. If we did, *timedOut + // will be set to whether we timed out or whether our mWaitingForResponse + // got set to false by the callback itself. + CHIP_ERROR RunOnMatterQueue(MatterWorkCallback callback, chip::System::Clock::Timeout timeout, bool * timedOut); + + CHIP_ERROR mCommandExitStatus = CHIP_ERROR_INTERNAL; + + CHIP_ERROR StartWaiting(chip::System::Clock::Timeout seconds); + void StopWaiting(); + +#if CONFIG_USE_SEPARATE_EVENTLOOP + std::condition_variable cvWaitingForResponse; + std::mutex cvWaitingForResponseMutex; + bool mWaitingForResponse{ true }; +#endif // CONFIG_USE_SEPARATE_EVENTLOOP + + void StartTracing(); + void StopTracing(); + +#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED + chip::Optional mTraceFile; + chip::Optional mTraceLog; + chip::Optional mTraceDecode; +#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED + + chip::CommandLineApp::TracingSetup mTracingSetup; + chip::Optional> mTraceTo; +}; diff --git a/examples/fabric-admin/commands/common/Command.cpp b/examples/fabric-admin/commands/common/Command.cpp new file mode 100644 index 00000000000000..7e5689130e3e97 --- /dev/null +++ b/examples/fabric-admin/commands/common/Command.cpp @@ -0,0 +1,1088 @@ +/* + * 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 "Command.h" +#include "CustomStringPrefix.h" +#include "HexConversion.h" +#include "platform/PlatformManager.h" + +#include +#include +#include +#include +#include +#include + +#include // For INFINITY + +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr char kOptionalArgumentPrefix[] = "--"; +constexpr size_t kOptionalArgumentPrefixLength = 2; + +bool Command::InitArguments(int argc, char ** argv) +{ + bool isValidCommand = false; + + size_t argvExtraArgsCount = (size_t) argc; + size_t mandatoryArgsCount = 0; + size_t optionalArgsCount = 0; + for (auto & arg : mArgs) + { + if (arg.isOptional()) + { + optionalArgsCount++; + } + else + { + mandatoryArgsCount++; + argvExtraArgsCount--; + } + } + + VerifyOrExit((size_t) (argc) >= mandatoryArgsCount && (argvExtraArgsCount == 0 || (argvExtraArgsCount && optionalArgsCount)), + ChipLogError(NotSpecified, "InitArgs: Wrong arguments number: %d instead of %u", argc, + static_cast(mandatoryArgsCount))); + + // Initialize mandatory arguments + for (size_t i = 0; i < mandatoryArgsCount; i++) + { + char * arg = argv[i]; + if (!InitArgument(i, arg)) + { + ExitNow(); + } + } + + // Initialize optional arguments + // Optional arguments expect a name and a value, so i is increased by 2 on every step. + for (size_t i = mandatoryArgsCount; i < (size_t) argc; i += 2) + { + bool found = false; + for (size_t j = mandatoryArgsCount; j < mandatoryArgsCount + optionalArgsCount; j++) + { + // optional arguments starts with kOptionalArgumentPrefix + if (strlen(argv[i]) <= kOptionalArgumentPrefixLength && + strncmp(argv[i], kOptionalArgumentPrefix, kOptionalArgumentPrefixLength) != 0) + { + continue; + } + + if (strcmp(argv[i] + strlen(kOptionalArgumentPrefix), mArgs[j].name) == 0) + { + found = true; + + VerifyOrExit((size_t) argc > (i + 1), + ChipLogError(NotSpecified, "InitArgs: Optional argument %s missing value.", argv[i])); + if (!InitArgument(j, argv[i + 1])) + { + ExitNow(); + } + } + } + VerifyOrExit(found, ChipLogError(NotSpecified, "InitArgs: Optional argument %s does not exist.", argv[i])); + } + + isValidCommand = true; + +exit: + return isValidCommand; +} + +static bool ParseAddressWithInterface(const char * addressString, Command::AddressWithInterface * address) +{ + struct addrinfo hints; + struct addrinfo * result; + int ret; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + ret = getaddrinfo(addressString, nullptr, &hints, &result); + if (ret < 0) + { + ChipLogError(NotSpecified, "Invalid address: %s", addressString); + return false; + } + + if (result->ai_family == AF_INET6) + { + struct sockaddr_in6 * addr = reinterpret_cast(result->ai_addr); + address->address = ::chip::Inet::IPAddress::FromSockAddr(*addr); + address->interfaceId = ::chip::Inet::InterfaceId(addr->sin6_scope_id); + } +#if INET_CONFIG_ENABLE_IPV4 + else if (result->ai_family == AF_INET) + { + address->address = ::chip::Inet::IPAddress::FromSockAddr(*reinterpret_cast(result->ai_addr)); + address->interfaceId = chip::Inet::InterfaceId::Null(); + } +#endif // INET_CONFIG_ENABLE_IPV4 + else + { + ChipLogError(NotSpecified, "Unsupported address: %s", addressString); + return false; + } + + return true; +} + +// The callback should return whether the argument is valid, for the non-null +// case. It can't directly write to isValidArgument (by closing over it) +// because in the nullable-and-null case we need to do that from this function, +// via the return value. +template +bool HandleNullableOptional(Argument & arg, char * argValue, std::function callback) +{ + if (arg.isOptional()) + { + if (arg.isNullable()) + { + arg.value = &(reinterpret_cast> *>(arg.value)->Emplace()); + } + else + { + arg.value = &(reinterpret_cast *>(arg.value)->Emplace()); + } + } + + if (arg.isNullable()) + { + auto * nullable = reinterpret_cast *>(arg.value); + if (argValue != nullptr && strncmp(argValue, "null", 4) == 0) + { + nullable->SetNull(); + return true; + } + + arg.value = &(nullable->SetNonNull()); + } + + return callback(reinterpret_cast(arg.value)); +} + +bool Command::InitArgument(size_t argIndex, char * argValue) +{ + bool isValidArgument = false; + bool isHexNotation = strncmp(argValue, "0x", 2) == 0 || strncmp(argValue, "0X", 2) == 0; + + Argument arg = mArgs.at(argIndex); + + // We have two places where we handle uint8_t-typed args (actual int8u and + // bool args), so declare the handler function here so it can be reused. + auto uint8Handler = [&](uint8_t * value) { + // stringstream treats uint8_t as char, which is not what we want here. + uint16_t tmpValue; + std::stringstream ss; + isHexNotation ? (ss << std::hex << argValue) : (ss << argValue); + ss >> tmpValue; + if (chip::CanCastTo(tmpValue)) + { + *value = static_cast(tmpValue); + + uint64_t min = chip::CanCastTo(arg.min) ? static_cast(arg.min) : 0; + uint64_t max = arg.max; + return (!ss.fail() && ss.eof() && *value >= min && *value <= max); + } + + return false; + }; + + switch (arg.type) + { + case ArgumentType::Complex: { + // Complex arguments may be optional, but they are not currently supported via the class. + // Instead, they must be explicitly specified as optional using the kOptional flag, + // and the base TypedComplexArgument class is still referenced. + auto complexArgument = static_cast(arg.value); + return CHIP_NO_ERROR == complexArgument->Parse(arg.name, argValue); + } + + case ArgumentType::Custom: { + auto customArgument = static_cast(arg.value); + return CHIP_NO_ERROR == customArgument->Parse(arg.name, argValue); + } + + case ArgumentType::VectorString: { + std::vector vectorArgument; + + chip::StringSplitter splitter(argValue, ','); + chip::CharSpan value; + + while (splitter.Next(value)) + { + vectorArgument.push_back(std::string(value.data(), value.size())); + } + + if (arg.flags == Argument::kOptional) + { + auto argument = static_cast> *>(arg.value); + argument->SetValue(vectorArgument); + } + else + { + auto argument = static_cast *>(arg.value); + *argument = vectorArgument; + } + return true; + } + case ArgumentType::VectorBool: { + // Currently only chip::Optional> is supported. + if (arg.flags != Argument::kOptional) + { + return false; + } + + std::vector vectorArgument; + std::stringstream ss(argValue); + while (ss.good()) + { + std::string valueAsString; + getline(ss, valueAsString, ','); + + if (strcasecmp(valueAsString.c_str(), "true") == 0) + { + vectorArgument.push_back(true); + } + else if (strcasecmp(valueAsString.c_str(), "false") == 0) + { + vectorArgument.push_back(false); + } + else + { + return false; + } + } + + auto optionalArgument = static_cast> *>(arg.value); + optionalArgument->SetValue(vectorArgument); + return true; + } + + case ArgumentType::Vector16: + case ArgumentType::Vector32: { + std::vector values; + uint64_t min = chip::CanCastTo(arg.min) ? static_cast(arg.min) : 0; + uint64_t max = arg.max; + + std::stringstream ss(argValue); + while (ss.good()) + { + std::string valueAsString; + getline(ss, valueAsString, ','); + isHexNotation = strncmp(valueAsString.c_str(), "0x", 2) == 0 || strncmp(valueAsString.c_str(), "0X", 2) == 0; + + std::stringstream subss; + isHexNotation ? subss << std::hex << valueAsString : subss << valueAsString; + + uint64_t value; + subss >> value; + VerifyOrReturnError(!subss.fail() && subss.eof() && value >= min && value <= max, false); + values.push_back(value); + } + + if (arg.type == ArgumentType::Vector16) + { + auto vectorArgument = static_cast *>(arg.value); + for (uint64_t v : values) + { + vectorArgument->push_back(static_cast(v)); + } + } + else if (arg.type == ArgumentType::Vector32 && arg.flags != Argument::kOptional) + { + auto vectorArgument = static_cast *>(arg.value); + for (uint64_t v : values) + { + vectorArgument->push_back(static_cast(v)); + } + } + else if (arg.type == ArgumentType::Vector32 && arg.flags == Argument::kOptional) + { + std::vector vectorArgument; + for (uint64_t v : values) + { + vectorArgument.push_back(static_cast(v)); + } + + auto optionalArgument = static_cast> *>(arg.value); + optionalArgument->SetValue(vectorArgument); + } + else + { + return false; + } + + return true; + } + + case ArgumentType::VectorCustom: { + auto vectorArgument = static_cast *>(arg.value); + + std::stringstream ss(argValue); + while (ss.good()) + { + std::string valueAsString; + // By default the parameter separator is ";" in order to not collapse with the argument itself if it contains commas + // (e.g a struct argument with multiple fields). In case one needs to use ";" it can be overriden with the following + // environment variable. + static constexpr char kSeparatorVariable[] = "NotSpecified,_CUSTOM_ARGUMENTS_SEPARATOR"; + char * getenvSeparatorVariableResult = getenv(kSeparatorVariable); + getline(ss, valueAsString, getenvSeparatorVariableResult ? getenvSeparatorVariableResult[0] : ';'); + + CustomArgument * customArgument = new CustomArgument(); + vectorArgument->push_back(customArgument); + VerifyOrReturnError(CHIP_NO_ERROR == vectorArgument->back()->Parse(arg.name, valueAsString.c_str()), false); + } + + return true; + } + + case ArgumentType::String: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + *value = argValue; + return true; + }); + break; + } + + case ArgumentType::CharString: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + *value = chip::Span(argValue, strlen(argValue)); + return true; + }); + break; + } + + case ArgumentType::OctetString: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + // We support two ways to pass an octet string argument. If it happens + // to be all-ASCII, you can just pass it in. Otherwise you can pass in + // "hex:" followed by the hex-encoded bytes. + size_t argLen = strlen(argValue); + + if (IsHexString(argValue)) + { + // Hex-encoded. Decode it into a temporary buffer first, so if we + // run into errors we can do correct "argument is not valid" logging + // that actually shows the value that was passed in. After we + // determine it's valid, modify the passed-in value to hold the + // right bytes, so we don't need to worry about allocating storage + // for this somewhere else. This works because the hex + // representation is always longer than the octet string it encodes, + // so we have enough space in argValue for the decoded version. + chip::Platform::ScopedMemoryBuffer buffer; + + size_t octetCount; + CHIP_ERROR err = HexToBytes( + chip::CharSpan(argValue + kHexStringPrefixLen, argLen - kHexStringPrefixLen), + [&buffer](size_t allocSize) { + buffer.Calloc(allocSize); + return buffer.Get(); + }, + &octetCount); + if (err != CHIP_NO_ERROR) + { + return false; + } + + memcpy(argValue, buffer.Get(), octetCount); + *value = chip::ByteSpan(chip::Uint8::from_char(argValue), octetCount); + return true; + } + + // Just ASCII. Check for the "str:" prefix. + if (IsStrString(argValue)) + { + // Skip the prefix + argValue += kStrStringPrefixLen; + argLen -= kStrStringPrefixLen; + } + *value = chip::ByteSpan(chip::Uint8::from_char(argValue), argLen); + return true; + }); + break; + } + + case ArgumentType::Bool: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + // Start with checking for actual boolean values. + if (strcasecmp(argValue, "true") == 0) + { + *value = true; + return true; + } + + if (strcasecmp(argValue, "false") == 0) + { + *value = false; + return true; + } + + // For backwards compat, keep accepting 0 and 1 for now as synonyms + // for false and true. Since we set our min to 0 and max to 1 for + // booleans, calling uint8Handler does the right thing in terms of + // only allowing those two values. + uint8_t temp = 0; + if (!uint8Handler(&temp)) + { + return false; + } + *value = (temp == 1); + return true; + }); + break; + } + + case ArgumentType::Number_uint8: { + isValidArgument = HandleNullableOptional(arg, argValue, uint8Handler); + break; + } + + case ArgumentType::Number_uint16: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + std::stringstream ss; + isHexNotation ? ss << std::hex << argValue : ss << argValue; + ss >> *value; + + uint64_t min = chip::CanCastTo(arg.min) ? static_cast(arg.min) : 0; + uint64_t max = arg.max; + return (!ss.fail() && ss.eof() && *value >= min && *value <= max); + }); + break; + } + + case ArgumentType::Number_uint32: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + std::stringstream ss; + isHexNotation ? ss << std::hex << argValue : ss << argValue; + ss >> *value; + + uint64_t min = chip::CanCastTo(arg.min) ? static_cast(arg.min) : 0; + uint64_t max = arg.max; + return (!ss.fail() && ss.eof() && *value >= min && *value <= max); + }); + break; + } + + case ArgumentType::Number_uint64: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + std::stringstream ss; + isHexNotation ? ss << std::hex << argValue : ss << argValue; + ss >> *value; + + uint64_t min = chip::CanCastTo(arg.min) ? static_cast(arg.min) : 0; + uint64_t max = arg.max; + return (!ss.fail() && ss.eof() && *value >= min && *value <= max); + }); + break; + } + + case ArgumentType::Number_int8: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + // stringstream treats int8_t as char, which is not what we want here. + int16_t tmpValue; + std::stringstream ss; + isHexNotation ? ss << std::hex << argValue : ss << argValue; + ss >> tmpValue; + if (chip::CanCastTo(tmpValue)) + { + *value = static_cast(tmpValue); + + int64_t min = arg.min; + int64_t max = chip::CanCastTo(arg.max) ? static_cast(arg.max) : INT64_MAX; + return (!ss.fail() && ss.eof() && *value >= min && *value <= max); + } + + return false; + }); + break; + } + + case ArgumentType::Number_int16: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + std::stringstream ss; + isHexNotation ? ss << std::hex << argValue : ss << argValue; + ss >> *value; + + int64_t min = arg.min; + int64_t max = chip::CanCastTo(arg.max) ? static_cast(arg.max) : INT64_MAX; + return (!ss.fail() && ss.eof() && *value >= min && *value <= max); + }); + break; + } + + case ArgumentType::Number_int32: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + std::stringstream ss; + isHexNotation ? ss << std::hex << argValue : ss << argValue; + ss >> *value; + + int64_t min = arg.min; + int64_t max = chip::CanCastTo(arg.max) ? static_cast(arg.max) : INT64_MAX; + return (!ss.fail() && ss.eof() && *value >= min && *value <= max); + }); + break; + } + + case ArgumentType::Number_int64: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + std::stringstream ss; + isHexNotation ? ss << std::hex << argValue : ss << argValue; + ss >> *value; + + int64_t min = arg.min; + int64_t max = chip::CanCastTo(arg.max) ? static_cast(arg.max) : INT64_MAX; + return (!ss.fail() && ss.eof() && *value >= min && *value <= max); + }); + break; + } + + case ArgumentType::Float: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + if (strcmp(argValue, "Infinity") == 0) + { + *value = INFINITY; + return true; + } + + if (strcmp(argValue, "-Infinity") == 0) + { + *value = -INFINITY; + return true; + } + + std::stringstream ss; + ss << argValue; + ss >> *value; + return (!ss.fail() && ss.eof()); + }); + break; + } + + case ArgumentType::Double: { + isValidArgument = HandleNullableOptional(arg, argValue, [&](auto * value) { + if (strcmp(argValue, "Infinity") == 0) + { + *value = INFINITY; + return true; + } + + if (strcmp(argValue, "-Infinity") == 0) + { + *value = -INFINITY; + return true; + } + + std::stringstream ss; + ss << argValue; + ss >> *value; + return (!ss.fail() && ss.eof()); + }); + break; + } + + case ArgumentType::Address: { + isValidArgument = HandleNullableOptional( + arg, argValue, [&](auto * value) { return ParseAddressWithInterface(argValue, value); }); + break; + } + } + + if (!isValidArgument) + { + ChipLogError(NotSpecified, "InitArgs: Invalid argument %s: %s", arg.name, argValue); + } + + return isValidArgument; +} + +void Command::AddArgument(const char * name, const char * value, const char * desc) +{ + ReadOnlyGlobalCommandArgument arg; + arg.name = name; + arg.value = value; + arg.desc = desc; + + mReadOnlyGlobalCommandArgument.SetValue(arg); +} + +size_t Command::AddArgument(const char * name, char ** value, const char * desc, uint8_t flags) +{ + Argument arg; + arg.type = ArgumentType::String; + arg.name = name; + arg.value = reinterpret_cast(value); + arg.flags = flags; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, chip::CharSpan * value, const char * desc, uint8_t flags) +{ + Argument arg; + arg.type = ArgumentType::CharString; + arg.name = name; + arg.value = reinterpret_cast(value); + arg.flags = flags; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, chip::ByteSpan * value, const char * desc, uint8_t flags) +{ + Argument arg; + arg.type = ArgumentType::OctetString; + arg.name = name; + arg.value = reinterpret_cast(value); + arg.flags = flags; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, AddressWithInterface * out, const char * desc, uint8_t flags) +{ + Argument arg; + arg.type = ArgumentType::Address; + arg.name = name; + arg.value = reinterpret_cast(out); + arg.flags = flags; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, std::vector * value, const char * desc) +{ + Argument arg; + arg.type = ArgumentType::Vector16; + arg.name = name; + arg.value = static_cast(value); + arg.min = min; + arg.max = max; + arg.flags = 0; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, std::vector * value, const char * desc) +{ + Argument arg; + arg.type = ArgumentType::Vector32; + arg.name = name; + arg.value = static_cast(value); + arg.min = min; + arg.max = max; + arg.flags = 0; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional> * value, + const char * desc) +{ + Argument arg; + arg.type = ArgumentType::Vector32; + arg.name = name; + arg.value = static_cast(value); + arg.min = min; + arg.max = max; + arg.flags = Argument::kOptional; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional> * value, + const char * desc) +{ + Argument arg; + arg.type = ArgumentType::VectorBool; + arg.name = name; + arg.value = static_cast(value); + arg.min = min; + arg.max = max; + arg.flags = Argument::kOptional; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, ComplexArgument * value, const char * desc, uint8_t flags) +{ + Argument arg; + arg.type = ArgumentType::Complex; + arg.name = name; + arg.value = static_cast(value); + arg.flags = flags; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, CustomArgument * value, const char * desc) +{ + Argument arg; + arg.type = ArgumentType::Custom; + arg.name = name; + arg.value = const_cast(reinterpret_cast(value)); + arg.flags = 0; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, std::vector * value, const char * desc) +{ + Argument arg; + arg.type = ArgumentType::VectorCustom; + arg.name = name; + arg.value = static_cast(value); + arg.flags = 0; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, float min, float max, float * out, const char * desc, uint8_t flags) +{ + Argument arg; + arg.type = ArgumentType::Float; + arg.name = name; + arg.value = reinterpret_cast(out); + arg.flags = flags; + arg.desc = desc; + // Ignore min/max for now; they're always +-Infinity anyway. + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, double min, double max, double * out, const char * desc, uint8_t flags) +{ + Argument arg; + arg.type = ArgumentType::Double; + arg.name = name; + arg.value = reinterpret_cast(out); + arg.flags = flags; + arg.desc = desc; + // Ignore min/max for now; they're always +-Infinity anyway. + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, void * out, ArgumentType type, const char * desc, + uint8_t flags) +{ + Argument arg; + arg.type = type; + arg.name = name; + arg.value = out; + arg.min = min; + arg.max = max; + arg.flags = flags; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, void * out, const char * desc, uint8_t flags) +{ + Argument arg; + arg.type = ArgumentType::Number_uint8; + arg.name = name; + arg.value = out; + arg.min = min; + arg.max = max; + arg.flags = flags; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, std::vector * value, const char * desc) +{ + Argument arg; + arg.type = ArgumentType::VectorString; + arg.name = name; + arg.value = static_cast(value); + arg.flags = 0; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +size_t Command::AddArgument(const char * name, chip::Optional> * value, const char * desc) +{ + Argument arg; + arg.type = ArgumentType::VectorString; + arg.name = name; + arg.value = static_cast(value); + arg.flags = Argument::kOptional; + arg.desc = desc; + + return AddArgumentToList(std::move(arg)); +} + +const char * Command::GetArgumentName(size_t index) const +{ + if (index < mArgs.size()) + { + return mArgs.at(index).name; + } + + return nullptr; +} + +const char * Command::GetArgumentDescription(size_t index) const +{ + if (index < mArgs.size()) + { + return mArgs.at(index).desc; + } + + return nullptr; +} + +const char * Command::GetReadOnlyGlobalCommandArgument() const +{ + if (GetAttribute()) + { + return GetAttribute(); + } + + if (GetEvent()) + { + return GetEvent(); + } + + return nullptr; +} + +const char * Command::GetAttribute() const +{ + if (mReadOnlyGlobalCommandArgument.HasValue()) + { + return mReadOnlyGlobalCommandArgument.Value().value; + } + + return nullptr; +} + +const char * Command::GetEvent() const +{ + if (mReadOnlyGlobalCommandArgument.HasValue()) + { + return mReadOnlyGlobalCommandArgument.Value().value; + } + + return nullptr; +} + +size_t Command::AddArgumentToList(Argument && argument) +{ + if (argument.isOptional() || mArgs.empty() || !mArgs.back().isOptional()) + { + // Safe to just append. + mArgs.emplace_back(std::move(argument)); + return mArgs.size(); + } + + // We're inserting a non-optional arg but we already have something optional + // in the list. Insert before the first optional arg. + for (auto cur = mArgs.cbegin(), end = mArgs.cend(); cur != end; ++cur) + { + if ((*cur).isOptional()) + { + mArgs.emplace(cur, std::move(argument)); + return mArgs.size(); + } + } + + // Never reached. + VerifyOrDie(false); + return 0; +} + +namespace { +template +void ResetOptionalArg(const Argument & arg) +{ + VerifyOrDie(arg.isOptional()); + + if (arg.isNullable()) + { + reinterpret_cast> *>(arg.value)->ClearValue(); + } + else + { + reinterpret_cast *>(arg.value)->ClearValue(); + } +} +} // anonymous namespace + +void Command::ResetArguments() +{ + for (const auto & arg : mArgs) + { + const ArgumentType type = arg.type; + if (arg.isOptional()) + { + // Must always clean these up so they don't carry over to the next + // command invocation in interactive mode. + switch (type) + { + case ArgumentType::Complex: { + // Optional Complex arguments are not currently supported via the class. + // Instead, they must be explicitly specified as optional using the kOptional flag, + // and the base TypedComplexArgument class is referenced. + auto argument = static_cast(arg.value); + argument->Reset(); + break; + } + case ArgumentType::Custom: { + // No optional custom arguments so far. + VerifyOrDie(false); + break; + } + case ArgumentType::VectorString: { + ResetOptionalArg>(arg); + break; + } + case ArgumentType::VectorBool: { + ResetOptionalArg>(arg); + break; + } + case ArgumentType::Vector16: { + // No optional Vector16 arguments so far. + VerifyOrDie(false); + break; + } + case ArgumentType::Vector32: { + ResetOptionalArg>(arg); + break; + } + case ArgumentType::VectorCustom: { + // No optional VectorCustom arguments so far. + VerifyOrDie(false); + break; + } + case ArgumentType::String: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::CharString: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::OctetString: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Bool: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Number_uint8: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Number_uint16: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Number_uint32: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Number_uint64: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Number_int8: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Number_int16: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Number_int32: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Number_int64: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Float: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Double: { + ResetOptionalArg(arg); + break; + } + case ArgumentType::Address: { + ResetOptionalArg(arg); + break; + } + } + } + else + { + // Some non-optional arguments have state that needs to be cleaned + // up too. + if (type == ArgumentType::Vector16) + { + auto vectorArgument = static_cast *>(arg.value); + vectorArgument->clear(); + } + else if (type == ArgumentType::Vector32) + { + auto vectorArgument = static_cast *>(arg.value); + vectorArgument->clear(); + } + else if (type == ArgumentType::VectorCustom) + { + auto vectorArgument = static_cast *>(arg.value); + for (auto & customArgument : *vectorArgument) + { + delete customArgument; + } + vectorArgument->clear(); + } + else if (type == ArgumentType::Complex) + { + auto argument = static_cast(arg.value); + argument->Reset(); + } + } + } +} diff --git a/examples/fabric-admin/commands/common/Command.h b/examples/fabric-admin/commands/common/Command.h new file mode 100644 index 00000000000000..ec8b51dd87b8f4 --- /dev/null +++ b/examples/fabric-admin/commands/common/Command.h @@ -0,0 +1,307 @@ +/* + * 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. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +class Command; + +template +std::unique_ptr make_unique(Args &&... args) +{ + return std::unique_ptr(new T(std::forward(args)...)); +} + +struct movable_initializer_list +{ + movable_initializer_list(std::unique_ptr && in) : item(std::move(in)) {} + operator std::unique_ptr() const && { return std::move(item); } + mutable std::unique_ptr item; +}; + +typedef std::initializer_list commands_list; + +enum ArgumentType +{ + Number_uint8, + Number_uint16, + Number_uint32, + Number_uint64, + Number_int8, + Number_int16, + Number_int32, + Number_int64, + Float, + Double, + Bool, + String, + CharString, + OctetString, + Address, + Complex, + Custom, + VectorBool, + Vector16, + Vector32, + VectorCustom, + VectorString, // comma separated string items +}; + +struct Argument +{ + const char * name; + ArgumentType type; + int64_t min; + uint64_t max; + void * value; + uint8_t flags; + const char * desc; + + enum + { + kOptional = (1 << 0), + kNullable = (1 << 1), + }; + + bool isOptional() const { return flags & kOptional; } + bool isNullable() const { return flags & kNullable; } +}; + +struct ReadOnlyGlobalCommandArgument +{ + const char * name; + const char * value; + const char * desc; +}; + +class Command +{ +public: + struct AddressWithInterface + { + ::chip::Inet::IPAddress address; + ::chip::Inet::InterfaceId interfaceId; + }; + + Command(const char * commandName, const char * helpText = nullptr) : mName(commandName), mHelpText(helpText) {} + virtual ~Command() {} + + const char * GetName(void) const { return mName; } + const char * GetHelpText() const { return mHelpText; } + const char * GetReadOnlyGlobalCommandArgument(void) const; + const char * GetAttribute(void) const; + const char * GetEvent(void) const; + const char * GetArgumentName(size_t index) const; + const char * GetArgumentDescription(size_t index) const; + bool GetArgumentIsOptional(size_t index) const { return mArgs[index].isOptional(); } + size_t GetArgumentsCount(void) const { return mArgs.size(); } + + bool InitArguments(int argc, char ** argv); + void AddArgument(const char * name, const char * value, const char * desc = ""); + /** + * @brief + * Add a char string command argument + * + * @param name The name that will be displayed in the command help + * @param value A pointer to a `char *` where the argv value will be stored + * @param flags + * @param desc The description of the argument that will be displayed in the command help + * @returns The number of arguments currently added to the command + */ + size_t AddArgument(const char * name, char ** value, const char * desc = "", uint8_t flags = 0); + + /** + * Add an octet string command argument + */ + size_t AddArgument(const char * name, chip::ByteSpan * value, const char * desc = "", uint8_t flags = 0); + size_t AddArgument(const char * name, chip::Span * value, const char * desc = "", uint8_t flags = 0); + size_t AddArgument(const char * name, AddressWithInterface * out, const char * desc = "", uint8_t flags = 0); + // Optional Complex arguments are not currently supported via the class. + // Instead, they must be explicitly specified as optional using kOptional in the flags parameter, + // and the base TypedComplexArgument class is referenced. + size_t AddArgument(const char * name, ComplexArgument * value, const char * desc = "", uint8_t flags = 0); + size_t AddArgument(const char * name, CustomArgument * value, const char * desc = ""); + size_t AddArgument(const char * name, int64_t min, uint64_t max, bool * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Bool, desc, flags); + } + size_t AddArgument(const char * name, int64_t min, uint64_t max, int8_t * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Number_int8, desc, flags); + } + size_t AddArgument(const char * name, int64_t min, uint64_t max, int16_t * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Number_int16, desc, flags); + } + size_t AddArgument(const char * name, int64_t min, uint64_t max, int32_t * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Number_int32, desc, flags); + } + size_t AddArgument(const char * name, int64_t min, uint64_t max, int64_t * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Number_int64, desc, flags); + } + size_t AddArgument(const char * name, int64_t min, uint64_t max, uint8_t * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Number_uint8, desc, flags); + } + size_t AddArgument(const char * name, int64_t min, uint64_t max, uint16_t * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Number_uint16, desc, flags); + } + size_t AddArgument(const char * name, int64_t min, uint64_t max, uint32_t * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Number_uint32, desc, flags); + } + size_t AddArgument(const char * name, int64_t min, uint64_t max, uint64_t * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(out), Number_uint64, desc, flags); + } + + size_t AddArgument(const char * name, float min, float max, float * out, const char * desc = "", uint8_t flags = 0); + size_t AddArgument(const char * name, double min, double max, double * out, const char * desc = "", uint8_t flags = 0); + + size_t AddArgument(const char * name, int64_t min, uint64_t max, std::vector * value, const char * desc = ""); + size_t AddArgument(const char * name, int64_t min, uint64_t max, std::vector * value, const char * desc = ""); + size_t AddArgument(const char * name, std::vector * value, const char * desc = ""); + size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional> * value, + const char * desc = ""); + size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional> * value, + const char * desc = ""); + + template ::value>> + size_t AddArgument(const char * name, int64_t min, uint64_t max, T * out, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast *>(out), desc, flags); + } + + template + size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::BitFlags * out, const char * desc = "", + uint8_t flags = 0) + { + // This is a terrible hack that relies on BitFlags only having the one + // mValue member. + return AddArgument(name, min, max, reinterpret_cast(out), desc, flags); + } + + template + size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::BitMask * out, const char * desc = "", + uint8_t flags = 0) + { + // This is a terrible hack that relies on BitMask only having the one + // mValue member. + return AddArgument(name, min, max, reinterpret_cast(out), desc, flags); + } + + template + size_t AddArgument(const char * name, chip::Optional * value, const char * desc = "") + { + return AddArgument(name, reinterpret_cast(value), desc, Argument::kOptional); + } + + template + size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional * value, const char * desc = "") + { + return AddArgument(name, min, max, reinterpret_cast(value), desc, Argument::kOptional); + } + + template + size_t AddArgument(const char * name, chip::app::DataModel::Nullable * value, const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, reinterpret_cast(value), desc, flags | Argument::kNullable); + } + + template + size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::app::DataModel::Nullable * value, + const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(value), desc, flags | Argument::kNullable); + } + + size_t AddArgument(const char * name, float min, float max, chip::app::DataModel::Nullable * value, + const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(value), desc, flags | Argument::kNullable); + } + + size_t AddArgument(const char * name, double min, double max, chip::app::DataModel::Nullable * value, + const char * desc = "", uint8_t flags = 0) + { + return AddArgument(name, min, max, reinterpret_cast(value), desc, flags | Argument::kNullable); + } + + size_t AddArgument(const char * name, std::vector * value, const char * desc); + size_t AddArgument(const char * name, chip::Optional> * value, const char * desc); + + void ResetArguments(); + + virtual CHIP_ERROR Run() = 0; + + bool IsInteractive() { return mIsInteractive; } + + CHIP_ERROR RunAsInteractive(const chip::Optional & interactiveStorageDirectory, bool advertiseOperational) + { + mStorageDirectory = interactiveStorageDirectory; + mIsInteractive = true; + mAdvertiseOperational = advertiseOperational; + return Run(); + } + + const chip::Optional & GetStorageDirectory() const { return mStorageDirectory; } + +protected: + // mStorageDirectory lives here so we can just set it in RunAsInteractive. + chip::Optional mStorageDirectory; + + // mAdvertiseOperational lives here so we can just set it in + // RunAsInteractive; it's only used by CHIPCommand. + bool mAdvertiseOperational = false; + +private: + bool InitArgument(size_t argIndex, char * argValue); + size_t AddArgument(const char * name, int64_t min, uint64_t max, void * out, ArgumentType type, const char * desc, + uint8_t flags); + size_t AddArgument(const char * name, int64_t min, uint64_t max, void * out, const char * desc, uint8_t flags); + + /** + * Add the Argument to our list. This preserves the property that all + * optional arguments come at the end of the list. + */ + size_t AddArgumentToList(Argument && argument); + + const char * mName = nullptr; + const char * mHelpText = nullptr; + bool mIsInteractive = false; + + chip::Optional mReadOnlyGlobalCommandArgument; + std::vector mArgs; +}; diff --git a/examples/fabric-admin/commands/common/Commands.cpp b/examples/fabric-admin/commands/common/Commands.cpp new file mode 100644 index 00000000000000..3742978443796a --- /dev/null +++ b/examples/fabric-admin/commands/common/Commands.cpp @@ -0,0 +1,703 @@ +/* + * 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 "Commands.h" + +#include "Command.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../clusters/JsonParser.h" + +namespace { + +char kInteractiveModeName[] = ""; +constexpr size_t kInteractiveModeArgumentsMaxLength = 32; +constexpr char kOptionalArgumentPrefix[] = "--"; +constexpr char kJsonClusterKey[] = "cluster"; +constexpr char kJsonCommandKey[] = "command"; +constexpr char kJsonCommandSpecifierKey[] = "command_specifier"; +constexpr char kJsonArgumentsKey[] = "arguments"; + +#if !CHIP_DISABLE_PLATFORM_KVS +template +struct HasInitWithString +{ + template + static constexpr auto check(U *) -> typename std::is_same().Init("")), CHIP_ERROR>::type; + + template + static constexpr std::false_type check(...); + + typedef decltype(check>(nullptr)) type; + +public: + static constexpr bool value = type::value; +}; + +// Template so we can do conditional enabling +template ::value, int> = 0> +static void UseStorageDirectory(T & storageManagerImpl, const char * storageDirectory) +{ + std::string platformKVS = std::string(storageDirectory) + "/chip_tool_kvs"; + storageManagerImpl.Init(platformKVS.c_str()); +} + +template ::value, int> = 0> +static void UseStorageDirectory(T & storageManagerImpl, const char * storageDirectory) +{} +#endif // !CHIP_DISABLE_PLATFORM_KVS + +bool GetArgumentsFromJson(Command * command, Json::Value & value, bool optional, std::vector & outArgs) +{ + auto memberNames = value.getMemberNames(); + + std::vector args; + for (size_t i = 0; i < command->GetArgumentsCount(); i++) + { + auto argName = command->GetArgumentName(i); + auto memberNamesIterator = memberNames.begin(); + while (memberNamesIterator != memberNames.end()) + { + auto memberName = *memberNamesIterator; + if (strcasecmp(argName, memberName.c_str()) != 0) + { + memberNamesIterator++; + continue; + } + + if (command->GetArgumentIsOptional(i) != optional) + { + memberNamesIterator = memberNames.erase(memberNamesIterator); + continue; + } + + if (optional) + { + args.push_back(std::string(kOptionalArgumentPrefix) + argName); + } + + auto argValue = value[memberName].asString(); + args.push_back(std::move(argValue)); + memberNamesIterator = memberNames.erase(memberNamesIterator); + break; + } + } + + if (memberNames.size()) + { + auto memberName = memberNames.front(); + ChipLogError(NotSpecified, "The argument \"\%s\" is not supported.", memberName.c_str()); + return false; + } + + outArgs = args; + return true; +}; + +// Check for arguments with a starting '"' but no ending '"': those +// would indicate that people are using double-quoting, not single +// quoting, on arguments with spaces. +static void DetectAndLogMismatchedDoubleQuotes(int argc, char ** argv) +{ + for (int curArg = 0; curArg < argc; ++curArg) + { + char * arg = argv[curArg]; + if (!arg) + { + continue; + } + + auto len = strlen(arg); + if (len == 0) + { + continue; + } + + if (arg[0] == '"' && arg[len - 1] != '"') + { + ChipLogError(NotSpecified, + "Mismatched '\"' detected in argument: '%s'. Use single quotes to delimit arguments with spaces " + "in them: 'x y', not \"x y\".", + arg); + } + } +} + +} // namespace + +void Commands::Register(const char * commandSetName, commands_list commandsList, const char * helpText, bool isCluster) +{ + VerifyOrDieWithMsg(isCluster || helpText != nullptr, NotSpecified, "Non-cluster command sets must have help text"); + mCommandSets[commandSetName].isCluster = isCluster; + mCommandSets[commandSetName].helpText = helpText; + for (auto & command : commandsList) + { + mCommandSets[commandSetName].commands.push_back(std::move(command)); + } +} + +int Commands::Run(int argc, char ** argv) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + err = chip::Platform::MemoryInit(); + VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Init Memory failure: %s", chip::ErrorStr(err))); + +#ifdef CONFIG_USE_LOCAL_STORAGE + err = mStorage.Init(); + VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Init Storage failure: %s", chip::ErrorStr(err))); + + chip::Logging::SetLogFilter(mStorage.GetLoggingLevel()); +#endif // CONFIG_USE_LOCAL_STORAGE + + err = RunCommand(argc, argv); + VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(NotSpecified, "Run command failure: %s", chip::ErrorStr(err))); + +exit: + return (err == CHIP_NO_ERROR) ? EXIT_SUCCESS : EXIT_FAILURE; +} + +int Commands::RunInteractive(const char * command, const chip::Optional & storageDirectory, bool advertiseOperational) +{ + std::vector arguments; + VerifyOrReturnValue(DecodeArgumentsFromInteractiveMode(command, arguments), EXIT_FAILURE); + + if (arguments.size() > (kInteractiveModeArgumentsMaxLength - 1 /* for interactive mode name */)) + { + ChipLogError(NotSpecified, "Too many arguments. Ignoring."); + arguments.resize(kInteractiveModeArgumentsMaxLength - 1); + } + + int argc = 0; + char * argv[kInteractiveModeArgumentsMaxLength] = {}; + argv[argc++] = kInteractiveModeName; + + std::string commandStr; + for (auto & arg : arguments) + { + argv[argc] = new char[arg.size() + 1]; + strcpy(argv[argc++], arg.c_str()); + commandStr += arg; + commandStr += " "; + } + + ChipLogProgress(NotSpecified, "Command: %s", commandStr.c_str()); + auto err = RunCommand(argc, argv, true, storageDirectory, advertiseOperational); + + // Do not delete arg[0] + for (auto i = 1; i < argc; i++) + { + delete[] argv[i]; + } + + return (err == CHIP_NO_ERROR) ? EXIT_SUCCESS : EXIT_FAILURE; +} + +CHIP_ERROR Commands::RunCommand(int argc, char ** argv, bool interactive, + const chip::Optional & interactiveStorageDirectory, bool interactiveAdvertiseOperational) +{ + Command * command = nullptr; + + if (argc <= 1) + { + ChipLogError(NotSpecified, "Missing cluster or command set name"); + ShowCommandSets(argv[0]); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + auto commandSetIter = GetCommandSet(argv[1]); + if (commandSetIter == mCommandSets.end()) + { + ChipLogError(NotSpecified, "Unknown cluster or command set: %s", argv[1]); + ShowCommandSets(argv[0]); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + auto & commandList = commandSetIter->second.commands; + auto * helpText = commandSetIter->second.helpText; + + if (argc <= 2) + { + ChipLogError(NotSpecified, "Missing command name"); + ShowCommandSet(argv[0], argv[1], commandList, helpText); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + bool isGlobalCommand = IsGlobalCommand(argv[2]); + if (!isGlobalCommand) + { + command = GetCommand(commandList, argv[2]); + if (command == nullptr) + { + ChipLogError(NotSpecified, "Unknown command: %s", argv[2]); + ShowCommandSet(argv[0], argv[1], commandList, helpText); + return CHIP_ERROR_INVALID_ARGUMENT; + } + } + else if (IsEventCommand(argv[2])) + { + if (argc <= 3) + { + ChipLogError(NotSpecified, "Missing event name"); + ShowClusterEvents(argv[0], argv[1], argv[2], commandList); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + command = GetGlobalCommand(commandList, argv[2], argv[3]); + if (command == nullptr) + { + ChipLogError(NotSpecified, "Unknown event: %s", argv[3]); + ShowClusterEvents(argv[0], argv[1], argv[2], commandList); + return CHIP_ERROR_INVALID_ARGUMENT; + } + } + else + { + if (argc <= 3) + { + ChipLogError(NotSpecified, "Missing attribute name"); + ShowClusterAttributes(argv[0], argv[1], argv[2], commandList); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + command = GetGlobalCommand(commandList, argv[2], argv[3]); + if (command == nullptr) + { + ChipLogError(NotSpecified, "Unknown attribute: %s", argv[3]); + ShowClusterAttributes(argv[0], argv[1], argv[2], commandList); + return CHIP_ERROR_INVALID_ARGUMENT; + } + } + + int argumentsPosition = isGlobalCommand ? 4 : 3; + if (!command->InitArguments(argc - argumentsPosition, &argv[argumentsPosition])) + { + if (interactive) + { + DetectAndLogMismatchedDoubleQuotes(argc - argumentsPosition, &argv[argumentsPosition]); + } + ShowCommand(argv[0], argv[1], command); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + if (interactive) + { + return command->RunAsInteractive(interactiveStorageDirectory, interactiveAdvertiseOperational); + } + + // Now that the command is initialized, get our storage from it as needed + // and set up our loging level. +#ifdef CONFIG_USE_LOCAL_STORAGE + CHIP_ERROR err = mStorage.Init(nullptr, command->GetStorageDirectory().ValueOr(nullptr)); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "Init Storage failure: %s", chip::ErrorStr(err)); + return err; + } + + chip::Logging::SetLogFilter(mStorage.GetLoggingLevel()); + +#if !CHIP_DISABLE_PLATFORM_KVS + UseStorageDirectory(chip::DeviceLayer::PersistedStorage::KeyValueStoreMgrImpl(), mStorage.GetDirectory()); +#endif // !CHIP_DISABLE_PLATFORM_KVS + +#endif // CONFIG_USE_LOCAL_STORAGE + + return command->Run(); +} + +Commands::CommandSetMap::iterator Commands::GetCommandSet(std::string commandSetName) +{ + for (auto & commandSet : mCommandSets) + { + std::string key(commandSet.first); + std::transform(key.begin(), key.end(), key.begin(), ::tolower); + if (key.compare(commandSetName) == 0) + { + return mCommandSets.find(commandSet.first); + } + } + + return mCommandSets.end(); +} + +Command * Commands::GetCommand(CommandsVector & commands, std::string commandName) +{ + for (auto & command : commands) + { + if (commandName.compare(command->GetName()) == 0) + { + return command.get(); + } + } + + return nullptr; +} + +Command * Commands::GetGlobalCommand(CommandsVector & commands, std::string commandName, std::string attributeName) +{ + for (auto & command : commands) + { + if (commandName.compare(command->GetName()) == 0 && attributeName.compare(command->GetAttribute()) == 0) + { + return command.get(); + } + } + + return nullptr; +} + +bool Commands::IsAttributeCommand(std::string commandName) const +{ + return commandName.compare("read") == 0 || commandName.compare("write") == 0 || commandName.compare("force-write") == 0 || + commandName.compare("subscribe") == 0; +} + +bool Commands::IsEventCommand(std::string commandName) const +{ + return commandName.compare("read-event") == 0 || commandName.compare("subscribe-event") == 0; +} + +bool Commands::IsGlobalCommand(std::string commandName) const +{ + return IsAttributeCommand(commandName) || IsEventCommand(commandName); +} + +void Commands::ShowCommandSetOverview(std::string commandSetName, const CommandSet & commandSet) +{ + std::transform(commandSetName.begin(), commandSetName.end(), commandSetName.begin(), + [](unsigned char c) { return std::tolower(c); }); + fprintf(stderr, " | * %-82s|\n", commandSetName.c_str()); + ShowHelpText(commandSet.helpText); +} + +void Commands::ShowCommandSets(std::string executable) +{ + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " %s cluster_name command_name [param1 param2 ...]\n", executable.c_str()); + fprintf(stderr, "or:\n"); + fprintf(stderr, " %s command_set_name command_name [param1 param2 ...]\n", executable.c_str()); + fprintf(stderr, "\n"); + // Table of clusters + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + fprintf(stderr, " | Clusters: |\n"); + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + for (auto & commandSet : mCommandSets) + { + if (commandSet.second.isCluster) + { + ShowCommandSetOverview(commandSet.first, commandSet.second); + } + } + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + fprintf(stderr, "\n"); + + // Table of command sets + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + fprintf(stderr, " | Command sets: |\n"); + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + for (auto & commandSet : mCommandSets) + { + if (!commandSet.second.isCluster) + { + ShowCommandSetOverview(commandSet.first, commandSet.second); + } + } + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); +} + +void Commands::ShowCommandSet(std::string executable, std::string commandSetName, CommandsVector & commands, const char * helpText) +{ + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " %s %s command_name [param1 param2 ...]\n", executable.c_str(), commandSetName.c_str()); + + if (helpText) + { + fprintf(stderr, "\n%s\n", helpText); + } + + fprintf(stderr, "\n"); + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + fprintf(stderr, " | Commands: |\n"); + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + bool readCommand = false; + bool writeCommand = false; + bool writeOverrideCommand = false; + bool subscribeCommand = false; + bool readEventCommand = false; + bool subscribeEventCommand = false; + for (auto & command : commands) + { + bool shouldPrint = true; + + if (IsGlobalCommand(command->GetName())) + { + if (strcmp(command->GetName(), "read") == 0 && !readCommand) + { + readCommand = true; + } + else if (strcmp(command->GetName(), "write") == 0 && !writeCommand) + { + writeCommand = true; + } + else if (strcmp(command->GetName(), "force-write") == 0 && !writeOverrideCommand) + { + writeOverrideCommand = true; + } + else if (strcmp(command->GetName(), "subscribe") == 0 && !subscribeCommand) + { + subscribeCommand = true; + } + else if (strcmp(command->GetName(), "read-event") == 0 && !readEventCommand) + { + readEventCommand = true; + } + else if (strcmp(command->GetName(), "subscribe-event") == 0 && !subscribeEventCommand) + { + subscribeEventCommand = true; + } + else + { + shouldPrint = false; + } + } + + if (shouldPrint) + { + fprintf(stderr, " | * %-82s|\n", command->GetName()); + ShowHelpText(command->GetHelpText()); + } + } + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); +} + +void Commands::ShowClusterAttributes(std::string executable, std::string clusterName, std::string commandName, + CommandsVector & commands) +{ + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " %s %s %s attribute-name [param1 param2 ...]\n", executable.c_str(), clusterName.c_str(), + commandName.c_str()); + fprintf(stderr, "\n"); + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + fprintf(stderr, " | Attributes: |\n"); + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + for (auto & command : commands) + { + if (commandName.compare(command->GetName()) == 0) + { + fprintf(stderr, " | * %-82s|\n", command->GetAttribute()); + } + } + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); +} + +void Commands::ShowClusterEvents(std::string executable, std::string clusterName, std::string commandName, + CommandsVector & commands) +{ + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " %s %s %s event-name [param1 param2 ...]\n", executable.c_str(), clusterName.c_str(), commandName.c_str()); + fprintf(stderr, "\n"); + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + fprintf(stderr, " | Events: |\n"); + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); + for (auto & command : commands) + { + if (commandName.compare(command->GetName()) == 0) + { + fprintf(stderr, " | * %-82s|\n", command->GetEvent()); + } + } + fprintf(stderr, " +-------------------------------------------------------------------------------------+\n"); +} + +void Commands::ShowCommand(std::string executable, std::string clusterName, Command * command) +{ + fprintf(stderr, "Usage:\n"); + + std::string arguments; + std::string description; + arguments += command->GetName(); + + if (command->GetReadOnlyGlobalCommandArgument()) + { + arguments += ' '; + arguments += command->GetReadOnlyGlobalCommandArgument(); + } + + size_t argumentsCount = command->GetArgumentsCount(); + for (size_t i = 0; i < argumentsCount; i++) + { + std::string arg; + bool isOptional = command->GetArgumentIsOptional(i); + if (isOptional) + { + arg += "[--"; + } + arg += command->GetArgumentName(i); + if (isOptional) + { + arg += "]"; + } + arguments += " "; + arguments += arg; + + const char * argDescription = command->GetArgumentDescription(i); + if ((argDescription != nullptr) && (strlen(argDescription) > 0)) + { + description += "\n"; + description += arg; + description += ":\n "; + description += argDescription; + description += "\n"; + } + } + fprintf(stderr, " %s %s %s\n", executable.c_str(), clusterName.c_str(), arguments.c_str()); + + if (command->GetHelpText()) + { + fprintf(stderr, "\n%s\n", command->GetHelpText()); + } + + if (description.size() > 0) + { + fprintf(stderr, "%s\n", description.c_str()); + } +} + +bool Commands::DecodeArgumentsFromInteractiveMode(const char * command, std::vector & args) +{ + // Remote clients may not know the ordering of arguments, so instead of a strict ordering arguments can + // be passed in as a json payload encoded in base64 and are reordered on the fly. + return IsJsonString(command) ? DecodeArgumentsFromBase64EncodedJson(command, args) + : DecodeArgumentsFromStringStream(command, args); +} + +bool Commands::DecodeArgumentsFromBase64EncodedJson(const char * json, std::vector & args) +{ + Json::Value jsonValue; + bool parsed = JsonParser::ParseCustomArgument(json, json + kJsonStringPrefixLen, jsonValue); + VerifyOrReturnValue(parsed, false, ChipLogError(NotSpecified, "Error while parsing json.")); + VerifyOrReturnValue(jsonValue.isObject(), false, ChipLogError(NotSpecified, "Unexpected json type.")); + VerifyOrReturnValue(jsonValue.isMember(kJsonClusterKey), false, + ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonClusterKey)); + VerifyOrReturnValue(jsonValue.isMember(kJsonCommandKey), false, + ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonCommandKey)); + VerifyOrReturnValue(jsonValue.isMember(kJsonArgumentsKey), false, + ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonArgumentsKey)); + VerifyOrReturnValue(IsBase64String(jsonValue[kJsonArgumentsKey].asString().c_str()), false, + ChipLogError(NotSpecified, "'arguments' is not a base64 string.")); + + auto clusterName = jsonValue[kJsonClusterKey].asString(); + auto commandName = jsonValue[kJsonCommandKey].asString(); + auto arguments = jsonValue[kJsonArgumentsKey].asString(); + + auto clusterIter = GetCommandSet(clusterName); + VerifyOrReturnValue(clusterIter != mCommandSets.end(), false, + ChipLogError(NotSpecified, "Cluster '%s' is not supported.", clusterName.c_str())); + + auto & commandList = clusterIter->second.commands; + + auto command = GetCommand(commandList, commandName); + + if (jsonValue.isMember(kJsonCommandSpecifierKey) && IsGlobalCommand(commandName)) + { + auto commandSpecifierName = jsonValue[kJsonCommandSpecifierKey].asString(); + command = GetGlobalCommand(commandList, commandName, commandSpecifierName); + } + VerifyOrReturnValue(nullptr != command, false, ChipLogError(NotSpecified, "Unknown command.")); + + auto encodedData = arguments.c_str(); + encodedData += kBase64StringPrefixLen; + + size_t encodedDataSize = strlen(encodedData); + size_t expectedMaxDecodedSize = BASE64_MAX_DECODED_LEN(encodedDataSize); + + chip::Platform::ScopedMemoryBuffer decodedData; + VerifyOrReturnValue(decodedData.Calloc(expectedMaxDecodedSize + 1 /* for null */), false); + + size_t decodedDataSize = chip::Base64Decode(encodedData, static_cast(encodedDataSize), decodedData.Get()); + VerifyOrReturnValue(decodedDataSize != 0, false, ChipLogError(NotSpecified, "Error while decoding base64 data.")); + + decodedData.Get()[decodedDataSize] = '\0'; + + Json::Value jsonArguments; + bool parsedArguments = JsonParser::ParseCustomArgument(encodedData, chip::Uint8::to_char(decodedData.Get()), jsonArguments); + VerifyOrReturnValue(parsedArguments, false, ChipLogError(NotSpecified, "Error while parsing json.")); + VerifyOrReturnValue(jsonArguments.isObject(), false, ChipLogError(NotSpecified, "Unexpected json type, expects and object.")); + + std::vector mandatoryArguments; + std::vector optionalArguments; + VerifyOrReturnValue(GetArgumentsFromJson(command, jsonArguments, false /* addOptional */, mandatoryArguments), false); + VerifyOrReturnValue(GetArgumentsFromJson(command, jsonArguments, true /* addOptional */, optionalArguments), false); + + args.push_back(std::move(clusterName)); + args.push_back(std::move(commandName)); + if (jsonValue.isMember(kJsonCommandSpecifierKey)) + { + auto commandSpecifierName = jsonValue[kJsonCommandSpecifierKey].asString(); + args.push_back(std::move(commandSpecifierName)); + } + args.insert(args.end(), mandatoryArguments.begin(), mandatoryArguments.end()); + args.insert(args.end(), optionalArguments.begin(), optionalArguments.end()); + + return true; +} + +bool Commands::DecodeArgumentsFromStringStream(const char * command, std::vector & args) +{ + std::string arg; + std::stringstream ss(command); + while (ss >> std::quoted(arg, '\'')) + { + args.push_back(std::move(arg)); + } + + return true; +} + +void Commands::ShowHelpText(const char * helpText) +{ + if (helpText == nullptr) + { + return; + } + + // We leave 82 chars for command/cluster names. The help text starts + // two chars further to the right, so there are 80 chars left + // for it. + if (strlen(helpText) > 80) + { + // Add "..." at the end to indicate truncation, and only + // show the first 77 chars, since that's what will fit. + fprintf(stderr, " | - %.77s...|\n", helpText); + } + else + { + fprintf(stderr, " | - %-80s|\n", helpText); + } +} diff --git a/examples/fabric-admin/commands/common/Commands.h b/examples/fabric-admin/commands/common/Commands.h new file mode 100644 index 00000000000000..8638ededcb3b8c --- /dev/null +++ b/examples/fabric-admin/commands/common/Commands.h @@ -0,0 +1,90 @@ +/* + * 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. + * + */ + +#pragma once + +#ifdef CONFIG_USE_LOCAL_STORAGE +#include +#endif // CONFIG_USE_LOCAL_STORAGE + +#include "Command.h" +#include +#include + +class Commands +{ +public: + using CommandsVector = ::std::vector>; + + void RegisterCluster(const char * clusterName, commands_list commandsList) + { + Register(clusterName, commandsList, nullptr, true); + } + // Command sets represent fabric-admin functionality that is not actually + // XML-defined clusters. All command sets should have help text explaining + // what sort of commands one should expect to find in the set. + void RegisterCommandSet(const char * commandSetName, commands_list commandsList, const char * helpText) + { + Register(commandSetName, commandsList, helpText, false); + } + int Run(int argc, char ** argv); + int RunInteractive(const char * command, const chip::Optional & storageDirectory, bool advertiseOperational); + +private: + struct CommandSet + { + CommandsVector commands; + bool isCluster = false; + const char * helpText = nullptr; + }; + // The tuple contains the commands, whether it's a synthetic cluster, and + // the help text for the cluster (which may be null). + using CommandSetMap = std::map; + + CHIP_ERROR RunCommand(int argc, char ** argv, bool interactive = false, + const chip::Optional & interactiveStorageDirectory = chip::NullOptional, + bool interactiveAdvertiseOperational = false); + + CommandSetMap::iterator GetCommandSet(std::string commandSetName); + Command * GetCommand(CommandsVector & commands, std::string commandName); + Command * GetGlobalCommand(CommandsVector & commands, std::string commandName, std::string attributeName); + bool IsAttributeCommand(std::string commandName) const; + bool IsEventCommand(std::string commandName) const; + bool IsGlobalCommand(std::string commandName) const; + + void ShowCommandSets(std::string executable); + static void ShowCommandSetOverview(std::string commandSetName, const CommandSet & commandSet); + void ShowCommandSet(std::string executable, std::string commandSetName, CommandsVector & commands, const char * helpText); + void ShowClusterAttributes(std::string executable, std::string clusterName, std::string commandName, CommandsVector & commands); + void ShowClusterEvents(std::string executable, std::string clusterName, std::string commandName, CommandsVector & commands); + void ShowCommand(std::string executable, std::string clusterName, Command * command); + + bool DecodeArgumentsFromInteractiveMode(const char * command, std::vector & args); + bool DecodeArgumentsFromBase64EncodedJson(const char * encodedData, std::vector & args); + bool DecodeArgumentsFromStringStream(const char * command, std::vector & args); + + // helpText may be null, in which case it's not shown. + static void ShowHelpText(const char * helpText); + + void Register(const char * commandSetName, commands_list commandsList, const char * helpText, bool isCluster); + + CommandSetMap mCommandSets; +#ifdef CONFIG_USE_LOCAL_STORAGE + PersistentStorage mStorage; +#endif // CONFIG_USE_LOCAL_STORAGE +}; diff --git a/examples/fabric-admin/commands/common/CredentialIssuerCommands.h b/examples/fabric-admin/commands/common/CredentialIssuerCommands.h new file mode 100644 index 00000000000000..c36948fe69c992 --- /dev/null +++ b/examples/fabric-admin/commands/common/CredentialIssuerCommands.h @@ -0,0 +1,117 @@ +/* + * 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. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace Controller { +struct SetupParams; +class OperationalCredentialsDelegate; +} // namespace Controller +} // namespace chip + +class CredentialIssuerCommands +{ +public: + virtual ~CredentialIssuerCommands() {} + + /** + * @brief + * This function is used to initialize the Credentials Issuer, if needed. + * + * @param[in] storage A reference to the storage, where the Credentials Issuer can optionally use to access the keypair in + * storage. + * + * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code. + */ + virtual CHIP_ERROR InitializeCredentialsIssuer(chip::PersistentStorageDelegate & storage) = 0; + + /** + * @brief + * This function is used to setup Device Attestation Singletons and intialize Setup/Commissioning Parameters with a custom + * Device Attestation Verifier object. + * + * @param[in] setupParams A reference to the Setup/Commissioning Parameters, to be initialized with custom Device Attestation + * Verifier. + * @param[in] trustStore A pointer to the PAA trust store to use to find valid PAA roots. + * + * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code. + */ + virtual CHIP_ERROR SetupDeviceAttestation(chip::Controller::SetupParams & setupParams, + const chip::Credentials::AttestationTrustStore * trustStore) = 0; + + /** + * @brief Add a list of additional non-default CD verifying keys (by certificate) + * + * Must be called AFTER SetupDeviceAttestation. + * + * @param additionalCdCerts - vector of X.509 DER verifying cert bodies + * @return CHIP_NO_ERROR on succes, another CHIP_ERROR on internal failures. + */ + virtual CHIP_ERROR AddAdditionalCDVerifyingCerts(const std::vector> & additionalCdCerts) = 0; + + virtual chip::Controller::OperationalCredentialsDelegate * GetCredentialIssuer() = 0; + + virtual void SetCredentialIssuerCATValues(chip::CATValues cats) = 0; + + /** + * @brief + * This function is used to Generate NOC Chain for the Controller/Commissioner. Parameters follow the example implementation, + * so some parameters may not translate to the real remote Credentials Issuer policy. + * + * @param[in] nodeId The desired NodeId for the generated NOC Chain - May be optional/unused in some implementations. + * @param[in] fabricId The desired FabricId for the generated NOC Chain - May be optional/unused in some implementations. + * @param[in] cats The desired CATs for the generated NOC Chain - May be optional/unused in some implementations. + * @param[in] keypair The desired Keypair for the generated NOC Chain - May be optional/unused in some implementations. + * @param[in,out] rcac Buffer to hold the Root Certificate of the generated NOC Chain. + * @param[in,out] icac Buffer to hold the Intermediate Certificate of the generated NOC Chain. + * @param[in,out] noc Buffer to hold the Leaf Certificate of the generated NOC Chain. + * + * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code. + */ + virtual CHIP_ERROR GenerateControllerNOCChain(chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats, + chip::Crypto::P256Keypair & keypair, chip::MutableByteSpan & rcac, + chip::MutableByteSpan & icac, chip::MutableByteSpan & noc) = 0; + + // All options must start false + enum CredentialIssuerOptions : uint8_t + { + kMaximizeCertificateSizes = 0, // If set, certificate chains will be maximized for testing via padding + kAllowTestCdSigningKey = 1, // If set, allow development/test SDK CD verifying key to be used + }; + + virtual void SetCredentialIssuerOption(CredentialIssuerOptions option, bool isEnabled) + { + // Do nothing + (void) option; + (void) isEnabled; + } + + virtual bool GetCredentialIssuerOption(CredentialIssuerOptions option) + { + // All options always start false + return false; + } +}; diff --git a/examples/fabric-admin/commands/common/CustomStringPrefix.h b/examples/fabric-admin/commands/common/CustomStringPrefix.h new file mode 100644 index 00000000000000..be8442993d1bf2 --- /dev/null +++ b/examples/fabric-admin/commands/common/CustomStringPrefix.h @@ -0,0 +1,55 @@ +/* + * 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. + * + */ + +#pragma once + +#include + +#include + +static constexpr char kJsonStringPrefix[] = "json:"; +inline constexpr size_t kJsonStringPrefixLen = ArraySize(kJsonStringPrefix) - 1; // Don't count the null + +static constexpr char kBase64StringPrefix[] = "base64:"; +inline constexpr size_t kBase64StringPrefixLen = ArraySize(kBase64StringPrefix) - 1; // Don't count the null + +static constexpr char kHexStringPrefix[] = "hex:"; +inline constexpr size_t kHexStringPrefixLen = ArraySize(kHexStringPrefix) - 1; // Don't count the null + +static constexpr char kStrStringPrefix[] = "str:"; +inline constexpr size_t kStrStringPrefixLen = ArraySize(kStrStringPrefix) - 1; // Don't count the null + +inline bool IsJsonString(const char * str) +{ + return strncmp(str, kJsonStringPrefix, kJsonStringPrefixLen) == 0; +} + +inline bool IsBase64String(const char * str) +{ + return strncmp(str, kBase64StringPrefix, kBase64StringPrefixLen) == 0; +} + +inline bool IsHexString(const char * str) +{ + return strncmp(str, kHexStringPrefix, kHexStringPrefixLen) == 0; +} + +inline bool IsStrString(const char * str) +{ + return strncmp(str, kStrStringPrefix, kStrStringPrefixLen) == 0; +} diff --git a/examples/fabric-admin/commands/common/DeviceScanner.cpp b/examples/fabric-admin/commands/common/DeviceScanner.cpp new file mode 100644 index 00000000000000..e49eb852fe4808 --- /dev/null +++ b/examples/fabric-admin/commands/common/DeviceScanner.cpp @@ -0,0 +1,245 @@ +/* + * 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 "DeviceScanner.h" + +using namespace chip; +using namespace chip::Dnssd; + +#if CONFIG_NETWORK_LAYER_BLE +using namespace chip::Ble; +constexpr char kBleKey[] = "BLE"; +#endif // CONFIG_NETWORK_LAYER_BLE + +CHIP_ERROR DeviceScanner::Start() +{ + mDiscoveredResults.clear(); + +#if CONFIG_NETWORK_LAYER_BLE + ReturnErrorOnFailure(DeviceLayer::PlatformMgrImpl().StartBleScan(this)); +#endif // CONFIG_NETWORK_LAYER_BLE + + ReturnErrorOnFailure(chip::Dnssd::Resolver::Instance().Init(DeviceLayer::UDPEndPointManager())); + + char serviceName[kMaxCommissionableServiceNameSize]; + auto filter = DiscoveryFilterType::kNone; + ReturnErrorOnFailure(MakeServiceTypeName(serviceName, sizeof(serviceName), filter, DiscoveryType::kCommissionableNode)); + + return ChipDnssdBrowse(serviceName, DnssdServiceProtocol::kDnssdProtocolUdp, Inet::IPAddressType::kAny, + Inet::InterfaceId::Null(), this); +} + +CHIP_ERROR DeviceScanner::Stop() +{ +#if CONFIG_NETWORK_LAYER_BLE + ReturnErrorOnFailure(DeviceLayer::PlatformMgrImpl().StopBleScan()); +#endif // CONFIG_NETWORK_LAYER_BLE + + return ChipDnssdStopBrowse(this); +} + +void DeviceScanner::OnNodeDiscovered(const DiscoveredNodeData & nodeData) +{ + VerifyOrReturn(nodeData.Is()); + auto & commissionData = nodeData.Get(); + + auto discriminator = commissionData.longDiscriminator; + auto vendorId = static_cast(commissionData.vendorId); + auto productId = commissionData.productId; + + ChipLogProgress(NotSpecified, "OnNodeDiscovered (MDNS): discriminator: %u, vendorId: %u, productId: %u", discriminator, + vendorId, productId); + + const CommonResolutionData & resolutionData = commissionData; + + auto & instanceData = mDiscoveredResults[commissionData.instanceName]; + auto & interfaceData = instanceData[resolutionData.interfaceId.GetPlatformInterface()]; + + for (size_t i = 0; i < resolutionData.numIPs; i++) + { + auto params = Controller::SetUpCodePairerParameters(resolutionData, i); + DeviceScannerResult result = { params, vendorId, productId, discriminator, chip::MakeOptional(resolutionData) }; + interfaceData.push_back(result); + } + + commissionData.LogDetail(); +} + +void DeviceScanner::OnBrowseAdd(chip::Dnssd::DnssdService service) +{ + ChipLogProgress(NotSpecified, "OnBrowseAdd: %s", service.mName); + LogErrorOnFailure(ChipDnssdResolve(&service, service.mInterface, this)); + + auto & instanceData = mDiscoveredResults[service.mName]; + auto & interfaceData = instanceData[service.mInterface.GetPlatformInterface()]; + (void) interfaceData; +} + +void DeviceScanner::OnBrowseRemove(chip::Dnssd::DnssdService service) +{ + ChipLogProgress(NotSpecified, "OnBrowseRemove: %s", service.mName); + auto & instanceData = mDiscoveredResults[service.mName]; + auto & interfaceData = instanceData[service.mInterface.GetPlatformInterface()]; + + // Check if the interface data has been resolved already, otherwise, just inform the + // back end that we may not need it anymore. + if (interfaceData.size() == 0) + { + ChipDnssdResolveNoLongerNeeded(service.mName); + } + + // Delete the interface placeholder. + instanceData.erase(service.mInterface.GetPlatformInterface()); + + // If there is nothing else to resolve for the given instance name, just remove it + // too. + if (instanceData.size() == 0) + { + mDiscoveredResults.erase(service.mName); + } +} + +void DeviceScanner::OnBrowseStop(CHIP_ERROR error) +{ + ChipLogProgress(NotSpecified, "OnBrowseStop: %" CHIP_ERROR_FORMAT, error.Format()); + + for (auto & instance : mDiscoveredResults) + { + for (auto & interface : instance.second) + { + if (interface.second.size() == 0) + { + ChipDnssdResolveNoLongerNeeded(instance.first.c_str()); + } + } + } +} + +#if CONFIG_NETWORK_LAYER_BLE +void DeviceScanner::OnBleScanAdd(BLE_CONNECTION_OBJECT connObj, const ChipBLEDeviceIdentificationInfo & info) +{ + auto discriminator = info.GetDeviceDiscriminator(); + auto vendorId = static_cast(info.GetVendorId()); + auto productId = info.GetProductId(); + + ChipLogProgress(NotSpecified, "OnBleScanAdd (BLE): %p, discriminator: %u, vendorId: %u, productId: %u", connObj, discriminator, + vendorId, productId); + + auto params = Controller::SetUpCodePairerParameters(connObj, false /* connected */); + DeviceScannerResult result = { params, vendorId, productId, discriminator }; + + auto & instanceData = mDiscoveredResults[kBleKey]; + auto & interfaceData = instanceData[chip::Inet::InterfaceId::Null().GetPlatformInterface()]; + interfaceData.push_back(result); +} + +void DeviceScanner::OnBleScanRemove(BLE_CONNECTION_OBJECT connObj) +{ + ChipLogProgress(NotSpecified, "OnBleScanRemove: %p", connObj); + + auto & instanceData = mDiscoveredResults[kBleKey]; + auto & interfaceData = instanceData[chip::Inet::InterfaceId::Null().GetPlatformInterface()]; + + interfaceData.erase(std::remove_if(interfaceData.begin(), interfaceData.end(), + [connObj](const DeviceScannerResult & result) { + return result.mParams.HasDiscoveredObject() && + result.mParams.GetDiscoveredObject() == connObj; + }), + interfaceData.end()); + + if (interfaceData.size() == 0) + { + instanceData.clear(); + mDiscoveredResults.erase(kBleKey); + } +} +#endif // CONFIG_NETWORK_LAYER_BLE + +CHIP_ERROR DeviceScanner::Get(uint16_t index, RendezvousParameters & params) +{ + uint16_t currentIndex = 0; + for (auto & instance : mDiscoveredResults) + { + for (auto & interface : instance.second) + { + for (auto & result : interface.second) + { + if (currentIndex == index) + { + params = result.mParams; + return CHIP_NO_ERROR; + } + currentIndex++; + } + } + } + + return CHIP_ERROR_NOT_FOUND; +} + +CHIP_ERROR DeviceScanner::Get(uint16_t index, Dnssd::CommonResolutionData & resolutionData) +{ + uint16_t currentIndex = 0; + for (auto & instance : mDiscoveredResults) + { + for (auto & interface : instance.second) + { + for (auto & result : interface.second) + { + if (currentIndex == index && result.mResolutionData.HasValue()) + { + resolutionData = result.mResolutionData.Value(); + return CHIP_NO_ERROR; + } + currentIndex++; + } + } + } + + return CHIP_ERROR_NOT_FOUND; +} + +void DeviceScanner::Log() const +{ + auto resultsCount = mDiscoveredResults.size(); + VerifyOrReturn(resultsCount > 0, ChipLogProgress(NotSpecified, "No device discovered.")); + + [[maybe_unused]] uint16_t index = 0; + for (auto & instance : mDiscoveredResults) + { + ChipLogProgress(NotSpecified, "Instance Name: %s ", instance.first.c_str()); + for (auto & interface : instance.second) + { + for (auto & result : interface.second) + { + char addr[Transport::PeerAddress::kMaxToStringSize]; + result.mParams.GetPeerAddress().ToString(addr); + + ChipLogProgress(NotSpecified, "\t %u - Discriminator: %u - Vendor: %u - Product: %u - %s", index, + result.mDiscriminator, result.mVendorId, result.mProductId, addr); + index++; + } + } + } +} + +DeviceScanner & GetDeviceScanner() +{ + static DeviceScanner scanner; + return scanner; +} diff --git a/examples/fabric-admin/commands/common/DeviceScanner.h b/examples/fabric-admin/commands/common/DeviceScanner.h new file mode 100644 index 00000000000000..c3e81a51c413cd --- /dev/null +++ b/examples/fabric-admin/commands/common/DeviceScanner.h @@ -0,0 +1,80 @@ +/* + * 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. + * + */ + +#pragma once + +#include + +#if CHIP_DEVICE_LAYER_TARGET_DARWIN + +#include +#include + +#include +#include +#include + +#if CONFIG_NETWORK_LAYER_BLE +#include +#endif // CONFIG_NETWORK_LAYER_BLE + +struct DeviceScannerResult +{ + chip::Controller::SetUpCodePairerParameters mParams; + chip::VendorId mVendorId; + uint16_t mProductId; + uint16_t mDiscriminator; + chip::Optional mResolutionData; +}; + +class DeviceScanner : public chip::Dnssd::DiscoverNodeDelegate, + public chip::Dnssd::DnssdBrowseDelegate +#if CONFIG_NETWORK_LAYER_BLE + , + public chip::DeviceLayer::BleScannerDelegate +#endif // CONFIG_NETWORK_LAYER_BLE +{ +public: + CHIP_ERROR Start(); + CHIP_ERROR Stop(); + CHIP_ERROR Get(uint16_t index, chip::RendezvousParameters & params); + CHIP_ERROR Get(uint16_t index, chip::Dnssd::CommonResolutionData & resolutionData); + void Log() const; + + /////////// DiscoverNodeDelegate Interface ///////// + void OnNodeDiscovered(const chip::Dnssd::DiscoveredNodeData & nodeData) override; + + /////////// DnssdBrowseDelegate Interface ///////// + void OnBrowseAdd(chip::Dnssd::DnssdService service) override; + void OnBrowseRemove(chip::Dnssd::DnssdService service) override; + void OnBrowseStop(CHIP_ERROR error) override; + +#if CONFIG_NETWORK_LAYER_BLE + /////////// BleScannerDelegate Interface ///////// + void OnBleScanAdd(BLE_CONNECTION_OBJECT connObj, const chip::Ble::ChipBLEDeviceIdentificationInfo & info) override; + void OnBleScanRemove(BLE_CONNECTION_OBJECT connObj) override; +#endif // CONFIG_NETWORK_LAYER_BLE + +private: + std::unordered_map>> + mDiscoveredResults; +}; + +DeviceScanner & GetDeviceScanner(); + +#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN diff --git a/examples/fabric-admin/commands/common/HexConversion.h b/examples/fabric-admin/commands/common/HexConversion.h new file mode 100644 index 00000000000000..99b7f651734d12 --- /dev/null +++ b/examples/fabric-admin/commands/common/HexConversion.h @@ -0,0 +1,65 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +/** + * Utility for converting a hex string to bytes, with the right error checking + * and allocation size computation. + * + * Takes a functor to allocate the buffer to use for the hex bytes. The functor + * is expected to return uint8_t *. The caller is responsible for cleaning up + * this buffer as needed. + * + * On success, *octetCount is filled with the number of octets placed in the + * buffer. On failure, the value of *octetCount is undefined. + */ +template +CHIP_ERROR HexToBytes(chip::CharSpan hex, F bufferAllocator, size_t * octetCount) +{ + *octetCount = 0; + + if (hex.size() % 2 != 0) + { + ChipLogError(NotSpecified, "Error while encoding '%.*s' as an octet string: Odd number of characters.", + static_cast(hex.size()), hex.data()); + return CHIP_ERROR_INVALID_STRING_LENGTH; + } + + const size_t bufferSize = hex.size() / 2; + uint8_t * buffer = bufferAllocator(bufferSize); + if (buffer == nullptr && bufferSize != 0) + { + ChipLogError(NotSpecified, "Failed to allocate buffer of size: %llu", static_cast(bufferSize)); + return CHIP_ERROR_NO_MEMORY; + } + + size_t byteCount = chip::Encoding::HexToBytes(hex.data(), hex.size(), buffer, bufferSize); + if (byteCount == 0 && hex.size() != 0) + { + ChipLogError(NotSpecified, "Error while encoding '%.*s' as an octet string.", static_cast(hex.size()), hex.data()); + return CHIP_ERROR_INTERNAL; + } + + *octetCount = byteCount; + return CHIP_NO_ERROR; +} diff --git a/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp b/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp new file mode 100644 index 00000000000000..c8e9d5a5450703 --- /dev/null +++ b/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp @@ -0,0 +1,279 @@ +/* + * 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 "RemoteDataModelLogger.h" + +#include +#include + +constexpr char kEventNumberKey[] = "eventNumber"; +constexpr char kDataVersionKey[] = "dataVersion"; +constexpr char kClusterIdKey[] = "clusterId"; +constexpr char kEndpointIdKey[] = "endpointId"; +constexpr char kAttributeIdKey[] = "attributeId"; +constexpr char kEventIdKey[] = "eventId"; +constexpr char kCommandIdKey[] = "commandId"; +constexpr char kErrorIdKey[] = "error"; +constexpr char kClusterErrorIdKey[] = "clusterError"; +constexpr char kValueKey[] = "value"; +constexpr char kNodeIdKey[] = "nodeId"; +constexpr char kNOCKey[] = "NOC"; +constexpr char kICACKey[] = "ICAC"; +constexpr char kRCACKey[] = "RCAC"; +constexpr char kIPKKey[] = "IPK"; + +namespace { +RemoteDataModelLoggerDelegate * gDelegate; + +CHIP_ERROR LogError(Json::Value & value, const chip::app::StatusIB & status) +{ + if (status.mClusterStatus.HasValue()) + { + auto statusValue = status.mClusterStatus.Value(); + value[kClusterErrorIdKey] = statusValue; + } + +#if CHIP_CONFIG_IM_STATUS_CODE_VERBOSE_FORMAT + auto statusName = chip::Protocols::InteractionModel::StatusName(status.mStatus); + value[kErrorIdKey] = statusName; +#else + auto statusName = status.mStatus; + value[kErrorIdKey] = chip::to_underlying(statusName); +#endif // CHIP_CONFIG_IM_STATUS_CODE_VERBOSE_FORMAT + + auto valueStr = chip::JsonToString(value); + return gDelegate->LogJSON(valueStr.c_str()); +} + +} // namespace + +namespace RemoteDataModelLogger { +CHIP_ERROR LogAttributeAsJSON(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value value; + value[kClusterIdKey] = path.mClusterId; + value[kEndpointIdKey] = path.mEndpointId; + value[kAttributeIdKey] = path.mAttributeId; + if (path.mDataVersion.HasValue()) + { + value[kDataVersionKey] = path.mDataVersion.Value(); + } + + chip::TLV::TLVReader reader; + reader.Init(*data); + ReturnErrorOnFailure(chip::TlvToJson(reader, value)); + + auto valueStr = chip::JsonToString(value); + return gDelegate->LogJSON(valueStr.c_str()); +} + +CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteDataAttributePath & path, const chip::app::StatusIB & status) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value value; + value[kClusterIdKey] = path.mClusterId; + value[kEndpointIdKey] = path.mEndpointId; + value[kAttributeIdKey] = path.mAttributeId; + + return LogError(value, status); +} + +CHIP_ERROR LogCommandAsJSON(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value value; + value[kClusterIdKey] = path.mClusterId; + value[kEndpointIdKey] = path.mEndpointId; + value[kCommandIdKey] = path.mCommandId; + + chip::TLV::TLVReader reader; + reader.Init(*data); + ReturnErrorOnFailure(chip::TlvToJson(reader, value)); + + auto valueStr = chip::JsonToString(value); + return gDelegate->LogJSON(valueStr.c_str()); +} + +CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteCommandPath & path, const chip::app::StatusIB & status) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value value; + value[kClusterIdKey] = path.mClusterId; + value[kEndpointIdKey] = path.mEndpointId; + value[kCommandIdKey] = path.mCommandId; + + return LogError(value, status); +} + +CHIP_ERROR LogEventAsJSON(const chip::app::EventHeader & header, chip::TLV::TLVReader * data) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value value; + value[kClusterIdKey] = header.mPath.mClusterId; + value[kEndpointIdKey] = header.mPath.mEndpointId; + value[kEventIdKey] = header.mPath.mEventId; + value[kEventNumberKey] = header.mEventNumber; + + chip::TLV::TLVReader reader; + reader.Init(*data); + ReturnErrorOnFailure(chip::TlvToJson(reader, value)); + + auto valueStr = chip::JsonToString(value); + return gDelegate->LogJSON(valueStr.c_str()); +} + +CHIP_ERROR LogErrorAsJSON(const chip::app::EventHeader & header, const chip::app::StatusIB & status) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value value; + value[kClusterIdKey] = header.mPath.mClusterId; + value[kEndpointIdKey] = header.mPath.mEndpointId; + value[kEventIdKey] = header.mPath.mEventId; + + return LogError(value, status); +} + +CHIP_ERROR LogErrorAsJSON(const CHIP_ERROR & error) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value value; + chip::app::StatusIB status; + status.InitFromChipError(error); + return LogError(value, status); +} + +CHIP_ERROR LogGetCommissionerNodeId(chip::NodeId value) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value rootValue; + rootValue[kValueKey] = Json::Value(); + rootValue[kValueKey][kNodeIdKey] = value; + + auto valueStr = chip::JsonToString(rootValue); + return gDelegate->LogJSON(valueStr.c_str()); +} + +CHIP_ERROR LogGetCommissionerRootCertificate(const char * value) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value rootValue; + rootValue[kValueKey] = Json::Value(); + rootValue[kValueKey][kRCACKey] = value; + + auto valueStr = chip::JsonToString(rootValue); + return gDelegate->LogJSON(valueStr.c_str()); +} + +CHIP_ERROR LogIssueNOCChain(const char * noc, const char * icac, const char * rcac, const char * ipk) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + Json::Value rootValue; + rootValue[kValueKey] = Json::Value(); + rootValue[kValueKey][kNOCKey] = noc; + rootValue[kValueKey][kICACKey] = icac; + rootValue[kValueKey][kRCACKey] = rcac; + rootValue[kValueKey][kIPKKey] = ipk; + + auto valueStr = chip::JsonToString(rootValue); + return gDelegate->LogJSON(valueStr.c_str()); +} + +CHIP_ERROR LogDiscoveredNodeData(const chip::Dnssd::CommissionNodeData & nodeData) +{ + VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR); + + auto & commissionData = nodeData; + auto & resolutionData = commissionData; + + if (!chip::CanCastTo(resolutionData.numIPs)) + { + ChipLogError(NotSpecified, "Too many ips."); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + if (!chip::CanCastTo(commissionData.rotatingIdLen)) + { + ChipLogError(NotSpecified, "Can not convert rotatingId to json format."); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + char rotatingId[chip::Dnssd::kMaxRotatingIdLen * 2 + 1] = ""; + ReturnErrorOnFailure(chip::Encoding::BytesToUppercaseHexString(commissionData.rotatingId, commissionData.rotatingIdLen, + rotatingId, sizeof(rotatingId))); + + Json::Value value; + value["hostName"] = resolutionData.hostName; + value["instanceName"] = commissionData.instanceName; + value["longDiscriminator"] = commissionData.longDiscriminator; + value["shortDiscriminator"] = ((commissionData.longDiscriminator >> 8) & 0x0F); + value["vendorId"] = commissionData.vendorId; + value["productId"] = commissionData.productId; + value["commissioningMode"] = commissionData.commissioningMode; + value["deviceType"] = commissionData.deviceType; + value["deviceName"] = commissionData.deviceName; + value["rotatingId"] = rotatingId; + value["rotatingIdLen"] = static_cast(commissionData.rotatingIdLen); + value["pairingHint"] = commissionData.pairingHint; + value["pairingInstruction"] = commissionData.pairingInstruction; + value["supportsTcp"] = resolutionData.supportsTcp; + value["port"] = resolutionData.port; + value["numIPs"] = static_cast(resolutionData.numIPs); + + if (resolutionData.mrpRetryIntervalIdle.has_value()) + { + value["mrpRetryIntervalIdle"] = resolutionData.mrpRetryIntervalIdle->count(); + } + + if (resolutionData.mrpRetryIntervalActive.has_value()) + { + value["mrpRetryIntervalActive"] = resolutionData.mrpRetryIntervalActive->count(); + } + + if (resolutionData.mrpRetryActiveThreshold.has_value()) + { + value["mrpRetryActiveThreshold"] = resolutionData.mrpRetryActiveThreshold->count(); + } + + if (resolutionData.isICDOperatingAsLIT.has_value()) + { + value["isICDOperatingAsLIT"] = *(resolutionData.isICDOperatingAsLIT); + } + + Json::Value rootValue; + rootValue[kValueKey] = value; + + auto valueStr = chip::JsonToString(rootValue); + return gDelegate->LogJSON(valueStr.c_str()); +} + +void SetDelegate(RemoteDataModelLoggerDelegate * delegate) +{ + gDelegate = delegate; +} +}; // namespace RemoteDataModelLogger diff --git a/examples/fabric-admin/commands/common/RemoteDataModelLogger.h b/examples/fabric-admin/commands/common/RemoteDataModelLogger.h new file mode 100644 index 00000000000000..c31636ea1fd8a9 --- /dev/null +++ b/examples/fabric-admin/commands/common/RemoteDataModelLogger.h @@ -0,0 +1,48 @@ +/* + * 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. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class RemoteDataModelLoggerDelegate +{ +public: + CHIP_ERROR virtual LogJSON(const char *) = 0; + virtual ~RemoteDataModelLoggerDelegate(){}; +}; + +namespace RemoteDataModelLogger { +CHIP_ERROR LogAttributeAsJSON(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data); +CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteDataAttributePath & path, const chip::app::StatusIB & status); +CHIP_ERROR LogCommandAsJSON(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data); +CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteCommandPath & path, const chip::app::StatusIB & status); +CHIP_ERROR LogEventAsJSON(const chip::app::EventHeader & header, chip::TLV::TLVReader * data); +CHIP_ERROR LogErrorAsJSON(const chip::app::EventHeader & header, const chip::app::StatusIB & status); +CHIP_ERROR LogErrorAsJSON(const CHIP_ERROR & error); +CHIP_ERROR LogGetCommissionerNodeId(chip::NodeId value); +CHIP_ERROR LogGetCommissionerRootCertificate(const char * value); +CHIP_ERROR LogIssueNOCChain(const char * noc, const char * icac, const char * rcac, const char * ipk); +CHIP_ERROR LogDiscoveredNodeData(const chip::Dnssd::CommissionNodeData & nodeData); +void SetDelegate(RemoteDataModelLoggerDelegate * delegate); +}; // namespace RemoteDataModelLogger diff --git a/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h b/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h new file mode 100644 index 00000000000000..a739ac7dbfa9dd --- /dev/null +++ b/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h @@ -0,0 +1,111 @@ +/* + * 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. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class ExampleCredentialIssuerCommands : public CredentialIssuerCommands +{ +public: + CHIP_ERROR InitializeCredentialsIssuer(chip::PersistentStorageDelegate & storage) override + { + return mOpCredsIssuer.Initialize(storage); + } + CHIP_ERROR SetupDeviceAttestation(chip::Controller::SetupParams & setupParams, + const chip::Credentials::AttestationTrustStore * trustStore) override + { + chip::Credentials::SetDeviceAttestationCredentialsProvider(chip::Credentials::Examples::GetExampleDACProvider()); + + mDacVerifier = chip::Credentials::GetDefaultDACVerifier(trustStore); + setupParams.deviceAttestationVerifier = mDacVerifier; + mDacVerifier->EnableCdTestKeySupport(mAllowTestCdSigningKey); + + return CHIP_NO_ERROR; + } + chip::Controller::OperationalCredentialsDelegate * GetCredentialIssuer() override { return &mOpCredsIssuer; } + void SetCredentialIssuerCATValues(chip::CATValues cats) override { mOpCredsIssuer.SetCATValuesForNextNOCRequest(cats); } + CHIP_ERROR GenerateControllerNOCChain(chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats, + chip::Crypto::P256Keypair & keypair, chip::MutableByteSpan & rcac, + chip::MutableByteSpan & icac, chip::MutableByteSpan & noc) override + { + return mOpCredsIssuer.GenerateNOCChainAfterValidation(nodeId, fabricId, cats, keypair.Pubkey(), rcac, icac, noc); + } + + CHIP_ERROR AddAdditionalCDVerifyingCerts(const std::vector> & additionalCdCerts) override + { + VerifyOrReturnError(mDacVerifier != nullptr, CHIP_ERROR_INCORRECT_STATE); + + for (const auto & cert : additionalCdCerts) + { + auto cdTrustStore = mDacVerifier->GetCertificationDeclarationTrustStore(); + VerifyOrReturnError(cdTrustStore != nullptr, CHIP_ERROR_INCORRECT_STATE); + ReturnErrorOnFailure(cdTrustStore->AddTrustedKey(chip::ByteSpan(cert.data(), cert.size()))); + } + + return CHIP_NO_ERROR; + } + + void SetCredentialIssuerOption(CredentialIssuerOptions option, bool isEnabled) override + { + switch (option) + { + case CredentialIssuerOptions::kMaximizeCertificateSizes: + mUsesMaxSizedCerts = isEnabled; + mOpCredsIssuer.SetMaximallyLargeCertsUsed(mUsesMaxSizedCerts); + break; + case CredentialIssuerOptions::kAllowTestCdSigningKey: + mAllowTestCdSigningKey = isEnabled; + if (mDacVerifier != nullptr) + { + mDacVerifier->EnableCdTestKeySupport(isEnabled); + } + break; + default: + break; + } + } + + bool GetCredentialIssuerOption(CredentialIssuerOptions option) override + { + switch (option) + { + case CredentialIssuerOptions::kMaximizeCertificateSizes: + return mUsesMaxSizedCerts; + case CredentialIssuerOptions::kAllowTestCdSigningKey: + return mAllowTestCdSigningKey; + default: + return false; + } + } + +protected: + bool mUsesMaxSizedCerts = false; + // Starts true for legacy purposes + bool mAllowTestCdSigningKey = true; + +private: + chip::Controller::ExampleOperationalCredentialsIssuer mOpCredsIssuer; + chip::Credentials::DeviceAttestationVerifier * mDacVerifier; +}; diff --git a/examples/fabric-admin/commands/interactive/Commands.h b/examples/fabric-admin/commands/interactive/Commands.h new file mode 100644 index 00000000000000..e324ddae2680ae --- /dev/null +++ b/examples/fabric-admin/commands/interactive/Commands.h @@ -0,0 +1,34 @@ +/* + * 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. + * + */ + +#pragma once + +#include "commands/common/CHIPCommand.h" +#include "commands/common/Commands.h" +#include "commands/interactive/InteractiveCommands.h" + +void registerCommandsInteractive(Commands & commands, CredentialIssuerCommands * credsIssuerConfig) +{ + const char * clusterName = "interactive"; + + commands_list clusterCommands = { + make_unique(&commands, credsIssuerConfig), + }; + + commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for starting long-lived interactive modes."); +} diff --git a/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp b/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp new file mode 100644 index 00000000000000..9ef07e79450300 --- /dev/null +++ b/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp @@ -0,0 +1,139 @@ +/* + * 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 "InteractiveCommands.h" + +#include + +#include + +#include +#include + +constexpr char kInteractiveModePrompt[] = ">>> "; +constexpr char kInteractiveModeHistoryFileName[] = "chip_tool_history"; +constexpr char kInteractiveModeStopCommand[] = "quit()"; + +namespace { + +void ClearLine() +{ + printf("\r\x1B[0J"); // Move cursor to the beginning of the line and clear from cursor to end of the screen +} + +void ENFORCE_FORMAT(3, 0) LoggingCallback(const char * module, uint8_t category, const char * msg, va_list args) +{ + ClearLine(); + chip::Logging::Platform::LogV(module, category, msg, args); + ClearLine(); +} + +} // namespace + +char * InteractiveStartCommand::GetCommand(char * command) +{ + if (command != nullptr) + { + free(command); + command = nullptr; + } + + command = readline(kInteractiveModePrompt); + + // Do not save empty lines + if (command != nullptr && *command) + { + add_history(command); + write_history(GetHistoryFilePath().c_str()); + } + + return command; +} + +std::string InteractiveStartCommand::GetHistoryFilePath() const +{ + std::string storageDir; + if (GetStorageDirectory().HasValue()) + { + storageDir = GetStorageDirectory().Value(); + } + else + { + // Match what GetFilename in ExamplePersistentStorage.cpp does. + const char * dir = getenv("TMPDIR"); + if (dir == nullptr) + { + dir = "/tmp"; + } + storageDir = dir; + } + + return storageDir + "/" + kInteractiveModeHistoryFileName; +} + +CHIP_ERROR InteractiveStartCommand::RunCommand() +{ + read_history(GetHistoryFilePath().c_str()); + + // Logs needs to be redirected in order to refresh the screen appropriately when something + // is dumped to stdout while the user is typing a command. + chip::Logging::SetLogRedirectCallback(LoggingCallback); + + char * command = nullptr; + int status; + while (true) + { + command = GetCommand(command); + if (command != nullptr && !ParseCommand(command, &status)) + { + break; + } + } + + if (command != nullptr) + { + free(command); + command = nullptr; + } + + SetCommandExitStatus(CHIP_NO_ERROR); + return CHIP_NO_ERROR; +} + +bool InteractiveCommand::ParseCommand(char * command, int * status) +{ + if (strcmp(command, kInteractiveModeStopCommand) == 0) + { + // If scheduling the cleanup fails, there is not much we can do. + // But if something went wrong while the application is leaving it could be because things have + // not been cleaned up properly, so it is still useful to log the failure. + LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().ScheduleWork(ExecuteDeferredCleanups, 0)); + return false; + } + + ClearLine(); + + *status = mHandler->RunInteractive(command, GetStorageDirectory(), NeedsOperationalAdvertising()); + + return true; +} + +bool InteractiveCommand::NeedsOperationalAdvertising() +{ + return mAdvertiseOperational.ValueOr(true); +} diff --git a/examples/fabric-admin/commands/interactive/InteractiveCommands.h b/examples/fabric-admin/commands/interactive/InteractiveCommands.h new file mode 100644 index 00000000000000..21c14a7a4c95ce --- /dev/null +++ b/examples/fabric-admin/commands/interactive/InteractiveCommands.h @@ -0,0 +1,68 @@ +/* + * 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. + * + */ + +#pragma once + +#include "../clusters/DataModelLogger.h" +#include "../common/CHIPCommand.h" +#include "../common/Commands.h" + +#include + +#include + +class Commands; + +class InteractiveCommand : public CHIPCommand +{ +public: + InteractiveCommand(const char * name, Commands * commandsHandler, const char * helpText, + CredentialIssuerCommands * credsIssuerConfig) : + CHIPCommand(name, credsIssuerConfig, helpText), + mHandler(commandsHandler) + { + AddArgument("advertise-operational", 0, 1, &mAdvertiseOperational, + "Advertise operational node over DNS-SD and accept incoming CASE sessions."); + } + + /////////// CHIPCommand Interface ///////// + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(0); } + bool NeedsOperationalAdvertising() override; + + bool ParseCommand(char * command, int * status); + +private: + Commands * mHandler = nullptr; + chip::Optional mAdvertiseOperational; +}; + +class InteractiveStartCommand : public InteractiveCommand +{ +public: + InteractiveStartCommand(Commands * commandsHandler, CredentialIssuerCommands * credsIssuerConfig) : + InteractiveCommand("start", commandsHandler, "Start an interactive shell that can then run other commands.", + credsIssuerConfig) + {} + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override; + +private: + char * GetCommand(char * command); + std::string GetHistoryFilePath() const; +}; diff --git a/examples/fabric-admin/commands/pairing/Commands.h b/examples/fabric-admin/commands/pairing/Commands.h new file mode 100644 index 00000000000000..6fdfacef79e34f --- /dev/null +++ b/examples/fabric-admin/commands/pairing/Commands.h @@ -0,0 +1,259 @@ +/* + * 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. + * + */ + +#pragma once + +#include "commands/common/Commands.h" +#include "commands/pairing/GetCommissionerNodeIdCommand.h" +#include "commands/pairing/GetCommissionerRootCertificateCommand.h" +#include "commands/pairing/IssueNOCChainCommand.h" +#include "commands/pairing/OpenCommissioningWindowCommand.h" +#include "commands/pairing/PairingCommand.h" + +#include +#include +#include + +class Unpair : public PairingCommand +{ +public: + Unpair(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("unpair", PairingMode::None, PairingNetworkType::None, credsIssuerConfig) + {} +}; + +class PairCode : public PairingCommand +{ +public: + PairCode(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("code", PairingMode::Code, PairingNetworkType::None, credsIssuerConfig) + {} +}; + +class PairCodePase : public PairingCommand +{ +public: + PairCodePase(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("code-paseonly", PairingMode::CodePaseOnly, PairingNetworkType::None, credsIssuerConfig) + {} +}; + +class PairCodeWifi : public PairingCommand +{ +public: + PairCodeWifi(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("code-wifi", PairingMode::Code, PairingNetworkType::WiFi, credsIssuerConfig) + {} +}; + +class PairCodeThread : public PairingCommand +{ +public: + PairCodeThread(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("code-thread", PairingMode::Code, PairingNetworkType::Thread, credsIssuerConfig) + {} +}; + +class PairOnNetwork : public PairingCommand +{ +public: + PairOnNetwork(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig) + {} +}; + +class PairOnNetworkShort : public PairingCommand +{ +public: + PairOnNetworkShort(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork-short", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig, + chip::Dnssd::DiscoveryFilterType::kShortDiscriminator) + {} +}; + +class PairOnNetworkLong : public PairingCommand +{ +public: + PairOnNetworkLong(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork-long", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig, + chip::Dnssd::DiscoveryFilterType::kLongDiscriminator) + {} +}; + +class PairOnNetworkVendor : public PairingCommand +{ +public: + PairOnNetworkVendor(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork-vendor", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig, + chip::Dnssd::DiscoveryFilterType::kVendorId) + {} +}; + +class PairOnNetworkFabric : public PairingCommand +{ +public: + PairOnNetworkFabric(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork-fabric", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig, + chip::Dnssd::DiscoveryFilterType::kCompressedFabricId) + {} +}; + +class PairOnNetworkCommissioningMode : public PairingCommand +{ +public: + PairOnNetworkCommissioningMode(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork-commissioning-mode", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig, + chip::Dnssd::DiscoveryFilterType::kCommissioningMode) + {} +}; + +class PairOnNetworkCommissioner : public PairingCommand +{ +public: + PairOnNetworkCommissioner(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork-commissioner", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig, + chip::Dnssd::DiscoveryFilterType::kCommissioner) + {} +}; + +class PairOnNetworkDeviceType : public PairingCommand +{ +public: + PairOnNetworkDeviceType(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork-device-type", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig, + chip::Dnssd::DiscoveryFilterType::kDeviceType) + {} +}; + +class PairOnNetworkInstanceName : public PairingCommand +{ +public: + PairOnNetworkInstanceName(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("onnetwork-instance-name", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig, + chip::Dnssd::DiscoveryFilterType::kInstanceName) + {} +}; + +class PairBleWiFi : public PairingCommand +{ +public: + PairBleWiFi(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("ble-wifi", PairingMode::Ble, PairingNetworkType::WiFi, credsIssuerConfig) + {} +}; + +class PairBleThread : public PairingCommand +{ +public: + PairBleThread(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("ble-thread", PairingMode::Ble, PairingNetworkType::Thread, credsIssuerConfig) + {} +}; + +class PairSoftAP : public PairingCommand +{ +public: + PairSoftAP(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("softap", PairingMode::SoftAP, PairingNetworkType::WiFi, credsIssuerConfig) + {} +}; + +class PairAlreadyDiscovered : public PairingCommand +{ +public: + PairAlreadyDiscovered(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("already-discovered", PairingMode::AlreadyDiscovered, PairingNetworkType::None, credsIssuerConfig) + {} +}; + +class PairAlreadyDiscoveredByIndex : public PairingCommand +{ +public: + PairAlreadyDiscoveredByIndex(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("already-discovered-by-index", PairingMode::AlreadyDiscoveredByIndex, PairingNetworkType::None, + credsIssuerConfig) + {} +}; + +class PairAlreadyDiscoveredByIndexWithWiFi : public PairingCommand +{ +public: + PairAlreadyDiscoveredByIndexWithWiFi(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("already-discovered-by-index-with-wifi", PairingMode::AlreadyDiscoveredByIndex, PairingNetworkType::WiFi, + credsIssuerConfig) + {} +}; + +class PairAlreadyDiscoveredByIndexWithCode : public PairingCommand +{ +public: + PairAlreadyDiscoveredByIndexWithCode(CredentialIssuerCommands * credsIssuerConfig) : + PairingCommand("already-discovered-by-index-with-code", PairingMode::AlreadyDiscoveredByIndexWithCode, + PairingNetworkType::None, credsIssuerConfig) + {} +}; + +class StartUdcServerCommand : public CHIPCommand +{ +public: + StartUdcServerCommand(CredentialIssuerCommands * credsIssuerConfig) : CHIPCommand("start-udc-server", credsIssuerConfig) {} + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(300); } + + CHIP_ERROR RunCommand() override + { + chip::app::DnssdServer::Instance().StartServer(chip::Dnssd::CommissioningMode::kDisabled); + return CHIP_NO_ERROR; + } +}; + +void registerCommandsPairing(Commands & commands, CredentialIssuerCommands * credsIssuerConfig) +{ + const char * clusterName = "Pairing"; + + commands_list clusterCommands = { + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + // TODO(#13973) - enable CommissionedListCommand once DNS Cache is implemented + // make_unique(), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + make_unique(credsIssuerConfig), + }; + + commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for commissioning devices."); +} diff --git a/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.h b/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.h new file mode 100644 index 00000000000000..3234cfe456a956 --- /dev/null +++ b/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.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. + * + */ + +#pragma once + +#include "../common/CHIPCommand.h" +#include "../common/RemoteDataModelLogger.h" + +class GetCommissionerNodeIdCommand : public CHIPCommand +{ +public: + GetCommissionerNodeIdCommand(CredentialIssuerCommands * credIssuerCommands) : + CHIPCommand("get-commissioner-node-id", credIssuerCommands) + {} + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override + { + chip::NodeId id; + ReturnErrorOnFailure(GetIdentityNodeId(GetIdentity(), &id)); + ChipLogProgress(NotSpecified, "Commissioner Node Id 0x:" ChipLogFormatX64, ChipLogValueX64(id)); + + ReturnErrorOnFailure(RemoteDataModelLogger::LogGetCommissionerNodeId(id)); + SetCommandExitStatus(CHIP_NO_ERROR); + return CHIP_NO_ERROR; + } + + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); } +}; diff --git a/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h b/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h new file mode 100644 index 00000000000000..1d25efcc38224d --- /dev/null +++ b/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h @@ -0,0 +1,53 @@ +/* + * 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. + * + */ + +#pragma once + +#include "../common/CHIPCommand.h" +#include "../common/RemoteDataModelLogger.h" + +#include "ToTLVCert.h" + +#include + +class GetCommissionerRootCertificateCommand : public CHIPCommand +{ +public: + GetCommissionerRootCertificateCommand(CredentialIssuerCommands * credIssuerCommands) : + CHIPCommand("get-commissioner-root-certificate", credIssuerCommands, + "Returns a base64-encoded RCAC prefixed with: 'base64:'") + {} + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override + { + chip::ByteSpan span; + ReturnErrorOnFailure(GetIdentityRootCertificate(GetIdentity(), span)); + + std::string rcac; + ReturnErrorOnFailure(ToTLVCert(span, rcac)); + ChipLogProgress(NotSpecified, "RCAC: %s", rcac.c_str()); + + ReturnErrorOnFailure(RemoteDataModelLogger::LogGetCommissionerRootCertificate(rcac.c_str())); + + SetCommandExitStatus(CHIP_NO_ERROR); + return CHIP_NO_ERROR; + } + + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); } +}; diff --git a/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h b/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h new file mode 100644 index 00000000000000..0103b26977136d --- /dev/null +++ b/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h @@ -0,0 +1,91 @@ +/* + * 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. + * + */ + +#pragma once + +#include "../common/CHIPCommand.h" +#include "../common/RemoteDataModelLogger.h" + +#include "ToTLVCert.h" + +#include + +class IssueNOCChainCommand : public CHIPCommand +{ +public: + IssueNOCChainCommand(CredentialIssuerCommands * credIssuerCommands) : + CHIPCommand("issue-noc-chain", credIssuerCommands, + "Returns a base64-encoded NOC, ICAC, RCAC, and IPK prefixed with: 'base64:'"), + mDeviceNOCChainCallback(OnDeviceNOCChainGeneration, this) + { + AddArgument("elements", &mNOCSRElements, "NOCSRElements encoded in hexadecimal"); + AddArgument("node-id", 0, UINT64_MAX, &mNodeId, "The target node id"); + } + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override + { + auto & commissioner = CurrentCommissioner(); + ReturnErrorOnFailure(commissioner.IssueNOCChain(mNOCSRElements, mNodeId, &mDeviceNOCChainCallback)); + return CHIP_NO_ERROR; + } + + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); } + + static void OnDeviceNOCChainGeneration(void * context, CHIP_ERROR status, const chip::ByteSpan & noc, + const chip::ByteSpan & icac, const chip::ByteSpan & rcac, + chip::Optional ipk, + chip::Optional adminSubject) + { + auto command = static_cast(context); + + auto err = status; + VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err)); + + std::string nocStr; + err = ToTLVCert(noc, nocStr); + VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err)); + ChipLogProgress(NotSpecified, "NOC: %s", nocStr.c_str()); + + std::string icacStr; + err = ToTLVCert(icac, icacStr); + VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err)); + ChipLogProgress(NotSpecified, "ICAC: %s", icacStr.c_str()); + + std::string rcacStr; + err = ToTLVCert(rcac, rcacStr); + VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err)); + ChipLogProgress(NotSpecified, "RCAC: %s", rcacStr.c_str()); + + std::string ipkStr; + if (ipk.HasValue()) + { + err = ToBase64(ipk.Value(), ipkStr); + VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err)); + } + ChipLogProgress(NotSpecified, "IPK: %s", ipkStr.c_str()); + + err = RemoteDataModelLogger::LogIssueNOCChain(nocStr.c_str(), icacStr.c_str(), rcacStr.c_str(), ipkStr.c_str()); + command->SetCommandExitStatus(err); + } + +private: + chip::Callback::Callback mDeviceNOCChainCallback; + chip::ByteSpan mNOCSRElements; + chip::NodeId mNodeId; +}; diff --git a/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp new file mode 100644 index 00000000000000..bc4af6c4a51cec --- /dev/null +++ b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp @@ -0,0 +1,62 @@ +/* + * 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 "OpenCommissioningWindowCommand.h" + +#include + +using namespace ::chip; + +CHIP_ERROR OpenCommissioningWindowCommand::RunCommand() +{ + mWindowOpener = Platform::MakeUnique(&CurrentCommissioner()); + if (mCommissioningWindowOption == Controller::CommissioningWindowOpener::CommissioningWindowOption::kOriginalSetupCode) + { + return mWindowOpener->OpenBasicCommissioningWindow(mNodeId, System::Clock::Seconds16(mCommissioningWindowTimeout), + &mOnOpenBasicCommissioningWindowCallback); + } + + if (mCommissioningWindowOption == Controller::CommissioningWindowOpener::CommissioningWindowOption::kTokenWithRandomPIN) + { + SetupPayload ignored; + return mWindowOpener->OpenCommissioningWindow(mNodeId, System::Clock::Seconds16(mCommissioningWindowTimeout), mIteration, + mDiscriminator, NullOptional, NullOptional, + &mOnOpenCommissioningWindowCallback, ignored, + /* readVIDPIDAttributes */ true); + } + + ChipLogError(NotSpecified, "Unknown commissioning window option: %d", to_underlying(mCommissioningWindowOption)); + return CHIP_ERROR_INVALID_ARGUMENT; +} + +void OpenCommissioningWindowCommand::OnOpenCommissioningWindowResponse(void * context, NodeId remoteId, CHIP_ERROR err, + chip::SetupPayload payload) +{ + LogErrorOnFailure(err); + + OnOpenBasicCommissioningWindowResponse(context, remoteId, err); +} + +void OpenCommissioningWindowCommand::OnOpenBasicCommissioningWindowResponse(void * context, NodeId remoteId, CHIP_ERROR err) +{ + LogErrorOnFailure(err); + + OpenCommissioningWindowCommand * command = reinterpret_cast(context); + VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnOpenCommissioningWindowCommand: context is null")); + command->SetCommandExitStatus(err); +} diff --git a/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h new file mode 100644 index 00000000000000..99b179d8753125 --- /dev/null +++ b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h @@ -0,0 +1,67 @@ +/* + * 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. + * + */ + +#pragma once + +#include "../common/CHIPCommand.h" + +#include +#include + +class OpenCommissioningWindowCommand : public CHIPCommand +{ +public: + OpenCommissioningWindowCommand(CredentialIssuerCommands * credIssuerCommands) : + CHIPCommand("open-commissioning-window", credIssuerCommands), + mOnOpenCommissioningWindowCallback(OnOpenCommissioningWindowResponse, this), + mOnOpenBasicCommissioningWindowCallback(OnOpenBasicCommissioningWindowResponse, this) + { + AddArgument("node-id", 0, UINT64_MAX, &mNodeId, "Node to send command to."); + AddArgument("option", 0, 2, &mCommissioningWindowOption, + "1 to use Enhanced Commissioning Method.\n 0 to use Basic Commissioning Method."); + AddArgument("window-timeout", 0, UINT16_MAX, &mCommissioningWindowTimeout, + "Time, in seconds, before the commissioning window closes."); + AddArgument("iteration", chip::Crypto::kSpake2p_Min_PBKDF_Iterations, chip::Crypto::kSpake2p_Max_PBKDF_Iterations, + &mIteration, "Number of PBKDF iterations to use to derive the verifier. Ignored if 'option' is 0."); + AddArgument("discriminator", 0, 4096, &mDiscriminator, "Discriminator to use for advertising. Ignored if 'option' is 0."); + AddArgument("timeout", 0, UINT16_MAX, &mTimeout, "Time, in seconds, before this command is considered to have timed out."); + } + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override; + // We issue multiple data model operations for this command, and the default + // timeout for those is 10 seconds, so default to 20 seconds. + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(20)); } + +private: + NodeId mNodeId; + chip::Controller::CommissioningWindowOpener::CommissioningWindowOption mCommissioningWindowOption; + uint16_t mCommissioningWindowTimeout; + uint32_t mIteration; + uint16_t mDiscriminator; + + chip::Optional mTimeout; + + chip::Platform::UniquePtr mWindowOpener; + + static void OnOpenCommissioningWindowResponse(void * context, NodeId deviceId, CHIP_ERROR status, chip::SetupPayload payload); + static void OnOpenBasicCommissioningWindowResponse(void * context, NodeId deviceId, CHIP_ERROR status); + + chip::Callback::Callback mOnOpenCommissioningWindowCallback; + chip::Callback::Callback mOnOpenBasicCommissioningWindowCallback; +}; diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.cpp b/examples/fabric-admin/commands/pairing/PairingCommand.cpp new file mode 100644 index 00000000000000..80775f0853d110 --- /dev/null +++ b/examples/fabric-admin/commands/pairing/PairingCommand.cpp @@ -0,0 +1,551 @@ +/* + * 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 "PairingCommand.h" +#include "platform/PlatformManager.h" +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace ::chip; +using namespace ::chip::Controller; + +CHIP_ERROR PairingCommand::RunCommand() +{ + CurrentCommissioner().RegisterPairingDelegate(this); + // Clear the CATs in OperationalCredentialsIssuer + mCredIssuerCmds->SetCredentialIssuerCATValues(kUndefinedCATs); + + mDeviceIsICD = false; + + if (mCASEAuthTags.HasValue() && mCASEAuthTags.Value().size() <= kMaxSubjectCATAttributeCount) + { + CATValues cats = kUndefinedCATs; + for (size_t index = 0; index < mCASEAuthTags.Value().size(); ++index) + { + cats.values[index] = mCASEAuthTags.Value()[index]; + } + if (cats.AreValid()) + { + mCredIssuerCmds->SetCredentialIssuerCATValues(cats); + } + } + return RunInternal(mNodeId); +} + +CHIP_ERROR PairingCommand::RunInternal(NodeId remoteId) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + switch (mPairingMode) + { + case PairingMode::None: + err = Unpair(remoteId); + break; + case PairingMode::Code: + err = PairWithCode(remoteId); + break; + case PairingMode::CodePaseOnly: + err = PaseWithCode(remoteId); + break; + case PairingMode::Ble: + err = Pair(remoteId, PeerAddress::BLE()); + break; + case PairingMode::OnNetwork: + err = PairWithMdns(remoteId); + break; + case PairingMode::SoftAP: + err = Pair(remoteId, PeerAddress::UDP(mRemoteAddr.address, mRemotePort, mRemoteAddr.interfaceId)); + break; + case PairingMode::AlreadyDiscovered: + err = Pair(remoteId, PeerAddress::UDP(mRemoteAddr.address, mRemotePort, mRemoteAddr.interfaceId)); + break; + case PairingMode::AlreadyDiscoveredByIndex: + err = PairWithMdnsOrBleByIndex(remoteId, mIndex); + break; + case PairingMode::AlreadyDiscoveredByIndexWithCode: + err = PairWithMdnsOrBleByIndexWithCode(remoteId, mIndex); + break; + } + + return err; +} + +CommissioningParameters PairingCommand::GetCommissioningParameters() +{ + auto params = CommissioningParameters(); + params.SetSkipCommissioningComplete(mSkipCommissioningComplete.ValueOr(false)); + if (mBypassAttestationVerifier.ValueOr(false)) + { + params.SetDeviceAttestationDelegate(this); + } + + switch (mNetworkType) + { + case PairingNetworkType::WiFi: + params.SetWiFiCredentials(Controller::WiFiCredentials(mSSID, mPassword)); + break; + case PairingNetworkType::Thread: + params.SetThreadOperationalDataset(mOperationalDataset); + break; + case PairingNetworkType::None: + break; + } + + if (mCountryCode.HasValue()) + { + params.SetCountryCode(CharSpan::fromCharString(mCountryCode.Value())); + } + + // mTimeZoneList is an optional argument managed by TypedComplexArgument mComplex_TimeZones. + // Since optional Complex arguments are not currently supported via the class, + // we will use mTimeZoneList.data() value to determine if the argument was provided. + if (mTimeZoneList.data()) + { + params.SetTimeZone(mTimeZoneList); + } + + // miDSTOffsetList is an optional argument managed by TypedComplexArgument mComplex_DSTOffsets. + // Since optional Complex arguments are not currently supported via the class, + // we will use mTimeZoneList.data() value to determine if the argument was provided. + if (mDSTOffsetList.data()) + { + params.SetDSTOffsets(mDSTOffsetList); + } + + if (mICDRegistration.ValueOr(false)) + { + params.SetICDRegistrationStrategy(ICDRegistrationStrategy::kBeforeComplete); + + if (!mICDSymmetricKey.HasValue()) + { + chip::Crypto::DRBG_get_bytes(mRandomGeneratedICDSymmetricKey, sizeof(mRandomGeneratedICDSymmetricKey)); + mICDSymmetricKey.SetValue(ByteSpan(mRandomGeneratedICDSymmetricKey)); + } + if (!mICDCheckInNodeId.HasValue()) + { + mICDCheckInNodeId.SetValue(CurrentCommissioner().GetNodeId()); + } + if (!mICDMonitoredSubject.HasValue()) + { + mICDMonitoredSubject.SetValue(mICDCheckInNodeId.Value()); + } + // These Optionals must have values now. + // The commissioner will verify these values. + params.SetICDSymmetricKey(mICDSymmetricKey.Value()); + if (mICDStayActiveDurationMsec.HasValue()) + { + params.SetICDStayActiveDurationMsec(mICDStayActiveDurationMsec.Value()); + } + params.SetICDCheckInNodeId(mICDCheckInNodeId.Value()); + params.SetICDMonitoredSubject(mICDMonitoredSubject.Value()); + } + + return params; +} + +CHIP_ERROR PairingCommand::PaseWithCode(NodeId remoteId) +{ + auto discoveryType = DiscoveryType::kAll; + if (mUseOnlyOnNetworkDiscovery.ValueOr(false)) + { + discoveryType = DiscoveryType::kDiscoveryNetworkOnly; + } + + if (mDiscoverOnce.ValueOr(false)) + { + discoveryType = DiscoveryType::kDiscoveryNetworkOnlyWithoutPASEAutoRetry; + } + + return CurrentCommissioner().EstablishPASEConnection(remoteId, mOnboardingPayload, discoveryType); +} + +CHIP_ERROR PairingCommand::PairWithCode(NodeId remoteId) +{ + CommissioningParameters commissioningParams = GetCommissioningParameters(); + + // If no network discovery behavior and no network credentials are provided, assume that the pairing command is trying to pair + // with an on-network device. + if (!mUseOnlyOnNetworkDiscovery.HasValue()) + { + auto threadCredentials = commissioningParams.GetThreadOperationalDataset(); + auto wiFiCredentials = commissioningParams.GetWiFiCredentials(); + mUseOnlyOnNetworkDiscovery.SetValue(!threadCredentials.HasValue() && !wiFiCredentials.HasValue()); + } + + auto discoveryType = DiscoveryType::kAll; + if (mUseOnlyOnNetworkDiscovery.ValueOr(false)) + { + discoveryType = DiscoveryType::kDiscoveryNetworkOnly; + } + + if (mDiscoverOnce.ValueOr(false)) + { + discoveryType = DiscoveryType::kDiscoveryNetworkOnlyWithoutPASEAutoRetry; + } + + return CurrentCommissioner().PairDevice(remoteId, mOnboardingPayload, commissioningParams, discoveryType); +} + +CHIP_ERROR PairingCommand::Pair(NodeId remoteId, PeerAddress address) +{ + auto params = RendezvousParameters().SetSetupPINCode(mSetupPINCode).SetDiscriminator(mDiscriminator).SetPeerAddress(address); + + CHIP_ERROR err = CHIP_NO_ERROR; + if (mPaseOnly.ValueOr(false)) + { + err = CurrentCommissioner().EstablishPASEConnection(remoteId, params); + } + else + { + auto commissioningParams = GetCommissioningParameters(); + err = CurrentCommissioner().PairDevice(remoteId, params, commissioningParams); + } + return err; +} + +CHIP_ERROR PairingCommand::PairWithMdnsOrBleByIndex(NodeId remoteId, uint16_t index) +{ +#if CHIP_DEVICE_LAYER_TARGET_DARWIN + VerifyOrReturnError(IsInteractive(), CHIP_ERROR_INCORRECT_STATE); + + RendezvousParameters params; + ReturnErrorOnFailure(GetDeviceScanner().Get(index, params)); + params.SetSetupPINCode(mSetupPINCode); + + CHIP_ERROR err = CHIP_NO_ERROR; + if (mPaseOnly.ValueOr(false)) + { + err = CurrentCommissioner().EstablishPASEConnection(remoteId, params); + } + else + { + auto commissioningParams = GetCommissioningParameters(); + err = CurrentCommissioner().PairDevice(remoteId, params, commissioningParams); + } + return err; +#else + return CHIP_ERROR_NOT_IMPLEMENTED; +#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN +} + +CHIP_ERROR PairingCommand::PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uint16_t index) +{ +#if CHIP_DEVICE_LAYER_TARGET_DARWIN + VerifyOrReturnError(IsInteractive(), CHIP_ERROR_INCORRECT_STATE); + + Dnssd::CommonResolutionData resolutionData; + auto err = GetDeviceScanner().Get(index, resolutionData); + if (CHIP_ERROR_NOT_FOUND == err) + { + // There is no device with this index that has some resolution data. This could simply + // be because the device is a ble device. In this case let's fall back to looking for + // a device with this index and some RendezvousParameters. + chip::SetupPayload payload; + bool isQRCode = strncmp(mOnboardingPayload, kQRCodePrefix, strlen(kQRCodePrefix)) == 0; + if (isQRCode) + { + ReturnErrorOnFailure(QRCodeSetupPayloadParser(mOnboardingPayload).populatePayload(payload)); + VerifyOrReturnError(payload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT); + } + else + { + ReturnErrorOnFailure(ManualSetupPayloadParser(mOnboardingPayload).populatePayload(payload)); + VerifyOrReturnError(payload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT); + } + + mSetupPINCode = payload.setUpPINCode; + return PairWithMdnsOrBleByIndex(remoteId, index); + } + + err = CHIP_NO_ERROR; + if (mPaseOnly.ValueOr(false)) + { + err = CurrentCommissioner().EstablishPASEConnection(remoteId, mOnboardingPayload, DiscoveryType::kDiscoveryNetworkOnly, + MakeOptional(resolutionData)); + } + else + { + auto commissioningParams = GetCommissioningParameters(); + err = CurrentCommissioner().PairDevice(remoteId, mOnboardingPayload, commissioningParams, + DiscoveryType::kDiscoveryNetworkOnly, MakeOptional(resolutionData)); + } + return err; +#else + return CHIP_ERROR_NOT_IMPLEMENTED; +#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN +} + +CHIP_ERROR PairingCommand::PairWithMdns(NodeId remoteId) +{ + Dnssd::DiscoveryFilter filter(mFilterType); + switch (mFilterType) + { + case chip::Dnssd::DiscoveryFilterType::kNone: + break; + case chip::Dnssd::DiscoveryFilterType::kShortDiscriminator: + case chip::Dnssd::DiscoveryFilterType::kLongDiscriminator: + case chip::Dnssd::DiscoveryFilterType::kCompressedFabricId: + case chip::Dnssd::DiscoveryFilterType::kVendorId: + case chip::Dnssd::DiscoveryFilterType::kDeviceType: + filter.code = mDiscoveryFilterCode; + break; + case chip::Dnssd::DiscoveryFilterType::kCommissioningMode: + break; + case chip::Dnssd::DiscoveryFilterType::kCommissioner: + filter.code = 1; + break; + case chip::Dnssd::DiscoveryFilterType::kInstanceName: + filter.code = 0; + filter.instanceName = mDiscoveryFilterInstanceName; + break; + } + + CurrentCommissioner().RegisterDeviceDiscoveryDelegate(this); + return CurrentCommissioner().DiscoverCommissionableNodes(filter); +} + +CHIP_ERROR PairingCommand::Unpair(NodeId remoteId) +{ + mCurrentFabricRemover = Platform::MakeUnique(&CurrentCommissioner()); + return mCurrentFabricRemover->RemoveCurrentFabric(remoteId, &mCurrentFabricRemoveCallback); +} + +void PairingCommand::OnStatusUpdate(DevicePairingDelegate::Status status) +{ + switch (status) + { + case DevicePairingDelegate::Status::SecurePairingSuccess: + ChipLogProgress(NotSpecified, "Secure Pairing Success"); + ChipLogProgress(NotSpecified, "CASE establishment successful"); + break; + case DevicePairingDelegate::Status::SecurePairingFailed: + ChipLogError(NotSpecified, "Secure Pairing Failed"); + SetCommandExitStatus(CHIP_ERROR_INCORRECT_STATE); + break; + } +} + +void PairingCommand::OnPairingComplete(CHIP_ERROR err) +{ + if (err == CHIP_NO_ERROR) + { + ChipLogProgress(NotSpecified, "Pairing Success"); + ChipLogProgress(NotSpecified, "PASE establishment successful"); + if (mPairingMode == PairingMode::CodePaseOnly || mPaseOnly.ValueOr(false)) + { + SetCommandExitStatus(err); + } + } + else + { + ChipLogProgress(NotSpecified, "Pairing Failure: %s", ErrorStr(err)); + } + + if (err != CHIP_NO_ERROR) + { + SetCommandExitStatus(err); + } +} + +void PairingCommand::OnPairingDeleted(CHIP_ERROR err) +{ + if (err == CHIP_NO_ERROR) + { + ChipLogProgress(NotSpecified, "Pairing Deleted Success"); + } + else + { + ChipLogProgress(NotSpecified, "Pairing Deleted Failure: %s", ErrorStr(err)); + } + + SetCommandExitStatus(err); +} + +void PairingCommand::OnCommissioningComplete(NodeId nodeId, CHIP_ERROR err) +{ + if (err == CHIP_NO_ERROR) + { + ChipLogProgress(NotSpecified, "Device commissioning completed with success"); + } + else + { + // When ICD device commissioning fails, the ICDClientInfo stored in OnICDRegistrationComplete needs to be removed. + if (mDeviceIsICD) + { + CHIP_ERROR deleteEntryError = + CHIPCommand::sICDClientStorage.DeleteEntry(ScopedNodeId(mNodeId, CurrentCommissioner().GetFabricIndex())); + if (deleteEntryError != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to delete ICD entry: %s", ErrorStr(err)); + } + } + ChipLogProgress(NotSpecified, "Device commissioning Failure: %s", ErrorStr(err)); + } + + SetCommandExitStatus(err); +} + +void PairingCommand::OnReadCommissioningInfo(const Controller::ReadCommissioningInfo & info) +{ + ChipLogProgress(AppServer, "OnReadCommissioningInfo - vendorId=0x%04X productId=0x%04X", info.basic.vendorId, + info.basic.productId); + + // The string in CharSpan received from the device is not null-terminated, we use std::string here for coping and + // appending a numm-terminator at the end of the string. + std::string userActiveModeTriggerInstruction; + + // Note: the callback doesn't own the buffer, should make a copy if it will be used it later. + if (info.icd.userActiveModeTriggerInstruction.size() != 0) + { + userActiveModeTriggerInstruction = + std::string(info.icd.userActiveModeTriggerInstruction.data(), info.icd.userActiveModeTriggerInstruction.size()); + } + + if (info.icd.userActiveModeTriggerHint.HasAny()) + { + ChipLogProgress(AppServer, "OnReadCommissioningInfo - LIT UserActiveModeTriggerHint=0x%08x", + info.icd.userActiveModeTriggerHint.Raw()); + ChipLogProgress(AppServer, "OnReadCommissioningInfo - LIT UserActiveModeTriggerInstruction=%s", + userActiveModeTriggerInstruction.c_str()); + } + ChipLogProgress(AppServer, "OnReadCommissioningInfo ICD - IdleModeDuration=%u activeModeDuration=%u activeModeThreshold=%u", + info.icd.idleModeDuration, info.icd.activeModeDuration, info.icd.activeModeThreshold); +} + +void PairingCommand::OnICDRegistrationComplete(NodeId nodeId, uint32_t icdCounter) +{ + char icdSymmetricKeyHex[chip::Crypto::kAES_CCM128_Key_Length * 2 + 1]; + + chip::Encoding::BytesToHex(mICDSymmetricKey.Value().data(), mICDSymmetricKey.Value().size(), icdSymmetricKeyHex, + sizeof(icdSymmetricKeyHex), chip::Encoding::HexFlags::kNullTerminate); + + app::ICDClientInfo clientInfo; + clientInfo.peer_node = ScopedNodeId(nodeId, CurrentCommissioner().GetFabricIndex()); + clientInfo.monitored_subject = mICDMonitoredSubject.Value(); + clientInfo.start_icd_counter = icdCounter; + + CHIP_ERROR err = CHIPCommand::sICDClientStorage.SetKey(clientInfo, mICDSymmetricKey.Value()); + if (err == CHIP_NO_ERROR) + { + err = CHIPCommand::sICDClientStorage.StoreEntry(clientInfo); + } + + if (err != CHIP_NO_ERROR) + { + CHIPCommand::sICDClientStorage.RemoveKey(clientInfo); + ChipLogError(NotSpecified, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s", ChipLogValueX64(nodeId), + err.AsString()); + SetCommandExitStatus(err); + return; + } + + mDeviceIsICD = true; + + ChipLogProgress(NotSpecified, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(nodeId)); + ChipLogProgress(NotSpecified, + "ICD Registration Complete for device " ChipLogFormatX64 " / Check-In NodeID: " ChipLogFormatX64 + " / Monitored Subject: " ChipLogFormatX64 " / Symmetric Key: %s / ICDCounter %u", + ChipLogValueX64(nodeId), ChipLogValueX64(mICDCheckInNodeId.Value()), + ChipLogValueX64(mICDMonitoredSubject.Value()), icdSymmetricKeyHex, icdCounter); +} + +void PairingCommand::OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration) +{ + ChipLogProgress(NotSpecified, "ICD Stay Active Complete for device " ChipLogFormatX64 " / promisedActiveDuration: %u", + ChipLogValueX64(deviceId), promisedActiveDuration); +} + +void PairingCommand::OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData) +{ + // Ignore nodes with closed commissioning window + VerifyOrReturn(nodeData.commissioningMode != 0); + + auto & resolutionData = nodeData; + + const uint16_t port = resolutionData.port; + char buf[chip::Inet::IPAddress::kMaxStringLength]; + resolutionData.ipAddress[0].ToString(buf); + ChipLogProgress(NotSpecified, "Discovered Device: %s:%u", buf, port); + + // Stop Mdns discovery. + auto err = CurrentCommissioner().StopCommissionableDiscovery(); + + // Some platforms does not implement a mechanism to stop mdns browse, so + // we just ignore CHIP_ERROR_NOT_IMPLEMENTED instead of bailing out. + if (CHIP_NO_ERROR != err && CHIP_ERROR_NOT_IMPLEMENTED != err) + { + SetCommandExitStatus(err); + return; + } + + CurrentCommissioner().RegisterDeviceDiscoveryDelegate(nullptr); + + auto interfaceId = resolutionData.ipAddress[0].IsIPv6LinkLocal() ? resolutionData.interfaceId : Inet::InterfaceId::Null(); + auto peerAddress = PeerAddress::UDP(resolutionData.ipAddress[0], port, interfaceId); + err = Pair(mNodeId, peerAddress); + if (CHIP_NO_ERROR != err) + { + SetCommandExitStatus(err); + } +} + +void PairingCommand::OnCurrentFabricRemove(void * context, NodeId nodeId, CHIP_ERROR err) +{ + PairingCommand * command = reinterpret_cast(context); + VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnCurrentFabricRemove: context is null")); + + if (err == CHIP_NO_ERROR) + { + ChipLogProgress(NotSpecified, "Device unpair completed with success: " ChipLogFormatX64, ChipLogValueX64(nodeId)); + } + else + { + ChipLogProgress(NotSpecified, "Device unpair Failure: " ChipLogFormatX64 " %s", ChipLogValueX64(nodeId), ErrorStr(err)); + } + + command->SetCommandExitStatus(err); +} + +chip::Optional PairingCommand::FailSafeExpiryTimeoutSecs() const +{ + // We don't need to set additional failsafe timeout as we don't ask the final user if he wants to continue + return chip::Optional(); +} + +void PairingCommand::OnDeviceAttestationCompleted(chip::Controller::DeviceCommissioner * deviceCommissioner, + chip::DeviceProxy * device, + const chip::Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info, + chip::Credentials::AttestationVerificationResult attestationResult) +{ + // Bypass attestation verification, continue with success + auto err = deviceCommissioner->ContinueCommissioningAfterDeviceAttestation( + device, chip::Credentials::AttestationVerificationResult::kSuccess); + if (CHIP_NO_ERROR != err) + { + SetCommandExitStatus(err); + } +} diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.h b/examples/fabric-admin/commands/pairing/PairingCommand.h new file mode 100644 index 00000000000000..4ff3903253be4e --- /dev/null +++ b/examples/fabric-admin/commands/pairing/PairingCommand.h @@ -0,0 +1,268 @@ +/* + * 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. + * + */ + +#pragma once + +#include "../common/CHIPCommand.h" +#include +#include + +#include +#include +#include + +enum class PairingMode +{ + None, + Code, + CodePaseOnly, + Ble, + SoftAP, + AlreadyDiscovered, + AlreadyDiscoveredByIndex, + AlreadyDiscoveredByIndexWithCode, + OnNetwork, +}; + +enum class PairingNetworkType +{ + None, + WiFi, + Thread, +}; + +class PairingCommand : public CHIPCommand, + public chip::Controller::DevicePairingDelegate, + public chip::Controller::DeviceDiscoveryDelegate, + public chip::Credentials::DeviceAttestationDelegate +{ +public: + PairingCommand(const char * commandName, PairingMode mode, PairingNetworkType networkType, + CredentialIssuerCommands * credIssuerCmds, + chip::Dnssd::DiscoveryFilterType filterType = chip::Dnssd::DiscoveryFilterType::kNone) : + CHIPCommand(commandName, credIssuerCmds), + mPairingMode(mode), mNetworkType(networkType), mFilterType(filterType), + mRemoteAddr{ IPAddress::Any, chip::Inet::InterfaceId::Null() }, mComplex_TimeZones(&mTimeZoneList), + mComplex_DSTOffsets(&mDSTOffsetList), mCurrentFabricRemoveCallback(OnCurrentFabricRemove, this) + { + AddArgument("node-id", 0, UINT64_MAX, &mNodeId); + AddArgument("bypass-attestation-verifier", 0, 1, &mBypassAttestationVerifier, + "Bypass the attestation verifier. If not provided or false, the attestation verifier is not bypassed." + " If true, the commissioning will continue in case of attestation verification failure."); + AddArgument("case-auth-tags", 1, UINT32_MAX, &mCASEAuthTags, "The CATs to be encoded in the NOC sent to the commissionee"); + AddArgument("icd-registration", 0, 1, &mICDRegistration, + "Whether to register for check-ins from ICDs during commissioning. Default: false"); + AddArgument("icd-check-in-nodeid", 0, UINT64_MAX, &mICDCheckInNodeId, + "The check-in node id for the ICD, default: node id of the commissioner."); + AddArgument("icd-monitored-subject", 0, UINT64_MAX, &mICDMonitoredSubject, + "The monitored subject of the ICD, default: The node id used for icd-check-in-nodeid."); + AddArgument("icd-symmetric-key", &mICDSymmetricKey, "The 16 bytes ICD symmetric key, default: randomly generated."); + AddArgument("icd-stay-active-duration", 0, UINT32_MAX, &mICDStayActiveDurationMsec, + "If set, a LIT ICD that is commissioned will be requested to stay active for this many milliseconds"); + switch (networkType) + { + case PairingNetworkType::None: + break; + case PairingNetworkType::WiFi: + AddArgument("ssid", &mSSID); + AddArgument("password", &mPassword); + break; + case PairingNetworkType::Thread: + AddArgument("operationalDataset", &mOperationalDataset); + break; + } + + switch (mode) + { + case PairingMode::None: + break; + case PairingMode::Code: + AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete); + FALLTHROUGH; + case PairingMode::CodePaseOnly: + AddArgument("payload", &mOnboardingPayload); + AddArgument("discover-once", 0, 1, &mDiscoverOnce); + AddArgument("use-only-onnetwork-discovery", 0, 1, &mUseOnlyOnNetworkDiscovery); + break; + case PairingMode::Ble: + AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete); + AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode); + AddArgument("discriminator", 0, 4096, &mDiscriminator); + break; + case PairingMode::OnNetwork: + AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete); + AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode); + AddArgument("pase-only", 0, 1, &mPaseOnly); + break; + case PairingMode::SoftAP: + AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete); + AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode); + AddArgument("discriminator", 0, 4096, &mDiscriminator); + AddArgument("device-remote-ip", &mRemoteAddr); + AddArgument("device-remote-port", 0, UINT16_MAX, &mRemotePort); + AddArgument("pase-only", 0, 1, &mPaseOnly); + break; + case PairingMode::AlreadyDiscovered: + AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete); + AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode); + AddArgument("device-remote-ip", &mRemoteAddr); + AddArgument("device-remote-port", 0, UINT16_MAX, &mRemotePort); + AddArgument("pase-only", 0, 1, &mPaseOnly); + break; + case PairingMode::AlreadyDiscoveredByIndex: + AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete); + AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode); + AddArgument("index", 0, UINT16_MAX, &mIndex); + AddArgument("pase-only", 0, 1, &mPaseOnly); + break; + case PairingMode::AlreadyDiscoveredByIndexWithCode: + AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete); + AddArgument("payload", &mOnboardingPayload); + AddArgument("index", 0, UINT16_MAX, &mIndex); + AddArgument("pase-only", 0, 1, &mPaseOnly); + break; + } + + switch (filterType) + { + case chip::Dnssd::DiscoveryFilterType::kNone: + break; + case chip::Dnssd::DiscoveryFilterType::kShortDiscriminator: + AddArgument("discriminator", 0, 15, &mDiscoveryFilterCode); + break; + case chip::Dnssd::DiscoveryFilterType::kLongDiscriminator: + AddArgument("discriminator", 0, 4096, &mDiscoveryFilterCode); + break; + case chip::Dnssd::DiscoveryFilterType::kVendorId: + AddArgument("vendor-id", 0, UINT16_MAX, &mDiscoveryFilterCode); + break; + case chip::Dnssd::DiscoveryFilterType::kCompressedFabricId: + AddArgument("fabric-id", 0, UINT64_MAX, &mDiscoveryFilterCode); + break; + case chip::Dnssd::DiscoveryFilterType::kCommissioningMode: + case chip::Dnssd::DiscoveryFilterType::kCommissioner: + break; + case chip::Dnssd::DiscoveryFilterType::kDeviceType: + AddArgument("device-type", 0, UINT16_MAX, &mDiscoveryFilterCode); + break; + case chip::Dnssd::DiscoveryFilterType::kInstanceName: + AddArgument("name", &mDiscoveryFilterInstanceName); + break; + } + + if (mode != PairingMode::None) + { + AddArgument("country-code", &mCountryCode, + "Country code to use to set the Basic Information cluster's Location attribute"); + + // mTimeZoneList is an optional argument managed by TypedComplexArgument mComplex_TimeZones. + // Since optional Complex arguments are not currently supported via the class, + // we explicitly set the kOptional flag. + AddArgument("time-zone", &mComplex_TimeZones, + "TimeZone list to use when setting Time Synchronization cluster's TimeZone attribute", Argument::kOptional); + + // mDSTOffsetList is an optional argument managed by TypedComplexArgument mComplex_DSTOffsets. + // Since optional Complex arguments are not currently supported via the class, + // we explicitly set the kOptional flag. + AddArgument("dst-offset", &mComplex_DSTOffsets, + "DSTOffset list to use when setting Time Synchronization cluster's DSTOffset attribute", + Argument::kOptional); + } + + AddArgument("timeout", 0, UINT16_MAX, &mTimeout); + } + + /////////// CHIPCommand Interface ///////// + CHIP_ERROR RunCommand() override; + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(120)); } + + /////////// DevicePairingDelegate Interface ///////// + void OnStatusUpdate(chip::Controller::DevicePairingDelegate::Status status) override; + void OnPairingComplete(CHIP_ERROR error) override; + void OnPairingDeleted(CHIP_ERROR error) override; + void OnReadCommissioningInfo(const chip::Controller::ReadCommissioningInfo & info) override; + void OnCommissioningComplete(NodeId deviceId, CHIP_ERROR error) override; + void OnICDRegistrationComplete(NodeId deviceId, uint32_t icdCounter) override; + void OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration) override; + + /////////// DeviceDiscoveryDelegate Interface ///////// + void OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData) override; + + /////////// DeviceAttestationDelegate ///////// + chip::Optional FailSafeExpiryTimeoutSecs() const override; + void OnDeviceAttestationCompleted(chip::Controller::DeviceCommissioner * deviceCommissioner, chip::DeviceProxy * device, + const chip::Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info, + chip::Credentials::AttestationVerificationResult attestationResult) override; + +private: + CHIP_ERROR RunInternal(NodeId remoteId); + CHIP_ERROR Pair(NodeId remoteId, PeerAddress address); + CHIP_ERROR PairWithMdns(NodeId remoteId); + CHIP_ERROR PairWithCode(NodeId remoteId); + CHIP_ERROR PaseWithCode(NodeId remoteId); + CHIP_ERROR PairWithMdnsOrBleByIndex(NodeId remoteId, uint16_t index); + CHIP_ERROR PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uint16_t index); + CHIP_ERROR Unpair(NodeId remoteId); + chip::Controller::CommissioningParameters GetCommissioningParameters(); + + const PairingMode mPairingMode; + const PairingNetworkType mNetworkType; + const chip::Dnssd::DiscoveryFilterType mFilterType; + Command::AddressWithInterface mRemoteAddr; + NodeId mNodeId; + chip::Optional mTimeout; + chip::Optional mDiscoverOnce; + chip::Optional mUseOnlyOnNetworkDiscovery; + chip::Optional mPaseOnly; + chip::Optional mSkipCommissioningComplete; + chip::Optional mBypassAttestationVerifier; + chip::Optional> mCASEAuthTags; + chip::Optional mCountryCode; + chip::Optional mICDRegistration; + chip::Optional mICDCheckInNodeId; + chip::Optional mICDSymmetricKey; + chip::Optional mICDMonitoredSubject; + chip::Optional mICDStayActiveDurationMsec; + chip::app::DataModel::List mTimeZoneList; + TypedComplexArgument> + mComplex_TimeZones; + chip::app::DataModel::List mDSTOffsetList; + TypedComplexArgument> + mComplex_DSTOffsets; + + uint16_t mRemotePort; + uint16_t mDiscriminator; + uint32_t mSetupPINCode; + uint16_t mIndex; + chip::ByteSpan mOperationalDataset; + chip::ByteSpan mSSID; + chip::ByteSpan mPassword; + char * mOnboardingPayload; + uint64_t mDiscoveryFilterCode; + char * mDiscoveryFilterInstanceName; + + bool mDeviceIsICD; + uint8_t mRandomGeneratedICDSymmetricKey[chip::Crypto::kAES_CCM128_Key_Length]; + + // For unpair + chip::Platform::UniquePtr mCurrentFabricRemover; + chip::Callback::Callback mCurrentFabricRemoveCallback; + + static void OnCurrentFabricRemove(void * context, NodeId remoteNodeId, CHIP_ERROR status); + void PersistIcdInfo(); +}; diff --git a/examples/fabric-admin/commands/pairing/ToTLVCert.cpp b/examples/fabric-admin/commands/pairing/ToTLVCert.cpp new file mode 100644 index 00000000000000..01f9156f744593 --- /dev/null +++ b/examples/fabric-admin/commands/pairing/ToTLVCert.cpp @@ -0,0 +1,54 @@ +/* + * 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 "ToTLVCert.h" + +#include +#include + +#include + +constexpr char kBase64Header[] = "base64:"; +constexpr size_t kBase64HeaderLen = ArraySize(kBase64Header) - 1; + +CHIP_ERROR ToBase64(const chip::ByteSpan & input, std::string & outputAsPrefixedBase64) +{ + chip::Platform::ScopedMemoryBuffer base64String; + base64String.Alloc(kBase64HeaderLen + BASE64_ENCODED_LEN(input.size()) + 1); + VerifyOrReturnError(base64String.Get() != nullptr, CHIP_ERROR_NO_MEMORY); + + auto encodedLen = chip::Base64Encode(input.data(), static_cast(input.size()), base64String.Get() + kBase64HeaderLen); + if (encodedLen) + { + memcpy(base64String.Get(), kBase64Header, kBase64HeaderLen); + encodedLen = static_cast(encodedLen + kBase64HeaderLen); + } + base64String.Get()[encodedLen] = '\0'; + outputAsPrefixedBase64 = std::string(base64String.Get(), encodedLen); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ToTLVCert(const chip::ByteSpan & derEncodedCertificate, std::string & tlvCertAsPrefixedBase64) +{ + uint8_t chipCertBuffer[chip::Credentials::kMaxCHIPCertLength]; + chip::MutableByteSpan chipCertBytes(chipCertBuffer); + ReturnErrorOnFailure(chip::Credentials::ConvertX509CertToChipCert(derEncodedCertificate, chipCertBytes)); + ReturnErrorOnFailure(ToBase64(chipCertBytes, tlvCertAsPrefixedBase64)); + return CHIP_NO_ERROR; +} diff --git a/examples/fabric-admin/commands/pairing/ToTLVCert.h b/examples/fabric-admin/commands/pairing/ToTLVCert.h new file mode 100644 index 00000000000000..29956470b529ce --- /dev/null +++ b/examples/fabric-admin/commands/pairing/ToTLVCert.h @@ -0,0 +1,25 @@ +/* + * 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. + * + */ + +#pragma once + +#include +#include + +CHIP_ERROR ToBase64(const chip::ByteSpan & input, std::string & outputAsPrefixedBase64); +CHIP_ERROR ToTLVCert(const chip::ByteSpan & derEncodedCertificate, std::string & tlvCertAsPrefixedBase64); diff --git a/examples/fabric-admin/fabric-admin.gni b/examples/fabric-admin/fabric-admin.gni new file mode 100644 index 00000000000000..021ab7792458d9 --- /dev/null +++ b/examples/fabric-admin/fabric-admin.gni @@ -0,0 +1,22 @@ +# Copyright (c) 2024 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("//build_overrides/build.gni") +import("//build_overrides/chip.gni") + +declare_args() { + # Use a separate eventloop for CHIP tasks + config_use_separate_eventloop = true + config_use_local_storage = true +} diff --git a/examples/fabric-admin/include/CHIPProjectAppConfig.h b/examples/fabric-admin/include/CHIPProjectAppConfig.h new file mode 100644 index 00000000000000..b3f85d69359e87 --- /dev/null +++ b/examples/fabric-admin/include/CHIPProjectAppConfig.h @@ -0,0 +1,67 @@ +/* + * + * Copyright (c) 2024 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. + */ + +/** + * @file + * Project configuration for Fabric Admin. + * + */ +#ifndef CHIPPROJECTCONFIG_H +#define CHIPPROJECTCONFIG_H + +#define CHIP_CONFIG_MAX_FABRICS 17 + +#define CHIP_CONFIG_EVENT_LOGGING_NUM_EXTERNAL_CALLBACKS 2 + +// Uncomment this for a large Tunnel MTU. +// #define CHIP_CONFIG_TUNNEL_INTERFACE_MTU (9000) + +// Enable support functions for parsing command-line arguments +#define CHIP_CONFIG_ENABLE_ARG_PARSER 1 + +// Use a default pairing code if one hasn't been provisioned in flash. +#define CHIP_DEVICE_CONFIG_USE_TEST_SETUP_PIN_CODE 20202021 +#define CHIP_DEVICE_CONFIG_USE_TEST_SETUP_DISCRIMINATOR 0xF00 + +// Enable reading DRBG seed data from /dev/(u)random. +// This is needed for test applications and the CHIP device manager to function +// properly when CHIP_CONFIG_RNG_IMPLEMENTATION_CHIPDRBG is enabled. +#define CHIP_CONFIG_DEV_RANDOM_DRBG_SEED 1 + +// For convenience, Chip Security Test Mode can be enabled and the +// requirement for authentication in various protocols can be disabled. +// +// WARNING: These options make it possible to circumvent basic Chip security functionality, +// including message encryption. Because of this they MUST NEVER BE ENABLED IN PRODUCTION BUILDS. +// +#define CHIP_CONFIG_SECURITY_TEST_MODE 0 + +#define CHIP_CONFIG_ENABLE_UPDATE 1 + +#define CHIP_SYSTEM_CONFIG_PACKETBUFFER_POOL_SIZE 0 + +#define CHIP_CONFIG_DATA_MANAGEMENT_CLIENT_EXPERIMENTAL 1 + +#define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY 1 + +// Enable some test-only interaction model APIs. +#define CONFIG_BUILD_FOR_HOST_UNIT_TEST 1 + +// Allow us, for test purposes, to encode invalid enum values. +#define CHIP_CONFIG_IM_ENABLE_ENCODING_SENTINEL_ENUM_VALUES 1 + +#endif /* CHIPPROJECTCONFIG_H */ diff --git a/examples/fabric-admin/main.cpp b/examples/fabric-admin/main.cpp new file mode 100644 index 00000000000000..e517c67f6b403f --- /dev/null +++ b/examples/fabric-admin/main.cpp @@ -0,0 +1,40 @@ +/* + * 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 "commands/common/Commands.h" + +#include "commands/clusters/SubscriptionsCommands.h" +#include "commands/interactive/Commands.h" +#include "commands/pairing/Commands.h" +#include + +// ================================================================================ +// Main Code +// ================================================================================ +int main(int argc, char * argv[]) +{ + ExampleCredentialIssuerCommands credIssuerCommands; + Commands commands; + + registerCommandsInteractive(commands, &credIssuerCommands); + registerCommandsPairing(commands, &credIssuerCommands); + registerClusters(commands, &credIssuerCommands); + registerCommandsSubscriptions(commands, &credIssuerCommands); + + return commands.Run(argc, argv); +} diff --git a/examples/fabric-admin/third_party/connectedhomeip b/examples/fabric-admin/third_party/connectedhomeip new file mode 120000 index 00000000000000..1b20c9fb816b63 --- /dev/null +++ b/examples/fabric-admin/third_party/connectedhomeip @@ -0,0 +1 @@ +../../../ \ No newline at end of file