]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
recursor webserver: allow accessing some API endpoints using password
authorChris Hofstaedtler <chris.hofstaedtler@deduktiva.com>
Fri, 15 Feb 2019 21:19:27 +0000 (22:19 +0100)
committerChris Hofstaedtler <chris.hofstaedtler@deduktiva.com>
Tue, 21 May 2019 12:28:34 +0000 (14:28 +0200)
Fixes #5942.

pdns/webserver.cc
pdns/webserver.hh
pdns/ws-recursor.cc
regression-tests.api/runtests.py
regression-tests.api/test_Basics.py
regression-tests.api/test_Servers.py
regression-tests.api/test_helper.py
regression-tests.dnsdist/test_API.py

index 5c221d1e7c0b30c847bc8cb097acdf035e5428fa..fa8df4837f9086117613c400aa79e9e911853755 100644 (file)
@@ -125,7 +125,7 @@ static bool optionsHandler(HttpRequest* req, HttpResponse* resp) {
   return false;
 }
 
-void WebServer::apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp) {
+void WebServer::apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp, bool allowPassword) {
   if (optionsHandler(req, resp)) return;
 
   resp->headers["access-control-allow-origin"] = "*";
@@ -136,7 +136,15 @@ void WebServer::apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req,
   }
 
   bool auth_ok = req->compareHeader("x-api-key", d_apikey) || req->getvars["api-key"] == d_apikey;
-  
+
+  if (!auth_ok && allowPassword) {
+    if (!d_webserverPassword.empty()) {
+      auth_ok = req->compareAuthorization(d_webserverPassword);
+    } else {
+      auth_ok = true;
+    }
+  }
+
   if (!auth_ok) {
     g_log<<Logger::Error<<req->logprefix<<"HTTP Request \"" << req->url.path << "\": Authentication by API Key failed" << endl;
     throw HttpUnauthorizedException("X-API-Key");
@@ -170,8 +178,8 @@ void WebServer::apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req,
   }
 }
 
-void WebServer::registerApiHandler(const string& url, HandlerFunction handler) {
-  HandlerFunction f = boost::bind(&WebServer::apiWrapper, this, handler, _1, _2);
+void WebServer::registerApiHandler(const string& url, HandlerFunction handler, bool allowPassword) {
+  HandlerFunction f = boost::bind(&WebServer::apiWrapper, this, handler, _1, _2, allowPassword);
   registerBareHandler(url, f);
 }
 
index e7d94b948941c4f2458669ed40e5e24d919c67cc..d7848847f3aab4dc75ab8a4c8f3d2af247b0bd14 100644 (file)
@@ -176,7 +176,7 @@ public:
   void handleRequest(HttpRequest& request, HttpResponse& resp) const;
 
   typedef boost::function<void(HttpRequest* req, HttpResponse* resp)> HandlerFunction;
-  void registerApiHandler(const string& url, HandlerFunction handler);
+  void registerApiHandler(const string& url, HandlerFunction handler, bool allowPassword=false);
   void registerWebHandler(const string& url, HandlerFunction handler);
 
   enum class LogLevel : uint8_t {
@@ -227,7 +227,7 @@ protected:
   std::shared_ptr<Server> d_server;
 
   std::string d_apikey;
-  void apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp);
+  void apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp, bool allowPassword);
   std::string d_webserverPassword;
   void webWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp);
 
index bf4038a2a51a92fbd10aa9489134dd855368e34e..fefb03bb06bd4ba2ae2e0ced85d3484cef59567c 100644 (file)
@@ -467,16 +467,16 @@ RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
   d_ws->bind();
 
   // legacy dispatch
-  d_ws->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat, this, _1, _2));
+  d_ws->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat, this, _1, _2), true);
   d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
   d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-from", &apiServerConfigAllowFrom);
   d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
   d_ws->registerApiHandler("/api/v1/servers/localhost/rpzstatistics", &apiServerRPZStats);
   d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
-  d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
+  d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics, true);
   d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
   d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
-  d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
+  d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail, true);
   d_ws->registerApiHandler("/api/v1/servers", &apiServer);
   d_ws->registerApiHandler("/api", &apiDiscovery);
 
index 4ef9c1ca26ba2115a4ffd926dc072cca9c75d3d5..56d5e3e7b2d2bad68ed062e3e18fce95dc36a5d4 100755 (executable)
@@ -20,6 +20,7 @@ SQLITE_DB = 'pdns.sqlite3'
 WEBPORT = 5556
 DNSPORT = 5300
 APIKEY = '1234567890abcdefghijklmnopq-key'
+WEBPASSWORD = 'something'
 PDNSUTIL_CMD = [os.environ.get("PDNSUTIL", "../pdns/pdnsutil"), "--config-dir=."]
 
 NAMED_CONF_TPL = """
@@ -103,7 +104,8 @@ pdns_recursor = os.environ.get("PDNSRECURSOR", "../pdns/recursordist/pdns_recurs
 common_args = [
     "--daemon=no", "--socket-dir=.", "--config-dir=.",
     "--local-address=127.0.0.1", "--local-port="+str(DNSPORT),
-    "--webserver=yes", "--webserver-port="+str(WEBPORT), "--webserver-address=127.0.0.1", "--webserver-password=something",
+    "--webserver=yes", "--webserver-port="+str(WEBPORT), "--webserver-address=127.0.0.1",
+    "--webserver-password="+WEBPASSWORD,
     "--api-key="+APIKEY
 ]
 
@@ -188,6 +190,7 @@ returncode = 0
 test_env = {}
 test_env.update(os.environ)
 test_env.update({
+    'WEBPASSWORD': WEBPASSWORD,
     'WEBPORT': str(WEBPORT),
     'APIKEY': APIKEY,
     'DAEMON': daemon,
index eca56505de2955124cd218f42d5a026178342022..f180e55e471c0265a56c4694222688f7c51799eb 100644 (file)
@@ -10,6 +10,10 @@ class TestBasics(ApiTestCase):
         r = requests.get(self.url("/api/v1/servers/localhost"))
         self.assertEquals(r.status_code, requests.codes.unauthorized)
 
+    def test_index_html(self):
+        r = requests.get(self.url("/"), auth=('admin', self.server_web_password))
+        self.assertEquals(r.status_code, requests.codes.ok)
+
     def test_split_request(self):
         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
index bc24d08c5b8f5a04295b87546ab64e19000bf9c7..6b30359e3861c486bd029d9c1d4d59bab05949f3 100644 (file)
@@ -1,3 +1,5 @@
+import requests
+import unittest
 from test_helper import ApiTestCase, is_auth, is_recursor
 
 
@@ -68,3 +70,9 @@ class Servers(ApiTestCase):
         r = self.session.get(self.url("/api/v1/servers/localhost/statistics?statistic=uptimeAAAA"))
         self.assertEquals(r.status_code, 422)
         self.assertIn("Unknown statistic name", r.json()['error'])
+
+    @unittest.skipIf(is_auth(), "Not applicable")
+    def test_read_statistics_using_password(self):
+        r = requests.get(self.url("/api/v1/servers/localhost/statistics"), auth=('admin', self.server_web_password))
+        self.assertEquals(r.status_code, requests.codes.ok)
+        self.assert_success_json(r)
index 9ef00b919c76defa3bf82b843c0140858a3f3d2c..9a6ee028207492a990e26585bf6a28772c55cb5e 100644 (file)
@@ -25,6 +25,7 @@ class ApiTestCase(unittest.TestCase):
         self.server_address = '127.0.0.1'
         self.server_port = int(os.environ.get('WEBPORT', '5580'))
         self.server_url = 'http://%s:%s/' % (self.server_address, self.server_port)
+        self.server_web_password = os.environ.get('WEBPASSWORD', 'MISSING')
         self.session = requests.Session()
         self.session.headers = {'X-API-Key': os.environ.get('APIKEY', 'changeme-key'), 'Origin': 'http://%s:%s' % (self.server_address, self.server_port)}
 
index 8ec87804a7f352e00a4a611830e668d210db6149..4a0bbd9869ccc08b98b76cc8c40a81cd12b7be3b 100644 (file)
@@ -57,6 +57,7 @@ class TestAPIBasics(DNSDistTest):
             url = 'http://127.0.0.1:' + str(self._webServerPort) + path
             r = requests.get(url, headers=headers, timeout=self._webTimeout)
             self.assertEquals(r.status_code, 401)
+
     def testBasicAuthOnly(self):
         """
         API: Basic Authentication Only