This is an automated email from the ASF dual-hosted git repository. szaszm pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git
commit c4c80e63012fa92c00c7a4f3b5ea383f1ffa55d3 Author: Martin Zink <[email protected]> AuthorDate: Tue Feb 28 13:10:01 2023 +0100 MINIFICPP-2047 added reverseDnsLookup to EL Closes #1510 Signed-off-by: Marton Szasz <[email protected]> --- EXPRESSIONS.md | 26 +++++++++ extensions/expression-language/Expression.cpp | 20 +++++++ .../tests/ExpressionLanguageTests.cpp | 64 ++++++++++++++++++++++ libminifi/include/utils/net/DNS.h | 7 +++ libminifi/src/utils/net/DNS.cpp | 36 ++++++++++++ libminifi/test/unit/NetUtilsTest.cpp | 34 +++++++++++- 6 files changed, 185 insertions(+), 2 deletions(-) diff --git a/EXPRESSIONS.md b/EXPRESSIONS.md index 79b9e0c11..97ba68341 100644 --- a/EXPRESSIONS.md +++ b/EXPRESSIONS.md @@ -224,6 +224,7 @@ token, filename. - [`hostname`](#hostname) - [`UUID`](#uuid) - [`literal`](#literal) +- [`reverseDnsLookup`](#reversednslookup) ### Evaluating Multiple Attributes @@ -1590,6 +1591,31 @@ to evaluate additional functions against. ${allMatchingAttributes('a.*'):count()} ):gt(3)}` returns true if there are more than 3 attributes whose names begin with the letter a. +### reverseDnsLookup + +**Description**: Performs a reverse DNS lookup on an ip address, and returns the corresponding hostname. + +**Subject Type**: No subject + +**Arguments**: + +| Argument | Description | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| IP address | The ip address to perform the reverse DNS lookup on. | +| Timeout duration milliseconds | Optional parameter that specifies the timeout duration of the operation in milliseconds. If not specified, defaults to 5000. | + + +**Return Type**: String + +**Examples**: + +| Expression | Value | +|----------------------------------------------------|--------------| +| `${reverseDnsLookup('127.0.0.1')}` | `localhost` | +| `${reverseDnsLookup('::1')}` | `localhost` | +| `${reverseDnsLookup('2001:4860:4860::8888'), 100}` | `dns.google` | + + ## Evaluating Multiple Attributes When it becomes necessary to evaluate the same conditions against multiple diff --git a/extensions/expression-language/Expression.cpp b/extensions/expression-language/Expression.cpp index c52411c74..f1570775c 100644 --- a/extensions/expression-language/Expression.cpp +++ b/extensions/expression-language/Expression.cpp @@ -64,6 +64,10 @@ #include "Driver.h" #include "date/tz.h" +#include "utils/net/DNS.h" +#include "utils/expected.h" + +using namespace std::literals::chrono_literals; namespace org::apache::nifi::minifi::expression { @@ -183,6 +187,20 @@ Value expr_ip(const std::vector<Value>& /*args*/) { return {}; } +Value expr_reverseDnsLookup(const std::vector<Value>& args) { + std::string ip_address_str = args[0].asString(); + + std::chrono::steady_clock::duration timeout_duration = 5s; + if (args.size() > 1) { + timeout_duration = std::chrono::milliseconds(args[1].asUnsignedLong()); + } + + return utils::net::addressFromString(ip_address_str) + | utils::flatMap([timeout_duration](const auto& ip_address) { return utils::net::reverseDnsLookup(ip_address, timeout_duration);}) + | utils::map([](const auto& hostname)-> Value { return Value(hostname); }) + | utils::valueOrElse([&](std::error_code error_code) { throw std::system_error(error_code);}); +} + Value expr_uuid(const std::vector<Value>& /*args*/) { return Value(utils::IdGenerator::getIdGenerator()->generate().to_string()); } @@ -1339,6 +1357,8 @@ Expression make_dynamic_function(const std::string &function_name, const std::ve return make_dynamic_function_incomplete<resolve_user_id>(function_name, args, 0); } else if (function_name == "ip") { return make_dynamic_function_incomplete<expr_ip>(function_name, args, 0); + } else if (function_name == "reverseDnsLookup") { + return make_dynamic_function_incomplete<expr_reverseDnsLookup>(function_name, args, 1); } else if (function_name == "UUID") { return make_dynamic_function_incomplete<expr_uuid>(function_name, args, 0); } else if (function_name == "toUpper") { diff --git a/extensions/expression-language/tests/ExpressionLanguageTests.cpp b/extensions/expression-language/tests/ExpressionLanguageTests.cpp index 77cd383af..477000981 100644 --- a/extensions/expression-language/tests/ExpressionLanguageTests.cpp +++ b/extensions/expression-language/tests/ExpressionLanguageTests.cpp @@ -41,6 +41,7 @@ #include "Catch.h" #include "unit/ProvenanceTestHelper.h" #include "date/tz.h" +#include "Utils.h" namespace expression = org::apache::nifi::minifi::expression; @@ -1280,6 +1281,69 @@ TEST_CASE("Full Hostname", "[expressionFullHostname]") { REQUIRE(!expr(expression::Parameters{ flow_file_a }).asString().empty()); } +TEST_CASE("Reverse DNS lookup with valid ip", "[ExpressionLanguage][reverseDnsLookup]") { + auto expr = expression::compile("${reverseDnsLookup(${ip_addr})}"); + + auto flow_file_a = std::make_shared<core::FlowFile>(); + std::string expected_hostname; + SECTION("dns.google IPv4") { + flow_file_a->addAttribute("ip_addr", "8.8.8.8"); + expected_hostname = "dns.google"; + } + + SECTION("dns.google IPv6") { + if (minifi::test::utils::isIPv6Disabled()) + return; + flow_file_a->addAttribute("ip_addr", "2001:4860:4860::8888"); + expected_hostname = "dns.google"; + } + + SECTION("Unresolvable address IPv4") { + flow_file_a->addAttribute("ip_addr", "192.0.2.0"); + expected_hostname = "192.0.2.0"; + } + + SECTION("Unresolvable address IPv6") { + if (minifi::test::utils::isIPv6Disabled()) + return; + flow_file_a->addAttribute("ip_addr", "2001:db8::"); + expected_hostname = "2001:db8::"; + } + + REQUIRE(expr(expression::Parameters{ flow_file_a }).asString() == expected_hostname); +} + +TEST_CASE("Reverse DNS lookup with invalid ip", "[ExpressionLanguage][reverseDnsLookup]") { + auto expr = expression::compile("${reverseDnsLookup(${ip_addr})}"); + + auto flow_file_a = std::make_shared<core::FlowFile>(); + flow_file_a->addAttribute("ip_addr", "banana"); + + REQUIRE_THROWS_AS(expr(expression::Parameters{flow_file_a}), std::runtime_error); +} + +TEST_CASE("Reverse DNS lookup with invalid timeout parameter", "[ExpressionLanguage][reverseDnsLookup]") { + auto expr = expression::compile("${reverseDnsLookup(${ip_addr}, ${timeout})}"); + + auto flow_file_a = std::make_shared<core::FlowFile>(); + flow_file_a->addAttribute("ip_addr", "192.0.2.1"); + flow_file_a->addAttribute("timeout", "strawberry"); + + REQUIRE_THROWS_AS(expr(expression::Parameters{ flow_file_a }), std::invalid_argument); +} + +TEST_CASE("Reverse DNS lookup with valid timeout parameter", "[ExpressionLanguage][reverseDnsLookup]") { + auto reverse_lookup_expr_500ms = expression::compile("${reverseDnsLookup(${ip_addr}, 500)}"); + auto reverse_lookup_expr_0ms = expression::compile("${reverseDnsLookup(${ip_addr}, 0)}"); // 0ms to make sure it times out + + auto flow_file_a = std::make_shared<core::FlowFile>(); + std::string expected_hostname; + flow_file_a->addAttribute("ip_addr", "192.0.2.1"); + + REQUIRE_THROWS_AS(reverse_lookup_expr_0ms(expression::Parameters{flow_file_a}), std::runtime_error); + REQUIRE_NOTHROW(reverse_lookup_expr_500ms(expression::Parameters{ flow_file_a })); +} + TEST_CASE("UUID", "[expressionUuid]") { auto expr = expression::compile("${UUID()}"); diff --git a/libminifi/include/utils/net/DNS.h b/libminifi/include/utils/net/DNS.h index 2b9a31b57..4b33609bd 100644 --- a/libminifi/include/utils/net/DNS.h +++ b/libminifi/include/utils/net/DNS.h @@ -15,6 +15,7 @@ * limitations under the License. */ #pragma once +#include <chrono> #include <memory> #include <string> #include <string_view> @@ -22,6 +23,7 @@ #include "nonstd/expected.hpp" #include "utils/gsl.h" #include "IpProtocol.h" +#include "asio/ip/address.hpp" struct addrinfo; @@ -41,4 +43,9 @@ inline auto resolveHost(const char* const hostname, const uint16_t port, const I inline auto resolveHost(const uint16_t port, const IpProtocol proto = IpProtocol::TCP, const bool need_canonname = false) { return resolveHost(nullptr, port, proto, need_canonname); } + +nonstd::expected<asio::ip::address, std::error_code> addressFromString(std::string_view ip_address_str); + +nonstd::expected<std::string, std::error_code> reverseDnsLookup(const asio::ip::address& ip_address, std::chrono::steady_clock::duration timeout = std::chrono::seconds(5)); + } // namespace org::apache::nifi::minifi::utils::net diff --git a/libminifi/src/utils/net/DNS.cpp b/libminifi/src/utils/net/DNS.cpp index b5eac92bb..ba8678912 100644 --- a/libminifi/src/utils/net/DNS.cpp +++ b/libminifi/src/utils/net/DNS.cpp @@ -17,6 +17,9 @@ #include "utils/net/DNS.h" #include "Exception.h" #include "utils/StringUtils.h" +#include "utils/net/AsioCoro.h" +#include "asio/detached.hpp" +#include "asio/ip/udp.hpp" #ifdef WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -91,4 +94,37 @@ nonstd::expected<gsl::not_null<std::unique_ptr<addrinfo, addrinfo_deleter>>, std return addr_info; } +nonstd::expected<asio::ip::address, std::error_code> addressFromString(const std::string_view ip_address_str) { + std::error_code ip_address_from_string_error; + auto ip_address = asio::ip::address::from_string(ip_address_str.data(), ip_address_from_string_error); + if (ip_address_from_string_error) + return nonstd::make_unexpected(ip_address_from_string_error); + return ip_address; +} + +namespace { +asio::awaitable<std::tuple<std::error_code, asio::ip::basic_resolver<asio::ip::udp>::results_type>> asyncReverseDnsLookup(const asio::ip::address& ip_address, + std::chrono::steady_clock::duration timeout_duration) { + asio::ip::basic_resolver<asio::ip::udp> resolver(co_await asio::this_coro::executor); + co_return co_await asyncOperationWithTimeout(resolver.async_resolve({ip_address, 0}, use_nothrow_awaitable), timeout_duration); +} +} // namespace + +nonstd::expected<std::string, std::error_code> reverseDnsLookup(const asio::ip::address& ip_address, std::chrono::steady_clock::duration timeout_duration) { + asio::io_context io_context; + + std::error_code resolve_error; + asio::ip::basic_resolver<asio::ip::udp>::results_type results; + + co_spawn(io_context, asyncReverseDnsLookup(ip_address, timeout_duration), [&resolve_error, &results](const std::exception_ptr&, const auto& resolve_results) { + resolve_error = std::get<std::error_code>(resolve_results); + results = std::get<asio::ip::basic_resolver<asio::ip::udp>::results_type>(resolve_results); + }); + io_context.run(); + + if (resolve_error) + return nonstd::make_unexpected(resolve_error); + return results->host_name(); +} + } // namespace org::apache::nifi::minifi::utils::net diff --git a/libminifi/test/unit/NetUtilsTest.cpp b/libminifi/test/unit/NetUtilsTest.cpp index 25fec78c4..add55e050 100644 --- a/libminifi/test/unit/NetUtilsTest.cpp +++ b/libminifi/test/unit/NetUtilsTest.cpp @@ -16,15 +16,15 @@ * limitations under the License. */ -#include <functional> #include <string> -#include <type_traits> #include "../TestBase.h" #include "../Catch.h" #include "utils/net/DNS.h" #include "utils/net/Socket.h" #include "utils/StringUtils.h" +#include "../Utils.h" +#include "range/v3/algorithm/contains.hpp" namespace utils = org::apache::nifi::minifi::utils; namespace net = utils::net; @@ -34,3 +34,33 @@ TEST_CASE("net::resolveHost", "[net][dns][utils][resolveHost]") { const auto localhost_address = net::sockaddr_ntop(net::resolveHost("localhost", "10080").value()->ai_addr); REQUIRE((utils::StringUtils::startsWith(localhost_address, "127") || localhost_address == "::1")); } + +TEST_CASE("net::reverseDnsLookup", "[net][dns][reverseDnsLookup]") { + SECTION("dns.google IPv4") { + auto dns_google_hostname = net::reverseDnsLookup(asio::ip::address::from_string("8.8.8.8")); + REQUIRE(dns_google_hostname.has_value()); + CHECK(dns_google_hostname == "dns.google"); + } + + SECTION("dns.google IPv6") { + if (minifi::test::utils::isIPv6Disabled()) + return; + auto dns_google_hostname = net::reverseDnsLookup(asio::ip::address::from_string("2001:4860:4860::8888")); + REQUIRE(dns_google_hostname.has_value()); + CHECK(dns_google_hostname == "dns.google"); + } + + SECTION("Unresolvable address IPv4") { + auto unresolvable_hostname = net::reverseDnsLookup(asio::ip::address::from_string("192.0.2.0")); + REQUIRE(unresolvable_hostname.has_value()); + CHECK(unresolvable_hostname == "192.0.2.0"); + } + + SECTION("Unresolvable address IPv6") { + if (minifi::test::utils::isIPv6Disabled()) + return; + auto unresolvable_hostname = net::reverseDnsLookup(asio::ip::address::from_string("2001:db8::")); + REQUIRE(unresolvable_hostname.has_value()); + CHECK(unresolvable_hostname == "2001:db8::"); + } +}
