1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "discovery/dnssd/public/dns_sd_instance.h"
6 
7 #include <algorithm>
8 #include <cctype>
9 #include <utility>
10 #include <vector>
11 
12 #include "util/osp_logging.h"
13 
14 namespace openscreen {
15 namespace discovery {
16 namespace {
17 
18 // Maximum number of octets allowed in a single domain name label.
19 constexpr size_t kMaxLabelLength = 63;
20 
IsValidUtf8(const std::string & string)21 bool IsValidUtf8(const std::string& string) {
22   for (size_t i = 0; i < string.size(); i++) {
23     if (string[i] >> 5 == 0x06) {  // 110xxxxx 10xxxxxx
24       if (i + 1 >= string.size() || string[++i] >> 6 != 0x02) {
25         return false;
26       }
27     } else if (string[i] >> 4 == 0x0E) {  // 1110xxxx 10xxxxxx 10xxxxxx
28       if (i + 2 >= string.size() || string[++i] >> 6 != 0x02 ||
29           string[++i] >> 6 != 0x02) {
30         return false;
31       }
32     } else if (string[i] >> 3 == 0x1E) {  // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
33       if (i + 3 >= string.size() || string[++i] >> 6 != 0x02 ||
34           string[++i] >> 6 != 0x02 || string[++i] >> 6 != 0x02) {
35         return false;
36       }
37     } else if ((string[i] & 0x80) != 0x0) {  // 0xxxxxxx
38       return false;
39     }
40   }
41   return true;
42 }
43 
HasControlCharacters(const std::string & string)44 bool HasControlCharacters(const std::string& string) {
45   for (auto ch : string) {
46     if ((ch >= 0x0 && ch <= 0x1F /* Ascii control characters */) ||
47         ch == 0x7F /* DEL character */) {
48       return true;
49     }
50   }
51   return false;
52 }
53 
54 }  // namespace
55 
DnsSdInstance(std::string instance_id,std::string service_id,std::string domain_id,DnsSdTxtRecord txt,uint16_t port,std::vector<Subtype> subtypes)56 DnsSdInstance::DnsSdInstance(std::string instance_id,
57                              std::string service_id,
58                              std::string domain_id,
59                              DnsSdTxtRecord txt,
60                              uint16_t port,
61                              std::vector<Subtype> subtypes)
62     : instance_id_(std::move(instance_id)),
63       service_id_(std::move(service_id)),
64       domain_id_(std::move(domain_id)),
65       txt_(std::move(txt)),
66       port_(port),
67       subtypes_(std::move(subtypes)) {
68   OSP_DCHECK(IsInstanceValid(instance_id_))
69       << instance_id_ << " is an invalid instance id";
70   OSP_DCHECK(IsServiceValid(service_id_))
71       << service_id_ << " is an invalid service id";
72   OSP_DCHECK(IsDomainValid(domain_id_))
73       << domain_id_ << " is an invalid domain";
74   for (const Subtype& subtype : subtypes_) {
75     OSP_DCHECK(IsSubtypeValid(subtype)) << subtype << " is an invalid subtype";
76   }
77 
78   std::sort(subtypes_.begin(), subtypes_.end());
79 }
80 
81 DnsSdInstance::DnsSdInstance(const DnsSdInstance& other) = default;
82 
83 DnsSdInstance::DnsSdInstance(DnsSdInstance&& other) = default;
84 
85 DnsSdInstance::~DnsSdInstance() = default;
86 
87 DnsSdInstance& DnsSdInstance::operator=(const DnsSdInstance& rhs) = default;
88 
89 DnsSdInstance& DnsSdInstance::operator=(DnsSdInstance&& rhs) = default;
90 
91 // static
IsInstanceValid(const std::string & instance)92 bool IsInstanceValid(const std::string& instance) {
93   // According to RFC6763, Instance names must:
94   // - Be encoded in Net-Unicode (which required UTF-8 formatting).
95   // - NOT contain ASCII control characters
96   // - Be no longer than 63 octets.
97 
98   return instance.size() <= kMaxLabelLength &&
99          !HasControlCharacters(instance) && IsValidUtf8(instance);
100 }
101 
102 // static
IsServiceValid(const std::string & service)103 bool IsServiceValid(const std::string& service) {
104   // According to RFC6763, the service name "consists of a pair of DNS labels".
105   // "The first label of the pair is an underscore character followed by the
106   // Service Name" and "The second label is either '_tcp' [...] or '_udp'".
107   // According to RFC6335 Section 5.1, the Service Name section must:
108   //   Contain from 1 to 15 characters.
109   // - Only contain A-Z, a-Z, 0-9, and the hyphen character.
110   // - Contain at least one letter.
111   // - NOT begin or end with a hyphen.
112   // - NOT contain two adjacent hyphens.
113   if (service.size() > 21 || service.size() < 7) {  // Service name size + 6.
114     return false;
115   }
116 
117   const std::string protocol = service.substr(service.size() - 5);
118   if (protocol != "._udp" && protocol != "._tcp") {
119     return false;
120   }
121 
122   if (service[0] != '_' || service[1] == '-' ||
123       service[service.size() - 6] == '-') {
124     return false;
125   }
126 
127   bool last_char_hyphen = false;
128   bool seen_letter = false;
129   for (size_t i = 1; i < service.size() - 5; i++) {
130     if (service[i] == '-') {
131       if (last_char_hyphen) {
132         return false;
133       }
134       last_char_hyphen = true;
135     } else if (std::isalpha(service[i])) {
136       last_char_hyphen = false;
137       seen_letter = true;
138     } else if (std::isdigit(service[i])) {
139       last_char_hyphen = false;
140     } else {
141       return false;
142     }
143   }
144 
145   return seen_letter;
146 }
147 
148 // static
IsDomainValid(const std::string & domain)149 bool IsDomainValid(const std::string& domain) {
150   // As RFC6763 Section 4.1.3 provides no validation requirements for the domain
151   // section, the following validations are used:
152   // - All labels must be no longer than 63 characters
153   // - Total length must be no more than 256 characters
154   // - Must be encoded using valid UTF8
155   // - Must not include any ASCII control characters
156 
157   if (domain.size() > 255) {
158     return false;
159   }
160 
161   size_t label_start = 0;
162   for (size_t next_dot = domain.find('.'); next_dot != std::string::npos;
163        next_dot = domain.find('.', label_start)) {
164     if (next_dot - label_start > kMaxLabelLength) {
165       return false;
166     }
167     label_start = next_dot + 1;
168   }
169 
170   return !HasControlCharacters(domain) && IsValidUtf8(domain);
171 }
172 
173 // static
IsSubtypeValid(const DnsSdInstance::Subtype & subtype)174 bool IsSubtypeValid(const DnsSdInstance::Subtype& subtype) {
175   // As specified in RFC6763 section 9.1, all subtypes may be arbitrary bit
176   // data. Despite this, this implementation has chosen to limit valid subtypes
177   // to only UTF8 character strings. Therefore, the subtype must:
178   // - Be encoded in Net-Unicode (which required UTF-8 formatting).
179   // - NOT contain ASCII control characters
180   // - Be no longer than 63 octets.
181   // - Be of length one label.
182   return subtype.size() <= kMaxLabelLength &&
183          subtype.find('.') == std::string::npos &&
184          !HasControlCharacters(subtype) && IsValidUtf8(subtype);
185 }
186 
operator <(const DnsSdInstance & lhs,const DnsSdInstance & rhs)187 bool operator<(const DnsSdInstance& lhs, const DnsSdInstance& rhs) {
188   if (lhs.port_ != rhs.port_) {
189     return lhs.port_ < rhs.port_;
190   }
191 
192   int comp = lhs.instance_id_.compare(rhs.instance_id_);
193   if (comp != 0) {
194     return comp < 0;
195   }
196 
197   comp = lhs.service_id_.compare(rhs.service_id_);
198   if (comp != 0) {
199     return comp < 0;
200   }
201 
202   comp = lhs.domain_id_.compare(rhs.domain_id_);
203   if (comp != 0) {
204     return comp < 0;
205   }
206 
207   if (lhs.subtypes_.size() != rhs.subtypes_.size()) {
208     return lhs.subtypes_.size() < rhs.subtypes_.size();
209   }
210 
211   for (size_t i = 0; i < lhs.subtypes_.size(); i++) {
212     comp = lhs.subtypes_[i].compare(rhs.subtypes_[i]);
213     if (comp != 0) {
214       return comp < 0;
215     }
216   }
217 
218   return lhs.txt_ < rhs.txt_;
219 }
220 
221 }  // namespace discovery
222 }  // namespace openscreen
223