return true;
}
}
+ if (req.method == "DELETE") {
+ if (req.url.path == "/api/v1/cache") {
+ return true;
+ }
+ }
return false;
}
}
#endif /* DISABLE_WEB_CONFIG */
+#ifndef DISABLE_WEB_CACHE_MANAGEMENT
+static void handleCacheManagement(const YaHTTP::Request& req, YaHTTP::Response& resp)
+{
+ handleCORS(req, resp);
+
+ resp.headers["Content-Type"] = "application/json";
+ resp.status = 200;
+
+ if (req.method != "DELETE") {
+ resp.status = 400;
+ Json::object obj{
+ { "status", "denied" },
+ { "error", "invalid method" }
+ };
+ resp.body = Json(obj).dump();
+ return;
+ }
+
+ const auto poolName = req.getvars.find("pool");
+ const auto expungeName = req.getvars.find("name");
+ const auto expungeType = req.getvars.find("type");
+ const auto suffix = req.getvars.find("suffix");
+ if (poolName == req.getvars.end() || expungeName == req.getvars.end()) {
+ resp.status = 400;
+ Json::object obj{
+ { "status", "denied" },
+ { "error", "missing 'pool' or 'name' parameter" },
+ };
+ resp.body = Json(obj).dump();
+ return;
+ }
+
+ DNSName name;
+ QType type(QType::ANY);
+ try {
+ name = DNSName(expungeName->second);
+ }
+ catch (const std::exception& e) {
+ resp.status = 404;
+ Json::object obj{
+ { "status", "error" },
+ { "error", "unable to parse the requested name" },
+ };
+ resp.body = Json(obj).dump();
+ return;
+ }
+ if (expungeType != req.getvars.end()) {
+ type = QType::chartocode(expungeType->second.c_str());
+ }
+
+ std::shared_ptr<ServerPool> pool;
+ try {
+ pool = getPool(g_pools.getCopy(), poolName->second);
+ }
+ catch (const std::exception& e) {
+ resp.status = 404;
+ Json::object obj{
+ { "status", "not found" },
+ { "error", "the requested pool does not exist" },
+ };
+ resp.body = Json(obj).dump();
+ return;
+ }
+
+ auto cache = pool->getCache();
+ if (cache == nullptr) {
+ resp.status = 404;
+ Json::object obj{
+ { "status", "not found" },
+ { "error", "there is no cache associated to the requested pool" },
+ };
+ resp.body = Json(obj).dump();
+ return;
+ }
+
+ auto removed = cache->expungeByName(name, type.getCode(), suffix != req.getvars.end());
+
+ Json::object obj{
+ { "status", "purged" },
+ { "count", std::to_string(removed) }
+ };
+ resp.body = Json(obj).dump();
+}
+#endif /* DISABLE_WEB_CACHE_MANAGEMENT */
+
static std::unordered_map<std::string, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)>> s_webHandlers;
void registerWebHandler(const std::string& endpoint, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)> handler);
registerWebHandler("/api/v1/servers/localhost/config", handleConfigDump);
registerWebHandler("/api/v1/servers/localhost/config/allow-from", handleAllowFrom);
#endif /* DISABLE_WEB_CONFIG */
+#ifndef DISABLE_WEB_CACHE_MANAGEMENT
+ registerWebHandler("/api/v1/cache", handleCacheManagement);
+#endif /* DISABLE_WEB_CACHE_MANAGEMENT */
#ifndef DISABLE_BUILTIN_HTML
registerWebHandler("/", redirectToIndex);
import dns
import clientsubnetoption
import cookiesoption
+import requests
from dnsdisttests import DNSDistTest
class TestCaching(DNSDistTest):
self.assertTrue(receivedQuery)
self.assertTrue(receivedResponse)
self.assertEqual(receivedResponse, expectedResponse)
+
+class TestAPICache(DNSDistTest):
+ _webTimeout = 2.0
+ _webServerPort = 8083
+ _webServerBasicAuthPassword = 'secret'
+ _webServerBasicAuthPasswordHashed = '$scrypt$ln=10,p=1,r=8$6DKLnvUYEeXWh3JNOd3iwg==$kSrhdHaRbZ7R74q3lGBqO1xetgxRxhmWzYJ2Qvfm7JM='
+ _webServerAPIKey = 'apisecret'
+ _webServerAPIKeyHashed = '$scrypt$ln=10,p=1,r=8$9v8JxDfzQVyTpBkTbkUqYg==$bDQzAOHeK1G9UvTPypNhrX48w974ZXbFPtRKS34+aso='
+ _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
+ _config_template = """
+ newServer{address="127.0.0.1:%s"}
+ webserver("127.0.0.1:%s")
+ setWebserverConfig({password="%s", apiKey="%s"})
+ pc = newPacketCache(100)
+ getPool(""):setCache(pc)
+ getPool("pool-with-cache"):setCache(pc)
+ getPool("pool-without-cache")
+ """
+
+ def testCacheClearingViaAPI(self):
+ """
+ Cache: Clear cache via API
+ """
+ headers = {'x-api-key': self._webServerAPIKey}
+ url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/cache'
+ name = 'cache-api.cache.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'AAAA', 'IN')
+ response = dns.message.make_response(query)
+ rrset = dns.rrset.from_text(name,
+ 3600,
+ dns.rdataclass.IN,
+ dns.rdatatype.AAAA,
+ '::1')
+ response.answer.append(rrset)
+
+ # first query to fill the cache
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ self.assertTrue(receivedQuery)
+ self.assertTrue(receivedResponse)
+ receivedQuery.id = query.id
+ self.assertEqual(query, receivedQuery)
+ self.assertEqual(receivedResponse, response)
+
+ # second query should be a hit
+ (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+ self.assertEqual(receivedResponse, response)
+
+ # GET should on the cache API should yield a 400
+ r = requests.get(url + '?pool=pool-without-cache&name=cache-api.cache.tests.powerdns.com.&type=AAAA', headers=headers, timeout=self._webTimeout)
+ self.assertEqual(r.status_code, 400)
+
+ # different pool
+ r = requests.delete(url + '?pool=pool-without-cache&name=cache-api.cache.tests.powerdns.com.&type=AAAA', headers=headers, timeout=self._webTimeout)
+ self.assertEqual(r.status_code, 404)
+
+ # no 'pool'
+ r = requests.delete(url + '?name=cache-api.cache.tests.powerdns.com.&type=AAAA', headers=headers, timeout=self._webTimeout)
+ self.assertEqual(r.status_code, 400)
+
+ # no 'name'
+ r = requests.delete(url + '?pool=pool-without-cache&type=AAAA', headers=headers, timeout=self._webTimeout)
+ self.assertEqual(r.status_code, 400)
+
+ # different name
+ r = requests.delete(url + '?pool=&name=not-cache-api.cache.tests.powerdns.com.', headers=headers, timeout=self._webTimeout)
+ self.assertTrue(r)
+ self.assertEqual(r.status_code, 200)
+ content = r.json()
+ self.assertIn('count', content)
+ self.assertEqual(int(content['count']), 0)
+
+ # different type
+ r = requests.delete(url + '?pool=&name=cache-api.cache.tests.powerdns.com.&type=A', headers=headers, timeout=self._webTimeout)
+ self.assertTrue(r)
+ self.assertEqual(r.status_code, 200)
+ content = r.json()
+ self.assertIn('count', content)
+ self.assertEqual(int(content['count']), 0)
+
+ # should still be a hit
+ (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+ self.assertEqual(receivedResponse, response)
+
+ # remove
+ r = requests.delete(url + '?pool=&name=cache-api.cache.tests.powerdns.com.&type=AAAA', headers=headers, timeout=self._webTimeout)
+ self.assertTrue(r)
+ self.assertEqual(r.status_code, 200)
+ content = r.json()
+ self.assertIn('count', content)
+ self.assertEqual(int(content['count']), 1)
+
+ # should be a miss
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ self.assertTrue(receivedQuery)
+ self.assertTrue(receivedResponse)
+ receivedQuery.id = query.id
+ self.assertEqual(query, receivedQuery)
+ self.assertEqual(receivedResponse, response)
+
+ # remove all types
+ r = requests.delete(url + '?pool=&name=cache-api.cache.tests.powerdns.com.', headers=headers, timeout=self._webTimeout)
+ self.assertTrue(r)
+ self.assertEqual(r.status_code, 200)
+ content = r.json()
+ self.assertIn('count', content)
+ self.assertEqual(int(content['count']), 1)
+
+ # should be a miss
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ self.assertTrue(receivedQuery)
+ self.assertTrue(receivedResponse)
+ receivedQuery.id = query.id
+ self.assertEqual(query, receivedQuery)
+ self.assertEqual(receivedResponse, response)
+
+ # suffix removal
+ r = requests.delete(url + '?pool=&name=cache.tests.powerdns.com.&suffix=true', headers=headers, timeout=self._webTimeout)
+ self.assertTrue(r)
+ self.assertEqual(r.status_code, 200)
+ content = r.json()
+ self.assertIn('count', content)
+ self.assertEqual(int(content['count']), 1)
+
+ # should be a miss
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ self.assertTrue(receivedQuery)
+ self.assertTrue(receivedResponse)
+ receivedQuery.id = query.id
+ self.assertEqual(query, receivedQuery)
+ self.assertEqual(receivedResponse, response)