mirror of
https://github.com/espressif/openthread.git
synced 2026-08-01 00:27:47 +00:00
[dns] add helper methods to append/parse DNS names (#5922)
This commit adds `Dns::Name` type which provides a set of helper methods to encode/decode DNS names. `AppendName()` method encodes and appends a full name (e.g., "test.example.com") to a message. Other helper methods enable appending labels (or groups of labels) and/or constructing a compressed name (using pointer labels). `ParseName()` method parses and skips over a full name in a message. `ReadLabel()` method reads labels one by one and works independently of whether the encoded name is compressed or not. `ReadName()` method read an entire name from message. The new methods are used in `Dns::Client`. This commit also add a unit test `test-dns` covering the behavior of the newly added helper methods. Finally, this commit adds few helper methods in `ResourceRecords` to get the size and check the type of a DNS record.
This commit is contained in:
@@ -237,6 +237,7 @@ LOCAL_SRC_FILES := \
|
||||
src/core/net/dhcp6_client.cpp \
|
||||
src/core/net/dhcp6_server.cpp \
|
||||
src/core/net/dns_client.cpp \
|
||||
src/core/net/dns_headers.cpp \
|
||||
src/core/net/icmp6.cpp \
|
||||
src/core/net/ip6.cpp \
|
||||
src/core/net/ip6_address.cpp \
|
||||
|
||||
@@ -466,6 +466,7 @@ openthread_core_files = [
|
||||
"net/dhcp6_server.hpp",
|
||||
"net/dns_client.cpp",
|
||||
"net/dns_client.hpp",
|
||||
"net/dns_headers.cpp",
|
||||
"net/dns_headers.hpp",
|
||||
"net/icmp6.cpp",
|
||||
"net/icmp6.hpp",
|
||||
|
||||
@@ -129,6 +129,7 @@ set(COMMON_SOURCES
|
||||
net/dhcp6_client.cpp
|
||||
net/dhcp6_server.cpp
|
||||
net/dns_client.cpp
|
||||
net/dns_headers.cpp
|
||||
net/icmp6.cpp
|
||||
net/ip6.cpp
|
||||
net/ip6_address.cpp
|
||||
|
||||
@@ -206,6 +206,7 @@ SOURCES_COMMON = \
|
||||
net/dhcp6_client.cpp \
|
||||
net/dhcp6_server.cpp \
|
||||
net/dns_client.cpp \
|
||||
net/dns_headers.cpp \
|
||||
net/icmp6.cpp \
|
||||
net/ip6.cpp \
|
||||
net/ip6_address.cpp \
|
||||
|
||||
@@ -49,6 +49,22 @@ uint16_t StringLength(const char *aString, uint16_t aMaxLength)
|
||||
return ret;
|
||||
}
|
||||
|
||||
const char *StringFind(const char *aString, char aChar)
|
||||
{
|
||||
const char *ret = nullptr;
|
||||
|
||||
for (; *aString != '\0'; aString++)
|
||||
{
|
||||
if (*aString == aChar)
|
||||
{
|
||||
ret = aString;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
otError StringBase::Write(char *aBuffer, uint16_t aSize, uint16_t &aLength, const char *aFormat, va_list aArgs)
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
|
||||
@@ -68,6 +68,17 @@ namespace ot {
|
||||
*/
|
||||
uint16_t StringLength(const char *aString, uint16_t aMaxLength);
|
||||
|
||||
/**
|
||||
* This function finds the first occurrence of a given character in a null-terminated string.
|
||||
*
|
||||
* @param[in] aString A pointer to the string.
|
||||
* @param[in] aChar A char to search for in the string.
|
||||
*
|
||||
* @returns The pointer to first occurrence of the @p aChar in @p aString, or nullptr if cannot be found.
|
||||
*
|
||||
*/
|
||||
const char *StringFind(const char *aString, char aChar);
|
||||
|
||||
/**
|
||||
* This class defines the base class for `String`.
|
||||
*
|
||||
|
||||
@@ -105,7 +105,7 @@ otError Client::Query(const QueryInfo &aQuery, ResponseHandler aHandler, void *a
|
||||
|
||||
VerifyOrExit((message = NewMessage(header)) != nullptr, error = OT_ERROR_NO_BUFS);
|
||||
|
||||
SuccessOrExit(error = AppendCompressedHostname(*message, aQuery.GetHostname()));
|
||||
SuccessOrExit(error = Name::AppendName(aQuery.GetHostname(), *message));
|
||||
SuccessOrExit(error = question.AppendTo(*message));
|
||||
|
||||
queryMetadata.mHostname = aQuery.GetHostname();
|
||||
@@ -199,46 +199,6 @@ exit:
|
||||
}
|
||||
}
|
||||
|
||||
otError Client::AppendCompressedHostname(Message &aMessage, const char *aHostname)
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
uint8_t index = 0;
|
||||
uint8_t labelPosition = 0;
|
||||
uint8_t labelSize = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Look for string separator.
|
||||
if (aHostname[index] == kLabelSeparator || aHostname[index] == kLabelTerminator)
|
||||
{
|
||||
VerifyOrExit(labelSize > 0, error = OT_ERROR_INVALID_ARGS);
|
||||
SuccessOrExit(error = aMessage.Append(labelSize));
|
||||
SuccessOrExit(error = aMessage.AppendBytes(&aHostname[labelPosition], labelSize));
|
||||
|
||||
labelPosition += labelSize + 1;
|
||||
labelSize = 0;
|
||||
|
||||
if (aHostname[index] == kLabelTerminator)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
labelSize++;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
// Add termination character at the end.
|
||||
labelSize = kLabelTerminator;
|
||||
SuccessOrExit(error = aMessage.Append(labelSize));
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Client::CompareQuestions(Message &aMessageResponse, Message &aMessageQuery, uint16_t &aOffset)
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
@@ -268,46 +228,6 @@ exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Client::SkipHostname(Message &aMessage, uint16_t &aOffset)
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
uint8_t buf[kBufSize];
|
||||
uint16_t index;
|
||||
uint16_t read = 0;
|
||||
uint16_t offset = aOffset;
|
||||
uint16_t length = aMessage.GetLength() - aOffset;
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
VerifyOrExit((read = aMessage.ReadBytes(offset, buf, sizeof(buf))) > 0, error = OT_ERROR_PARSE);
|
||||
|
||||
index = 0;
|
||||
|
||||
while (index < read)
|
||||
{
|
||||
if (buf[index] == kLabelTerminator)
|
||||
{
|
||||
ExitNow(aOffset = offset + 1);
|
||||
}
|
||||
|
||||
if ((buf[index] & kCompressionOffsetMask) == kCompressionOffsetMask)
|
||||
{
|
||||
ExitNow(aOffset = offset + 2);
|
||||
}
|
||||
|
||||
index++;
|
||||
offset++;
|
||||
}
|
||||
|
||||
length -= read;
|
||||
}
|
||||
|
||||
ExitNow(error = OT_ERROR_PARSE);
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
Message *Client::FindRelatedQuery(const Header &aResponseHeader, QueryMetadata &aQueryMetadata)
|
||||
{
|
||||
uint16_t messageId;
|
||||
@@ -438,18 +358,18 @@ void Client::HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessag
|
||||
{
|
||||
uint32_t newOffset;
|
||||
|
||||
SuccessOrExit(error = SkipHostname(aMessage, offset));
|
||||
SuccessOrExit(error = Name::ParseName(aMessage, offset));
|
||||
|
||||
SuccessOrExit(error = aMessage.Read(offset, record));
|
||||
|
||||
if (ResourceRecordAaaa::IsAaaa(record))
|
||||
if (record.Matches(ResourceRecord::kTypeAaaa))
|
||||
{
|
||||
// Return the first found IPv6 address.
|
||||
FinalizeDnsTransaction(*message, queryMetadata, &record.GetAddress(), record.GetTtl(), OT_ERROR_NONE);
|
||||
ExitNow(error = OT_ERROR_NONE);
|
||||
}
|
||||
|
||||
newOffset = offset + sizeof(ResourceRecord) + record.GetLength();
|
||||
newOffset = offset + record.GetSize();
|
||||
VerifyOrExit(newOffset <= aMessage.GetLength(), error = OT_ERROR_PARSE);
|
||||
offset = static_cast<uint16_t>(newOffset);
|
||||
}
|
||||
|
||||
@@ -154,19 +154,6 @@ private:
|
||||
kMaxRetransmit = OPENTHREAD_CONFIG_DNS_MAX_RETRANSMIT,
|
||||
};
|
||||
|
||||
/**
|
||||
* Special DNS symbols.
|
||||
*/
|
||||
enum
|
||||
{
|
||||
kLabelTerminator = 0,
|
||||
kLabelSeparator = '.',
|
||||
kCompressionOffsetMask = 0xc0
|
||||
};
|
||||
|
||||
/**
|
||||
* Operating on message buffers.
|
||||
*/
|
||||
enum
|
||||
{
|
||||
kBufSize = 16
|
||||
@@ -194,9 +181,7 @@ private:
|
||||
otError SendMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
|
||||
void SendCopy(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
|
||||
|
||||
otError AppendCompressedHostname(Message &aMessage, const char *aHostname);
|
||||
otError CompareQuestions(Message &aMessageResponse, Message &aMessageQuery, uint16_t &aOffset);
|
||||
otError SkipHostname(Message &aMessage, uint16_t &aOffset);
|
||||
|
||||
Message *FindRelatedQuery(const Header &aResponseHeader, QueryMetadata &aQueryMetadata);
|
||||
void FinalizeDnsTransaction(Message & aQuery,
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright (c) 2020, The OpenThread Authors.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of the copyright holder nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file
|
||||
* This file implements generating and processing of DNS headers and helper functions/methods.
|
||||
*/
|
||||
|
||||
#include "dns_headers.hpp"
|
||||
|
||||
#include "common/code_utils.hpp"
|
||||
#include "common/debug.hpp"
|
||||
#include "common/string.hpp"
|
||||
|
||||
namespace ot {
|
||||
namespace Dns {
|
||||
|
||||
using ot::Encoding::BigEndian::HostSwap16;
|
||||
|
||||
otError Name::AppendLabel(const char *aLabel, Message &aMessage)
|
||||
{
|
||||
return AppendLabel(aLabel, static_cast<uint8_t>(StringLength(aLabel, kMaxLabelLength + 1)), aMessage);
|
||||
}
|
||||
|
||||
otError Name::AppendLabel(const char *aLabel, uint8_t aLabelLength, Message &aMessage)
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
|
||||
VerifyOrExit((0 < aLabelLength) && (aLabelLength <= kMaxLabelLength), error = OT_ERROR_INVALID_ARGS);
|
||||
|
||||
SuccessOrExit(error = aMessage.Append(aLabelLength));
|
||||
error = aMessage.AppendBytes(aLabel, aLabelLength);
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Name::AppendMultipleLabels(const char *aLabels, Message &aMessage)
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
uint16_t index = 0;
|
||||
uint16_t labelStartIndex = 0;
|
||||
char ch;
|
||||
|
||||
VerifyOrExit(aLabels != nullptr);
|
||||
|
||||
do
|
||||
{
|
||||
VerifyOrExit(index < kMaxLength, error = OT_ERROR_INVALID_ARGS);
|
||||
|
||||
ch = aLabels[index];
|
||||
|
||||
if ((ch == kNullChar) || (ch == kLabelSeperatorChar))
|
||||
{
|
||||
uint8_t labelLength = static_cast<uint8_t>(index - labelStartIndex);
|
||||
|
||||
if (labelLength == 0)
|
||||
{
|
||||
// Empty label (e.g., consecutive dots) is invalid, but we
|
||||
// allow for two cases: (1) where `aLabels` ends with a dot
|
||||
// (`labelLength` is zero but we are at end of `aLabels` string
|
||||
// and `ch` is null char. (2) if `aLabels` is just "." (we
|
||||
// see a dot at index 0, and index 1 is null char).
|
||||
|
||||
error = ((ch == kNullChar) || ((index == 0) && (aLabels[1] == kNullChar))) ? OT_ERROR_NONE
|
||||
: OT_ERROR_INVALID_ARGS;
|
||||
ExitNow();
|
||||
}
|
||||
|
||||
SuccessOrExit(error = AppendLabel(&aLabels[labelStartIndex], labelLength, aMessage));
|
||||
|
||||
labelStartIndex = index + 1;
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
} while (ch != kNullChar);
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Name::AppendTerminator(Message &aMessage)
|
||||
{
|
||||
uint8_t terminator = 0;
|
||||
|
||||
return aMessage.Append(terminator);
|
||||
}
|
||||
|
||||
otError Name::AppendPointerLabel(uint16_t aOffset, Message &aMessage)
|
||||
{
|
||||
// A pointer label takes the form of a two byte sequence as a
|
||||
// `uint16_t` value. The first two bits are ones. This allows a
|
||||
// pointer to be distinguished from a text label, since the text
|
||||
// label must begin with two zero bits (note that labels are
|
||||
// restricted to 63 octets or less). The next 14-bits specify
|
||||
// an offset value relative to start of DNS header.
|
||||
|
||||
uint16_t value;
|
||||
|
||||
OT_ASSERT(aOffset < kPointerLabelTypeUint16);
|
||||
|
||||
value = HostSwap16(aOffset | kPointerLabelTypeUint16);
|
||||
|
||||
return aMessage.Append(value);
|
||||
}
|
||||
|
||||
otError Name::AppendName(const char *aName, Message &aMessage)
|
||||
{
|
||||
otError error;
|
||||
|
||||
SuccessOrExit(error = AppendMultipleLabels(aName, aMessage));
|
||||
error = AppendTerminator(aMessage);
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Name::ParseName(const Message &aMessage, uint16_t &aOffset)
|
||||
{
|
||||
otError error;
|
||||
LabelIterator iterator(aMessage, aOffset);
|
||||
|
||||
while (true)
|
||||
{
|
||||
error = iterator.GetNextLabel();
|
||||
|
||||
VerifyOrExit((error == OT_ERROR_NONE) || (error == OT_ERROR_NOT_FOUND));
|
||||
|
||||
if (iterator.IsEndOffsetSet())
|
||||
{
|
||||
aOffset = iterator.mNameEndOffset;
|
||||
ExitNow(error = OT_ERROR_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Name::ReadLabel(const Message &aMessage,
|
||||
uint16_t & aOffset,
|
||||
uint16_t aHeaderOffset,
|
||||
char * aLabelBuffer,
|
||||
uint8_t & aLabelLength)
|
||||
{
|
||||
otError error;
|
||||
LabelIterator iterator(aMessage, aOffset, aHeaderOffset);
|
||||
|
||||
SuccessOrExit(error = iterator.GetNextLabel());
|
||||
SuccessOrExit(error = iterator.ReadLabel(aLabelBuffer, aLabelLength, /* aAllowDotCharInLabel */ true));
|
||||
aOffset = iterator.mNextLabelOffset;
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Name::ReadName(const Message &aMessage,
|
||||
uint16_t & aOffset,
|
||||
uint16_t aHeaderOffset,
|
||||
char * aNameBuffer,
|
||||
uint16_t aNameBufferSize)
|
||||
{
|
||||
otError error;
|
||||
LabelIterator iterator(aMessage, aOffset, aHeaderOffset);
|
||||
bool firstLabel = true;
|
||||
uint8_t labelLength;
|
||||
|
||||
while (true)
|
||||
{
|
||||
error = iterator.GetNextLabel();
|
||||
|
||||
switch (error)
|
||||
{
|
||||
case OT_ERROR_NONE:
|
||||
|
||||
if (!firstLabel)
|
||||
{
|
||||
*aNameBuffer++ = kLabelSeperatorChar;
|
||||
aNameBufferSize--;
|
||||
|
||||
// No need to check if we have reached end of the name buffer
|
||||
// here since `iterator.ReadLabel()` would verify it.
|
||||
}
|
||||
|
||||
labelLength = static_cast<uint8_t>(OT_MIN(kMaxLabelLength + 1, aNameBufferSize));
|
||||
SuccessOrExit(error = iterator.ReadLabel(aNameBuffer, labelLength, /* aAllowDotCharInLabel */ false));
|
||||
aNameBuffer += labelLength;
|
||||
aNameBufferSize -= labelLength;
|
||||
firstLabel = false;
|
||||
break;
|
||||
|
||||
case OT_ERROR_NOT_FOUND:
|
||||
// We reach the end of name successfully. Always add a terminating dot
|
||||
// at the end.
|
||||
*aNameBuffer++ = kLabelSeperatorChar;
|
||||
aNameBufferSize--;
|
||||
VerifyOrExit(aNameBufferSize >= sizeof(uint8_t), error = OT_ERROR_NO_BUFS);
|
||||
*aNameBuffer = kNullChar;
|
||||
aOffset = iterator.mNameEndOffset;
|
||||
error = OT_ERROR_NONE;
|
||||
|
||||
// Fall through
|
||||
|
||||
default:
|
||||
ExitNow();
|
||||
}
|
||||
}
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Name::LabelIterator::GetNextLabel(void)
|
||||
{
|
||||
otError error;
|
||||
|
||||
while (true)
|
||||
{
|
||||
uint8_t labelLength;
|
||||
uint8_t labelType;
|
||||
|
||||
SuccessOrExit(error = mMessage.Read(mNextLabelOffset, labelLength));
|
||||
|
||||
labelType = labelLength & kLabelTypeMask;
|
||||
|
||||
if (labelType == kTextLabelType)
|
||||
{
|
||||
if (labelLength == 0)
|
||||
{
|
||||
// Zero label length indicates end of a name.
|
||||
|
||||
if (!IsEndOffsetSet())
|
||||
{
|
||||
mNameEndOffset = mNextLabelOffset + sizeof(uint8_t);
|
||||
}
|
||||
|
||||
ExitNow(error = OT_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
mLabelStartOffset = mNextLabelOffset + sizeof(uint8_t);
|
||||
mLabelLength = labelLength;
|
||||
mNextLabelOffset = mLabelStartOffset + labelLength;
|
||||
ExitNow();
|
||||
}
|
||||
else if (labelType == kPointerLabelType)
|
||||
{
|
||||
// A pointer label takes the form of a two byte sequence as a
|
||||
// `uint16_t` value. The first two bits are ones. The next 14 bits
|
||||
// specify an offset value from the start of the DNS header.
|
||||
|
||||
uint16_t pointerValue;
|
||||
|
||||
SuccessOrExit(error = mMessage.Read(mNextLabelOffset, pointerValue));
|
||||
|
||||
if (!IsEndOffsetSet())
|
||||
{
|
||||
mNameEndOffset = mNextLabelOffset + sizeof(uint16_t);
|
||||
}
|
||||
|
||||
mNextLabelOffset = mHeaderOffset + (HostSwap16(pointerValue) & kPointerLabelOffsetMask);
|
||||
|
||||
// Go back through the `while(true)` loop to get the next label.
|
||||
}
|
||||
else
|
||||
{
|
||||
ExitNow(error = OT_ERROR_PARSE);
|
||||
}
|
||||
}
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Name::LabelIterator::ReadLabel(char *aLabelBuffer, uint8_t &aLabelLength, bool aAllowDotCharInLabel) const
|
||||
{
|
||||
otError error;
|
||||
|
||||
VerifyOrExit(mLabelLength < aLabelLength, error = OT_ERROR_NO_BUFS);
|
||||
|
||||
SuccessOrExit(error = mMessage.Read(mLabelStartOffset, aLabelBuffer, mLabelLength));
|
||||
aLabelBuffer[mLabelLength] = kNullChar;
|
||||
aLabelLength = mLabelLength;
|
||||
|
||||
if (!aAllowDotCharInLabel)
|
||||
{
|
||||
VerifyOrExit(StringFind(aLabelBuffer, kLabelSeperatorChar) == nullptr, error = OT_ERROR_PARSE);
|
||||
}
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
void ResourceRecord::Init(uint16_t aType, uint16_t aClass, uint32_t aTtl)
|
||||
{
|
||||
SetType(aType);
|
||||
SetClass(aClass);
|
||||
SetTtl(aTtl);
|
||||
SetLength(0);
|
||||
}
|
||||
|
||||
void ResourceRecordAaaa::Init(void)
|
||||
{
|
||||
ResourceRecord::Init(kTypeAaaa);
|
||||
SetLength(sizeof(mAddress));
|
||||
mAddress.Clear();
|
||||
}
|
||||
|
||||
} // namespace Dns
|
||||
} // namespace ot
|
||||
+294
-29
@@ -374,6 +374,242 @@ private:
|
||||
|
||||
} OT_TOOL_PACKED_END;
|
||||
|
||||
/**
|
||||
* This class implement helper methods for encoding/decoding of DNS Names.
|
||||
*
|
||||
*/
|
||||
class Name
|
||||
{
|
||||
public:
|
||||
enum : uint8_t
|
||||
{
|
||||
kMaxLabelLength = 63, ///< Max number of characters in a label.
|
||||
kMaxLength = 255, ///< Max number of characters in a name.
|
||||
};
|
||||
|
||||
/**
|
||||
* This static method encodes and appends a single name label to a message.
|
||||
*
|
||||
* The @p aLabel is assumed to contain a single name label as a C string (null-terminated). Unlike
|
||||
* `AppendMultipleLabels()` which parses the label string and treats it as sequence of multiple (dot-separated)
|
||||
* labels, this method always appends @p aLabel as a single whole label. This allows the label string to even
|
||||
* contain dot '.' character, which, for example, is useful for "Service Instance Names" where <Instance> portion
|
||||
* is a user-friendly name and can contain dot characters.
|
||||
*
|
||||
* @param[in] aLabel The label string to append. MUST NOT be nullptr.
|
||||
* @param[in] aMessage The message to append to.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully encoded and appended the name label to @p aMessage.
|
||||
* @retval OT_ERROR_INVALID_ARGS @p aLabel is not valid (e.g., label length is not within valid range).
|
||||
* @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message.
|
||||
*
|
||||
*/
|
||||
static otError AppendLabel(const char *aLabel, Message &aMessage);
|
||||
|
||||
/**
|
||||
* This static method encodes and appends a sequence of name labels to a given message.
|
||||
*
|
||||
* The @p aLabels must follow "<label1>.<label2>.<label3>", i.e., a sequence of labels separated by dot '.' char.
|
||||
* E.g., "_http._tcp", "_http._tcp." (same as previous one), "host-1.test".
|
||||
*
|
||||
* This method validates that the @p aLabels is a valid name format, i.e., no empty label, and labels are
|
||||
* `kMaxLabelLength` (63) characters or less.
|
||||
*
|
||||
* @note This method NEVER adds a label terminator (empty label) to the message, even in the case where @p aLabels
|
||||
* ends with a dot character, e.g., "host-1.test." is treated same as "host-1.test".
|
||||
*
|
||||
* @param[in] aLabels A name label string. Can be nullptr (then treated as "").
|
||||
* @param[in] aMessage The message to which to append the encoded name.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully encoded and appended the name label(s) to @p aMessage.
|
||||
* @retval OT_ERROR_INVALID_ARGS Name label @p aLabels is not valid.
|
||||
* @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message.
|
||||
*
|
||||
*/
|
||||
static otError AppendMultipleLabels(const char *aLabels, Message &aMessage);
|
||||
|
||||
/**
|
||||
* This static method appends a name label terminator to a message.
|
||||
*
|
||||
* An encoded name is terminated by an empty label (a zero byte).
|
||||
*
|
||||
* @param[in] aMessage The message to append to.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully encoded and appended the terminator label to @p aMessage.
|
||||
* @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message.
|
||||
*
|
||||
*/
|
||||
static otError AppendTerminator(Message &aMessage);
|
||||
|
||||
/**
|
||||
* This static method appends a pointer type name label to a message.
|
||||
*
|
||||
* Pointer label is used for name compression. It allows an entire name or a list of labels at the end of an
|
||||
* encoded name to be replaced with a pointer to a prior occurrence of the same name within the message.
|
||||
*
|
||||
* @param[in] aOffset The offset from the start of DNS header to use for pointer value.
|
||||
* @param[in] aMessage The message to append to.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully encoded and appended the pointer label to @p aMessage.
|
||||
* @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message.
|
||||
*
|
||||
*/
|
||||
static otError AppendPointerLabel(uint16_t aOffset, Message &aMessage);
|
||||
|
||||
/**
|
||||
* This static method encodes and appends a full name to a message.
|
||||
*
|
||||
* The @p aName must follow "<label1>.<label2>.<label3>", i.e., a sequence of labels separated by dot '.' char.
|
||||
* E.g., "example.com", "example.com." (same as previous one), local.", "default.service.arpa", "." or "" (root).
|
||||
*
|
||||
* This method validates that the @p aName is a valid name format, i.e. no empty labels, and labels are
|
||||
* `kMaxLabelLength` (63) characters or less, and the name is `kMaxLength` (255) characters or less.
|
||||
*
|
||||
* @param[in] aName A name string. Can be nullptr (then treated as "." or root).
|
||||
* @param[in] aMessage The message to append to.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully encoded and appended the name to @p aMessage.
|
||||
* @retval OT_ERROR_INVALID_ARGS Name @p aName is not valid.
|
||||
* @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message.
|
||||
*
|
||||
*/
|
||||
static otError AppendName(const char *aName, Message &aMessage);
|
||||
|
||||
/**
|
||||
* This static method parses and skips over a full name in a message.
|
||||
*
|
||||
* @param[in] aMessage The message to parse the name from.
|
||||
* @param[inout] aOffset On input the offset in @p aMessage pointing to the start of the name field.
|
||||
* On exit (when parsed successfully), @p aOffset is updated to point to the byte
|
||||
* after the end of name field.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully parsed and skipped over name, @p Offset is updated.
|
||||
* @retval OT_ERROR_PARSE Name could not be parsed (invalid format).
|
||||
*
|
||||
*/
|
||||
static otError ParseName(const Message &aMessage, uint16_t &aOffset);
|
||||
|
||||
/**
|
||||
* This static method reads a name label from a message.
|
||||
*
|
||||
* This method can be used to read labels one by one in a name. After a successful label read, @p aOffset is
|
||||
* updated to point to the start of the next label. When we reach the end of the name, OT_ERROR_NOT_FOUND is
|
||||
* returned. This method handles compressed names which use pointer labels. So as the labels in a name are read,
|
||||
* the @p aOffset may jump back in the message and at the end the @p aOffset does not necessarily point to the end
|
||||
* of the original name field.
|
||||
*
|
||||
* Unlike `ReadName()` which requires and verifies that the read label to contain no dot '.' character, this method
|
||||
* allows the read label to include any character.
|
||||
*
|
||||
* @param[in] aMessage The message to read the label from.
|
||||
* @param[inout] aOffset On input, the offset in @p aMessage pointing to the start of the label to read.
|
||||
* On exit, when successfully read, @p aOffset is updated to point to the start of
|
||||
* the next label.
|
||||
* @param[in] aHeaderOffset The offset in @p aMessage to the start of the DNS header.
|
||||
* @param[out] aLabelBuffer A pointer to a char array to output the read label as a null-terminated C string.
|
||||
* @param[inout] aLabelLength On input, the maximum number chars in @p aLabelBuffer array.
|
||||
* On output, when label is successfully read, @aLabelLength is updated to return
|
||||
* the label's length (number of chars in the label string, excluding the null char).
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully read the label and updated @p aLabelBuffer, @p aLabelLength, and
|
||||
* @p aOffset.
|
||||
* @retval OT_ERROR_NOT_FOUND Reached the end of name and no more label to read.
|
||||
* @retval OT_ERROR_PARSE Name could not be parsed (invalid format).
|
||||
* @retval OT_ERROR_NO_BUFS Label could not fit in @p aLabelLength chars.
|
||||
*
|
||||
*/
|
||||
static otError ReadLabel(const Message &aMessage,
|
||||
uint16_t & aOffset,
|
||||
uint16_t aHeaderOffset,
|
||||
char * aLabelBuffer,
|
||||
uint8_t & aLabelLength);
|
||||
|
||||
/**
|
||||
* This static method reads a full name from a message.
|
||||
*
|
||||
* On successful read, the read name follows "<label1>.<label2>.<label3>.", i.e., a sequence of labels separated by
|
||||
* dot '.' character. The read name will ALWAYS end with a dot.
|
||||
*
|
||||
* This method verifies that the read labels in message do not contain any dot character, otherwise it returns
|
||||
* `OT_ERROR_PARSE`).
|
||||
*
|
||||
* @param[in] aMessage The message to read the name from.
|
||||
* @param[inout] aOffset On input, the offset in @p aMessage pointing to the start of the name field.
|
||||
* On exit (when parsed successfully), @p aOffset is updated to point to the byte
|
||||
* after the end of name field.
|
||||
* @param[in] aHeaderOffset The offset in @p aMessage to the start of the DNS header.
|
||||
* @param[out] aNameBuffer A pointer to a char array to output the read name as a null-terminated C string.
|
||||
* @param[inout] aNameBufferSize The maximum number chars in @p aNameBuffer array.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully read the name, @p aNameBuffer and @p Offset are updated.
|
||||
* @retval OT_ERROR_PARSE Name could not be parsed (invalid format).
|
||||
* @retval OT_ERROR_NO_BUFS Name could not fit in @p aNameBufferSize chars.
|
||||
*
|
||||
*/
|
||||
static otError ReadName(const Message &aMessage,
|
||||
uint16_t & aOffset,
|
||||
uint16_t aHeaderOffset,
|
||||
char * aNameBuffer,
|
||||
uint16_t aNameBufferSize);
|
||||
|
||||
private:
|
||||
enum : char
|
||||
{
|
||||
kNullChar = '\0',
|
||||
kLabelSeperatorChar = '.',
|
||||
};
|
||||
|
||||
enum : uint8_t
|
||||
{
|
||||
// The first 2 bits of the encoded label specifies label type.
|
||||
//
|
||||
// - Value 00 indicates normal text label (lower 6-bits indicates the label length).
|
||||
// - Value 11 indicates pointer label type (lower 14-bits indicates the pointer offset).
|
||||
// - Values 01,10 are reserved (RFC 6891 recommends to not use)
|
||||
|
||||
kLabelTypeMask = 0xc0, // 0b1100_0000 (first two bits)
|
||||
kTextLabelType = 0x00, // Text label type (00)
|
||||
kPointerLabelType = 0xc0, // Pointer label type - compressed name (11)
|
||||
};
|
||||
|
||||
enum : uint16_t
|
||||
{
|
||||
kPointerLabelTypeUint16 = 0xc000, // Pointer label type as `uint16_t` mask (first 2 bits).
|
||||
kPointerLabelOffsetMask = 0x3fff, // Mask to get the offset field in a pointer label (lower 14 bits).
|
||||
};
|
||||
|
||||
struct LabelIterator
|
||||
{
|
||||
enum : uint16_t
|
||||
{
|
||||
kUnsetNameEndOffset = 0, // Special value indicating `mNameEndOffset` is not yet set.
|
||||
};
|
||||
|
||||
LabelIterator(const Message &aMessage, uint16_t aLabelOffset, uint16_t aHeaderOffset = 0)
|
||||
: mMessage(aMessage)
|
||||
, mHeaderOffset(aHeaderOffset)
|
||||
, mNextLabelOffset(aLabelOffset)
|
||||
, mNameEndOffset(kUnsetNameEndOffset)
|
||||
{
|
||||
}
|
||||
|
||||
bool IsEndOffsetSet(void) const { return (mNameEndOffset != kUnsetNameEndOffset); }
|
||||
otError GetNextLabel(void);
|
||||
otError ReadLabel(char *aLabelBuffer, uint8_t &aLabelLength, bool aAllowDotCharInLabel) const;
|
||||
|
||||
const Message &mMessage; // Message to read labels from.
|
||||
const uint16_t mHeaderOffset; // Offset in `mMessage` to the start of DNS header.
|
||||
uint16_t mLabelStartOffset; // Offset in `mMessage` to the first char of current label text.
|
||||
uint8_t mLabelLength; // Length of current label (number of chars).
|
||||
uint16_t mNextLabelOffset; // Offset in `mMessage` to the start of the next label.
|
||||
uint16_t mNameEndOffset; // Offset in `mMessage` to the byte after the end of domain name field.
|
||||
};
|
||||
|
||||
Name(void) = default;
|
||||
|
||||
static otError AppendLabel(const char *aLabel, uint8_t aLabelLength, Message &aMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* This class implements Resource Record body format (RR).
|
||||
*
|
||||
@@ -382,6 +618,54 @@ OT_TOOL_PACKED_BEGIN
|
||||
class ResourceRecord
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Resource Record Types.
|
||||
*
|
||||
*/
|
||||
enum : uint16_t
|
||||
{
|
||||
kTypeA = 1, ///< Address record (IPv4).
|
||||
kTypePtr = 12, ///< PTR record.
|
||||
kTypeTxt = 16, ///< TXT record.
|
||||
kTypeSrv = 33, ///< SRV locator record.
|
||||
kTypeAaaa = 28, ///< IPv6 address record.
|
||||
kTypeKey = 25, ///< Key record.
|
||||
kTypeOpt = 41, ///< Option record.
|
||||
};
|
||||
|
||||
/**
|
||||
* Resource Record Class Codes.
|
||||
*
|
||||
*/
|
||||
enum : uint16_t
|
||||
{
|
||||
kClassInternet = 1, ///< Class code Internet (IN).
|
||||
};
|
||||
|
||||
/**
|
||||
* This method initializes the resource record.
|
||||
*
|
||||
* @param[in] aType The type of the resource record.
|
||||
* @param[in] aClass The class of the resource record (default is `kClassInternet`).
|
||||
* @param[in] aTtl The time to live field of the resource record (default is zero).
|
||||
*
|
||||
*/
|
||||
void Init(uint16_t aType, uint16_t aClass = kClassInternet, uint32_t aTtl = 0);
|
||||
|
||||
/**
|
||||
* This method indicates whether the resources records matches a given type and class code.
|
||||
*
|
||||
* @param[in] aType The resource record type to compare with.
|
||||
* @param[in] aClass The resource record class code to compare with (default is `kClassInternet`).
|
||||
*
|
||||
* @returns TRUE if the resources records matches @p aType and @p aClass, FALSE otherwise.
|
||||
*
|
||||
*/
|
||||
bool Matches(uint16_t aType, uint16_t aClass = kClassInternet)
|
||||
{
|
||||
return (mType == HostSwap16(aType)) && (mClass == HostSwap16(aClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the type of the resource record.
|
||||
*
|
||||
@@ -444,6 +728,15 @@ public:
|
||||
*/
|
||||
void SetLength(uint16_t aLength) { mLength = HostSwap16(aLength); }
|
||||
|
||||
/**
|
||||
* This method returns the size of (number of bytes) in resource record and its data RDATA section (excluding the
|
||||
* name field).
|
||||
*
|
||||
* @returns Size (number of bytes) of resource record and its data section (excluding the name field)
|
||||
*
|
||||
*/
|
||||
uint32_t GetSize(void) const { return sizeof(ResourceRecord) + GetLength(); }
|
||||
|
||||
private:
|
||||
uint16_t mType; // The type of the data in RDATA section.
|
||||
uint16_t mClass; // The class of the data in RDATA section.
|
||||
@@ -460,31 +753,11 @@ OT_TOOL_PACKED_BEGIN
|
||||
class ResourceRecordAaaa : public ResourceRecord
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* This static method indicated whether or not a given Resource Record is of AAAA type.
|
||||
*
|
||||
* @param[in] aRecord A Resource Record.
|
||||
*
|
||||
* @returns TRUE if @p aRecord is of AAAA type, FALSE otherwise.
|
||||
*
|
||||
*/
|
||||
static bool IsAaaa(const ResourceRecord &aRecord)
|
||||
{
|
||||
return (aRecord.GetType() == kType) && (aRecord.GetClass() == kClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method initializes the AAAA Resource Record.
|
||||
*
|
||||
*/
|
||||
void Init(void)
|
||||
{
|
||||
ResourceRecord::SetType(kType);
|
||||
ResourceRecord::SetClass(kClass);
|
||||
ResourceRecord::SetTtl(0);
|
||||
ResourceRecord::SetLength(kLength);
|
||||
mAddress.Clear();
|
||||
}
|
||||
void Init(void);
|
||||
|
||||
/**
|
||||
* This method sets the IPv6 address of the resource record.
|
||||
@@ -503,15 +776,7 @@ public:
|
||||
Ip6::Address &GetAddress(void) { return mAddress; }
|
||||
|
||||
private:
|
||||
enum
|
||||
{
|
||||
kType = 0x1C, // AAAA Resource Record type.
|
||||
kClass = 0x01, // The value of the Internet class.
|
||||
kLength = 16, // Size of the AAAA Resource Record type.
|
||||
};
|
||||
|
||||
Ip6::Address mAddress; // IPv6 Address of AAAA Resource Record.
|
||||
|
||||
} OT_TOOL_PACKED_END;
|
||||
|
||||
/**
|
||||
|
||||
@@ -454,6 +454,14 @@
|
||||
*/
|
||||
#define OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE 1
|
||||
|
||||
/**
|
||||
* @def OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
|
||||
*
|
||||
* Define to 1 to enable DNS Client support.
|
||||
*
|
||||
*/
|
||||
#define OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE 1
|
||||
|
||||
#if OPENTHREAD_RADIO
|
||||
/**
|
||||
* @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE
|
||||
|
||||
@@ -173,6 +173,27 @@ target_link_libraries(test-cmd-line-parser
|
||||
|
||||
add_test(NAME test-cmd-line-parser COMMAND test-cmd-line-parser)
|
||||
|
||||
add_executable(test-dns
|
||||
test_dns.cpp
|
||||
)
|
||||
|
||||
target_include_directories(test-dns
|
||||
PRIVATE
|
||||
${COMMON_INCLUDES}
|
||||
)
|
||||
|
||||
target_compile_options(test-dns
|
||||
PRIVATE
|
||||
${COMMON_COMPILE_OPTIONS}
|
||||
)
|
||||
|
||||
target_link_libraries(test-dns
|
||||
PRIVATE
|
||||
${COMMON_LIBS}
|
||||
)
|
||||
|
||||
add_test(NAME test-dns COMMAND test-dns)
|
||||
|
||||
add_executable(test-flash
|
||||
test_flash.cpp
|
||||
)
|
||||
@@ -644,6 +665,7 @@ set_target_properties(
|
||||
test-child
|
||||
test-child-table
|
||||
test-cmd-line-parser
|
||||
test-dns
|
||||
test-flash
|
||||
test-heap
|
||||
test-hmac-sha256
|
||||
|
||||
@@ -111,6 +111,7 @@ check_PROGRAMS += \
|
||||
test-child \
|
||||
test-child-table \
|
||||
test-cmd-line-parser \
|
||||
test-dns \
|
||||
test-flash \
|
||||
test-heap \
|
||||
test-hmac-sha256 \
|
||||
@@ -188,6 +189,9 @@ test_child_table_SOURCES = $(COMMON_SOURCES) test_child_table.cpp
|
||||
test_cmd_line_parser_LDADD = $(COMMON_LDADD)
|
||||
test_cmd_line_parser_SOURCES = $(COMMON_SOURCES) test_cmd_line_parser.cpp
|
||||
|
||||
test_dns_LDADD = $(COMMON_LDADD)
|
||||
test_dns_SOURCES = $(COMMON_SOURCES) test_dns.cpp
|
||||
|
||||
test_flash_LDADD = $(COMMON_LDADD)
|
||||
test_flash_SOURCES = $(COMMON_SOURCES) test_flash.cpp
|
||||
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
/*
|
||||
* Copyright (c) 2020, The OpenThread Authors.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of the copyright holder nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <openthread/config.h>
|
||||
|
||||
#include "test_platform.h"
|
||||
#include "test_util.hpp"
|
||||
|
||||
#include "common/instance.hpp"
|
||||
#include "net/dns_headers.hpp"
|
||||
|
||||
namespace ot {
|
||||
|
||||
void TestDnsName(void)
|
||||
{
|
||||
enum
|
||||
{
|
||||
kMaxSize = 300,
|
||||
kLabelSize = 64,
|
||||
kNameSize = 256,
|
||||
};
|
||||
|
||||
struct TestName
|
||||
{
|
||||
const char * mName;
|
||||
uint16_t mEncodedLength;
|
||||
const uint8_t *mEncodedData;
|
||||
const char ** mLabels;
|
||||
const char * mExpectedReadName;
|
||||
};
|
||||
|
||||
Instance * instance;
|
||||
MessagePool *messagePool;
|
||||
Message * message;
|
||||
uint8_t buffer[kMaxSize];
|
||||
uint16_t len;
|
||||
uint16_t offset;
|
||||
char label[kLabelSize];
|
||||
uint8_t labelLength;
|
||||
char name[kNameSize];
|
||||
|
||||
static const uint8_t kEncodedName1[] = {7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0};
|
||||
static const uint8_t kEncodedName2[] = {3, 'f', 'o', 'o', 1, 'a', 2, 'b', 'b', 3, 'e', 'd', 'u', 0};
|
||||
static const uint8_t kEncodedName3[] = {10, 'f', 'o', 'u', 'n', 'd', 'a', 't', 'i', 'o', 'n', 0};
|
||||
static const uint8_t kEncodedName4[] = {0};
|
||||
|
||||
static const char *kLabels1[] = {"example", "com", nullptr};
|
||||
static const char *kLabels2[] = {"foo", "a", "bb", "edu", nullptr};
|
||||
static const char *kLabels3[] = {"foundation", nullptr};
|
||||
static const char *kLabels4[] = {nullptr};
|
||||
|
||||
static const TestName kTestNames[] = {
|
||||
{"example.com", sizeof(kEncodedName1), kEncodedName1, kLabels1, "example.com."},
|
||||
{"example.com.", sizeof(kEncodedName1), kEncodedName1, kLabels1, "example.com."},
|
||||
{"foo.a.bb.edu", sizeof(kEncodedName2), kEncodedName2, kLabels2, "foo.a.bb.edu."},
|
||||
{"foo.a.bb.edu.", sizeof(kEncodedName2), kEncodedName2, kLabels2, "foo.a.bb.edu."},
|
||||
{"foundation", sizeof(kEncodedName3), kEncodedName3, kLabels3, "foundation."},
|
||||
{"foundation.", sizeof(kEncodedName3), kEncodedName3, kLabels3, "foundation."},
|
||||
{"", sizeof(kEncodedName4), kEncodedName4, kLabels4, "."},
|
||||
{".", sizeof(kEncodedName4), kEncodedName4, kLabels4, "."},
|
||||
{nullptr, sizeof(kEncodedName4), kEncodedName4, kLabels4, "."},
|
||||
};
|
||||
|
||||
static const char *kInvalidNames[] = {
|
||||
"foo..bar",
|
||||
"..",
|
||||
"a..",
|
||||
"..b",
|
||||
|
||||
// Long label
|
||||
"a.an-invalid-very-long-label-string-with-more-than-sixty-four-characters.com",
|
||||
|
||||
// Long name (more than 255 characters)
|
||||
"HereIsSomeoneHidden.MyHoldFromMeTaken.FromSelfHasMeDriven.MyLeadFromMeTaken."
|
||||
"HereIsSomeoneHidden.AsLifeSweeterThanLife.TakesMeToGardenOfSoul.MyFortFromMeTaken."
|
||||
"HereIsSomeoneHidden.LikeSugarInSugarCane.ASweetSugarTrader.MyShopFromMeTaken."
|
||||
"SorcererAndMagician.NoEyesCanEverSee.AnArtfulConjurer.MySenseFromMeTaken."
|
||||
"MyEyesWillNeverSee.BeautiesOfTheWholeWorld.BeholdWhoseVisionFine.MySightFromMeTaken"
|
||||
"PoemByRumiMolana",
|
||||
};
|
||||
|
||||
printf("================================================================\n");
|
||||
printf("TestDnsName()\n");
|
||||
|
||||
instance = static_cast<Instance *>(testInitInstance());
|
||||
VerifyOrQuit(instance != nullptr, "Null OpenThread instance");
|
||||
|
||||
messagePool = &instance->Get<MessagePool>();
|
||||
VerifyOrQuit((message = messagePool->New(Message::kTypeIp6, 0)) != nullptr, "Message::New failed");
|
||||
|
||||
printf("----------------------------------------------------------------\n");
|
||||
printf("Append names, check encoded bytes, parse name and read labels:\n");
|
||||
|
||||
for (const TestName &test : kTestNames)
|
||||
{
|
||||
IgnoreError(message->SetLength(0));
|
||||
|
||||
SuccessOrQuit(Dns::Name::AppendName(test.mName, *message), "Name::AppendName() failed");
|
||||
|
||||
len = message->GetLength();
|
||||
SuccessOrQuit(message->Read(0, buffer, len), "Message::Read() failed");
|
||||
|
||||
DumpBuffer(test.mName, buffer, len);
|
||||
|
||||
VerifyOrQuit(len == test.mEncodedLength, "Encoded length does not match expected value");
|
||||
VerifyOrQuit(memcmp(buffer, test.mEncodedData, len) == 0, "Encoded name data does not match expected data");
|
||||
|
||||
// Parse and skip over the name
|
||||
offset = 0;
|
||||
SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed");
|
||||
VerifyOrQuit(offset == len, "Name::ParseName() returned incorrect offset");
|
||||
|
||||
// Read labels one by one.
|
||||
offset = 0;
|
||||
|
||||
for (uint8_t index = 0; test.mLabels[index] != nullptr; index++)
|
||||
{
|
||||
labelLength = sizeof(label);
|
||||
SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, 0, label, labelLength), "Name::ReadLabel() failed");
|
||||
|
||||
printf("Label[%d] = \"%s\"\n", index, label);
|
||||
|
||||
VerifyOrQuit(strcmp(label, test.mLabels[index]) == 0, "Name::ReadLabel() did not get expected label");
|
||||
VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length");
|
||||
}
|
||||
|
||||
labelLength = sizeof(label);
|
||||
VerifyOrQuit(Dns::Name::ReadLabel(*message, offset, 0, label, labelLength) == OT_ERROR_NOT_FOUND,
|
||||
"Name::ReadLabel() failed at end of the name");
|
||||
|
||||
// Read entire name
|
||||
offset = 0;
|
||||
SuccessOrQuit(Dns::Name::ReadName(*message, offset, 0, name, sizeof(name)), "Name::ReadName() failed");
|
||||
|
||||
printf("Read name =\"%s\"\n", name);
|
||||
|
||||
VerifyOrQuit(strcmp(name, test.mExpectedReadName) == 0, "Name::ReadName() did not get expected name");
|
||||
VerifyOrQuit(offset == len, "Name::ReadName() returned incorrect offset");
|
||||
|
||||
// Read entire name with different name buffer sizes (just right and one byte off the expected size)
|
||||
offset = 0;
|
||||
SuccessOrQuit(
|
||||
Dns::Name::ReadName(*message, offset, 0, name, static_cast<uint16_t>(strlen(test.mExpectedReadName) + 1)),
|
||||
"Name::ReadName() failed with exact name buffer size");
|
||||
offset = 0;
|
||||
VerifyOrQuit(Dns::Name::ReadName(*message, offset, 0, name,
|
||||
static_cast<uint16_t>(strlen(test.mExpectedReadName))) == OT_ERROR_NO_BUFS,
|
||||
"Name::ReadName() did not fail with too small name buffer size");
|
||||
}
|
||||
|
||||
printf("----------------------------------------------------------------\n");
|
||||
printf("Invalid names:\n");
|
||||
|
||||
for (const char *&invalidName : kInvalidNames)
|
||||
{
|
||||
IgnoreError(message->SetLength(0));
|
||||
|
||||
printf("\"%s\"\n", invalidName);
|
||||
|
||||
VerifyOrQuit(Dns::Name::AppendName(invalidName, *message) == OT_ERROR_INVALID_ARGS,
|
||||
"Name::AppendName() did not fail with an invalid name");
|
||||
}
|
||||
|
||||
printf("----------------------------------------------------------------\n");
|
||||
printf("Append as multiple labels and terminator instead of full name:\n");
|
||||
|
||||
for (const TestName &test : kTestNames)
|
||||
{
|
||||
IgnoreError(message->SetLength(0));
|
||||
|
||||
SuccessOrQuit(Dns::Name::AppendMultipleLabels(test.mName, *message), "Name::Append() failed");
|
||||
SuccessOrQuit(Dns::Name::AppendTerminator(*message), "Name::AppendTerminator() failed");
|
||||
|
||||
len = message->GetLength();
|
||||
SuccessOrQuit(message->Read(0, buffer, len), "Message::Read() failed");
|
||||
|
||||
DumpBuffer(test.mName, buffer, len);
|
||||
|
||||
VerifyOrQuit(len == test.mEncodedLength, "Encoded length does not match expected value");
|
||||
VerifyOrQuit(memcmp(buffer, test.mEncodedData, len) == 0, "Encoded name data does not match expected data");
|
||||
}
|
||||
|
||||
printf("----------------------------------------------------------------\n");
|
||||
printf("Append labels one by one:\n");
|
||||
|
||||
for (const TestName &test : kTestNames)
|
||||
{
|
||||
IgnoreError(message->SetLength(0));
|
||||
|
||||
for (uint8_t index = 0; test.mLabels[index] != nullptr; index++)
|
||||
{
|
||||
SuccessOrQuit(Dns::Name::AppendLabel(test.mLabels[index], *message), "Name::AppendLabel() failed");
|
||||
}
|
||||
|
||||
SuccessOrQuit(Dns::Name::AppendTerminator(*message), "Name::AppendTerminator() failed");
|
||||
|
||||
len = message->GetLength();
|
||||
SuccessOrQuit(message->Read(0, buffer, len), "Message::Read() failed");
|
||||
|
||||
DumpBuffer(test.mName, buffer, len);
|
||||
|
||||
VerifyOrQuit(len == test.mEncodedLength, "Encoded length does not match expected value");
|
||||
VerifyOrQuit(memcmp(buffer, test.mEncodedData, len) == 0, "Encoded name data does not match expected data");
|
||||
}
|
||||
|
||||
message->Free();
|
||||
testFreeInstance(instance);
|
||||
}
|
||||
|
||||
void TestDnsCompressedName(void)
|
||||
{
|
||||
enum
|
||||
{
|
||||
kHeaderOffset = 10,
|
||||
kGuardBlockSize = 20,
|
||||
kMaxBufferSize = 100,
|
||||
kLabelSize = 64,
|
||||
kNameSize = 256,
|
||||
|
||||
kName2EncodedSize = 4 + 2, // encoded "FOO" + pointer label (2 bytes)
|
||||
kName3EncodedSize = 2, // pointer label (2 bytes)
|
||||
kName4EncodedSize = 15 + 2, // encoded "Human.Readable" + pointer label (2 bytes).
|
||||
|
||||
};
|
||||
|
||||
const char kName[] = "F.ISI.ARPA";
|
||||
const char kLabel1[] = "FOO";
|
||||
const char kInstanceLabel[] = "Human.Readable";
|
||||
|
||||
static const uint8_t kEncodedName[] = {1, 'F', 3, 'I', 'S', 'I', 4, 'A', 'R', 'P', 'A', 0};
|
||||
static const uint8_t kIsiRelativeIndex = 2; // Index in kEncodedName to the start of "ISI.ARPA" portion.
|
||||
|
||||
static const char *kName1Labels[] = {"F", "ISI", "ARPA"};
|
||||
static const char *kName2Labels[] = {"FOO", "F", "ISI", "ARPA"};
|
||||
static const char *kName3Labels[] = {"ISI", "ARPA"};
|
||||
static const char *kName4Labels[] = {"Human.Readable", "F", "ISI", "ARPA"};
|
||||
|
||||
static const char kExpectedReadName1[] = "F.ISI.ARPA.";
|
||||
static const char kExpectedReadName2[] = "FOO.F.ISI.ARPA.";
|
||||
static const char kExpectedReadName3[] = "ISI.ARPA.";
|
||||
|
||||
Instance * instance;
|
||||
MessagePool *messagePool;
|
||||
Message * message;
|
||||
uint16_t offset;
|
||||
uint16_t name1Offset;
|
||||
uint16_t name2Offset;
|
||||
uint16_t name3Offset;
|
||||
uint16_t name4Offset;
|
||||
uint8_t buffer[kMaxBufferSize];
|
||||
char label[kLabelSize];
|
||||
uint8_t labelLength;
|
||||
char name[kNameSize];
|
||||
|
||||
printf("================================================================\n");
|
||||
printf("TestDnsCompressedName()\n");
|
||||
|
||||
instance = static_cast<Instance *>(testInitInstance());
|
||||
VerifyOrQuit(instance != nullptr, "Null OpenThread instance");
|
||||
|
||||
messagePool = &instance->Get<MessagePool>();
|
||||
VerifyOrQuit((message = messagePool->New(Message::kTypeIp6, 0)) != nullptr, "Message::New failed");
|
||||
|
||||
// Append name1 "F.ISI.ARPA"
|
||||
|
||||
for (uint8_t index = 0; index < kHeaderOffset + kGuardBlockSize; index++)
|
||||
{
|
||||
SuccessOrQuit(message->Append(index), "Message::Append() failed");
|
||||
}
|
||||
|
||||
name1Offset = message->GetLength();
|
||||
SuccessOrQuit(Dns::Name::AppendName(kName, *message), "Name::AppendName() failed");
|
||||
|
||||
// Append name2 "FOO.F.ISI.ARPA" as a compressed name after some guard/extra bytes
|
||||
|
||||
for (uint8_t index = 0; index < kGuardBlockSize; index++)
|
||||
{
|
||||
uint8_t value = 0xff;
|
||||
SuccessOrQuit(message->Append(value), "Message::Append() failed");
|
||||
}
|
||||
|
||||
name2Offset = message->GetLength();
|
||||
|
||||
SuccessOrQuit(Dns::Name::AppendLabel(kLabel1, *message), "Name::AppendLabel() failed");
|
||||
SuccessOrQuit(Dns::Name::AppendPointerLabel(name1Offset - kHeaderOffset, *message),
|
||||
"Name::AppendPointerLabel() failed");
|
||||
|
||||
// Append name3 "ISI.ARPA" as a compressed name after some guard/extra bytes
|
||||
|
||||
for (uint8_t index = 0; index < kGuardBlockSize; index++)
|
||||
{
|
||||
uint8_t value = 0xaa;
|
||||
SuccessOrQuit(message->Append(value), "Message::Append() failed");
|
||||
}
|
||||
|
||||
name3Offset = message->GetLength();
|
||||
SuccessOrQuit(Dns::Name::AppendPointerLabel(name1Offset + kIsiRelativeIndex - kHeaderOffset, *message),
|
||||
"Name::AppendPointerLabel() failed");
|
||||
|
||||
name4Offset = message->GetLength();
|
||||
SuccessOrQuit(Dns::Name::AppendLabel(kInstanceLabel, *message), "Name::AppendLabel() failed");
|
||||
SuccessOrQuit(Dns::Name::AppendPointerLabel(name1Offset - kHeaderOffset, *message),
|
||||
"Name::AppendPointerLabel() failed");
|
||||
|
||||
printf("----------------------------------------------------------------\n");
|
||||
printf("Read and parse the uncompressed name-1 \"F.ISI.ARPA\"\n");
|
||||
|
||||
SuccessOrQuit(message->Read(name1Offset, buffer, sizeof(kEncodedName)), "Message::Read() failed");
|
||||
|
||||
DumpBuffer(kName, buffer, sizeof(kEncodedName));
|
||||
VerifyOrQuit(memcmp(buffer, kEncodedName, sizeof(kEncodedName)) == 0,
|
||||
"Encoded name data does not match expected data");
|
||||
|
||||
offset = name1Offset;
|
||||
SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed");
|
||||
|
||||
VerifyOrQuit(offset == name1Offset + sizeof(kEncodedName), "Name::ParseName() returned incorrect offset");
|
||||
|
||||
offset = name1Offset;
|
||||
|
||||
for (const char *nameLabel : kName1Labels)
|
||||
{
|
||||
labelLength = sizeof(label);
|
||||
SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength),
|
||||
"Name::ReadLabel() failed");
|
||||
|
||||
printf("label: \"%s\"\n", label);
|
||||
VerifyOrQuit(strcmp(label, nameLabel) == 0, "Name::ReadLabel() did not get expected label");
|
||||
VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length");
|
||||
}
|
||||
|
||||
labelLength = sizeof(label);
|
||||
VerifyOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength) == OT_ERROR_NOT_FOUND,
|
||||
"Name::ReadLabel() failed at end of the name");
|
||||
|
||||
offset = name1Offset;
|
||||
SuccessOrQuit(Dns::Name::ReadName(*message, offset, kHeaderOffset, name, sizeof(name)), "Name::ReadName() failed");
|
||||
printf("Read name =\"%s\"\n", name);
|
||||
VerifyOrQuit(strcmp(name, kExpectedReadName1) == 0, "Name::ReadName() did not return expected name");
|
||||
VerifyOrQuit(offset == name1Offset + sizeof(kEncodedName), "Name::ReadName() returned incorrect offset");
|
||||
|
||||
printf("----------------------------------------------------------------\n");
|
||||
printf("Read and parse compressed name-2 \"FOO.F.ISI.ARPA\"\n");
|
||||
|
||||
SuccessOrQuit(message->Read(name2Offset, buffer, kName2EncodedSize), "Message::Read() failed");
|
||||
DumpBuffer("name2(compressed)", buffer, kName2EncodedSize);
|
||||
|
||||
offset = name2Offset;
|
||||
SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed");
|
||||
VerifyOrQuit(offset == name2Offset + kName2EncodedSize, "Name::ParseName() returned incorrect offset");
|
||||
|
||||
offset = name2Offset;
|
||||
|
||||
for (const char *nameLabel : kName2Labels)
|
||||
{
|
||||
labelLength = sizeof(label);
|
||||
SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength),
|
||||
"Name::ReadLabel() failed");
|
||||
|
||||
printf("label: \"%s\"\n", label);
|
||||
VerifyOrQuit(strcmp(label, nameLabel) == 0, "Name::ReadLabel() did not get expected label");
|
||||
VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length");
|
||||
}
|
||||
|
||||
labelLength = sizeof(label);
|
||||
VerifyOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength) == OT_ERROR_NOT_FOUND,
|
||||
"Name::ReadLabel() failed at end of the name");
|
||||
|
||||
offset = name2Offset;
|
||||
SuccessOrQuit(Dns::Name::ReadName(*message, offset, kHeaderOffset, name, sizeof(name)), "Name::ReadName() failed");
|
||||
printf("Read name =\"%s\"\n", name);
|
||||
VerifyOrQuit(strcmp(name, kExpectedReadName2) == 0, "Name::ReadName() did not return expected name");
|
||||
VerifyOrQuit(offset == name2Offset + kName2EncodedSize, "Name::ReadName() returned incorrect offset");
|
||||
|
||||
printf("----------------------------------------------------------------\n");
|
||||
printf("Read and parse compressed name-3 \"ISI.ARPA\"\n");
|
||||
|
||||
SuccessOrQuit(message->Read(name3Offset, buffer, kName3EncodedSize), "Message::Read() failed");
|
||||
DumpBuffer("name2(compressed)", buffer, kName3EncodedSize);
|
||||
|
||||
offset = name3Offset;
|
||||
SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed");
|
||||
VerifyOrQuit(offset == name3Offset + kName3EncodedSize, "Name::ParseName() returned incorrect offset");
|
||||
|
||||
offset = name3Offset;
|
||||
|
||||
for (const char *nameLabel : kName3Labels)
|
||||
{
|
||||
labelLength = sizeof(label);
|
||||
SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength),
|
||||
"Name::ReadLabel() failed");
|
||||
|
||||
printf("label: \"%s\"\n", label);
|
||||
VerifyOrQuit(strcmp(label, nameLabel) == 0, "Name::ReadLabel() did not get expected label");
|
||||
VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length");
|
||||
}
|
||||
|
||||
labelLength = sizeof(label);
|
||||
VerifyOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength) == OT_ERROR_NOT_FOUND,
|
||||
"Name::ReadLabel() failed at end of the name");
|
||||
|
||||
offset = name3Offset;
|
||||
SuccessOrQuit(Dns::Name::ReadName(*message, offset, kHeaderOffset, name, sizeof(name)), "Name::ReadName() failed");
|
||||
printf("Read name =\"%s\"\n", name);
|
||||
VerifyOrQuit(strcmp(name, kExpectedReadName3) == 0, "Name::ReadName() did not return expected name");
|
||||
VerifyOrQuit(offset == name3Offset + kName3EncodedSize, "Name::ReadName() returned incorrect offset");
|
||||
|
||||
printf("----------------------------------------------------------------\n");
|
||||
printf("Read and parse the uncompressed name-4 \"Human\\.Readable.F.ISI.ARPA\"\n");
|
||||
|
||||
SuccessOrQuit(message->Read(name4Offset, buffer, kName4EncodedSize), "Message::Read() failed");
|
||||
DumpBuffer("name4(compressed)", buffer, kName4EncodedSize);
|
||||
|
||||
offset = name4Offset;
|
||||
SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed");
|
||||
VerifyOrQuit(offset == name4Offset + kName4EncodedSize, "Name::ParseName() returned incorrect offset");
|
||||
|
||||
offset = name4Offset;
|
||||
|
||||
for (const char *nameLabel : kName4Labels)
|
||||
{
|
||||
labelLength = sizeof(label);
|
||||
SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength),
|
||||
"Name::ReadLabel() failed");
|
||||
|
||||
printf("label: \"%s\"\n", label);
|
||||
VerifyOrQuit(strcmp(label, nameLabel) == 0, "Name::ReadLabel() did not get expected label");
|
||||
VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length");
|
||||
}
|
||||
|
||||
// `ReadName()` for name-4 should fails due to first label containing dot char.
|
||||
offset = name4Offset;
|
||||
VerifyOrQuit(Dns::Name::ReadName(*message, offset, kHeaderOffset, name, sizeof(name)) == OT_ERROR_PARSE,
|
||||
"Name::ReadName() did not fail with invalid label");
|
||||
|
||||
message->Free();
|
||||
testFreeInstance(instance);
|
||||
}
|
||||
|
||||
} // namespace ot
|
||||
|
||||
int main(void)
|
||||
{
|
||||
ot::TestDnsName();
|
||||
ot::TestDnsCompressedName();
|
||||
|
||||
printf("All tests passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -154,6 +154,27 @@ void TestUtf8(void)
|
||||
printf(" -- PASS\n");
|
||||
}
|
||||
|
||||
void TestStringFind(void)
|
||||
{
|
||||
char emptyString[1] = {'\0'};
|
||||
char testString[] = "foo.bar\\.";
|
||||
|
||||
printf("\nTest 6: StringFind() function\n");
|
||||
|
||||
VerifyOrQuit(StringFind(testString, 'f') == testString, "StringFind() failed");
|
||||
VerifyOrQuit(StringFind(testString, 'o') == &testString[1], "StringFind() failed");
|
||||
VerifyOrQuit(StringFind(testString, '.') == &testString[3], "StringFind() failed");
|
||||
VerifyOrQuit(StringFind(testString, 'r') == &testString[6], "StringFind() failed");
|
||||
VerifyOrQuit(StringFind(testString, '\\') == &testString[7], "StringFind() failed");
|
||||
VerifyOrQuit(StringFind(testString, 'x') == nullptr, "StringFind() failed");
|
||||
VerifyOrQuit(StringFind(testString, ',') == nullptr, "StringFind() failed");
|
||||
|
||||
VerifyOrQuit(StringFind(emptyString, 'f') == nullptr, "StringFind() failed");
|
||||
VerifyOrQuit(StringFind(emptyString, '.') == nullptr, "StringFind() failed");
|
||||
|
||||
printf(" -- PASS\n");
|
||||
}
|
||||
|
||||
} // namespace ot
|
||||
|
||||
int main(void)
|
||||
@@ -161,6 +182,7 @@ int main(void)
|
||||
ot::TestString();
|
||||
ot::TestStringLength();
|
||||
ot::TestUtf8();
|
||||
ot::TestStringFind();
|
||||
printf("\nAll tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user