face-uri.cpp
Go to the documentation of this file.
1 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
2 /*
3  * Copyright (c) 2013-2024 Regents of the University of California,
4  * Arizona Board of Regents,
5  * Colorado State University,
6  * University Pierre & Marie Curie, Sorbonne University,
7  * Washington University in St. Louis,
8  * Beijing Institute of Technology,
9  * The University of Memphis.
10  *
11  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
12  *
13  * ndn-cxx library is free software: you can redistribute it and/or modify it under the
14  * terms of the GNU Lesser General Public License as published by the Free Software
15  * Foundation, either version 3 of the License, or (at your option) any later version.
16  *
17  * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
18  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
19  * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
20  *
21  * You should have received copies of the GNU General Public License and GNU Lesser
22  * General Public License along with ndn-cxx, e.g., in COPYING.md file. If not, see
23  * <http://www.gnu.org/licenses/>.
24  *
25  * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
26  */
27 
28 #include "ndn-cxx/net/face-uri.hpp"
29 
31 #include "ndn-cxx/net/dns.hpp"
33 
34 #include <boost/algorithm/string/classification.hpp>
35 #include <boost/algorithm/string/split.hpp>
36 #include <boost/lexical_cast.hpp>
37 #include <boost/mp11/algorithm.hpp>
38 #include <boost/mp11/list.hpp>
39 #include <boost/mp11/set.hpp>
40 
41 #include <regex>
42 #include <set>
43 #include <sstream>
44 
45 namespace ndn {
46 
47 FaceUri::FaceUri() = default;
48 
49 FaceUri::FaceUri(const std::string& uri)
50 {
51  if (!parse(uri)) {
52  NDN_THROW(Error("Malformed URI: "s + uri));
53  }
54 }
55 
56 FaceUri::FaceUri(const char* uri)
57 {
58  if (!parse(uri)) {
59  NDN_THROW(Error("Malformed URI: "s + uri));
60  }
61 }
62 
63 bool
64 FaceUri::parse(std::string_view uri)
65 {
66  *this = {};
67 
68  static const std::regex protocolExp("(\\w+\\d?(\\+\\w+)?)://([^/]*)(\\/[^?]*)?");
69  std::match_results<std::string_view::const_iterator> protocolMatch;
70  if (!std::regex_match(uri.begin(), uri.end(), protocolMatch, protocolExp)) {
71  return false;
72  }
73  m_scheme = protocolMatch[1];
74  std::string authority = protocolMatch[3];
75  m_path = protocolMatch[4];
76 
77  // pattern for IPv6 link local address enclosed in [ ], with optional port number
78  static const std::regex v6LinkLocalExp("^\\[([a-fA-F0-9:]+)%([^\\s/:]+)\\](?:\\:(\\d+))?$");
79  // pattern for IPv6 address enclosed in [ ], with optional port number
80  static const std::regex v6Exp("^\\[([a-fA-F0-9:]+)\\](?:\\:(\\d+))?$");
81  // pattern for Ethernet address in standard hex-digits-and-colons notation
82  static const std::regex etherExp("^\\[((?:[a-fA-F0-9]{1,2}\\:){5}(?:[a-fA-F0-9]{1,2}))\\]$");
83  // pattern for IPv4-mapped IPv6 address, with optional port number
84  static const std::regex v4MappedV6Exp("^\\[::ffff:(\\d+(?:\\.\\d+){3})\\](?:\\:(\\d+))?$");
85  // pattern for IPv4/hostname/fd/ifname, with optional port number
86  static const std::regex v4HostExp("^([^:]+)(?:\\:(\\d+))?$");
87 
88  if (authority.empty()) {
89  // UNIX, internal
90  }
91  else {
92  std::smatch match;
93  if (std::regex_match(authority, match, v6LinkLocalExp)) {
94  m_isV6 = true;
95  m_host = match[1].str() + "%" + match[2].str();
96  m_port = match[3];
97  return true;
98  }
99 
100  m_isV6 = std::regex_match(authority, match, v6Exp);
101  if (m_isV6 ||
102  std::regex_match(authority, match, etherExp) ||
103  std::regex_match(authority, match, v4MappedV6Exp) ||
104  std::regex_match(authority, match, v4HostExp)) {
105  m_host = match[1];
106  m_port = match[2];
107  }
108  else {
109  return false;
110  }
111  }
112 
113  return true;
114 }
115 
116 FaceUri::FaceUri(const boost::asio::ip::udp::endpoint& endpoint)
117 {
118  m_isV6 = endpoint.address().is_v6();
119  m_scheme = m_isV6 ? "udp6" : "udp4";
120  m_host = endpoint.address().to_string();
121  m_port = std::to_string(endpoint.port());
122 }
123 
124 FaceUri::FaceUri(const boost::asio::ip::tcp::endpoint& endpoint)
125 {
126  m_isV6 = endpoint.address().is_v6();
127  m_scheme = m_isV6 ? "tcp6" : "tcp4";
128  m_host = endpoint.address().to_string();
129  m_port = std::to_string(endpoint.port());
130 }
131 
132 FaceUri::FaceUri(const boost::asio::ip::tcp::endpoint& endpoint, std::string_view scheme)
133 {
134  m_isV6 = endpoint.address().is_v6();
135  m_scheme = scheme;
136  m_host = endpoint.address().to_string();
137  m_port = std::to_string(endpoint.port());
138 }
139 
140 #ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
141 FaceUri::FaceUri(const boost::asio::local::stream_protocol::endpoint& endpoint)
142  : m_scheme("unix")
143  , m_path(endpoint.path())
144 {
145 }
146 #endif // BOOST_ASIO_HAS_LOCAL_SOCKETS
147 
149  : m_scheme("ether")
150  , m_host(address.toString())
151  , m_isV6(true)
152 {
153 }
154 
155 FaceUri
157 {
158  FaceUri uri;
159  uri.m_scheme = "fd";
160  uri.m_host = std::to_string(fd);
161  return uri;
162 }
163 
164 FaceUri
165 FaceUri::fromDev(std::string_view ifname)
166 {
167  FaceUri uri;
168  uri.m_scheme = "dev";
169  uri.m_host = ifname;
170  return uri;
171 }
172 
173 FaceUri
174 FaceUri::fromUdpDev(const boost::asio::ip::udp::endpoint& endpoint, std::string_view ifname)
175 {
176  FaceUri uri;
177  uri.m_scheme = endpoint.address().is_v6() ? "udp6+dev" : "udp4+dev";
178  uri.m_host = ifname;
179  uri.m_port = std::to_string(endpoint.port());
180  return uri;
181 }
182 
183 std::string
185 {
186  std::ostringstream os;
187  print(os);
188  return os.str();
189 }
190 
191 void
192 FaceUri::print(std::ostream& os) const
193 {
194  os << m_scheme << "://";
195  if (m_isV6) {
196  os << "[" << m_host << "]";
197  }
198  else {
199  os << m_host;
200  }
201  if (!m_port.empty()) {
202  os << ":" << m_port;
203  }
204  os << m_path;
205 }
206 
207 
211 class CanonizeProvider : noncopyable
212 {
213 public:
214  virtual
215  ~CanonizeProvider() = default;
216 
217  virtual std::set<std::string>
218  getSchemes() const = 0;
219 
220  virtual bool
221  isCanonical(const FaceUri& faceUri) const = 0;
222 
223  virtual void
224  canonize(const FaceUri& faceUri,
225  const FaceUri::CanonizeSuccessCallback& onSuccess,
226  const FaceUri::CanonizeFailureCallback& onFailure,
227  boost::asio::io_context& io, time::nanoseconds timeout) const = 0;
228 };
229 
230 template<typename Protocol>
231 class IpHostCanonizeProvider : public CanonizeProvider
232 {
233 public:
234  std::set<std::string>
235  getSchemes() const override
236  {
237  return {m_baseScheme, m_v4Scheme, m_v6Scheme};
238  }
239 
240  bool
241  isCanonical(const FaceUri& faceUri) const override
242  {
243  if (faceUri.getPort().empty()) {
244  return false;
245  }
246  if (!faceUri.getPath().empty()) {
247  return false;
248  }
249 
250  boost::system::error_code ec;
251  auto addr = boost::asio::ip::make_address(unescapeHost(faceUri.getHost()), ec);
252  if (ec) {
253  return false;
254  }
255 
256  bool hasCorrectScheme = (faceUri.getScheme() == m_v4Scheme && addr.is_v4()) ||
257  (faceUri.getScheme() == m_v6Scheme && addr.is_v6());
258  if (!hasCorrectScheme) {
259  return false;
260  }
261 
262  auto checkAddressWithUri = [] (const boost::asio::ip::address& addr,
263  const FaceUri& faceUri) -> bool {
264  if (addr.is_v4() || !addr.to_v6().is_link_local()) {
265  return addr.to_string() == faceUri.getHost();
266  }
267 
268  std::vector<std::string> addrFields, faceUriFields;
269  std::string addrString = addr.to_string();
270  std::string faceUriString = faceUri.getHost();
271 
272  boost::algorithm::split(addrFields, addrString, boost::is_any_of("%"));
273  boost::algorithm::split(faceUriFields, faceUriString, boost::is_any_of("%"));
274  if (addrFields.size() != 2 || faceUriFields.size() != 2) {
275  return false;
276  }
277 
278  if (faceUriFields[1].size() > 2 && faceUriFields[1].compare(0, 2, "25") == 0) {
279  // %25... is accepted, but not a canonical form
280  return false;
281  }
282 
283  return addrFields[0] == faceUriFields[0] &&
284  addrFields[1] == faceUriFields[1];
285  };
286 
287  return checkAddressWithUri(addr, faceUri) && checkAddress(addr).first;
288  }
289 
290  void
291  canonize(const FaceUri& faceUri,
292  const FaceUri::CanonizeSuccessCallback& onSuccess,
293  const FaceUri::CanonizeFailureCallback& onFailure,
294  boost::asio::io_context& io, time::nanoseconds timeout) const override
295  {
296  if (this->isCanonical(faceUri)) {
297  onSuccess(faceUri);
298  return;
299  }
300 
301  // make a copy because caller may modify faceUri
302  auto uri = std::make_shared<FaceUri>(faceUri);
303  boost::system::error_code ec;
304  auto ipAddress = boost::asio::ip::make_address(unescapeHost(faceUri.getHost()), ec);
305  if (!ec) {
306  // No need to resolve IP address if host is already an IP
307  if ((faceUri.getScheme() == m_v4Scheme && !ipAddress.is_v4()) ||
308  (faceUri.getScheme() == m_v6Scheme && !ipAddress.is_v6())) {
309  return onFailure("IPv4/v6 mismatch");
310  }
311 
312  onDnsSuccess(uri, onSuccess, onFailure, ipAddress);
313  }
314  else {
315  dns::AddressSelector addressSelector;
316  if (faceUri.getScheme() == m_v4Scheme) {
317  addressSelector = dns::Ipv4Only();
318  }
319  else if (faceUri.getScheme() == m_v6Scheme) {
320  addressSelector = dns::Ipv6Only();
321  }
322  else {
323  BOOST_ASSERT(faceUri.getScheme() == m_baseScheme);
324  addressSelector = dns::AnyAddress();
325  }
326 
327  dns::asyncResolve(unescapeHost(faceUri.getHost()),
328  [=] (const auto& ipAddr) { onDnsSuccess(uri, onSuccess, onFailure, ipAddr); },
329  [=] (const auto& reason) { onDnsFailure(uri, onFailure, reason); },
330  io, addressSelector, timeout);
331  }
332  }
333 
334 protected:
335  explicit
336  IpHostCanonizeProvider(const std::string& baseScheme,
337  uint16_t defaultUnicastPort = 6363,
338  uint16_t defaultMulticastPort = 56363)
339  : m_baseScheme(baseScheme)
340  , m_v4Scheme(baseScheme + '4')
341  , m_v6Scheme(baseScheme + '6')
342  , m_defaultUnicastPort(defaultUnicastPort)
343  , m_defaultMulticastPort(defaultMulticastPort)
344  {
345  }
346 
347 private:
348  void
349  onDnsSuccess(const std::shared_ptr<FaceUri>& faceUri,
350  const FaceUri::CanonizeSuccessCallback& onSuccess,
351  const FaceUri::CanonizeFailureCallback& onFailure,
352  const boost::asio::ip::address& ipAddress) const
353  {
354  auto [isOk, reason] = this->checkAddress(ipAddress);
355  if (!isOk) {
356  return onFailure(reason);
357  }
358 
359  uint16_t port = 0;
360  if (faceUri->getPort().empty()) {
361  port = ipAddress.is_multicast() ? m_defaultMulticastPort : m_defaultUnicastPort;
362  }
363  else {
364  try {
365  port = boost::lexical_cast<uint16_t>(faceUri->getPort());
366  }
367  catch (const boost::bad_lexical_cast&) {
368  return onFailure("invalid port number '" + faceUri->getPort() + "'");
369  }
370  }
371 
372  FaceUri canonicalUri(typename Protocol::endpoint(ipAddress, port));
373  BOOST_ASSERT(canonicalUri.isCanonical());
374  onSuccess(canonicalUri);
375  }
376 
377  void
378  onDnsFailure(const std::shared_ptr<FaceUri>&,
379  const FaceUri::CanonizeFailureCallback& onFailure,
380  const std::string& reason) const
381  {
382  onFailure(reason);
383  }
384 
389  virtual std::pair<bool, std::string>
390  checkAddress(const boost::asio::ip::address&) const
391  {
392  return {true, ""};
393  }
394 
395  static std::string
396  unescapeHost(std::string host)
397  {
398  auto escapePos = host.find("%25");
399  if (escapePos != std::string::npos && escapePos < host.size() - 3) {
400  host = unescape(host);
401  }
402  return host;
403  }
404 
405 private:
406  std::string m_baseScheme;
407  std::string m_v4Scheme;
408  std::string m_v6Scheme;
409  uint16_t m_defaultUnicastPort;
410  uint16_t m_defaultMulticastPort;
411 };
412 
413 class UdpCanonizeProvider : public IpHostCanonizeProvider<boost::asio::ip::udp>
414 {
415 public:
416  UdpCanonizeProvider()
417  : IpHostCanonizeProvider("udp")
418  {
419  }
420 };
421 
422 class TcpCanonizeProvider : public IpHostCanonizeProvider<boost::asio::ip::tcp>
423 {
424 public:
425  TcpCanonizeProvider()
426  : IpHostCanonizeProvider("tcp")
427  {
428  }
429 
430 protected:
431  std::pair<bool, std::string>
432  checkAddress(const boost::asio::ip::address& ipAddress) const override
433  {
434  if (ipAddress.is_multicast()) {
435  return {false, "cannot use multicast address"};
436  }
437  return {true, ""};
438  }
439 };
440 
441 class EtherCanonizeProvider : public CanonizeProvider
442 {
443 public:
444  std::set<std::string>
445  getSchemes() const override
446  {
447  return {"ether"};
448  }
449 
450  bool
451  isCanonical(const FaceUri& faceUri) const override
452  {
453  if (!faceUri.getPort().empty()) {
454  return false;
455  }
456  if (!faceUri.getPath().empty()) {
457  return false;
458  }
459 
460  auto addr = ethernet::Address::fromString(faceUri.getHost());
461  return addr.toString() == faceUri.getHost();
462  }
463 
464  void
465  canonize(const FaceUri& faceUri,
466  const FaceUri::CanonizeSuccessCallback& onSuccess,
467  const FaceUri::CanonizeFailureCallback& onFailure,
468  boost::asio::io_context&, time::nanoseconds timeout) const override
469  {
470  auto addr = ethernet::Address::fromString(faceUri.getHost());
471  if (addr.isNull()) {
472  return onFailure("invalid ethernet address '" + faceUri.getHost() + "'");
473  }
474 
475  FaceUri canonicalUri(addr);
476  BOOST_ASSERT(canonicalUri.isCanonical());
477  onSuccess(canonicalUri);
478  }
479 };
480 
481 class DevCanonizeProvider : public CanonizeProvider
482 {
483 public:
484  std::set<std::string>
485  getSchemes() const override
486  {
487  return {"dev"};
488  }
489 
490  bool
491  isCanonical(const FaceUri& faceUri) const override
492  {
493  return !faceUri.getHost().empty() && faceUri.getPort().empty() && faceUri.getPath().empty();
494  }
495 
496  void
497  canonize(const FaceUri& faceUri,
498  const FaceUri::CanonizeSuccessCallback& onSuccess,
499  const FaceUri::CanonizeFailureCallback& onFailure,
500  boost::asio::io_context&, time::nanoseconds timeout) const override
501  {
502  if (faceUri.getHost().empty()) {
503  onFailure("network interface name is missing");
504  return;
505  }
506  if (!faceUri.getPort().empty()) {
507  onFailure("port number is not allowed");
508  return;
509  }
510  if (!faceUri.getPath().empty() && faceUri.getPath() != "/") { // permit trailing slash only
511  onFailure("path is not allowed");
512  return;
513  }
514 
515  FaceUri canonicalUri = FaceUri::fromDev(faceUri.getHost());
516  BOOST_ASSERT(canonicalUri.isCanonical());
517  onSuccess(canonicalUri);
518  }
519 };
520 
521 class UdpDevCanonizeProvider : public CanonizeProvider
522 {
523 public:
524  std::set<std::string>
525  getSchemes() const override
526  {
527  return {"udp4+dev", "udp6+dev"};
528  }
529 
530  bool
531  isCanonical(const FaceUri& faceUri) const override
532  {
533  if (faceUri.getPort().empty()) {
534  return false;
535  }
536  if (!faceUri.getPath().empty()) {
537  return false;
538  }
539  return true;
540  }
541 
542  void
543  canonize(const FaceUri& faceUri,
544  const FaceUri::CanonizeSuccessCallback& onSuccess,
545  const FaceUri::CanonizeFailureCallback& onFailure,
546  boost::asio::io_context&, time::nanoseconds timeout) const override
547  {
548  if (this->isCanonical(faceUri)) {
549  onSuccess(faceUri);
550  }
551  else {
552  onFailure("cannot canonize " + faceUri.toString());
553  }
554  }
555 };
556 
557 using CanonizeProviders = boost::mp11::mp_list<UdpCanonizeProvider,
558  TcpCanonizeProvider,
559  EtherCanonizeProvider,
560  DevCanonizeProvider,
561  UdpDevCanonizeProvider>;
562 static_assert(boost::mp11::mp_is_set<CanonizeProviders>());
563 
564 using CanonizeProviderTable = std::map<std::string, std::shared_ptr<CanonizeProvider>>;
565 
566 struct CanonizeProviderTableInitializer
567 {
568  template<typename CP>
569  void
570  operator()(boost::mp11::mp_identity<CP>)
571  {
572  std::shared_ptr<CanonizeProvider> cp = std::make_shared<CP>();
573  auto schemes = cp->getSchemes();
574  BOOST_ASSERT(!schemes.empty());
575 
576  for (const auto& scheme : schemes) {
577  BOOST_ASSERT(m_providerTable.count(scheme) == 0);
578  m_providerTable[scheme] = cp;
579  }
580  }
581 
582  CanonizeProviderTable& m_providerTable;
583 };
584 
585 static const CanonizeProvider*
586 getCanonizeProvider(const std::string& scheme)
587 {
588  static const auto providerTable = [] {
589  using namespace boost::mp11;
590  CanonizeProviderTable table;
591  mp_for_each<mp_transform<mp_identity, CanonizeProviders>>(CanonizeProviderTableInitializer{table});
592  return table;
593  }();
594 
595  auto it = providerTable.find(scheme);
596  return it == providerTable.end() ? nullptr : it->second.get();
597 }
598 
599 bool
600 FaceUri::canCanonize(const std::string& scheme)
601 {
602  return getCanonizeProvider(scheme) != nullptr;
603 }
604 
605 bool
607 {
608  const auto* cp = getCanonizeProvider(getScheme());
609  if (cp == nullptr) {
610  return false;
611  }
612 
613  return cp->isCanonical(*this);
614 }
615 
616 void
618  const CanonizeFailureCallback& onFailure,
619  boost::asio::io_context& io, time::nanoseconds timeout) const
620 {
621  const auto* cp = getCanonizeProvider(getScheme());
622  if (cp == nullptr) {
623  if (onFailure) {
624  onFailure("scheme not supported");
625  }
626  return;
627  }
628 
629  cp->canonize(*this,
630  onSuccess ? onSuccess : [] (auto&&) {},
631  onFailure ? onFailure : [] (auto&&) {},
632  io, timeout);
633 }
634 
635 } // namespace ndn
The underlying protocol and address used by a Face.
Definition: face-uri.hpp:47
std::string toString() const
Return string representation.
Definition: face-uri.cpp:184
void canonize(const CanonizeSuccessCallback &onSuccess, const CanonizeFailureCallback &onFailure, boost::asio::io_context &io, time::nanoseconds timeout) const
Asynchronously convert this FaceUri to canonical form.
Definition: face-uri.cpp:617
std::function< void(const std::string &reason)> CanonizeFailureCallback
Definition: face-uri.hpp:163
static FaceUri fromDev(std::string_view ifname)
Construct a dev FaceUri from a network device name.
Definition: face-uri.cpp:165
FaceUri()
Construct an empty FaceUri.
const std::string & getScheme() const
Get scheme (protocol)
Definition: face-uri.hpp:117
static bool canCanonize(const std::string &scheme)
Return whether a FaceUri of the specified scheme can be canonized.
Definition: face-uri.cpp:600
bool isCanonical() const
Determine whether this FaceUri is in canonical form.
Definition: face-uri.cpp:606
bool parse(std::string_view uri)
Exception-safe parsing.
Definition: face-uri.cpp:64
std::function< void(const FaceUri &)> CanonizeSuccessCallback
Definition: face-uri.hpp:162
static FaceUri fromUdpDev(const boost::asio::ip::udp::endpoint &endpoint, std::string_view ifname)
Construct a udp4 or udp6 NIC-associated FaceUri from endpoint and network device name.
Definition: face-uri.cpp:174
static FaceUri fromFd(int fd)
Construct an fd FaceUri from a file descriptor.
Definition: face-uri.cpp:156
Represents an Ethernet hardware address.
Definition: ethernet.hpp:52
static Address fromString(const std::string &str)
Creates an Address from a string containing an Ethernet address in hexadecimal notation,...
Definition: ethernet.cpp:90
Common includes and macros used throughout the library.
#define NDN_THROW(e)
Definition: exception.hpp:56
std::function< bool(const boost::asio::ip::address &)> AddressSelector
Definition: dns.hpp:35
void asyncResolve(const std::string &host, const SuccessCallback &onSuccess, const ErrorCallback &onError, boost::asio::io_context &ioCtx, const AddressSelector &addressSelector, time::nanoseconds timeout)
Asynchronously resolve host.
Definition: dns.cpp:116
std::string to_string(const errinfo_stacktrace &x)
Definition: exception.cpp:30
std::string toString(const system_clock::time_point &timePoint, const std::string &format, const std::locale &locale)
Convert time point to string with specified format.
Definition: time.cpp:167
::boost::chrono::nanoseconds nanoseconds
Definition: time.hpp:54
Definition: data.cpp:25
boost::mp11::mp_list< UdpCanonizeProvider, TcpCanonizeProvider, EtherCanonizeProvider, DevCanonizeProvider, UdpDevCanonizeProvider > CanonizeProviders
Definition: face-uri.cpp:561
std::map< std::string, std::shared_ptr< CanonizeProvider > > CanonizeProviderTable
Definition: face-uri.cpp:564
std::string unescape(std::string_view str)
Decode a percent-encoded string.