From 40f0277f51d1ef1c93e9fd76a78e5547227ff454 Mon Sep 17 00:00:00 2001 From: dmachard Date: Fri, 7 Feb 2025 08:34:51 +0100 Subject: [PATCH] Add ifurlup() HTTP status code option. --- .github/actions/spell-check/expect.txt | 1 + docs/lua-records/functions.rst | 1 + pdns/lua-record.cc | 11 +++++-- pdns/minicurl.cc | 8 +++--- pdns/minicurl.hh | 5 ++-- regression-tests.auth-py/test_LuaRecords.py | 32 +++++++++++++++++++++ 6 files changed, 49 insertions(+), 9 deletions(-) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 851f849ecb97..9a179ae44d38 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -573,6 +573,7 @@ htbp htmlescape htmlhelp httpapi +httpcode httpdomain hubert iana diff --git a/docs/lua-records/functions.rst b/docs/lua-records/functions.rst index 45a63a1c258b..a10c55c56d46 100644 --- a/docs/lua-records/functions.rst +++ b/docs/lua-records/functions.rst @@ -149,6 +149,7 @@ Record creation functions - ``stringmatch``: check ``url`` for this string, only declare 'up' if found - ``useragent``: Set the HTTP "User-Agent" header in the requests. By default it is set to "PowerDNS Authoritative Server" - ``byteslimit``: Limit the maximum download size to ``byteslimit`` bytes (default 0 meaning no limit). + - ``httpcode``: Set the HTTP status code to match in response. (default is 200) An example of a list of address sets: diff --git a/pdns/lua-record.cc b/pdns/lua-record.cc index 36bfdccd0a84..cacaf318fdea 100644 --- a/pdns/lua-record.cc +++ b/pdns/lua-record.cc @@ -113,7 +113,12 @@ class IsUpOracle if (cd.opts.count("byteslimit")) { byteslimit = static_cast(std::atoi(cd.opts.at("byteslimit").c_str())); } - MiniCurl mc(useragent); + int http_code = 200; + if (cd.opts.count("httpcode") != 0) { + http_code = pdns::checked_stoi(cd.opts.at("httpcode")); + } + + MiniCurl minicurl(useragent, false); string content; const ComboAddress* rem = nullptr; @@ -126,10 +131,10 @@ class IsUpOracle if (cd.opts.count("source")) { ComboAddress src(cd.opts.at("source")); - content=mc.getURL(cd.url, rem, &src, timeout, false, false, byteslimit); + content=minicurl.getURL(cd.url, rem, &src, timeout, false, false, byteslimit, http_code); } else { - content=mc.getURL(cd.url, rem, nullptr, timeout, false, false, byteslimit); + content=minicurl.getURL(cd.url, rem, nullptr, timeout, false, false, byteslimit, http_code); } if (cd.opts.count("stringmatch") && content.find(cd.opts.at("stringmatch")) == string::npos) { throw std::runtime_error(boost::str(boost::format("unable to match content with `%s`") % cd.opts.at("stringmatch"))); diff --git a/pdns/minicurl.cc b/pdns/minicurl.cc index c1e16b810c7a..2bef4ef2f5dc 100644 --- a/pdns/minicurl.cc +++ b/pdns/minicurl.cc @@ -47,7 +47,7 @@ void MiniCurl::init() } } -MiniCurl::MiniCurl(const string& useragent) +MiniCurl::MiniCurl(const string& useragent, bool failonerror) : d_failonerror(failonerror) { #ifdef CURL_STRICTER d_curl = std::unique_ptr(curl_easy_init(), curl_easy_cleanup); @@ -170,7 +170,7 @@ void MiniCurl::setupURL(const std::string& str, const ComboAddress* rem, const C curl_easy_setopt(getCURLPtr(d_curl), CURLOPT_SSL_VERIFYPEER, verify); curl_easy_setopt(getCURLPtr(d_curl), CURLOPT_SSL_VERIFYHOST, verify ? 2 : 0); - curl_easy_setopt(getCURLPtr(d_curl), CURLOPT_FAILONERROR, true); + curl_easy_setopt(getCURLPtr(d_curl), CURLOPT_FAILONERROR, d_failonerror); curl_easy_setopt(getCURLPtr(d_curl), CURLOPT_URL, str.c_str()); curl_easy_setopt(getCURLPtr(d_curl), CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(getCURLPtr(d_curl), CURLOPT_WRITEDATA, this); @@ -198,14 +198,14 @@ void MiniCurl::setupURL(const std::string& str, const ComboAddress* rem, const C d_data.clear(); } -std::string MiniCurl::getURL(const std::string& str, const ComboAddress* rem, const ComboAddress* src, int timeout, [[maybe_unused]] bool fastopen, bool verify, size_t byteslimit) +std::string MiniCurl::getURL(const std::string& str, const ComboAddress* rem, const ComboAddress* src, int timeout, [[maybe_unused]] bool fastopen, bool verify, size_t byteslimit, int http_status) { setupURL(str, rem, src, timeout, byteslimit, fastopen, verify); auto res = curl_easy_perform(getCURLPtr(d_curl)); long http_code = 0; curl_easy_getinfo(getCURLPtr(d_curl), CURLINFO_RESPONSE_CODE, &http_code); - if ((res != CURLE_OK && res != CURLE_ABORTED_BY_CALLBACK) || http_code != 200) { + if ((res != CURLE_OK && res != CURLE_ABORTED_BY_CALLBACK) || http_code != http_status) { throw std::runtime_error("Unable to retrieve URL ("+std::to_string(http_code)+"): "+string(curl_easy_strerror(res))); } std::string ret = d_data; diff --git a/pdns/minicurl.hh b/pdns/minicurl.hh index 08c88a6d75a2..75325784119b 100644 --- a/pdns/minicurl.hh +++ b/pdns/minicurl.hh @@ -43,11 +43,11 @@ public: static void init(); - MiniCurl(const string& useragent="MiniCurl/0.0"); + MiniCurl(const string& useragent="MiniCurl/0.0", bool failonerror=true); ~MiniCurl(); MiniCurl& operator=(const MiniCurl&) = delete; - std::string getURL(const std::string& str, const ComboAddress* rem=nullptr, const ComboAddress* src=nullptr, int timeout = 2, bool fastopen = false, bool verify = false, size_t byteslimit = 0); + std::string getURL(const std::string& str, const ComboAddress* rem=nullptr, const ComboAddress* src=nullptr, int timeout = 2, bool fastopen = false, bool verify = false, size_t byteslimit = 0, int http_status = 200); std::string postURL(const std::string& str, const std::string& postdata, MiniCurlHeaders& headers, int timeout = 2, bool fastopen = false, bool verify = false); private: @@ -70,6 +70,7 @@ private: std::string d_data; size_t d_byteslimit{}; bool d_fresh{true}; + bool d_failonerror; void setupURL(const std::string& str, const ComboAddress* rem, const ComboAddress* src, int timeout, size_t byteslimit, bool fastopen, bool verify); void setHeaders(const MiniCurlHeaders& headers); diff --git a/regression-tests.auth-py/test_LuaRecords.py b/regression-tests.auth-py/test_LuaRecords.py index 3448f22eefa3..56cec895f96d 100644 --- a/regression-tests.auth-py/test_LuaRecords.py +++ b/regression-tests.auth-py/test_LuaRecords.py @@ -116,6 +116,10 @@ class BaseLuaTest(AuthTest): "{{ '192.168.42.101', '{prefix}.101' }}, " "{{ stringmatch='pong' }}) ") +usa-404 IN LUA A ( ";include('config') " + "return ifurlup('http://www.lua.org:8080/404', " + "USAips, {{ httpcode='404' }}) ") + ifurlextup IN LUA A "ifurlextup({{{{['192.168.0.1']='http://{prefix}.101:8080/404',['192.168.0.2']='http://{prefix}.102:8080/404'}}, {{['192.168.0.3']='http://{prefix}.101:8080/'}}}})" nl IN LUA A ( ";include('config') " @@ -463,6 +467,34 @@ def testIfurlup(self): self.assertRcodeEqual(res, dns.rcode.NOERROR) self.assertAnyRRsetInAnswer(res, reachable_rrs) + def testIfurlupHTTPCode(self): + """ + Basic ifurlup() test, with non-default HTTP code + """ + reachable = [ + '{prefix}.103'.format(prefix=self._PREFIX) + ] + unreachable = ['192.168.42.105'] + ips = reachable + unreachable + all_rrs = [] + reachable_rrs = [] + for ip in ips: + rr = dns.rrset.from_text('usa-404.example.org.', 0, dns.rdataclass.IN, 'A', ip) + all_rrs.append(rr) + if ip in reachable: + reachable_rrs.append(rr) + + query = dns.message.make_query('usa-404.example.org', 'A') + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, all_rrs) + + # the timeout in the LUA health checker is 2 second, so we make sure to wait slightly longer here + time.sleep(3) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, reachable_rrs) + def testIfurlupMultiSet(self): """ Basic ifurlup() test with mutiple sets