]>
Commit | Line | Data |
---|---|---|
12471842 PL |
1 | /* |
2 | * This file is part of PowerDNS or dnsdist. | |
3 | * Copyright -- PowerDNS.COM B.V. and its contributors | |
4 | * | |
5 | * This program is free software; you can redistribute it and/or modify | |
6 | * it under the terms of version 2 of the GNU General Public License as | |
7 | * published by the Free Software Foundation. | |
8 | * | |
9 | * In addition, for the avoidance of any doubt, permission is granted to | |
10 | * link this program with OpenSSL and to (re)distribute the binaries | |
11 | * produced as the result of such linking. | |
12 | * | |
13 | * This program is distributed in the hope that it will be useful, | |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | * GNU General Public License for more details. | |
17 | * | |
18 | * You should have received a copy of the GNU General Public License | |
19 | * along with this program; if not, write to the Free Software | |
20 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
21 | */ | |
3c115e0f | 22 | #pragma once |
23 | #include <string> | |
97e5c6bc | 24 | #include <vector> |
ceee6652 | 25 | #include <set> |
97e5c6bc | 26 | #include <deque> |
ceee6652 | 27 | #include <strings.h> |
0fae5c1e | 28 | #include <stdexcept> |
9f618bcc AD |
29 | #include <sstream> |
30 | #include <iterator> | |
cc0e7aa9 | 31 | #include <unordered_set> |
bae1b0a2 | 32 | |
9f0fd264 KM |
33 | #include <boost/version.hpp> |
34 | ||
c9186ca1 | 35 | // it crashes on OSX and doesn't compile on OpenBSD |
46104a7e | 36 | #if BOOST_VERSION >= 105300 && ! defined( __APPLE__ ) && ! defined(__OpenBSD__) |
ca12836d | 37 | #include <boost/container/string.hpp> |
bae1b0a2 | 38 | #endif |
ddb7e6c6 | 39 | |
2b62292d TF |
40 | #include "ascii.hh" |
41 | ||
5c57a75a | 42 | uint32_t burtleCI(const unsigned char* k, uint32_t length, uint32_t init); |
76cca09f | 43 | |
675fa24c | 44 | // #include "dns.hh" |
7abbc40f | 45 | // #include "logger.hh" |
3c115e0f | 46 | |
8ca15224 | 47 | //#include <ext/vstring.h> |
d8b778ff | 48 | |
3c115e0f | 49 | /* Quest in life: |
50 | accept escaped ascii presentations of DNS names and store them "natively" | |
51 | accept a DNS packet with an offset, and extract a DNS name from it | |
52 | build up DNSNames with prepend and append of 'raw' unescaped labels | |
53 | ||
54 | Be able to turn them into ASCII and "DNS name in a packet" again on request | |
55 | ||
56 | Provide some common operators for comparison, detection of being part of another domain | |
57 | ||
58 | NOTE: For now, everything MUST be . terminated, otherwise it is an error | |
59 | */ | |
60 | ||
3c115e0f | 61 | class DNSName |
62 | { | |
63 | public: | |
ae14c1f3 | 64 | DNSName() {} //!< Constructs an *empty* DNSName, NOT the root! |
8171ab83 | 65 | explicit DNSName(const char* p); //!< Constructs from a human formatted, escaped presentation |
bae1b0a2 | 66 | explicit DNSName(const std::string& str) : DNSName(str.c_str()) {}; //!< Constructs from a human formatted, escaped presentation |
af9f750c | 67 | DNSName(const char* p, int len, int offset, bool uncompress, uint16_t* qtype=nullptr, uint16_t* qclass=nullptr, unsigned int* consumed=nullptr, uint16_t minOffset=0); //!< Construct from a DNS Packet, taking the first question if offset=12. If supplied, consumed is set to the number of bytes consumed from the packet, which will not be equal to the wire length of the resulting name in case of compression. |
3c115e0f | 68 | |
69 | bool isPartOf(const DNSName& rhs) const; //!< Are we part of the rhs name? | |
c21484df | 70 | inline bool operator==(const DNSName& rhs) const; //!< DNS-native comparison (case insensitive) - empty compares to empty |
5fca2e23 PD |
71 | bool operator!=(const DNSName& other) const { return !(*this == other); } |
72 | ||
a61e8e59 | 73 | std::string toString(const std::string& separator=".", const bool trailing=true) const; //!< Our human-friendly, escaped, representation |
9ab84270 | 74 | std::string toLogString() const; //!< like plain toString, but returns (empty) on empty names |
a61e8e59 | 75 | std::string toStringNoDot() const { return toString(".", false); } |
3b295666 | 76 | std::string toStringRootDot() const { if(isRoot()) return "."; else return toString(".", false); } |
3c115e0f | 77 | std::string toDNSString() const; //!< Our representation in DNS native format |
0ba4e1ee | 78 | std::string toDNSStringLC() const; //!< Our representation in DNS native format, lower cased |
3c115e0f | 79 | void appendRawLabel(const std::string& str); //!< Append this unescaped label |
e14febcf | 80 | void appendRawLabel(const char* start, unsigned int length); //!< Append this unescaped label |
3c115e0f | 81 | void prependRawLabel(const std::string& str); //!< Prepend this unescaped label |
97e5c6bc | 82 | std::vector<std::string> getRawLabels() const; //!< Individual raw unescaped labels |
39c9bef5 | 83 | std::string getRawLabel(unsigned int pos) const; //!< Get the specified raw unescaped label |
41e01592 | 84 | DNSName getLastLabel() const; //!< Get the DNSName of the last label |
3c115e0f | 85 | bool chopOff(); //!< Turn www.powerdns.com. into powerdns.com., returns false for . |
a61e8e59 | 86 | DNSName makeRelative(const DNSName& zone) const; |
d42896fb | 87 | DNSName makeLowerCase() const |
88 | { | |
8b63f61f KM |
89 | DNSName ret(*this); |
90 | ret.makeUsLowerCase(); | |
91 | return ret; | |
92 | } | |
93 | void makeUsLowerCase() | |
94 | { | |
95 | for(auto & c : d_storage) { | |
2b62292d | 96 | c=dns_tolower(c); |
d42896fb | 97 | } |
d42896fb | 98 | } |
8ca15224 | 99 | void makeUsRelative(const DNSName& zone); |
9b061cf5 | 100 | DNSName getCommonLabels(const DNSName& other) const; //!< Return the list of common labels from the top, for example 'c.d' for 'a.b.c.d' and 'x.y.c.d' |
a61e8e59 | 101 | DNSName labelReverse() const; |
8aa5a28c | 102 | bool isWildcard() const; |
32cd4eb1 | 103 | bool isHostname() const; |
97e5c6bc | 104 | unsigned int countLabels() const; |
3155c04a | 105 | size_t wirelength() const; //!< Number of total bytes in the name |
ae14c1f3 | 106 | bool empty() const { return d_storage.empty(); } |
107 | bool isRoot() const { return d_storage.size()==1 && d_storage[0]==0; } | |
108 | void clear() { d_storage.clear(); } | |
c9262563 | 109 | void trimToLabels(unsigned int); |
7691e7df | 110 | size_t hash(size_t init=0) const |
76cca09f | 111 | { |
7691e7df | 112 | return burtleCI((const unsigned char*)d_storage.c_str(), d_storage.size(), init); |
76cca09f | 113 | } |
6dcedea6 | 114 | DNSName& operator+=(const DNSName& rhs) |
115 | { | |
1106afd9 | 116 | if(d_storage.size() + rhs.d_storage.size() > 256) // one extra byte for the second root label |
ac7921b0 | 117 | throw std::range_error("name too long"); |
ae14c1f3 | 118 | if(rhs.empty()) |
119 | return *this; | |
120 | ||
121 | if(d_storage.empty()) | |
122 | d_storage+=rhs.d_storage; | |
123 | else | |
124 | d_storage.replace(d_storage.length()-1, rhs.d_storage.length(), rhs.d_storage); | |
ac7921b0 | 125 | |
6dcedea6 | 126 | return *this; |
127 | } | |
c9262563 | 128 | |
6d8bc3c6 | 129 | bool operator<(const DNSName& rhs) const // this delivers _some_ kind of ordering, but not one useful in a DNS context. Really fast though. |
c9262563 | 130 | { |
97e5c6bc | 131 | return std::lexicographical_compare(d_storage.rbegin(), d_storage.rend(), |
132 | rhs.d_storage.rbegin(), rhs.d_storage.rend(), | |
1352c9b6 | 133 | [](const unsigned char& a, const unsigned char& b) { |
2b62292d | 134 | return dns_tolower(a) < dns_tolower(b); |
6d8bc3c6 | 135 | }); // note that this is case insensitive, including on the label lengths |
c9262563 | 136 | } |
137 | ||
ddb7e6c6 | 138 | inline bool canonCompare(const DNSName& rhs) const; |
f3da403d | 139 | bool slowCanonCompare(const DNSName& rhs) const; |
bae1b0a2 | 140 | |
46104a7e | 141 | #if BOOST_VERSION >= 105300 && ! defined( __APPLE__ ) && ! defined(__OpenBSD__) |
ca12836d | 142 | typedef boost::container::string string_t; |
9f0fd264 KM |
143 | #else |
144 | typedef std::string string_t; | |
bae1b0a2 | 145 | #endif |
c21484df | 146 | const string_t& getStorage() const { |
147 | return d_storage; | |
148 | } | |
bae1b0a2 | 149 | private: |
d8b778ff | 150 | string_t d_storage; |
520eb5a0 | 151 | |
83fc9d8a | 152 | void packetParser(const char* p, int len, int offset, bool uncompress, uint16_t* qtype, uint16_t* qclass, unsigned int* consumed, int depth, uint16_t minOffset); |
2430e53c | 153 | static void appendEscapedLabel(std::string& appendTo, const char* orig, size_t len); |
3c115e0f | 154 | static std::string unescapeLabel(const std::string& orig); |
155 | }; | |
ceee6652 | 156 | |
5e1031cd | 157 | size_t hash_value(DNSName const& d); |
6d8bc3c6 | 158 | |
ddb7e6c6 | 159 | |
160 | inline bool DNSName::canonCompare(const DNSName& rhs) const | |
161 | { | |
162 | // 01234567890abcd | |
163 | // us: 1a3www4ds9a2nl | |
164 | // rhs: 3www6online3com | |
165 | // to compare, we start at the back, is nl < com? no -> done | |
166 | // | |
167 | // 0,2,6,a | |
168 | // 0,4,a | |
169 | ||
170 | uint8_t ourpos[64], rhspos[64]; | |
171 | uint8_t ourcount=0, rhscount=0; | |
172 | //cout<<"Asked to compare "<<toString()<<" to "<<rhs.toString()<<endl; | |
ae14c1f3 | 173 | for(const unsigned char* p = (const unsigned char*)d_storage.c_str(); p < (const unsigned char*)d_storage.c_str() + d_storage.size() && *p && ourcount < sizeof(ourpos); p+=*p+1) |
f3da403d | 174 | ourpos[ourcount++]=(p-(const unsigned char*)d_storage.c_str()); |
ae14c1f3 | 175 | for(const unsigned char* p = (const unsigned char*)rhs.d_storage.c_str(); p < (const unsigned char*)rhs.d_storage.c_str() + rhs.d_storage.size() && *p && rhscount < sizeof(rhspos); p+=*p+1) |
f3da403d | 176 | rhspos[rhscount++]=(p-(const unsigned char*)rhs.d_storage.c_str()); |
ddb7e6c6 | 177 | |
178 | if(ourcount == sizeof(ourpos) || rhscount==sizeof(rhspos)) { | |
179 | return slowCanonCompare(rhs); | |
180 | } | |
181 | ||
182 | for(;;) { | |
183 | if(ourcount == 0 && rhscount != 0) | |
184 | return true; | |
5708a729 | 185 | if(rhscount == 0) |
ddb7e6c6 | 186 | return false; |
187 | ourcount--; | |
188 | rhscount--; | |
189 | ||
ddb7e6c6 | 190 | bool res=std::lexicographical_compare( |
191 | d_storage.c_str() + ourpos[ourcount] + 1, | |
192 | d_storage.c_str() + ourpos[ourcount] + 1 + *(d_storage.c_str() + ourpos[ourcount]), | |
193 | rhs.d_storage.c_str() + rhspos[rhscount] + 1, | |
194 | rhs.d_storage.c_str() + rhspos[rhscount] + 1 + *(rhs.d_storage.c_str() + rhspos[rhscount]), | |
1352c9b6 | 195 | [](const unsigned char& a, const unsigned char& b) { |
2b62292d | 196 | return dns_tolower(a) < dns_tolower(b); |
ddb7e6c6 | 197 | }); |
198 | ||
199 | // cout<<"Forward: "<<res<<endl; | |
200 | if(res) | |
201 | return true; | |
202 | ||
203 | res=std::lexicographical_compare( rhs.d_storage.c_str() + rhspos[rhscount] + 1, | |
204 | rhs.d_storage.c_str() + rhspos[rhscount] + 1 + *(rhs.d_storage.c_str() + rhspos[rhscount]), | |
205 | d_storage.c_str() + ourpos[ourcount] + 1, | |
206 | d_storage.c_str() + ourpos[ourcount] + 1 + *(d_storage.c_str() + ourpos[ourcount]), | |
1352c9b6 | 207 | [](const unsigned char& a, const unsigned char& b) { |
2b62292d | 208 | return dns_tolower(a) < dns_tolower(b); |
ddb7e6c6 | 209 | }); |
210 | // cout<<"Reverse: "<<res<<endl; | |
211 | if(res) | |
212 | return false; | |
213 | } | |
214 | return false; | |
215 | } | |
216 | ||
217 | ||
6d8bc3c6 | 218 | struct CanonDNSNameCompare: public std::binary_function<DNSName, DNSName, bool> |
219 | { | |
220 | bool operator()(const DNSName&a, const DNSName& b) const | |
221 | { | |
222 | return a.canonCompare(b); | |
223 | } | |
224 | }; | |
225 | ||
6dcedea6 | 226 | inline DNSName operator+(const DNSName& lhs, const DNSName& rhs) |
227 | { | |
228 | DNSName ret=lhs; | |
229 | ret += rhs; | |
230 | return ret; | |
231 | } | |
ceee6652 | 232 | |
40ea5b5b RG |
233 | template<typename T> |
234 | struct SuffixMatchTree | |
235 | { | |
af619119 | 236 | SuffixMatchTree(const std::string& name="", bool endNode_=false) : d_name(name), endNode(endNode_) |
40ea5b5b RG |
237 | {} |
238 | ||
6c58e309 | 239 | SuffixMatchTree(const SuffixMatchTree& rhs): d_name(rhs.d_name), children(rhs.children), endNode(rhs.endNode) |
40ea5b5b | 240 | { |
6c58e309 RG |
241 | if (endNode) { |
242 | d_value = rhs.d_value; | |
243 | } | |
40ea5b5b | 244 | } |
f238ce64 OM |
245 | SuffixMatchTree & operator=(const SuffixMatchTree &rhs) |
246 | { | |
247 | d_name = rhs.d_name; | |
248 | children = rhs.children; | |
249 | endNode = rhs.endNode; | |
250 | if (endNode) { | |
251 | d_value = rhs.d_value; | |
252 | } | |
253 | return *this; | |
254 | } | |
255 | ||
af619119 | 256 | std::string d_name; |
40ea5b5b RG |
257 | mutable std::set<SuffixMatchTree> children; |
258 | mutable bool endNode; | |
259 | mutable T d_value; | |
260 | bool operator<(const SuffixMatchTree& rhs) const | |
261 | { | |
af619119 | 262 | return strcasecmp(d_name.c_str(), rhs.d_name.c_str()) < 0; |
40ea5b5b RG |
263 | } |
264 | typedef SuffixMatchTree value_type; | |
265 | ||
266 | template<typename V> | |
267 | void visit(const V& v) const { | |
268 | for(const auto& c : children) | |
269 | c.visit(v); | |
270 | if(endNode) | |
271 | v(*this); | |
272 | } | |
273 | ||
274 | void add(const DNSName& name, const T& t) | |
275 | { | |
276 | add(name.getRawLabels(), t); | |
277 | } | |
278 | ||
279 | void add(std::vector<std::string> labels, const T& value) const | |
280 | { | |
281 | if(labels.empty()) { // this allows insertion of the root | |
282 | endNode=true; | |
283 | d_value=value; | |
284 | } | |
285 | else if(labels.size()==1) { | |
6c58e309 | 286 | auto res=children.emplace(*labels.begin(), true); |
1a083287 RG |
287 | if(!res.second) { |
288 | // we might already have had the node as an | |
289 | // intermediary one, but it's now an end node | |
290 | if(!res.first->endNode) { | |
291 | res.first->endNode = true; | |
292 | } | |
293 | } | |
294 | res.first->d_value = value; | |
40ea5b5b RG |
295 | } |
296 | else { | |
6c58e309 | 297 | auto res=children.emplace(*labels.rbegin(), false); |
40ea5b5b RG |
298 | labels.pop_back(); |
299 | res.first->add(labels, value); | |
300 | } | |
301 | } | |
302 | ||
a364fbfb PL |
303 | void remove(const DNSName &name) const |
304 | { | |
305 | remove(name.getRawLabels()); | |
306 | } | |
307 | ||
308 | /* Removes the node at `labels`, also make sure that no empty | |
309 | * children will be left behind in memory | |
310 | */ | |
311 | void remove(std::vector<std::string> labels) const | |
312 | { | |
dbeebfee RG |
313 | if (labels.empty()) { // this allows removal of the root |
314 | endNode = false; | |
315 | return; | |
316 | } | |
317 | ||
a364fbfb PL |
318 | SuffixMatchTree smt(*labels.rbegin()); |
319 | auto child = children.find(smt); | |
320 | if (child == children.end()) { | |
321 | // No subnode found, we're done | |
322 | return; | |
323 | } | |
324 | ||
325 | // We have found a child | |
326 | labels.pop_back(); | |
327 | if (labels.empty()) { | |
328 | // The child is no longer an endnode | |
329 | child->endNode = false; | |
67689ec7 | 330 | |
a364fbfb PL |
331 | // If the child has no further children, just remove it from the set. |
332 | if (child->children.empty()) { | |
333 | children.erase(child); | |
334 | } | |
335 | return; | |
336 | } | |
337 | ||
338 | // We are not at the end, let the child figure out what to do | |
339 | child->remove(labels); | |
340 | } | |
341 | ||
40ea5b5b RG |
342 | T* lookup(const DNSName& name) const |
343 | { | |
344 | if(children.empty()) { // speed up empty set | |
345 | if(endNode) | |
346 | return &d_value; | |
99517c1b | 347 | return nullptr; |
40ea5b5b | 348 | } |
99517c1b | 349 | |
40ea5b5b RG |
350 | return lookup(name.getRawLabels()); |
351 | } | |
352 | ||
353 | T* lookup(std::vector<std::string> labels) const | |
354 | { | |
355 | if(labels.empty()) { // optimization | |
356 | if(endNode) | |
357 | return &d_value; | |
99517c1b | 358 | return nullptr; |
40ea5b5b RG |
359 | } |
360 | ||
361 | SuffixMatchTree smn(*labels.rbegin()); | |
362 | auto child = children.find(smn); | |
363 | if(child == children.end()) { | |
364 | if(endNode) | |
365 | return &d_value; | |
99517c1b | 366 | return nullptr; |
40ea5b5b RG |
367 | } |
368 | labels.pop_back(); | |
99517c1b RG |
369 | auto result = child->lookup(labels); |
370 | if (result) { | |
371 | return result; | |
372 | } | |
373 | return endNode ? &d_value : nullptr; | |
40ea5b5b RG |
374 | } |
375 | ||
606400e7 PL |
376 | // Returns all end-nodes, fully qualified (not as separate labels) |
377 | std::vector<DNSName> getNodes() const { | |
378 | std::vector<DNSName> ret; | |
379 | if (endNode) { | |
380 | ret.push_back(DNSName(d_name)); | |
381 | } | |
382 | for (const auto& child : children) { | |
383 | auto nodes = child.getNodes(); | |
384 | for (const auto &node: nodes) { | |
385 | ret.push_back(node + DNSName(d_name)); | |
386 | } | |
387 | } | |
388 | return ret; | |
389 | } | |
40ea5b5b RG |
390 | }; |
391 | ||
0eaa51e5 RG |
392 | /* Quest in life: serve as a rapid block list. If you add a DNSName to a root SuffixMatchNode, |
393 | anything part of that domain will return 'true' in check */ | |
394 | struct SuffixMatchNode | |
395 | { | |
15d0cc71 PL |
396 | public: |
397 | SuffixMatchNode() | |
398 | {} | |
399 | SuffixMatchTree<bool> d_tree; | |
400 | ||
401 | void add(const DNSName& dnsname) | |
402 | { | |
403 | d_tree.add(dnsname, true); | |
404 | d_nodes.insert(dnsname); | |
15d0cc71 | 405 | } |
0eaa51e5 | 406 | |
41ff82c9 PL |
407 | void add(const std::string& name) |
408 | { | |
409 | add(DNSName(name)); | |
410 | } | |
411 | ||
15d0cc71 PL |
412 | void add(std::vector<std::string> labels) |
413 | { | |
414 | d_tree.add(labels, true); | |
415 | DNSName tmp; | |
416 | while (!labels.empty()) { | |
417 | tmp.appendRawLabel(labels.back()); | |
418 | labels.pop_back(); // This is safe because we have a copy of labels | |
419 | } | |
420 | d_nodes.insert(tmp); | |
15d0cc71 | 421 | } |
0eaa51e5 | 422 | |
15d0cc71 PL |
423 | void remove(const DNSName& name) |
424 | { | |
425 | d_tree.remove(name); | |
426 | d_nodes.erase(name); | |
15d0cc71 | 427 | } |
0eaa51e5 | 428 | |
15d0cc71 PL |
429 | void remove(std::vector<std::string> labels) |
430 | { | |
431 | d_tree.remove(labels); | |
432 | DNSName tmp; | |
433 | while (!labels.empty()) { | |
434 | tmp.appendRawLabel(labels.back()); | |
435 | labels.pop_back(); // This is safe because we have a copy of labels | |
436 | } | |
437 | d_nodes.erase(tmp); | |
15d0cc71 | 438 | } |
a364fbfb | 439 | |
15d0cc71 PL |
440 | bool check(const DNSName& dnsname) const |
441 | { | |
442 | return d_tree.lookup(dnsname) != nullptr; | |
443 | } | |
a364fbfb | 444 | |
15d0cc71 PL |
445 | std::string toString() const |
446 | { | |
3f5115d1 | 447 | std::string ret; |
15d0cc71 PL |
448 | bool first = true; |
449 | for (const auto& n : d_nodes) { | |
450 | if (!first) { | |
3f5115d1 | 451 | ret += ", "; |
15d0cc71 PL |
452 | } |
453 | first = false; | |
3f5115d1 | 454 | ret += n.toString(); |
606400e7 | 455 | } |
3f5115d1 | 456 | return ret; |
606400e7 | 457 | } |
3f5115d1 PL |
458 | |
459 | private: | |
3f5115d1 | 460 | mutable std::set<DNSName> d_nodes; // Only used for string generation |
0eaa51e5 RG |
461 | }; |
462 | ||
8171ab83 | 463 | std::ostream & operator<<(std::ostream &os, const DNSName& d); |
6106128d | 464 | namespace std { |
465 | template <> | |
466 | struct hash<DNSName> { | |
467 | size_t operator () (const DNSName& dn) const { return dn.hash(0); } | |
468 | }; | |
469 | } | |
bae1b0a2 | 470 | |
471 | DNSName::string_t segmentDNSNameRaw(const char* input); // from ragel | |
c21484df | 472 | bool DNSName::operator==(const DNSName& rhs) const |
473 | { | |
474 | if(rhs.empty() != empty() || rhs.d_storage.size() != d_storage.size()) | |
475 | return false; | |
476 | ||
477 | auto us = d_storage.cbegin(); | |
478 | auto p = rhs.d_storage.cbegin(); | |
479 | for(; us != d_storage.cend() && p != rhs.d_storage.cend(); ++us, ++p) { | |
2b62292d | 480 | if(dns_tolower(*p) != dns_tolower(*us)) |
c21484df | 481 | return false; |
482 | } | |
483 | return true; | |
484 | } | |
485 | ||
486 | extern const DNSName g_rootdnsname, g_wildcarddnsname; | |
9f618bcc | 487 | |
cc0e7aa9 | 488 | struct DNSNameSet: public std::unordered_set<DNSName> { |
9f618bcc AD |
489 | std::string toString() const { |
490 | std::ostringstream oss; | |
491 | std::copy(begin(), end(), std::ostream_iterator<DNSName>(oss, "\n")); | |
492 | return oss.str(); | |
493 | } | |
494 | }; |