// 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 #include #include #include #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 value = txt.GetValue(key); if (value.is_value()) { const std::vector& 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(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(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(info.protocol_version))) .ok() && txt.SetValue(kCapabilitiesKey, std::to_string(info.capabilities)).ok() && txt.SetValue(kStatusKey, std::to_string(static_cast(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 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(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