From: Peter van Dijk Date: Fri, 21 Jan 2022 14:58:46 +0000 (+0100) Subject: auth LUA: add ifurlextup function X-Git-Tag: auth-4.7.0-alpha1~27^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=95d0df69a79243d4a510d24afa07e08734ac5200;p=thirdparty%2Fpdns.git auth LUA: add ifurlextup function --- diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 1b8fe796f7..64cc2a4dcb 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -723,6 +723,7 @@ iers ietf ifdef ifportup +ifurlextup ifurlup ihsinme IJajghd diff --git a/docs/lua-records/functions.rst b/docs/lua-records/functions.rst index 9299fd6f2d..63c3984bd6 100644 --- a/docs/lua-records/functions.rst +++ b/docs/lua-records/functions.rst @@ -92,6 +92,30 @@ Record creation functions ifurlup("https://example.com/", { {"192.0.2.20", "203.0.113.4"}, {"203.0.113.2"} }) +.. function:: ifurlextup(groups-of-address-url-pairs[, options]) + + Very similar to ``ifurlup``, but the returned IPs are decoupled from their external health check URLs. + This is useful when health checking already happens elsewhere, and that state is exposed over HTTP(S). + Health checks are considered positive if the HTTP response code is 200 and optionally if the content matches the ``stringmatch`` option. + + Options are identical to those for ``ifurlup``. + + Example: + + .. code-block:: lua + + ifurlextup({{['192.168.0.1']='https://example.com/',['192.168.0.2']='https://example.com/404'}}) + + Example with two groups: + + .. code-block:: lua + + ifurlextup({{['192.168.0.1']='https://example.net/404',['192.168.0.2']='https://example.com/404'}, {['192.168.0.3']='https://example.net/'}})" + + The health checker will look up the first two URLs (using normal DNS resolution to find them - whenever possible, use URLs with IPs in them). + The 404s will cause the first group of IPs to get marked as down, after which the URL in the second group is tested. + The third IP will get marked up assuming ``https://example.net/`` responds with HTTP response code 200. + .. function:: pickrandom(addresses) Returns a random IP address from the list supplied. diff --git a/pdns/lua-record.cc b/pdns/lua-record.cc index dfa118a6fb..3571facf89 100644 --- a/pdns/lua-record.cc +++ b/pdns/lua-record.cc @@ -87,6 +87,7 @@ public: private: void checkURL(const CheckDesc& cd, const bool status, const bool first = false) { + string remstring; try { int timeout = 2; if (cd.opts.count("timeout")) { @@ -99,24 +100,33 @@ private: MiniCurl mc(useragent); string content; + const ComboAddress* rem = nullptr; + if(cd.rem.sin4.sin_family != AF_UNSPEC) { + rem = &cd.rem; + remstring = rem->toString(); + } else { + remstring = "[externally checked IP]"; + } + if (cd.opts.count("source")) { ComboAddress src(cd.opts.at("source")); - content=mc.getURL(cd.url, &cd.rem, &src, timeout); + content=mc.getURL(cd.url, rem, &src, timeout); } else { - content=mc.getURL(cd.url, &cd.rem, nullptr, timeout); + content=mc.getURL(cd.url, rem, nullptr, timeout); } 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"))); } + if(!status) { - g_log< >& ipurls, boost::optional options) { + vector candidates; + opts_t opts; + if(options) + opts = *options; + + ComboAddress ca_unspec; + ca_unspec.sin4.sin_family=AF_UNSPEC; + + // ipurls: { { ["192.0.2.1"] = "https://example.com", ["192.0.2.2"] = "https://example.com/404" } } + for (const auto& [count, unitmap] : ipurls) { + // unitmap: 1 = { ["192.0.2.1"] = "https://example.com", ["192.0.2.2"] = "https://example.com/404" } + vector available; + + for (const auto& [ipStr, url] : unitmap) { + // unit: ["192.0.2.1"] = "https://example.com" + ComboAddress ip(ipStr); + candidates.push_back(ip); + if (g_up.isUp(ca_unspec, url, opts)) { + available.push_back(ip); + } + } + if(!available.empty()) { + vector res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available); + return convIpListToString(res); + } + } + + // All units down, apply backupSelector on all candidates + vector res = useSelector(getOptionValue(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, candidates); + return convIpListToString(res); + }); + lua.writeFunction("ifurlup", [](const std::string& url, const boost::variant& ips, boost::optional options) { diff --git a/regression-tests.auth-py/test_LuaRecords.py b/regression-tests.auth-py/test_LuaRecords.py index 152b5bab51..409d3951ef 100644 --- a/regression-tests.auth-py/test_LuaRecords.py +++ b/regression-tests.auth-py/test_LuaRecords.py @@ -11,14 +11,19 @@ from authtests import AuthTest from http.server import BaseHTTPRequestHandler, HTTPServer class FakeHTTPServer(BaseHTTPRequestHandler): - def _set_headers(self): - self.send_response(200) + def _set_headers(self, response_code=200): + self.send_response(response_code) self.send_header('Content-type', 'text/html') self.end_headers() def do_GET(self): + if self.path == '/404': + self._set_headers(404) + self.wfile.write(bytes('this page does not exist', 'utf-8')) + return + self._set_headers() - if (self.path == '/ping.json'): + if self.path == '/ping.json': self.wfile.write(bytes('{"ping":"pong"}', 'utf-8')) else: self.wfile.write(bytes("

hi!

Programming in Lua !

", "utf-8")) @@ -81,6 +86,8 @@ eu-west IN LUA A ( ";include('config') " "return ifurlup('http://www.lua.org:8080/', " "{{EUWips, EUEips, USAips}}, settings) ") +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') " "return ifportup(8081, NLips) ") latlon.geo IN LUA TXT "latlon()" @@ -324,6 +331,20 @@ createforward6.example.org. 3600 IN NS ns2.example.org. self.assertRcodeEqual(res, dns.rcode.NOERROR) self.assertAnyRRsetInAnswer(res, reachable_rrs) + def testIfurlextup(self): + expected = [dns.rrset.from_text('ifurlextup.example.org.', 0, dns.rdataclass.IN, dns.rdatatype.A, '192.168.0.3')] + + query = dns.message.make_query('ifurlextup.example.org', 'A') + res = self.sendUDPQuery(query) + + # wait for health checks to happen + time.sleep(5) + + res = self.sendUDPQuery(query) + + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertEqual(res.answer, expected) + def testIfurlupSimplified(self): """ Basic ifurlup() test with the simplified list of ips