blob: da5905f080c869ecd858f0b76d0eb286558e83ab [file] [log] [blame]
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
* Copyright (c) 2013-2023 Regents of the University of California,
* Arizona Board of Regents,
* Colorado State University,
* University Pierre & Marie Curie, Sorbonne University,
* Washington University in St. Louis,
* Beijing Institute of Technology,
* The University of Memphis.
*
* This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
*
* ndn-cxx library is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received copies of the GNU General Public License and GNU Lesser
* General Public License along with ndn-cxx, e.g., in COPYING.md file. If not, see
* <http://www.gnu.org/licenses/>.
*
* See AUTHORS.md for complete list of ndn-cxx authors and contributors.
*/
#include "ndn-cxx/net/face-uri.hpp"
#include "ndn-cxx/net/dns.hpp"
#include "ndn-cxx/util/string-helper.hpp"
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/mpl/for_each.hpp>
#include <boost/mpl/vector.hpp>
#include <regex>
#include <set>
#include <sstream>
namespace ndn {
FaceUri::FaceUri() = default;
FaceUri::FaceUri(const std::string& uri)
{
if (!parse(uri)) {
NDN_THROW(Error("Malformed URI: "s + uri));
}
}
FaceUri::FaceUri(const char* uri)
{
if (!parse(uri)) {
NDN_THROW(Error("Malformed URI: "s + uri));
}
}
bool
FaceUri::parse(std::string_view uri)
{
*this = {};
static const std::regex protocolExp("(\\w+\\d?(\\+\\w+)?)://([^/]*)(\\/[^?]*)?");
std::match_results<std::string_view::const_iterator> protocolMatch;
if (!std::regex_match(uri.begin(), uri.end(), protocolMatch, protocolExp)) {
return false;
}
m_scheme = protocolMatch[1];
std::string authority = protocolMatch[3];
m_path = protocolMatch[4];
// pattern for IPv6 link local address enclosed in [ ], with optional port number
static const std::regex v6LinkLocalExp("^\\[([a-fA-F0-9:]+)%([^\\s/:]+)\\](?:\\:(\\d+))?$");
// pattern for IPv6 address enclosed in [ ], with optional port number
static const std::regex v6Exp("^\\[([a-fA-F0-9:]+)\\](?:\\:(\\d+))?$");
// pattern for Ethernet address in standard hex-digits-and-colons notation
static const std::regex etherExp("^\\[((?:[a-fA-F0-9]{1,2}\\:){5}(?:[a-fA-F0-9]{1,2}))\\]$");
// pattern for IPv4-mapped IPv6 address, with optional port number
static const std::regex v4MappedV6Exp("^\\[::ffff:(\\d+(?:\\.\\d+){3})\\](?:\\:(\\d+))?$");
// pattern for IPv4/hostname/fd/ifname, with optional port number
static const std::regex v4HostExp("^([^:]+)(?:\\:(\\d+))?$");
if (authority.empty()) {
// UNIX, internal
}
else {
std::smatch match;
if (std::regex_match(authority, match, v6LinkLocalExp)) {
m_isV6 = true;
m_host = match[1].str() + "%" + match[2].str();
m_port = match[3];
return true;
}
m_isV6 = std::regex_match(authority, match, v6Exp);
if (m_isV6 ||
std::regex_match(authority, match, etherExp) ||
std::regex_match(authority, match, v4MappedV6Exp) ||
std::regex_match(authority, match, v4HostExp)) {
m_host = match[1];
m_port = match[2];
}
else {
return false;
}
}
return true;
}
FaceUri::FaceUri(const boost::asio::ip::udp::endpoint& endpoint)
{
m_isV6 = endpoint.address().is_v6();
m_scheme = m_isV6 ? "udp6" : "udp4";
m_host = endpoint.address().to_string();
m_port = to_string(endpoint.port());
}
FaceUri::FaceUri(const boost::asio::ip::tcp::endpoint& endpoint)
{
m_isV6 = endpoint.address().is_v6();
m_scheme = m_isV6 ? "tcp6" : "tcp4";
m_host = endpoint.address().to_string();
m_port = to_string(endpoint.port());
}
FaceUri::FaceUri(const boost::asio::ip::tcp::endpoint& endpoint, std::string_view scheme)
{
m_isV6 = endpoint.address().is_v6();
m_scheme = scheme;
m_host = endpoint.address().to_string();
m_port = to_string(endpoint.port());
}
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
FaceUri::FaceUri(const boost::asio::local::stream_protocol::endpoint& endpoint)
: m_scheme("unix")
, m_path(endpoint.path())
{
}
#endif // BOOST_ASIO_HAS_LOCAL_SOCKETS
FaceUri::FaceUri(const ethernet::Address& address)
: m_scheme("ether")
, m_host(address.toString())
, m_isV6(true)
{
}
FaceUri
FaceUri::fromFd(int fd)
{
FaceUri uri;
uri.m_scheme = "fd";
uri.m_host = to_string(fd);
return uri;
}
FaceUri
FaceUri::fromDev(std::string_view ifname)
{
FaceUri uri;
uri.m_scheme = "dev";
uri.m_host = ifname;
return uri;
}
FaceUri
FaceUri::fromUdpDev(const boost::asio::ip::udp::endpoint& endpoint, std::string_view ifname)
{
FaceUri uri;
uri.m_scheme = endpoint.address().is_v6() ? "udp6+dev" : "udp4+dev";
uri.m_host = ifname;
uri.m_port = to_string(endpoint.port());
return uri;
}
std::string
FaceUri::toString() const
{
std::ostringstream os;
print(os);
return os.str();
}
void
FaceUri::print(std::ostream& os) const
{
os << m_scheme << "://";
if (m_isV6) {
os << "[" << m_host << "]";
}
else {
os << m_host;
}
if (!m_port.empty()) {
os << ":" << m_port;
}
os << m_path;
}
/** \brief A CanonizeProvider provides FaceUri canonization functionality for a group of schemes.
*/
class CanonizeProvider : noncopyable
{
public:
virtual
~CanonizeProvider() = default;
virtual std::set<std::string>
getSchemes() const = 0;
virtual bool
isCanonical(const FaceUri& faceUri) const = 0;
virtual void
canonize(const FaceUri& faceUri,
const FaceUri::CanonizeSuccessCallback& onSuccess,
const FaceUri::CanonizeFailureCallback& onFailure,
boost::asio::io_service& io, time::nanoseconds timeout) const = 0;
};
template<typename Protocol>
class IpHostCanonizeProvider : public CanonizeProvider
{
public:
std::set<std::string>
getSchemes() const override
{
return {m_baseScheme, m_v4Scheme, m_v6Scheme};
}
bool
isCanonical(const FaceUri& faceUri) const override
{
if (faceUri.getPort().empty()) {
return false;
}
if (!faceUri.getPath().empty()) {
return false;
}
boost::system::error_code ec;
auto addr = boost::asio::ip::address::from_string(unescapeHost(faceUri.getHost()), ec);
if (ec) {
return false;
}
bool hasCorrectScheme = (faceUri.getScheme() == m_v4Scheme && addr.is_v4()) ||
(faceUri.getScheme() == m_v6Scheme && addr.is_v6());
if (!hasCorrectScheme) {
return false;
}
auto checkAddressWithUri = [] (const boost::asio::ip::address& addr,
const FaceUri& faceUri) -> bool {
if (addr.is_v4() || !addr.to_v6().is_link_local()) {
return addr.to_string() == faceUri.getHost();
}
std::vector<std::string> addrFields, faceUriFields;
std::string addrString = addr.to_string();
std::string faceUriString = faceUri.getHost();
boost::algorithm::split(addrFields, addrString, boost::is_any_of("%"));
boost::algorithm::split(faceUriFields, faceUriString, boost::is_any_of("%"));
if (addrFields.size() != 2 || faceUriFields.size() != 2) {
return false;
}
if (faceUriFields[1].size() > 2 && faceUriFields[1].compare(0, 2, "25") == 0) {
// %25... is accepted, but not a canonical form
return false;
}
return addrFields[0] == faceUriFields[0] &&
addrFields[1] == faceUriFields[1];
};
return checkAddressWithUri(addr, faceUri) && checkAddress(addr).first;
}
void
canonize(const FaceUri& faceUri,
const FaceUri::CanonizeSuccessCallback& onSuccess,
const FaceUri::CanonizeFailureCallback& onFailure,
boost::asio::io_service& io, time::nanoseconds timeout) const override
{
if (this->isCanonical(faceUri)) {
onSuccess(faceUri);
return;
}
// make a copy because caller may modify faceUri
auto uri = make_shared<FaceUri>(faceUri);
boost::system::error_code ec;
auto ipAddress = boost::asio::ip::address::from_string(unescapeHost(faceUri.getHost()), ec);
if (!ec) {
// No need to resolve IP address if host is already an IP
if ((faceUri.getScheme() == m_v4Scheme && !ipAddress.is_v4()) ||
(faceUri.getScheme() == m_v6Scheme && !ipAddress.is_v6())) {
return onFailure("IPv4/v6 mismatch");
}
onDnsSuccess(uri, onSuccess, onFailure, ipAddress);
}
else {
dns::AddressSelector addressSelector;
if (faceUri.getScheme() == m_v4Scheme) {
addressSelector = dns::Ipv4Only();
}
else if (faceUri.getScheme() == m_v6Scheme) {
addressSelector = dns::Ipv6Only();
}
else {
BOOST_ASSERT(faceUri.getScheme() == m_baseScheme);
addressSelector = dns::AnyAddress();
}
dns::asyncResolve(unescapeHost(faceUri.getHost()),
[=] (const auto& ipAddr) { onDnsSuccess(uri, onSuccess, onFailure, ipAddr); },
[=] (const auto& reason) { onDnsFailure(uri, onFailure, reason); },
io, addressSelector, timeout);
}
}
protected:
explicit
IpHostCanonizeProvider(const std::string& baseScheme,
uint16_t defaultUnicastPort = 6363,
uint16_t defaultMulticastPort = 56363)
: m_baseScheme(baseScheme)
, m_v4Scheme(baseScheme + '4')
, m_v6Scheme(baseScheme + '6')
, m_defaultUnicastPort(defaultUnicastPort)
, m_defaultMulticastPort(defaultMulticastPort)
{
}
private:
void
onDnsSuccess(const shared_ptr<FaceUri>& faceUri,
const FaceUri::CanonizeSuccessCallback& onSuccess,
const FaceUri::CanonizeFailureCallback& onFailure,
const dns::IpAddress& ipAddress) const
{
auto [isOk, reason] = this->checkAddress(ipAddress);
if (!isOk) {
return onFailure(reason);
}
uint16_t port = 0;
if (faceUri->getPort().empty()) {
port = ipAddress.is_multicast() ? m_defaultMulticastPort : m_defaultUnicastPort;
}
else {
try {
port = boost::lexical_cast<uint16_t>(faceUri->getPort());
}
catch (const boost::bad_lexical_cast&) {
return onFailure("invalid port number '" + faceUri->getPort() + "'");
}
}
FaceUri canonicalUri(typename Protocol::endpoint(ipAddress, port));
BOOST_ASSERT(canonicalUri.isCanonical());
onSuccess(canonicalUri);
}
void
onDnsFailure(const shared_ptr<FaceUri>&,
const FaceUri::CanonizeFailureCallback& onFailure,
const std::string& reason) const
{
onFailure(reason);
}
/** \brief When overriden in a subclass, check the IP address is allowable.
* \return (true,ignored) if the address is allowable;
* (false,reason) if the address is not allowable.
*/
virtual std::pair<bool, std::string>
checkAddress(const dns::IpAddress&) const
{
return {true, ""};
}
static std::string
unescapeHost(std::string host)
{
auto escapePos = host.find("%25");
if (escapePos != std::string::npos && escapePos < host.size() - 3) {
host = unescape(host);
}
return host;
}
private:
std::string m_baseScheme;
std::string m_v4Scheme;
std::string m_v6Scheme;
uint16_t m_defaultUnicastPort;
uint16_t m_defaultMulticastPort;
};
class UdpCanonizeProvider : public IpHostCanonizeProvider<boost::asio::ip::udp>
{
public:
UdpCanonizeProvider()
: IpHostCanonizeProvider("udp")
{
}
};
class TcpCanonizeProvider : public IpHostCanonizeProvider<boost::asio::ip::tcp>
{
public:
TcpCanonizeProvider()
: IpHostCanonizeProvider("tcp")
{
}
protected:
std::pair<bool, std::string>
checkAddress(const dns::IpAddress& ipAddress) const override
{
if (ipAddress.is_multicast()) {
return {false, "cannot use multicast address"};
}
return {true, ""};
}
};
class EtherCanonizeProvider : public CanonizeProvider
{
public:
std::set<std::string>
getSchemes() const override
{
return {"ether"};
}
bool
isCanonical(const FaceUri& faceUri) const override
{
if (!faceUri.getPort().empty()) {
return false;
}
if (!faceUri.getPath().empty()) {
return false;
}
auto addr = ethernet::Address::fromString(faceUri.getHost());
return addr.toString() == faceUri.getHost();
}
void
canonize(const FaceUri& faceUri,
const FaceUri::CanonizeSuccessCallback& onSuccess,
const FaceUri::CanonizeFailureCallback& onFailure,
boost::asio::io_service&, time::nanoseconds timeout) const override
{
auto addr = ethernet::Address::fromString(faceUri.getHost());
if (addr.isNull()) {
return onFailure("invalid ethernet address '" + faceUri.getHost() + "'");
}
FaceUri canonicalUri(addr);
BOOST_ASSERT(canonicalUri.isCanonical());
onSuccess(canonicalUri);
}
};
class DevCanonizeProvider : public CanonizeProvider
{
public:
std::set<std::string>
getSchemes() const override
{
return {"dev"};
}
bool
isCanonical(const FaceUri& faceUri) const override
{
return !faceUri.getHost().empty() && faceUri.getPort().empty() && faceUri.getPath().empty();
}
void
canonize(const FaceUri& faceUri,
const FaceUri::CanonizeSuccessCallback& onSuccess,
const FaceUri::CanonizeFailureCallback& onFailure,
boost::asio::io_service&, time::nanoseconds timeout) const override
{
if (faceUri.getHost().empty()) {
onFailure("network interface name is missing");
return;
}
if (!faceUri.getPort().empty()) {
onFailure("port number is not allowed");
return;
}
if (!faceUri.getPath().empty() && faceUri.getPath() != "/") { // permit trailing slash only
onFailure("path is not allowed");
return;
}
FaceUri canonicalUri = FaceUri::fromDev(faceUri.getHost());
BOOST_ASSERT(canonicalUri.isCanonical());
onSuccess(canonicalUri);
}
};
class UdpDevCanonizeProvider : public CanonizeProvider
{
public:
std::set<std::string>
getSchemes() const override
{
return {"udp4+dev", "udp6+dev"};
}
bool
isCanonical(const FaceUri& faceUri) const override
{
if (faceUri.getPort().empty()) {
return false;
}
if (!faceUri.getPath().empty()) {
return false;
}
return true;
}
void
canonize(const FaceUri& faceUri,
const FaceUri::CanonizeSuccessCallback& onSuccess,
const FaceUri::CanonizeFailureCallback& onFailure,
boost::asio::io_service&, time::nanoseconds timeout) const override
{
if (this->isCanonical(faceUri)) {
onSuccess(faceUri);
}
else {
onFailure("cannot canonize " + faceUri.toString());
}
}
};
using CanonizeProviders = boost::mpl::vector<UdpCanonizeProvider*,
TcpCanonizeProvider*,
EtherCanonizeProvider*,
DevCanonizeProvider*,
UdpDevCanonizeProvider*>;
using CanonizeProviderTable = std::map<std::string, shared_ptr<CanonizeProvider>>;
class CanonizeProviderTableInitializer
{
public:
explicit
CanonizeProviderTableInitializer(CanonizeProviderTable& providerTable)
: m_providerTable(providerTable)
{
}
template<typename CP>
void
operator()(CP*)
{
shared_ptr<CanonizeProvider> cp = make_shared<CP>();
auto schemes = cp->getSchemes();
BOOST_ASSERT(!schemes.empty());
for (const auto& scheme : schemes) {
BOOST_ASSERT(m_providerTable.count(scheme) == 0);
m_providerTable[scheme] = cp;
}
}
private:
CanonizeProviderTable& m_providerTable;
};
static const CanonizeProvider*
getCanonizeProvider(const std::string& scheme)
{
static CanonizeProviderTable providerTable;
if (providerTable.empty()) {
boost::mpl::for_each<CanonizeProviders>(CanonizeProviderTableInitializer(providerTable));
BOOST_ASSERT(!providerTable.empty());
}
auto it = providerTable.find(scheme);
return it == providerTable.end() ? nullptr : it->second.get();
}
bool
FaceUri::canCanonize(const std::string& scheme)
{
return getCanonizeProvider(scheme) != nullptr;
}
bool
FaceUri::isCanonical() const
{
const CanonizeProvider* cp = getCanonizeProvider(this->getScheme());
if (cp == nullptr) {
return false;
}
return cp->isCanonical(*this);
}
void
FaceUri::canonize(const CanonizeSuccessCallback& onSuccess,
const CanonizeFailureCallback& onFailure,
boost::asio::io_service& io, time::nanoseconds timeout) const
{
const CanonizeProvider* cp = getCanonizeProvider(this->getScheme());
if (cp == nullptr) {
if (onFailure) {
onFailure("scheme not supported");
}
return;
}
cp->canonize(*this,
onSuccess ? onSuccess : [] (auto&&) {},
onFailure ? onFailure : [] (auto&&) {},
io, timeout);
}
} // namespace ndn