| /* | |
| * Copyright (c) 2026 Project CHIP Authors | |
| * All rights reserved. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include <controller/ThreadMeshcopCommissionProxy.h> | |
| #include <json/json.h> | |
| #include <pw_unit_test/framework.h> | |
| #include <cstdint> | |
| #include <sstream> | |
| #include <string_view> | |
| #include <vector> | |
| namespace chip { | |
| namespace Controller { | |
| namespace { | |
| constexpr uint8_t kJoinerId[] = { 0x1a, 0x6f, 0x7e, 0xf3, 0x12, 0x66, 0xa6, 0x30 }; | |
| constexpr uint8_t kMattercSrvQuery[] = { | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DNS header | |
| 0x08, '_', 'm', 'a', 't', 't', 'e', 'r', 'c', // _matterc | |
| 0x04, '_', 'u', 'd', 'p', // _udp | |
| 0x05, 'l', 'o', 'c', 'a', 'l', // local | |
| 0x00, 0x00, 0x21, 0x00, 0x01, // SRV, IN | |
| }; | |
| constexpr uint8_t kMalformedMattercSrvResponse[] = { | |
| 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // DNS response header | |
| 0x08, '_', 'm', 'a', 't', 't', 'e', 'r', 'c', // _matterc | |
| 0x04, '_', 'u', 'd', 'p', // _udp | |
| 0x05, 'l', 'o', 'c', 'a', 'l', // local | |
| 0x00, 0x00, 0x21, 0x00, 0x01, // SRV, IN | |
| 0x00, 0x00, 0x00, 0x78, 0x00, 0x01, 0x00, // TTL, invalid one-byte SRV data | |
| }; | |
| constexpr uint8_t kMattercSrvOnlyResponse[] = { | |
| 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // DNS response header | |
| 0x08, '_', 'm', 'a', 't', 't', 'e', 'r', 'c', // _matterc | |
| 0x04, '_', 'u', 'd', 'p', // _udp | |
| 0x05, 'l', 'o', 'c', 'a', 'l', // local | |
| 0x00, 0x00, 0x21, 0x00, 0x01, // SRV, IN | |
| 0x00, 0x00, 0x00, 0x78, 0x00, 0x07, // TTL, data length | |
| 0x00, 0x00, 0x00, 0x00, 0x15, 0xa4, 0x00, // priority, weight, port, root target | |
| }; | |
| constexpr uint8_t kCompleteMattercResponse[] = { | |
| 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, // DNS response header | |
| 0x08, '_', 'm', 'a', 't', 't', 'e', 'r', 'c', // _matterc | |
| 0x04, '_', 'u', 'd', 'p', // _udp | |
| 0x05, 'l', 'o', 'c', 'a', 'l', // local | |
| 0x00, 0x00, 0x21, 0x00, 0x01, // SRV, IN | |
| 0x00, 0x00, 0x00, 0x78, 0x00, 0x07, // TTL, data length | |
| 0x00, 0x00, 0x00, 0x00, 0x15, 0xa4, 0x00, // priority, weight, port, root target | |
| 0x08, '_', 'm', 'a', 't', 't', 'e', 'r', 'c', // _matterc | |
| 0x04, '_', 'u', 'd', 'p', // _udp | |
| 0x05, 'l', 'o', 'c', 'a', 'l', // local | |
| 0x00, 0x00, 0x10, 0x00, 0x01, // TXT, IN | |
| 0x00, 0x00, 0x00, 0x78, 0x00, 0x04, 0x03, 'D', '=', '0', // TTL and D=0 TXT entry | |
| }; | |
| std::vector<uint8_t> MakeMattercResponseWithDiscriminator(std::string_view discriminator) | |
| { | |
| constexpr size_t kEncodedDiscriminatorTxtRecordSize = 6; | |
| std::vector<uint8_t> response(std::begin(kCompleteMattercResponse), | |
| std::end(kCompleteMattercResponse) - kEncodedDiscriminatorTxtRecordSize); | |
| const size_t txtEntrySize = 2 + discriminator.size(); // "D=" and its value | |
| response.push_back(0); | |
| response.push_back(static_cast<uint8_t>(txtEntrySize + 1)); | |
| response.push_back(static_cast<uint8_t>(txtEntrySize)); | |
| response.push_back('D'); | |
| response.push_back('='); | |
| response.insert(response.end(), discriminator.begin(), discriminator.end()); | |
| return response; | |
| } | |
| template <typename Packet> | |
| void SendJoinerPacket(ThreadMeshcopCommissionProxy & proxy, const Packet & packet) | |
| { | |
| proxy.OnJoinerMessage(std::vector<uint8_t>(std::begin(kJoinerId), std::end(kJoinerId)), 49154, | |
| std::vector<uint8_t>(std::begin(packet), std::end(packet))); | |
| } | |
| bool LastDiscoveryIsValid(ThreadMeshcopCommissionProxy & proxy) | |
| { | |
| Json::Value root; | |
| std::string errors; | |
| std::istringstream diagnostic(proxy.GetLastDiscoveryDiagnosticJson()); | |
| bool parsed = Json::parseFromStream(Json::CharReaderBuilder(), diagnostic, &root, &errors); | |
| EXPECT_TRUE(parsed); | |
| return parsed && root["valid"].asBool(); | |
| } | |
| TEST(TestThreadMeshcopCommissionProxy, WaitsForCompleteResponseAfterSrvQuery) | |
| { | |
| ThreadMeshcopCommissionProxy proxy; | |
| SendJoinerPacket(proxy, kMattercSrvQuery); | |
| EXPECT_FALSE(LastDiscoveryIsValid(proxy)); | |
| SendJoinerPacket(proxy, kCompleteMattercResponse); | |
| EXPECT_TRUE(LastDiscoveryIsValid(proxy)); | |
| } | |
| TEST(TestThreadMeshcopCommissionProxy, WaitsForCompleteResponseAfterMalformedSrv) | |
| { | |
| ThreadMeshcopCommissionProxy proxy; | |
| SendJoinerPacket(proxy, kMalformedMattercSrvResponse); | |
| EXPECT_FALSE(LastDiscoveryIsValid(proxy)); | |
| SendJoinerPacket(proxy, kCompleteMattercResponse); | |
| EXPECT_TRUE(LastDiscoveryIsValid(proxy)); | |
| } | |
| TEST(TestThreadMeshcopCommissionProxy, WaitsForCompleteResponseAfterSrvOnlyResponse) | |
| { | |
| ThreadMeshcopCommissionProxy proxy; | |
| SendJoinerPacket(proxy, kMattercSrvOnlyResponse); | |
| EXPECT_FALSE(LastDiscoveryIsValid(proxy)); | |
| SendJoinerPacket(proxy, kCompleteMattercResponse); | |
| EXPECT_TRUE(LastDiscoveryIsValid(proxy)); | |
| } | |
| TEST(TestThreadMeshcopCommissionProxy, WaitsForValidDiscriminatorAfterMalformedValues) | |
| { | |
| constexpr std::string_view kMalformedDiscriminators[] = { "", "abc", "0123", "70000" }; | |
| ThreadMeshcopCommissionProxy proxy; | |
| for (std::string_view discriminator : kMalformedDiscriminators) | |
| { | |
| SendJoinerPacket(proxy, MakeMattercResponseWithDiscriminator(discriminator)); | |
| EXPECT_FALSE(LastDiscoveryIsValid(proxy)); | |
| } | |
| SendJoinerPacket(proxy, kCompleteMattercResponse); | |
| EXPECT_TRUE(LastDiscoveryIsValid(proxy)); | |
| } | |
| TEST(TestThreadMeshcopCommissionProxy, IgnoresMismatchWithShortDiscriminator) | |
| { | |
| ThreadMeshcopCommissionProxy proxy; | |
| SetupDiscriminator expectedDiscriminator; | |
| expectedDiscriminator.SetShortValue(1); | |
| ByteSpan invalidPskc; | |
| Transport::PeerAddress peerAddress; | |
| Dnssd::DiscoveredNodeData nodeData; | |
| EXPECT_EQ(proxy.Discover(invalidPskc, peerAddress, Thread::DiscoveryCode(), expectedDiscriminator, nodeData, 0), | |
| CHIP_ERROR_INVALID_ARGUMENT); | |
| SendJoinerPacket(proxy, kCompleteMattercResponse); | |
| EXPECT_FALSE(LastDiscoveryIsValid(proxy)); | |
| } | |
| TEST(TestThreadMeshcopCommissionProxy, AcceptsCompleteResponse) | |
| { | |
| ThreadMeshcopCommissionProxy proxy; | |
| SendJoinerPacket(proxy, kCompleteMattercResponse); | |
| EXPECT_TRUE(LastDiscoveryIsValid(proxy)); | |
| } | |
| TEST(TestThreadMeshcopCommissionProxy, CanRediscoverAfterStoppingProxy) | |
| { | |
| ThreadMeshcopCommissionProxy proxy; | |
| ByteSpan invalidPskc; | |
| Transport::PeerAddress peerAddress; | |
| Dnssd::DiscoveredNodeData nodeData; | |
| for (unsigned i = 0; i < 4; ++i) | |
| { | |
| SendJoinerPacket(proxy, kCompleteMattercResponse); | |
| ASSERT_TRUE(LastDiscoveryIsValid(proxy)); | |
| // Discover stops the previous receiver before validating the PSKc. An empty PSKc | |
| // avoids contacting a border agent while exercising socket teardown under TSAN. | |
| ASSERT_EQ(proxy.Discover(invalidPskc, peerAddress, Thread::DiscoveryCode(), SetupDiscriminator(), nodeData, 0), | |
| CHIP_ERROR_INVALID_ARGUMENT); | |
| EXPECT_FALSE(LastDiscoveryIsValid(proxy)); | |
| } | |
| // A fresh receiver must also be stopped when the proxy is destroyed. | |
| SendJoinerPacket(proxy, kCompleteMattercResponse); | |
| EXPECT_TRUE(LastDiscoveryIsValid(proxy)); | |
| } | |
| } // namespace | |
| } // namespace Controller | |
| } // namespace chip |