From c563cbe5b9390bb609362e68d982df36c319f3e9 Mon Sep 17 00:00:00 2001 From: Chris Hofstaedtler Date: Fri, 15 Feb 2019 22:19:27 +0100 Subject: [PATCH] recursor webserver: allow accessing some API endpoints using password Fixes #5942. --- pdns/webserver.cc | 16 ++++++++++++---- pdns/webserver.hh | 4 ++-- pdns/ws-recursor.cc | 6 +++--- regression-tests.api/runtests.py | 5 ++++- regression-tests.api/test_Basics.py | 4 ++++ regression-tests.api/test_Servers.py | 8 ++++++++ regression-tests.api/test_helper.py | 1 + regression-tests.dnsdist/test_API.py | 1 + 8 files changed, 35 insertions(+), 10 deletions(-) diff --git a/pdns/webserver.cc b/pdns/webserver.cc index 5c221d1e7..fa8df4837 100644 --- a/pdns/webserver.cc +++ b/pdns/webserver.cc @@ -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<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); } diff --git a/pdns/webserver.hh b/pdns/webserver.hh index e7d94b948..d7848847f 100644 --- a/pdns/webserver.hh +++ b/pdns/webserver.hh @@ -176,7 +176,7 @@ public: void handleRequest(HttpRequest& request, HttpResponse& resp) const; typedef boost::function 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 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); diff --git a/pdns/ws-recursor.cc b/pdns/ws-recursor.cc index bf4038a2a..fefb03bb0 100644 --- a/pdns/ws-recursor.cc +++ b/pdns/ws-recursor.cc @@ -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/", &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); diff --git a/regression-tests.api/runtests.py b/regression-tests.api/runtests.py index 4ef9c1ca2..56d5e3e7b 100755 --- a/regression-tests.api/runtests.py +++ b/regression-tests.api/runtests.py @@ -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, diff --git a/regression-tests.api/test_Basics.py b/regression-tests.api/test_Basics.py index eca56505d..f180e55e4 100644 --- a/regression-tests.api/test_Basics.py +++ b/regression-tests.api/test_Basics.py @@ -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) diff --git a/regression-tests.api/test_Servers.py b/regression-tests.api/test_Servers.py index bc24d08c5..6b30359e3 100644 --- a/regression-tests.api/test_Servers.py +++ b/regression-tests.api/test_Servers.py @@ -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) diff --git a/regression-tests.api/test_helper.py b/regression-tests.api/test_helper.py index 9ef00b919..9a6ee0282 100644 --- a/regression-tests.api/test_helper.py +++ b/regression-tests.api/test_helper.py @@ -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)} diff --git a/regression-tests.dnsdist/test_API.py b/regression-tests.dnsdist/test_API.py index 8ec87804a..4a0bbd986 100644 --- a/regression-tests.dnsdist/test_API.py +++ b/regression-tests.dnsdist/test_API.py @@ -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 -- 2.40.0