blob: 732688f8b6d9cc64150baa65f2230ae38e001406 [file] [log] [blame]
// 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