| // 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 |