From: Pieter Lexis Date: Fri, 6 Mar 2026 10:58:09 +0000 (+0100) Subject: feat(auth): Allow HTTP Headers in ifurlup requests X-Git-Tag: auth-5.1.0-alpha1~17^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e3a720e1e62da9cb181ec23e3500a962a0906bc4;p=thirdparty%2Fpdns.git feat(auth): Allow HTTP Headers in ifurlup requests Closes: #8295 Closes: #11610 --- diff --git a/docs/lua-records/functions.rst b/docs/lua-records/functions.rst index f8c9565434..bd68e4e931 100644 --- a/docs/lua-records/functions.rst +++ b/docs/lua-records/functions.rst @@ -106,6 +106,7 @@ Record creation functions - ``byteslimit``: Limit the maximum download size to ``byteslimit`` bytes (default 0 meaning no limit). - ``minimumFailures``: The number of unsuccessful checks in a row required to mark the address as down. Defaults to 1 if not specified, i.e. report as down on the first unsuccessful check. - ``failOnIncompleteCheck``: if set to ``true``, return SERVFAIL instead of applying ``backupSelector``, if none of the addresses have completed their background health check yet. + - ``headers``: A table of HTTP headers to be added to the request. Any ``_`` in the header name will be replaced with a ``-``. An example of a list of address sets: @@ -113,6 +114,12 @@ Record creation functions ifurlup("https://example.com/", { {"192.0.2.20", "203.0.113.4"}, {"203.0.113.2"} }) + An example usage of headers: + + .. code-block:: lua + + ifurlup("https://example.com/", { {"192.0.2.20", "203.0.113.4"}, {"203.0.113.2"} }, { headers={X_API_Key="example-key", Cache_Control="no-cache"} }) + .. function:: ifurlextup(groups-of-address-url-pairs[, options]) Very similar to ``ifurlup``, but the returned IPs are decoupled from their external health check URLs. diff --git a/pdns/lua-record.cc b/pdns/lua-record.cc index b9b1147221..c653f06f1e 100644 --- a/pdns/lua-record.cc +++ b/pdns/lua-record.cc @@ -143,6 +143,13 @@ private: int http_code = pdns::checked_stoi(cd.getOption("httpcode", "200")); MiniCurl minicurl(useragent, false); + + MiniCurl::MiniCurlHeaders mch; + for (auto const & header:cd.getOption>("headers", {})) { + auto headername = header.first; + std::replace(headername.begin(), headername.end(), '_', '-'); + mch.emplace(headername, header.second); + } string content; const ComboAddress* rem = nullptr; @@ -155,10 +162,10 @@ private: if (cd.opts.count("source")) { ComboAddress src{cd.getOption("source")}; - content=minicurl.getURL(cd.url, rem, &src, timeout, nullptr, false, false, byteslimit, http_code); + content=minicurl.getURL(cd.url, rem, &src, timeout, &mch, false, false, byteslimit, http_code); } else { - content=minicurl.getURL(cd.url, rem, nullptr, timeout, nullptr, false, false, byteslimit, http_code); + content=minicurl.getURL(cd.url, rem, nullptr, timeout, &mch, false, false, byteslimit, http_code); } if (cd.opts.count("stringmatch") && content.find(cd.getOption("stringmatch")) == string::npos) { throw std::runtime_error(boost::str(boost::format("unable to match content with `%s`") % cd.getOption("stringmatch"))); diff --git a/regression-tests.auth-py/test_LuaRecords.py b/regression-tests.auth-py/test_LuaRecords.py index 9088ff4e17..bfcef3ea12 100644 --- a/regression-tests.auth-py/test_LuaRecords.py +++ b/regression-tests.auth-py/test_LuaRecords.py @@ -1,7 +1,10 @@ #!/usr/bin/env python import unittest import threading -import dns +import dns.rrset +import dns.rcode +import dns.rdataclass +import dns.message import time import clientsubnetoption @@ -23,6 +26,12 @@ class FakeHTTPServer(BaseHTTPRequestHandler): self.wfile.write(bytes('this page does not exist', 'utf-8')) return + if self.path == "/check-headers": + if self.headers.get("my-header", "") != "myvalue": + self._set_headers(400) + self.wfile.write(bytes('Wrong Header value!', 'utf-8')) + return + self._set_headers() if self.path == '/ping.json': self.wfile.write(bytes('{"ping":"pong"}', 'utf-8')) @@ -129,6 +138,14 @@ usa-404 IN LUA A ( ";include('config') " 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/'}}}})" +goodheaders.ifurlup IN LUA A ("ifurlup('http://example.com:8080/check-headers', " + " {{'{prefix}.102', '192.168.42.105'}}, " + " {{headers={{my_header='myvalue'}}}}) ") + +badheaders.ifurlup IN LUA A ("ifurlup('http://example.com:8080/check-headers', " + " {{'{prefix}.102', '192.168.42.105'}}, " + " {{headers={{my_header='wrong-value'}}}}) ") + nl IN LUA A ( ";include('config') " "return ifportup(8081, NLips) ") latlon.geo IN LUA TXT "latlon()" @@ -585,6 +602,60 @@ class TestLuaRecords(BaseLuaTest): self.assertRcodeEqual(res, dns.rcode.NOERROR) self.assertAnyRRsetInAnswer(res, reachable_rrs) + def testIfurlupHeaders(self): + """ + ifurlup() test where send headers. + """ + reachable = [ + '{prefix}.102'.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('goodheaders.ifurlup.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('goodheaders.ifurlup.example.org', 'A') + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, all_rrs) + + time.sleep(3) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, reachable_rrs) + + def testIfurlupHeadersBad(self): + """ + ifurlup() test where send headers, but the value is wrong + """ + reachable = [ + '{prefix}.102'.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('badheaders.ifurlup.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('badheaders.ifurlup.example.org', 'A') + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, all_rrs) + + time.sleep(3) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, all_rrs) + def testLatlon(self): """ Basic latlon() test