587 lines
19 KiB
C++
587 lines
19 KiB
C++
// Copyright 2022 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "net/dns/host_resolver_internal_result.h"
|
|
|
|
#include <map>
|
|
#include <memory>
|
|
#include <ostream>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "base/check_op.h"
|
|
#include "base/json/values_util.h"
|
|
#include "base/memory/ptr_util.h"
|
|
#include "base/strings/string_piece.h"
|
|
#include "base/time/time.h"
|
|
#include "base/values.h"
|
|
#include "net/base/connection_endpoint_metadata.h"
|
|
#include "net/base/host_port_pair.h"
|
|
#include "net/base/ip_endpoint.h"
|
|
#include "net/base/net_errors.h"
|
|
#include "net/dns/https_record_rdata.h"
|
|
#include "net/dns/public/dns_query_type.h"
|
|
#include "third_party/abseil-cpp/absl/types/optional.h"
|
|
#include "url/url_canon.h"
|
|
#include "url/url_canon_stdstring.h"
|
|
|
|
namespace net {
|
|
|
|
namespace {
|
|
|
|
// base::Value keys
|
|
constexpr base::StringPiece kValueDomainNameKey = "domain_name";
|
|
constexpr base::StringPiece kValueQueryTypeKey = "query_type";
|
|
constexpr base::StringPiece kValueTypeKey = "type";
|
|
constexpr base::StringPiece kValueSourceKey = "source";
|
|
constexpr base::StringPiece kValueTimedExpirationKey = "timed_expiration";
|
|
constexpr base::StringPiece kValueEndpointsKey = "endpoints";
|
|
constexpr base::StringPiece kValueStringsKey = "strings";
|
|
constexpr base::StringPiece kValueHostsKey = "hosts";
|
|
constexpr base::StringPiece kValueMetadatasKey = "metadatas";
|
|
constexpr base::StringPiece kValueMetadataWeightKey = "metadata_weight";
|
|
constexpr base::StringPiece kValueMetadataValueKey = "metadata_value";
|
|
constexpr base::StringPiece kValueErrorKey = "error";
|
|
constexpr base::StringPiece kValueAliasTargetKey = "alias_target";
|
|
|
|
// Returns `domain_name` as-is if it could not be canonicalized.
|
|
std::string MaybeCanonicalizeName(std::string domain_name) {
|
|
std::string canonicalized;
|
|
url::StdStringCanonOutput output(&canonicalized);
|
|
url::CanonHostInfo host_info;
|
|
|
|
url::CanonicalizeHostVerbose(domain_name.data(),
|
|
url::Component(0, domain_name.size()), &output,
|
|
&host_info);
|
|
|
|
if (host_info.family == url::CanonHostInfo::Family::NEUTRAL) {
|
|
output.Complete();
|
|
return canonicalized;
|
|
} else {
|
|
return domain_name;
|
|
}
|
|
}
|
|
|
|
base::Value EndpointMetadataPairToValue(
|
|
const std::pair<HttpsRecordPriority, ConnectionEndpointMetadata>& pair) {
|
|
base::Value::Dict dictionary;
|
|
dictionary.Set(kValueMetadataWeightKey, pair.first);
|
|
dictionary.Set(kValueMetadataValueKey, pair.second.ToValue());
|
|
return base::Value(std::move(dictionary));
|
|
}
|
|
|
|
absl::optional<std::pair<HttpsRecordPriority, ConnectionEndpointMetadata>>
|
|
EndpointMetadataPairFromValue(const base::Value& value) {
|
|
const base::Value::Dict* dict = value.GetIfDict();
|
|
if (!dict)
|
|
return absl::nullopt;
|
|
|
|
absl::optional<int> weight = dict->FindInt(kValueMetadataWeightKey);
|
|
if (!weight || !base::IsValueInRangeForNumericType<HttpsRecordPriority>(
|
|
weight.value())) {
|
|
return absl::nullopt;
|
|
}
|
|
|
|
const base::Value* metadata_value = dict->Find(kValueMetadataValueKey);
|
|
if (!metadata_value)
|
|
return absl::nullopt;
|
|
absl::optional<ConnectionEndpointMetadata> metadata =
|
|
ConnectionEndpointMetadata::FromValue(*metadata_value);
|
|
if (!metadata)
|
|
return absl::nullopt;
|
|
|
|
return std::make_pair(base::checked_cast<HttpsRecordPriority>(weight.value()),
|
|
std::move(metadata).value());
|
|
}
|
|
|
|
absl::optional<DnsQueryType> QueryTypeFromValue(const base::Value& value) {
|
|
const std::string* query_type_string = value.GetIfString();
|
|
if (!query_type_string)
|
|
return absl::nullopt;
|
|
const auto* query_type_it =
|
|
base::ranges::find(kDnsQueryTypes, *query_type_string,
|
|
&decltype(kDnsQueryTypes)::value_type::second);
|
|
if (query_type_it == kDnsQueryTypes.end())
|
|
return absl::nullopt;
|
|
|
|
return query_type_it->first;
|
|
}
|
|
|
|
base::Value TypeToValue(HostResolverInternalResult::Type type) {
|
|
switch (type) {
|
|
case HostResolverInternalResult::Type::kData:
|
|
return base::Value("data");
|
|
case HostResolverInternalResult::Type::kMetadata:
|
|
return base::Value("metadata");
|
|
case HostResolverInternalResult::Type::kError:
|
|
return base::Value("error");
|
|
case HostResolverInternalResult::Type::kAlias:
|
|
return base::Value("alias");
|
|
}
|
|
}
|
|
|
|
absl::optional<HostResolverInternalResult::Type> TypeFromValue(
|
|
const base::Value& value) {
|
|
const std::string* string = value.GetIfString();
|
|
if (!string)
|
|
return absl::nullopt;
|
|
|
|
if (*string == "data") {
|
|
return HostResolverInternalResult::Type::kData;
|
|
} else if (*string == "metadata") {
|
|
return HostResolverInternalResult::Type::kMetadata;
|
|
} else if (*string == "error") {
|
|
return HostResolverInternalResult::Type::kError;
|
|
} else if (*string == "alias") {
|
|
return HostResolverInternalResult::Type::kAlias;
|
|
} else {
|
|
return absl::nullopt;
|
|
}
|
|
}
|
|
|
|
base::Value SourceToValue(HostResolverInternalResult::Source source) {
|
|
switch (source) {
|
|
case HostResolverInternalResult::Source::kDns:
|
|
return base::Value("dns");
|
|
case HostResolverInternalResult::Source::kHosts:
|
|
return base::Value("hosts");
|
|
case HostResolverInternalResult::Source::kUnknown:
|
|
return base::Value("unknown");
|
|
}
|
|
}
|
|
|
|
absl::optional<HostResolverInternalResult::Source> SourceFromValue(
|
|
const base::Value& value) {
|
|
const std::string* string = value.GetIfString();
|
|
if (!string)
|
|
return absl::nullopt;
|
|
|
|
if (*string == "dns") {
|
|
return HostResolverInternalResult::Source::kDns;
|
|
} else if (*string == "hosts") {
|
|
return HostResolverInternalResult::Source::kHosts;
|
|
} else if (*string == "unknown") {
|
|
return HostResolverInternalResult::Source::kUnknown;
|
|
} else {
|
|
return absl::nullopt;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// static
|
|
std::unique_ptr<HostResolverInternalResult>
|
|
HostResolverInternalResult::FromValue(const base::Value& value) {
|
|
const base::Value::Dict* dict = value.GetIfDict();
|
|
if (!dict)
|
|
return nullptr;
|
|
|
|
const base::Value* type_value = dict->Find(kValueTypeKey);
|
|
if (!type_value)
|
|
return nullptr;
|
|
absl::optional<Type> type = TypeFromValue(*type_value);
|
|
if (!type.has_value())
|
|
return nullptr;
|
|
|
|
switch (type.value()) {
|
|
case Type::kData:
|
|
return HostResolverInternalDataResult::FromValue(value);
|
|
case Type::kMetadata:
|
|
return HostResolverInternalMetadataResult::FromValue(value);
|
|
case Type::kError:
|
|
return HostResolverInternalErrorResult::FromValue(value);
|
|
case Type::kAlias:
|
|
return HostResolverInternalAliasResult::FromValue(value);
|
|
}
|
|
}
|
|
|
|
const HostResolverInternalDataResult& HostResolverInternalResult::AsData()
|
|
const {
|
|
CHECK_EQ(type_, Type::kData);
|
|
return *static_cast<const HostResolverInternalDataResult*>(this);
|
|
}
|
|
|
|
const HostResolverInternalMetadataResult&
|
|
HostResolverInternalResult::AsMetadata() const {
|
|
CHECK_EQ(type_, Type::kMetadata);
|
|
return *static_cast<const HostResolverInternalMetadataResult*>(this);
|
|
}
|
|
|
|
const HostResolverInternalErrorResult& HostResolverInternalResult::AsError()
|
|
const {
|
|
CHECK_EQ(type_, Type::kError);
|
|
return *static_cast<const HostResolverInternalErrorResult*>(this);
|
|
}
|
|
|
|
const HostResolverInternalAliasResult& HostResolverInternalResult::AsAlias()
|
|
const {
|
|
CHECK_EQ(type_, Type::kAlias);
|
|
return *static_cast<const HostResolverInternalAliasResult*>(this);
|
|
}
|
|
|
|
HostResolverInternalResult::HostResolverInternalResult(
|
|
std::string domain_name,
|
|
DnsQueryType query_type,
|
|
absl::optional<base::TimeTicks> expiration,
|
|
absl::optional<base::Time> timed_expiration,
|
|
Type type,
|
|
Source source)
|
|
: domain_name_(MaybeCanonicalizeName(std::move(domain_name))),
|
|
query_type_(query_type),
|
|
type_(type),
|
|
source_(source),
|
|
expiration_(expiration),
|
|
timed_expiration_(timed_expiration) {
|
|
DCHECK(!domain_name_.empty());
|
|
// If `expiration` has a value, `timed_expiration` must too.
|
|
DCHECK(!expiration_.has_value() || timed_expiration.has_value());
|
|
}
|
|
|
|
HostResolverInternalResult::HostResolverInternalResult(
|
|
const base::Value::Dict& dict)
|
|
: domain_name_(*dict.FindString(kValueDomainNameKey)),
|
|
query_type_(QueryTypeFromValue(*dict.Find(kValueQueryTypeKey)).value()),
|
|
type_(TypeFromValue(*dict.Find(kValueTypeKey)).value()),
|
|
source_(SourceFromValue(*dict.Find(kValueSourceKey)).value()),
|
|
timed_expiration_(
|
|
dict.contains(kValueTimedExpirationKey)
|
|
? base::ValueToTime(*dict.Find(kValueTimedExpirationKey))
|
|
: absl::optional<base::Time>()) {}
|
|
|
|
// static
|
|
bool HostResolverInternalResult::ValidateValueBaseDict(
|
|
const base::Value::Dict& dict,
|
|
bool require_timed_expiration) {
|
|
const std::string* domain_name = dict.FindString(kValueDomainNameKey);
|
|
if (!domain_name)
|
|
return false;
|
|
|
|
const std::string* query_type_string = dict.FindString(kValueQueryTypeKey);
|
|
if (!query_type_string)
|
|
return false;
|
|
const auto* query_type_it =
|
|
base::ranges::find(kDnsQueryTypes, *query_type_string,
|
|
&decltype(kDnsQueryTypes)::value_type::second);
|
|
if (query_type_it == kDnsQueryTypes.end())
|
|
return false;
|
|
|
|
const base::Value* type_value = dict.Find(kValueTypeKey);
|
|
if (!type_value)
|
|
return false;
|
|
absl::optional<Type> type = TypeFromValue(*type_value);
|
|
if (!type.has_value())
|
|
return false;
|
|
|
|
const base::Value* source_value = dict.Find(kValueSourceKey);
|
|
if (!source_value)
|
|
return false;
|
|
absl::optional<Source> source = SourceFromValue(*source_value);
|
|
if (!source.has_value())
|
|
return false;
|
|
|
|
absl::optional<base::Time> timed_expiration;
|
|
const base::Value* timed_expiration_value =
|
|
dict.Find(kValueTimedExpirationKey);
|
|
if (require_timed_expiration && !timed_expiration_value)
|
|
return false;
|
|
if (timed_expiration_value) {
|
|
timed_expiration = base::ValueToTime(timed_expiration_value);
|
|
if (!timed_expiration.has_value())
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
base::Value::Dict HostResolverInternalResult::ToValueBaseDict() const {
|
|
base::Value::Dict dict;
|
|
|
|
dict.Set(kValueDomainNameKey, domain_name_);
|
|
dict.Set(kValueQueryTypeKey, kDnsQueryTypes.at(query_type_));
|
|
dict.Set(kValueTypeKey, TypeToValue(type_));
|
|
dict.Set(kValueSourceKey, SourceToValue(source_));
|
|
|
|
// `expiration_` is not serialized because it is TimeTicks.
|
|
|
|
if (timed_expiration_.has_value()) {
|
|
dict.Set(kValueTimedExpirationKey,
|
|
base::TimeToValue(timed_expiration_.value()));
|
|
}
|
|
|
|
return dict;
|
|
}
|
|
|
|
// static
|
|
std::unique_ptr<HostResolverInternalDataResult>
|
|
HostResolverInternalDataResult::FromValue(const base::Value& value) {
|
|
const base::Value::Dict* dict = value.GetIfDict();
|
|
if (!dict || !ValidateValueBaseDict(*dict, /*require_timed_expiration=*/true))
|
|
return nullptr;
|
|
|
|
const base::Value::List* endpoint_values = dict->FindList(kValueEndpointsKey);
|
|
if (!endpoint_values)
|
|
return nullptr;
|
|
|
|
std::vector<IPEndPoint> endpoints;
|
|
endpoints.reserve(endpoint_values->size());
|
|
for (const base::Value& endpoint_value : *endpoint_values) {
|
|
absl::optional<IPEndPoint> endpoint = IPEndPoint::FromValue(endpoint_value);
|
|
if (!endpoint.has_value())
|
|
return nullptr;
|
|
|
|
endpoints.push_back(std::move(endpoint).value());
|
|
}
|
|
|
|
const base::Value::List* string_values = dict->FindList(kValueStringsKey);
|
|
if (!string_values)
|
|
return nullptr;
|
|
|
|
std::vector<std::string> strings;
|
|
strings.reserve(string_values->size());
|
|
for (const base::Value& string_value : *string_values) {
|
|
const std::string* string = string_value.GetIfString();
|
|
if (!string)
|
|
return nullptr;
|
|
|
|
strings.push_back(*string);
|
|
}
|
|
|
|
const base::Value::List* host_values = dict->FindList(kValueHostsKey);
|
|
if (!host_values)
|
|
return nullptr;
|
|
|
|
std::vector<HostPortPair> hosts;
|
|
hosts.reserve(host_values->size());
|
|
for (const base::Value& host_value : *host_values) {
|
|
absl::optional<HostPortPair> host = HostPortPair::FromValue(host_value);
|
|
if (!host.has_value())
|
|
return nullptr;
|
|
|
|
hosts.push_back(std::move(host).value());
|
|
}
|
|
|
|
// WrapUnique due to private constructor.
|
|
return base::WrapUnique(new HostResolverInternalDataResult(
|
|
*dict, std::move(endpoints), std::move(strings), std::move(hosts)));
|
|
}
|
|
|
|
HostResolverInternalDataResult::HostResolverInternalDataResult(
|
|
std::string domain_name,
|
|
DnsQueryType query_type,
|
|
absl::optional<base::TimeTicks> expiration,
|
|
base::Time timed_expiration,
|
|
Source source,
|
|
std::vector<IPEndPoint> endpoints,
|
|
std::vector<std::string> strings,
|
|
std::vector<HostPortPair> hosts)
|
|
: HostResolverInternalResult(std::move(domain_name),
|
|
query_type,
|
|
expiration,
|
|
timed_expiration,
|
|
Type::kData,
|
|
source),
|
|
endpoints_(std::move(endpoints)),
|
|
strings_(std::move(strings)),
|
|
hosts_(std::move(hosts)) {
|
|
DCHECK(!endpoints_.empty() || !strings_.empty() || !hosts_.empty());
|
|
}
|
|
|
|
HostResolverInternalDataResult::~HostResolverInternalDataResult() = default;
|
|
|
|
base::Value HostResolverInternalDataResult::ToValue() const {
|
|
base::Value::Dict dict = ToValueBaseDict();
|
|
|
|
base::Value::List endpoints_list;
|
|
endpoints_list.reserve(endpoints_.size());
|
|
for (IPEndPoint endpoint : endpoints_) {
|
|
endpoints_list.Append(endpoint.ToValue());
|
|
}
|
|
dict.Set(kValueEndpointsKey, std::move(endpoints_list));
|
|
|
|
base::Value::List strings_list;
|
|
strings_list.reserve(strings_.size());
|
|
for (const std::string& string : strings_) {
|
|
strings_list.Append(string);
|
|
}
|
|
dict.Set(kValueStringsKey, std::move(strings_list));
|
|
|
|
base::Value::List hosts_list;
|
|
hosts_list.reserve(hosts_.size());
|
|
for (const HostPortPair& host : hosts_) {
|
|
hosts_list.Append(host.ToValue());
|
|
}
|
|
dict.Set(kValueHostsKey, std::move(hosts_list));
|
|
|
|
return base::Value(std::move(dict));
|
|
}
|
|
|
|
HostResolverInternalDataResult::HostResolverInternalDataResult(
|
|
const base::Value::Dict& dict,
|
|
std::vector<IPEndPoint> endpoints,
|
|
std::vector<std::string> strings,
|
|
std::vector<HostPortPair> hosts)
|
|
: HostResolverInternalResult(dict),
|
|
endpoints_(std::move(endpoints)),
|
|
strings_(std::move(strings)),
|
|
hosts_(std::move(hosts)) {}
|
|
|
|
// static
|
|
std::unique_ptr<HostResolverInternalMetadataResult>
|
|
HostResolverInternalMetadataResult::FromValue(const base::Value& value) {
|
|
const base::Value::Dict* dict = value.GetIfDict();
|
|
if (!dict || !ValidateValueBaseDict(*dict, /*require_timed_expiration=*/true))
|
|
return nullptr;
|
|
|
|
const base::Value::List* metadata_values = dict->FindList(kValueMetadatasKey);
|
|
if (!metadata_values)
|
|
return nullptr;
|
|
|
|
std::multimap<HttpsRecordPriority, ConnectionEndpointMetadata> metadatas;
|
|
for (const base::Value& metadata_value : *metadata_values) {
|
|
absl::optional<std::pair<HttpsRecordPriority, ConnectionEndpointMetadata>>
|
|
metadata = EndpointMetadataPairFromValue(metadata_value);
|
|
if (!metadata.has_value())
|
|
return nullptr;
|
|
metadatas.insert(std::move(metadata).value());
|
|
}
|
|
|
|
// WrapUnique due to private constructor.
|
|
return base::WrapUnique(
|
|
new HostResolverInternalMetadataResult(*dict, std::move(metadatas)));
|
|
}
|
|
|
|
HostResolverInternalMetadataResult::HostResolverInternalMetadataResult(
|
|
std::string domain_name,
|
|
DnsQueryType query_type,
|
|
absl::optional<base::TimeTicks> expiration,
|
|
base::Time timed_expiration,
|
|
Source source,
|
|
std::multimap<HttpsRecordPriority, ConnectionEndpointMetadata> metadatas)
|
|
: HostResolverInternalResult(std::move(domain_name),
|
|
query_type,
|
|
expiration,
|
|
timed_expiration,
|
|
Type::kMetadata,
|
|
source),
|
|
metadatas_(std::move(metadatas)) {}
|
|
|
|
HostResolverInternalMetadataResult::~HostResolverInternalMetadataResult() =
|
|
default;
|
|
|
|
base::Value HostResolverInternalMetadataResult::ToValue() const {
|
|
base::Value::Dict dict = ToValueBaseDict();
|
|
|
|
base::Value::List metadatas_list;
|
|
metadatas_list.reserve(metadatas_.size());
|
|
for (const std::pair<const HttpsRecordPriority, ConnectionEndpointMetadata>&
|
|
metadata_pair : metadatas_) {
|
|
metadatas_list.Append(EndpointMetadataPairToValue(metadata_pair));
|
|
}
|
|
dict.Set(kValueMetadatasKey, std::move(metadatas_list));
|
|
|
|
return base::Value(std::move(dict));
|
|
}
|
|
|
|
HostResolverInternalMetadataResult::HostResolverInternalMetadataResult(
|
|
const base::Value::Dict& dict,
|
|
std::multimap<HttpsRecordPriority, ConnectionEndpointMetadata> metadatas)
|
|
: HostResolverInternalResult(dict), metadatas_(std::move(metadatas)) {}
|
|
|
|
// static
|
|
std::unique_ptr<HostResolverInternalErrorResult>
|
|
HostResolverInternalErrorResult::FromValue(const base::Value& value) {
|
|
const base::Value::Dict* dict = value.GetIfDict();
|
|
if (!dict ||
|
|
!ValidateValueBaseDict(*dict, /*require_timed_expiration=*/false)) {
|
|
return nullptr;
|
|
}
|
|
|
|
absl::optional<int> error = dict->FindInt(kValueErrorKey);
|
|
if (!error.has_value())
|
|
return nullptr;
|
|
|
|
// WrapUnique due to private constructor.
|
|
return base::WrapUnique(
|
|
new HostResolverInternalErrorResult(*dict, error.value()));
|
|
}
|
|
|
|
HostResolverInternalErrorResult::HostResolverInternalErrorResult(
|
|
std::string domain_name,
|
|
DnsQueryType query_type,
|
|
absl::optional<base::TimeTicks> expiration,
|
|
absl::optional<base::Time> timed_expiration,
|
|
Source source,
|
|
int error)
|
|
: HostResolverInternalResult(std::move(domain_name),
|
|
query_type,
|
|
expiration,
|
|
timed_expiration,
|
|
Type::kError,
|
|
source),
|
|
error_(error) {}
|
|
|
|
base::Value HostResolverInternalErrorResult::ToValue() const {
|
|
base::Value::Dict dict = ToValueBaseDict();
|
|
|
|
dict.Set(kValueErrorKey, error_);
|
|
|
|
return base::Value(std::move(dict));
|
|
}
|
|
|
|
HostResolverInternalErrorResult::HostResolverInternalErrorResult(
|
|
const base::Value::Dict& dict,
|
|
int error)
|
|
: HostResolverInternalResult(dict), error_(error) {
|
|
DCHECK_NE(error_, OK);
|
|
}
|
|
|
|
// static
|
|
std::unique_ptr<HostResolverInternalAliasResult>
|
|
HostResolverInternalAliasResult::FromValue(const base::Value& value) {
|
|
const base::Value::Dict* dict = value.GetIfDict();
|
|
if (!dict || !ValidateValueBaseDict(*dict, /*require_timed_expiration=*/true))
|
|
return nullptr;
|
|
|
|
const std::string* target = dict->FindString(kValueAliasTargetKey);
|
|
if (!target)
|
|
return nullptr;
|
|
|
|
// WrapUnique due to private constructor.
|
|
return base::WrapUnique(new HostResolverInternalAliasResult(*dict, *target));
|
|
}
|
|
|
|
HostResolverInternalAliasResult::HostResolverInternalAliasResult(
|
|
std::string domain_name,
|
|
DnsQueryType query_type,
|
|
absl::optional<base::TimeTicks> expiration,
|
|
base::Time timed_expiration,
|
|
Source source,
|
|
std::string alias_target)
|
|
: HostResolverInternalResult(std::move(domain_name),
|
|
query_type,
|
|
expiration,
|
|
timed_expiration,
|
|
Type::kAlias,
|
|
source),
|
|
alias_target_(MaybeCanonicalizeName(std::move(alias_target))) {
|
|
DCHECK(!alias_target_.empty());
|
|
}
|
|
|
|
base::Value HostResolverInternalAliasResult::ToValue() const {
|
|
base::Value::Dict dict = ToValueBaseDict();
|
|
|
|
dict.Set(kValueAliasTargetKey, alias_target_);
|
|
|
|
return base::Value(std::move(dict));
|
|
}
|
|
|
|
HostResolverInternalAliasResult::HostResolverInternalAliasResult(
|
|
const base::Value::Dict& dict,
|
|
std::string alias_target)
|
|
: HostResolverInternalResult(dict),
|
|
alias_target_(MaybeCanonicalizeName(std::move(alias_target))) {}
|
|
|
|
} // namespace net
|