]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
feat(auth): Allow HTTP Headers in ifurlup requests 16955/head
authorPieter Lexis <pieter.lexis@powerdns.com>
Fri, 6 Mar 2026 10:58:09 +0000 (11:58 +0100)
committerPieter Lexis <pieter.lexis@powerdns.com>
Fri, 6 Mar 2026 11:23:32 +0000 (12:23 +0100)
Closes: #8295
Closes: #11610
docs/lua-records/functions.rst
pdns/lua-record.cc
regression-tests.auth-py/test_LuaRecords.py

index f8c95654343e25b5e77785f881a1cef1aca72ae0..bd68e4e931ca7cd91df02b4e419d076ab99eaa99 100644 (file)
@@ -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.
index b9b11472217de2ba03f1d54250e2f86ab9973207..c653f06f1e03d3108fbea8f012624901b53b7dbd 100644 (file)
@@ -143,6 +143,13 @@ private:
       int http_code = pdns::checked_stoi<int>(cd.getOption<string>("httpcode", "200"));
 
       MiniCurl minicurl(useragent, false);
+      
+      MiniCurl::MiniCurlHeaders mch;
+      for (auto const & header:cd.getOption<LuaAssociativeTable<string>>("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<string>("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<string>("stringmatch")) == string::npos) {
         throw std::runtime_error(boost::str(boost::format("unable to match content with `%s`") % cd.getOption<string>("stringmatch")));
index 9088ff4e17e1faacbf1293a8a321b38b5b91ef8b..bfcef3ea12c66c08b9cbebf66ffed46f90861786 100644 (file)
@@ -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