209 lines
7.7 KiB
209 lines
7.7 KiB
4 months ago
|
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style license that can be
|
||
|
// found in the LICENSE file.
|
||
|
|
||
|
#include "cast/common/public/service_info.h"
|
||
|
|
||
|
#include <cctype>
|
||
|
#include <cinttypes>
|
||
|
#include <string>
|
||
|
#include <vector>
|
||
|
|
||
|
#include "absl/strings/numbers.h"
|
||
|
#include "absl/strings/str_replace.h"
|
||
|
#include "util/osp_logging.h"
|
||
|
|
||
|
namespace openscreen {
|
||
|
namespace cast {
|
||
|
namespace {
|
||
|
|
||
|
// Maximum size for registered MDNS service instance names.
|
||
|
const size_t kMaxDeviceNameSize = 63;
|
||
|
|
||
|
// Maximum size for the device model prefix at start of MDNS service instance
|
||
|
// names. Any model names that are larger than this size will be truncated.
|
||
|
const size_t kMaxDeviceModelSize = 20;
|
||
|
|
||
|
// Build the MDNS instance name for service. This will be the device model (up
|
||
|
// to 20 bytes) appended with the virtual device ID (device UUID) and optionally
|
||
|
// appended with extension at the end to resolve name conflicts. The total MDNS
|
||
|
// service instance name is kept below 64 bytes so it can easily fit into a
|
||
|
// single domain name label.
|
||
|
//
|
||
|
// NOTE: This value is based on what is currently done by Eureka, not what is
|
||
|
// called out in the CastV2 spec. Eureka uses |model|-|uuid|, so the same
|
||
|
// convention will be followed here. That being said, the Eureka receiver does
|
||
|
// not use the instance ID in any way, so the specific calculation used should
|
||
|
// not be important.
|
||
|
std::string CalculateInstanceId(const ServiceInfo& info) {
|
||
|
// First set the device model, truncated to 20 bytes at most. Replace any
|
||
|
// whitespace characters (" ") with hyphens ("-") in the device model before
|
||
|
// truncation.
|
||
|
std::string instance_name =
|
||
|
absl::StrReplaceAll(info.model_name, {{" ", "-"}});
|
||
|
instance_name = std::string(instance_name, 0, kMaxDeviceModelSize);
|
||
|
|
||
|
// Append the virtual device ID to the instance name separated by a single
|
||
|
// '-' character if not empty. Strip all hyphens from the device ID prior
|
||
|
// to appending it.
|
||
|
std::string device_id = absl::StrReplaceAll(info.unique_id, {{"-", ""}});
|
||
|
|
||
|
if (!instance_name.empty()) {
|
||
|
instance_name.push_back('-');
|
||
|
}
|
||
|
instance_name.append(device_id);
|
||
|
|
||
|
return std::string(instance_name, 0, kMaxDeviceNameSize);
|
||
|
}
|
||
|
|
||
|
// Returns the value for the provided |key| in the |txt| record if it exists;
|
||
|
// otherwise, returns an empty string.
|
||
|
std::string GetStringFromRecord(const discovery::DnsSdTxtRecord& txt,
|
||
|
const std::string& key) {
|
||
|
std::string result;
|
||
|
const ErrorOr<discovery::DnsSdTxtRecord::ValueRef> value = txt.GetValue(key);
|
||
|
if (value.is_value()) {
|
||
|
const std::vector<uint8_t>& txt_value = value.value().get();
|
||
|
result.assign(txt_value.begin(), txt_value.end());
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
} // namespace
|
||
|
|
||
|
const std::string& ServiceInfo::GetInstanceId() const {
|
||
|
if (instance_id_ == std::string("")) {
|
||
|
instance_id_ = CalculateInstanceId(*this);
|
||
|
}
|
||
|
|
||
|
return instance_id_;
|
||
|
}
|
||
|
|
||
|
bool ServiceInfo::IsValid() const {
|
||
|
return (
|
||
|
discovery::IsInstanceValid(GetInstanceId()) && port != 0 &&
|
||
|
!unique_id.empty() &&
|
||
|
discovery::DnsSdTxtRecord::IsValidTxtValue(kUniqueIdKey, unique_id) &&
|
||
|
protocol_version >= 2 &&
|
||
|
discovery::DnsSdTxtRecord::IsValidTxtValue(
|
||
|
kVersionKey, std::to_string(static_cast<int>(protocol_version))) &&
|
||
|
discovery::DnsSdTxtRecord::IsValidTxtValue(
|
||
|
kCapabilitiesKey, std::to_string(capabilities)) &&
|
||
|
(status == ReceiverStatus::kIdle || status == ReceiverStatus::kBusy) &&
|
||
|
discovery::DnsSdTxtRecord::IsValidTxtValue(
|
||
|
kStatusKey, std::to_string(static_cast<int>(status))) &&
|
||
|
discovery::DnsSdTxtRecord::IsValidTxtValue(kModelNameKey, model_name) &&
|
||
|
!friendly_name.empty() &&
|
||
|
discovery::DnsSdTxtRecord::IsValidTxtValue(kFriendlyNameKey,
|
||
|
friendly_name));
|
||
|
}
|
||
|
|
||
|
discovery::DnsSdInstance ServiceInfoToDnsSdInstance(const ServiceInfo& info) {
|
||
|
OSP_DCHECK(discovery::IsServiceValid(kCastV2ServiceId));
|
||
|
OSP_DCHECK(discovery::IsDomainValid(kCastV2DomainId));
|
||
|
|
||
|
OSP_DCHECK(info.IsValid());
|
||
|
|
||
|
discovery::DnsSdTxtRecord txt;
|
||
|
const bool did_set_everything =
|
||
|
txt.SetValue(kUniqueIdKey, info.unique_id).ok() &&
|
||
|
txt.SetValue(kVersionKey,
|
||
|
std::to_string(static_cast<int>(info.protocol_version)))
|
||
|
.ok() &&
|
||
|
txt.SetValue(kCapabilitiesKey, std::to_string(info.capabilities)).ok() &&
|
||
|
txt.SetValue(kStatusKey, std::to_string(static_cast<int>(info.status)))
|
||
|
.ok() &&
|
||
|
txt.SetValue(kModelNameKey, info.model_name).ok() &&
|
||
|
txt.SetValue(kFriendlyNameKey, info.friendly_name).ok();
|
||
|
OSP_DCHECK(did_set_everything);
|
||
|
|
||
|
return discovery::DnsSdInstance(info.GetInstanceId(), kCastV2ServiceId,
|
||
|
kCastV2DomainId, std::move(txt), info.port);
|
||
|
}
|
||
|
|
||
|
ErrorOr<ServiceInfo> DnsSdInstanceEndpointToServiceInfo(
|
||
|
const discovery::DnsSdInstanceEndpoint& endpoint) {
|
||
|
if (endpoint.service_id() != kCastV2ServiceId) {
|
||
|
return {Error::Code::kParameterInvalid, "Not a Cast device."};
|
||
|
}
|
||
|
|
||
|
ServiceInfo record;
|
||
|
for (const IPAddress& address : endpoint.addresses()) {
|
||
|
if (!record.v4_address && address.IsV4()) {
|
||
|
record.v4_address = address;
|
||
|
} else if (!record.v6_address && address.IsV6()) {
|
||
|
record.v6_address = address;
|
||
|
}
|
||
|
}
|
||
|
if (!record.v4_address && !record.v6_address) {
|
||
|
return {Error::Code::kParameterInvalid,
|
||
|
"No IPv4 nor IPv6 address in record."};
|
||
|
}
|
||
|
record.port = endpoint.port();
|
||
|
if (record.port == 0) {
|
||
|
return {Error::Code::kParameterInvalid, "Invalid TCP port in record."};
|
||
|
}
|
||
|
|
||
|
// 128-bit integer in hexadecimal format.
|
||
|
record.unique_id = GetStringFromRecord(endpoint.txt(), kUniqueIdKey);
|
||
|
if (record.unique_id.empty()) {
|
||
|
return {Error::Code::kParameterInvalid,
|
||
|
"Missing device unique ID in record."};
|
||
|
}
|
||
|
|
||
|
// Cast protocol version supported. Begins at 2 and is incremented by 1 with
|
||
|
// each version.
|
||
|
std::string a_decimal_number =
|
||
|
GetStringFromRecord(endpoint.txt(), kVersionKey);
|
||
|
if (a_decimal_number.empty()) {
|
||
|
return {Error::Code::kParameterInvalid,
|
||
|
"Missing Cast protocol version in record."};
|
||
|
}
|
||
|
constexpr int kMinVersion = 2; // According to spec.
|
||
|
constexpr int kMaxVersion = 99; // Implied by spec (field is max of 2 bytes).
|
||
|
int version;
|
||
|
if (!absl::SimpleAtoi(a_decimal_number, &version) || version < kMinVersion ||
|
||
|
version > kMaxVersion) {
|
||
|
return {Error::Code::kParameterInvalid,
|
||
|
"Invalid Cast protocol version in record."};
|
||
|
}
|
||
|
record.protocol_version = static_cast<uint8_t>(version);
|
||
|
|
||
|
// A bitset of device capabilities.
|
||
|
a_decimal_number = GetStringFromRecord(endpoint.txt(), kCapabilitiesKey);
|
||
|
if (a_decimal_number.empty()) {
|
||
|
return {Error::Code::kParameterInvalid,
|
||
|
"Missing device capabilities in record."};
|
||
|
}
|
||
|
if (!absl::SimpleAtoi(a_decimal_number, &record.capabilities)) {
|
||
|
return {Error::Code::kParameterInvalid,
|
||
|
"Invalid device capabilities field in record."};
|
||
|
}
|
||
|
|
||
|
// Receiver status flag.
|
||
|
a_decimal_number = GetStringFromRecord(endpoint.txt(), kStatusKey);
|
||
|
if (a_decimal_number == "0") {
|
||
|
record.status = ReceiverStatus::kIdle;
|
||
|
} else if (a_decimal_number == "1") {
|
||
|
record.status = ReceiverStatus::kBusy;
|
||
|
} else {
|
||
|
return {Error::Code::kParameterInvalid,
|
||
|
"Missing/Invalid receiver status flag in record."};
|
||
|
}
|
||
|
|
||
|
// [Optional] Receiver model name.
|
||
|
record.model_name = GetStringFromRecord(endpoint.txt(), kModelNameKey);
|
||
|
|
||
|
// The friendly name of the device.
|
||
|
record.friendly_name = GetStringFromRecord(endpoint.txt(), kFriendlyNameKey);
|
||
|
if (record.friendly_name.empty()) {
|
||
|
return {Error::Code::kParameterInvalid,
|
||
|
"Missing device friendly name in record."};
|
||
|
}
|
||
|
|
||
|
return record;
|
||
|
}
|
||
|
|
||
|
} // namespace cast
|
||
|
} // namespace openscreen
|