From 77ea4f4c21e8cfe7a589e5bd9abd4ba88def2b3a Mon Sep 17 00:00:00 2001 From: Pieter Lexis Date: Thu, 30 Jul 2020 15:52:17 +0200 Subject: [PATCH] Add SvcParam class plus tests --- pdns/Makefile.am | 4 +- pdns/svc-records.cc | 160 ++++++++++++++++++++++++++++ pdns/svc-records.hh | 94 +++++++++++++++++ pdns/test-svc_records_cc.cc | 203 ++++++++++++++++++++++++++++++++++++ 4 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 pdns/svc-records.cc create mode 100644 pdns/svc-records.hh create mode 100644 pdns/test-svc_records_cc.cc diff --git a/pdns/Makefile.am b/pdns/Makefile.am index 10c77ebc79..d0fae49483 100644 --- a/pdns/Makefile.am +++ b/pdns/Makefile.am @@ -1263,6 +1263,7 @@ testrunner_SOURCES = \ proxy-protocol.cc proxy-protocol.hh \ qtype.cc \ rcpgenerator.cc \ + svc-records.cc \ responsestats.cc \ responsestats-auth.cc \ shuffle.cc shuffle.hh \ @@ -1294,9 +1295,10 @@ testrunner_SOURCES = \ test-packetcache_hh.cc \ test-proxy_protocol_cc.cc \ test-rcpgenerator_cc.cc \ - test-signers.cc \ test-sha_hh.cc \ + test-signers.cc \ test-statbag_cc.cc \ + test-svc_records_cc.cc \ test-tsig.cc \ test-ueberbackend_cc.cc \ test-zoneparser_tng_cc.cc \ diff --git a/pdns/svc-records.cc b/pdns/svc-records.cc new file mode 100644 index 0000000000..1431a718d4 --- /dev/null +++ b/pdns/svc-records.cc @@ -0,0 +1,160 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#include "svc-records.hh" +#include "misc.hh" +#include "base64.hh" + +const std::map SvcParam::SvcParams = { + {"mandatory", SvcParam::SvcParamKey::mandatory}, + {"alpn", SvcParam::SvcParamKey::alpn}, + {"no-default-alpn", SvcParam::SvcParamKey::no_default_alpn}, + {"port", SvcParam::SvcParamKey::port}, + {"ipv4hint", SvcParam::SvcParamKey::ipv4hint}, + {"echconfig", SvcParam::SvcParamKey::echconfig}, + {"ipv6hint", SvcParam::SvcParamKey::ipv6hint} +}; + +SvcParam::SvcParamKey SvcParam::keyFromString(const std::string& k) { + auto it = SvcParams.find(k); + if (it != SvcParams.end()) { + return it->second; + } + if (k.substr(0, 3) == "key") { + try { + return SvcParam::SvcParamKey(pdns_stou(k.substr(3))); + } + catch (...) { + } + } + throw std::invalid_argument("SvcParam '" + k + "'is not recognized or in keyNNNN format"); +} + +std::string SvcParam::keyToString(const SvcParam::SvcParamKey& k) { + auto ret = std::find_if(SvcParams.begin(), SvcParams.end(), [&](const std::pair& e) { return e.second == k; }); + if (ret != SvcParams.end()) { + return ret->first; + } + return "key" + std::to_string(k); +} + +SvcParam::SvcParam() {}; + +SvcParam::SvcParam(const SvcParamKey &key) { + d_key = key; + if (d_key != SvcParamKey::no_default_alpn) { + throw std::invalid_argument("can not create non-empty SvcParam for key '" + keyToString(key) + "'"); + } +} + +SvcParam::SvcParam(const SvcParamKey &key, const std::string &value) { + d_key = key; + if (d_key != SvcParamKey::echconfig && d_key < 7) { + throw std::invalid_argument("can not create SvcParam for " + keyToString(key) + " with a string value"); + } + if (d_key == SvcParamKey::echconfig) { + std::string d; + // TODO check Base64 decode + d_echconfig = value; + return; + } + d_value = value; +} + +SvcParam::SvcParam(const SvcParamKey &key, const std::set &value) { + d_key = key; + if (d_key != SvcParamKey::alpn && d_key != SvcParamKey::mandatory) { + throw std::invalid_argument("can not create SvcParam for " + keyToString(key) + " with a string-set value"); + } + if (d_key == SvcParamKey::alpn) { + d_alpn = value; + } + if (d_key == SvcParamKey::mandatory) { + // TODO validate entries + d_mandatory = value; + } +} + +SvcParam::SvcParam(const SvcParamKey &key, const std::set &value) { + d_key = key; + if (d_key != SvcParamKey::ipv6hint && d_key != SvcParamKey::ipv4hint) { + throw std::invalid_argument("can not create SvcParam for " + keyToString(key) + " with an IP address value"); + } + for (auto const &addr : value) { + if (d_key == SvcParam::ipv6hint && !addr.isIPv6()) { + throw std::invalid_argument("non-IPv6 address ('" + addr.toString() + "') passed for " + keyToString(key)); + } + if (d_key == SvcParam::ipv4hint && !addr.isIPv4()) { + throw std::invalid_argument("non-IPv4 address ('" + addr.toString() + "') passed for " + keyToString(key)); + } + } + d_ipHints = value; +} + +SvcParam::SvcParam(const SvcParamKey &key, const uint16_t value) { + d_key = key; + if (d_key != SvcParamKey::port) { + throw std::invalid_argument("can not create SvcParam for " + keyToString(key) + " with an port value"); + } + d_port = value; +} + +std::set SvcParam::getIPHints() const { + if (d_key != SvcParamKey::ipv6hint && d_key != SvcParamKey::ipv4hint) { + throw std::invalid_argument("getIPHints called for non-IP address key '" + keyToString(d_key) + "'"); + } + return d_ipHints; +} + +uint16_t SvcParam::getPort() const { + if (d_key != SvcParam::port) { + throw std::invalid_argument("getPort called for non-port key '" + keyToString(d_key) + "'"); + } + return d_port; +} + +std::set SvcParam::getALPN() const { + if (d_key != SvcParam::alpn) { + throw std::invalid_argument("getALPN called for non-alpn key '" + keyToString(d_key) + "'"); + } + return d_alpn; +} + +std::set SvcParam::getMandatory() const { + if (d_key != SvcParam::mandatory) { + throw std::invalid_argument("getMandatory called for non-mandatory key '" + keyToString(d_key) + "'"); + } + return d_mandatory; +} + +std::string SvcParam::getEchConfig() const { + if (d_key != SvcParam::echconfig) { + throw std::invalid_argument("getEchConfig called for non-echconfig key '" + keyToString(d_key) + "'"); + } + return d_echconfig; +} + +std::string SvcParam::getValue() const { + if (d_key < 7) { + throw std::invalid_argument("getValue called for non-single value key '" + keyToString(d_key) + "'"); + } + return d_value; +} \ No newline at end of file diff --git a/pdns/svc-records.hh b/pdns/svc-records.hh new file mode 100644 index 0000000000..5de7f0fd01 --- /dev/null +++ b/pdns/svc-records.hh @@ -0,0 +1,94 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#pragma once +#include +#include +#include +#include "iputils.hh" + +class SvcParam { + public: + enum SvcParamKey: uint16_t { + // TODO link to IANA registry + /* When adding new values, you *must* update SvcParam::SvcParam(const std::string &key, const std::string &value) + * in svc-record.cc with the new numbers + */ + mandatory = 0, + alpn = 1, + no_default_alpn = 2, + port = 3, + ipv4hint = 4, + echconfig = 5, + ipv6hint = 6 + }; + + //! empty Param, unusable + SvcParam(); + + //! To create a value-less SvcParam (like no-default-alpn) + SvcParam(const SvcParamKey &key); + + //! To create a "generic" SvcParam (for keyNNNNN and echconfig) + SvcParam(const SvcParamKey &key, const std::string &value); + + //! To create a multi-value SvcParam (like alpn and mandatory) + SvcParam(const SvcParamKey &key, const std::set &value); + + //! To create and ipv{4,6}hists SvcParam + SvcParam(const SvcParamKey &key, const std::set &value); + + //! To create a port SvcParam + SvcParam(const SvcParamKey &key, const uint16_t value); + + //! Returns the SvcParamKey based on the input + static SvcParamKey keyFromString(const std::string &k); + + //! Returns the string value of the SvcParamKey + static std::string keyToString(const SvcParamKey &k); + + bool operator< (const SvcParam &other) const { + return this->d_key < other.d_key; + }; + + SvcParamKey getKey() const { + return d_key; + } + + uint16_t getPort() const; + std::set getIPHints() const; + std::set getALPN() const; + std::set getMandatory() const; + std::string getEchConfig() const; + std::string getValue() const; + + private: + SvcParamKey d_key; + std::string d_value; // For keyNNNNN vals + + std::set d_alpn; // For ALPN + std::set d_mandatory; // For mandatory + std::set d_ipHints; // For ipv{6,4}hints + std::string d_echconfig; // For echconfig + uint16_t d_port; // For port + + static const std::map SvcParams; +}; diff --git a/pdns/test-svc_records_cc.cc b/pdns/test-svc_records_cc.cc new file mode 100644 index 0000000000..1c08e4333a --- /dev/null +++ b/pdns/test-svc_records_cc.cc @@ -0,0 +1,203 @@ +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include +#include "svc-records.hh" +#include "base64.hh" + +using namespace boost; + +BOOST_AUTO_TEST_SUITE(test_svc_records_cc) +BOOST_AUTO_TEST_CASE(test_SvcParam_keyFromString) { + SvcParam::SvcParamKey k; + + k = SvcParam::keyFromString("mandatory"); + BOOST_CHECK_EQUAL(k, 0); + BOOST_CHECK_EQUAL(k, SvcParam::mandatory); + + k = SvcParam::keyFromString("alpn"); + BOOST_CHECK_EQUAL(k, 1); + BOOST_CHECK_EQUAL(k, SvcParam::alpn); + + k = SvcParam::keyFromString("no-default-alpn"); + BOOST_CHECK_EQUAL(k, 2); + BOOST_CHECK_EQUAL(k, SvcParam::no_default_alpn); + + k = SvcParam::keyFromString("port"); + BOOST_CHECK_EQUAL(k, 3); + BOOST_CHECK_EQUAL(k, SvcParam::port); + + k = SvcParam::keyFromString("ipv4hint"); + BOOST_CHECK_EQUAL(k, 4); + BOOST_CHECK_EQUAL(k, SvcParam::ipv4hint); + + k = SvcParam::keyFromString("echconfig"); + BOOST_CHECK_EQUAL(k, 5); + BOOST_CHECK_EQUAL(k, SvcParam::echconfig); + + k = SvcParam::keyFromString("ipv6hint"); + BOOST_CHECK_EQUAL(k, 6); + BOOST_CHECK_EQUAL(k, SvcParam::ipv6hint); + + k = SvcParam::keyFromString("key0"); + BOOST_CHECK_EQUAL(k, 0); + BOOST_CHECK_EQUAL(k, SvcParam::mandatory); + + k = SvcParam::keyFromString("key666"); + BOOST_CHECK_EQUAL(k, 666); + + BOOST_CHECK_THROW(SvcParam::keyFromString("MANDATORY"), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(test_SvcParam_keyToString) { + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::mandatory), "mandatory"); + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::alpn), "alpn"); + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::no_default_alpn), "no-default-alpn"); + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::port), "port"); + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::ipv4hint), "ipv4hint"); + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::echconfig), "echconfig"); + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::ipv6hint), "ipv6hint"); + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::SvcParamKey(7)), "key7"); + BOOST_CHECK_EQUAL(SvcParam::keyToString(SvcParam::SvcParamKey(666)), "key666"); +} + +BOOST_AUTO_TEST_CASE(test_SvcParam_ctor_no_value) { + BOOST_CHECK_NO_THROW(SvcParam(SvcParam::no_default_alpn)); + BOOST_CHECK_THROW(SvcParam(SvcParam::alpn), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::keyFromString("key666")), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(test_SvcParam_ctor_value) { + string val = "foobar"; + auto base64val = Base64Encode(val); + SvcParam param; + + BOOST_CHECK_THROW(SvcParam(SvcParam::mandatory, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::alpn, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::no_default_alpn, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::port, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv4hint, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv6hint, val), std::invalid_argument); + + BOOST_CHECK_NO_THROW(param = SvcParam(SvcParam::echconfig, base64val)); + BOOST_CHECK_EQUAL(param.getEchConfig(), base64val); + BOOST_CHECK_THROW(param.getValue(), std::invalid_argument); + BOOST_CHECK_THROW(param.getALPN(), std::invalid_argument); + BOOST_CHECK_THROW(param.getIPHints(), std::invalid_argument); + BOOST_CHECK_THROW(param.getMandatory(), std::invalid_argument); + BOOST_CHECK_THROW(param.getPort(), std::invalid_argument); + + // TODO test bad base64 value + // BOOST_CHECK_THROW(SvcParam(SvcParam::echconfig, val), std::invalid_argument); + + // Any string is allowed..... + BOOST_CHECK_NO_THROW(param = SvcParam(SvcParam::keyFromString("key666"), base64val)); + BOOST_CHECK_EQUAL(param.getValue(), base64val); + BOOST_CHECK_NO_THROW(param = SvcParam(SvcParam::keyFromString("key666"), val)); + BOOST_CHECK_EQUAL(param.getValue(), val); + + BOOST_CHECK_THROW(param.getALPN(), std::invalid_argument); + BOOST_CHECK_THROW(param.getEchConfig(), std::invalid_argument); + BOOST_CHECK_THROW(param.getIPHints(), std::invalid_argument); + BOOST_CHECK_THROW(param.getMandatory(), std::invalid_argument); + BOOST_CHECK_THROW(param.getPort(), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(test_SvcParam_ctor_set_string_value) { + set val({"foo", "bar", "baz"}); + + BOOST_CHECK_THROW(SvcParam(SvcParam::no_default_alpn, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::port, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv4hint, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::echconfig, val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv6hint, val), std::invalid_argument); + + SvcParam param; + BOOST_CHECK_NO_THROW(param = SvcParam(SvcParam::keyFromString("mandatory"), val)); // TODO this will fail once we start checking the contents + + auto retval = param.getMandatory(); + BOOST_CHECK_EQUAL_COLLECTIONS(retval.begin(), retval.end(), val.begin(), val.end()); + BOOST_CHECK_THROW(param.getALPN(), std::invalid_argument); + BOOST_CHECK_THROW(param.getEchConfig(), std::invalid_argument); + BOOST_CHECK_THROW(param.getIPHints(), std::invalid_argument); + BOOST_CHECK_THROW(param.getPort(), std::invalid_argument); + BOOST_CHECK_THROW(param.getValue(), std::invalid_argument); + + retval.clear(); + BOOST_CHECK_NO_THROW(param = SvcParam(SvcParam::keyFromString("alpn"), val)); + retval = param.getALPN(); + BOOST_CHECK_EQUAL_COLLECTIONS(retval.begin(), retval.end(), val.begin(), val.end()); + BOOST_CHECK_THROW(param.getMandatory(), std::invalid_argument); + BOOST_CHECK_THROW(param.getEchConfig(), std::invalid_argument); + BOOST_CHECK_THROW(param.getIPHints(), std::invalid_argument); + BOOST_CHECK_THROW(param.getPort(), std::invalid_argument); + BOOST_CHECK_THROW(param.getValue(), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(test_SvcParam_ctor_set_comboaddress_value) { + ComboAddress ca1("192.0.2.1"); + ComboAddress ca2("192.0.2.2"); + ComboAddress ca3("2001:db8::1"); + ComboAddress ca4("2001:db8::2"); + + set mixedVal({ca1, ca3}); + set v4Val({ca1, ca2}); + set v6Val({ca3, ca4}); + + BOOST_CHECK_THROW(SvcParam(SvcParam::mandatory, v4Val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::alpn, v4Val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::no_default_alpn, v4Val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::port, v4Val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::echconfig, v4Val), std::invalid_argument); + + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv6hint, v4Val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv4hint, v6Val), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv6hint, mixedVal), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv4hint, mixedVal), std::invalid_argument); + + SvcParam param; + BOOST_CHECK_NO_THROW(param = SvcParam(SvcParam::ipv4hint, v4Val)); + + auto retval = param.getIPHints(); + BOOST_CHECK(retval == v4Val); + BOOST_CHECK_THROW(param.getMandatory(), std::invalid_argument); + BOOST_CHECK_THROW(param.getALPN(), std::invalid_argument); + BOOST_CHECK_THROW(param.getEchConfig(), std::invalid_argument); + BOOST_CHECK_THROW(param.getPort(), std::invalid_argument); + BOOST_CHECK_THROW(param.getValue(), std::invalid_argument); + + BOOST_CHECK_NO_THROW(param = SvcParam(SvcParam::ipv6hint, v6Val)); + retval.clear(); + retval = param.getIPHints(); + BOOST_CHECK(retval == v6Val); + BOOST_CHECK_THROW(param.getMandatory(), std::invalid_argument); + BOOST_CHECK_THROW(param.getALPN(), std::invalid_argument); + BOOST_CHECK_THROW(param.getEchConfig(), std::invalid_argument); + BOOST_CHECK_THROW(param.getPort(), std::invalid_argument); + BOOST_CHECK_THROW(param.getValue(), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(test_SvcParam_ctor_uint16_value) { + uint16_t port(53); + + BOOST_CHECK_THROW(SvcParam(SvcParam::mandatory, port), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::alpn, port), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::no_default_alpn, port), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv4hint, port), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::echconfig, port), std::invalid_argument); + BOOST_CHECK_THROW(SvcParam(SvcParam::ipv6hint, port), std::invalid_argument); + + SvcParam param; + BOOST_CHECK_NO_THROW(param = SvcParam(SvcParam::port, port)); + BOOST_CHECK_EQUAL(param.getPort(), port); + BOOST_CHECK_THROW(param.getMandatory(), std::invalid_argument); + BOOST_CHECK_THROW(param.getALPN(), std::invalid_argument); + BOOST_CHECK_THROW(param.getEchConfig(), std::invalid_argument); + BOOST_CHECK_THROW(param.getIPHints(), std::invalid_argument); + BOOST_CHECK_THROW(param.getValue(), std::invalid_argument); +} + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file -- 2.47.2