2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 #include "webserver.hh"
29 #include "threadname.hh"
37 #include "uuid-utils.hh"
38 #include <yahttp/router.hpp>
40 json11::Json
HttpRequest::json()
43 if(this->body
.empty()) {
44 SLOG(g_log
<<Logger::Debug
<<logprefix
<<"JSON document expected in request body, but body was empty" << endl
,
45 d_slog
->info(Logr::Debug
, "JSON document expected in request body, but body was empty"));
46 throw HttpBadRequestException();
48 json11::Json doc
= json11::Json::parse(this->body
, err
);
50 SLOG(g_log
<<Logger::Debug
<<logprefix
<<"parsing of JSON document failed:" << err
<< endl
,
51 d_slog
->error(Logr::Debug
, err
, "parsing of JSON document failed"));
52 throw HttpBadRequestException();
57 bool HttpRequest::compareAuthorization(const CredentialsHolder
& credentials
) const
60 auto header
= headers
.find("authorization");
62 if (header
!= headers
.end() && toLower(header
->second
).find("basic ") == 0) {
63 string cookie
= header
->second
.substr(6);
66 B64Decode(cookie
, plain
);
68 vector
<string
> cparts
;
69 stringtok(cparts
, plain
, ":");
71 auth_ok
= (cparts
.size() == 2 && credentials
.matches(cparts
[1].c_str()));
76 bool HttpRequest::compareHeader(const string
&header_name
, const string
&expected_value
) const
78 auto header
= headers
.find(header_name
);
79 if (header
== headers
.end()) {
83 // this gets rid of terminating zeros
84 return (0==strcmp(header
->second
.c_str(), expected_value
.c_str()));
87 bool HttpRequest::compareHeader(const string
&header_name
, const CredentialsHolder
& credentials
) const
89 auto header
= headers
.find(header_name
);
90 if (header
== headers
.end()) {
94 return credentials
.matches(header
->second
);
97 void HttpResponse::setPlainBody(const string
& document
)
99 this->headers
["Content-Type"] = "text/plain; charset=utf-8";
101 this->body
= document
;
104 void HttpResponse::setYamlBody(const string
& document
)
106 this->headers
["Content-Type"] = "application/x-yaml";
108 this->body
= document
;
111 void HttpResponse::setJsonBody(const string
& document
)
113 this->headers
["Content-Type"] = "application/json";
115 this->body
= document
;
118 void HttpResponse::setJsonBody(const json11::Json
& document
)
120 this->headers
["Content-Type"] = "application/json";
122 document
.dump(this->body
);
125 void HttpResponse::setErrorResult(const std::string
& message
, const int status_
)
127 setJsonBody(json11::Json::object
{ { "error", message
} });
128 this->status
= status_
;
131 void HttpResponse::setSuccessResult(const std::string
& message
, const int status_
)
133 setJsonBody(json11::Json::object
{ { "result", message
} });
134 this->status
= status_
;
137 static void bareHandlerWrapper(const WebServer::HandlerFunction
& handler
, YaHTTP::Request
* req
, YaHTTP::Response
* resp
)
139 // wrapper to convert from YaHTTP::* to our subclasses
140 handler(static_cast<HttpRequest
*>(req
), static_cast<HttpResponse
*>(resp
));
143 void WebServer::registerBareHandler(const string
& url
, const HandlerFunction
& handler
)
145 YaHTTP::THandlerFunction f
= [=](YaHTTP::Request
* req
, YaHTTP::Response
* resp
){return bareHandlerWrapper(handler
, req
, resp
);};
146 YaHTTP::Router::Any(url
, f
);
149 static bool optionsHandler(HttpRequest
* req
, HttpResponse
* resp
) {
150 if (req
->method
== "OPTIONS") {
151 resp
->headers
["access-control-allow-origin"] = "*";
152 resp
->headers
["access-control-allow-headers"] = "Content-Type, X-API-Key";
153 resp
->headers
["access-control-allow-methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS";
154 resp
->headers
["access-control-max-age"] = "3600";
156 resp
->headers
["content-type"]= "text/plain";
163 void WebServer::apiWrapper(const WebServer::HandlerFunction
& handler
, HttpRequest
* req
, HttpResponse
* resp
, bool allowPassword
) {
164 if (optionsHandler(req
, resp
)) return;
166 resp
->headers
["access-control-allow-origin"] = "*";
169 SLOG(g_log
<<Logger::Error
<<req
->logprefix
<<"HTTP API Request \"" << req
->url
.path
<< "\": Authentication failed, API Key missing in config" << endl
,
170 d_slog
->info(Logr::Error
, "Authentication failed, API Key missing in config", "urlpath", Logging::Loggable(req
->url
.path
)));
171 throw HttpUnauthorizedException("X-API-Key");
174 bool auth_ok
= req
->compareHeader("x-api-key", *d_apikey
) || d_apikey
->matches(req
->getvars
["api-key"]);
176 if (!auth_ok
&& allowPassword
) {
177 if (d_webserverPassword
) {
178 auth_ok
= req
->compareAuthorization(*d_webserverPassword
);
185 SLOG(g_log
<<Logger::Error
<<req
->logprefix
<<"HTTP Request \"" << req
->url
.path
<< "\": Authentication by API Key failed" << endl
,
186 d_slog
->info(Logr::Error
, "Authentication by API Key failed", "urlpath", Logging::Loggable(req
->url
.path
)));
187 throw HttpUnauthorizedException("X-API-Key");
191 resp
->headers
["X-Content-Type-Options"] = "nosniff";
192 resp
->headers
["X-Frame-Options"] = "deny";
193 resp
->headers
["X-Permitted-Cross-Domain-Policies"] = "none";
194 resp
->headers
["X-XSS-Protection"] = "1; mode=block";
195 resp
->headers
["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'";
197 req
->getvars
.erase("_"); // jQuery cache buster
202 } catch (ApiException
&e
) {
203 resp
->setErrorResult(e
.what(), 422);
205 } catch (JsonException
&e
) {
206 resp
->setErrorResult(e
.what(), 422);
210 if (resp
->status
== 204) {
211 // No Content -> no Content-Type.
212 resp
->headers
.erase("Content-Type");
216 void WebServer::registerApiHandler(const string
& url
, const HandlerFunction
& handler
, bool allowPassword
) {
217 auto f
= [=](HttpRequest
*req
, HttpResponse
* resp
){apiWrapper(handler
, req
, resp
, allowPassword
);};
218 registerBareHandler(url
, f
);
221 void WebServer::webWrapper(const WebServer::HandlerFunction
& handler
, HttpRequest
* req
, HttpResponse
* resp
) {
222 if (d_webserverPassword
) {
223 bool auth_ok
= req
->compareAuthorization(*d_webserverPassword
);
225 SLOG(g_log
<<Logger::Debug
<<req
->logprefix
<<"HTTP Request \"" << req
->url
.path
<< "\": Web Authentication failed" << endl
,
226 d_slog
->info(Logr::Debug
, "HTTP Request: Web Authentication failed", "urlpath", Logging::Loggable(req
->url
.path
)));
227 throw HttpUnauthorizedException("Basic");
234 void WebServer::registerWebHandler(const string
& url
, const HandlerFunction
& handler
) {
235 auto f
= [=](HttpRequest
*req
, HttpResponse
*resp
){webWrapper(handler
, req
, resp
);};
236 registerBareHandler(url
, f
);
239 static void *WebServerConnectionThreadStart(const WebServer
* webServer
, std::shared_ptr
<Socket
> client
) {
240 setThreadName("rec/webhndlr");
241 const std::string msg
= "Exception while serving a connection in main webserver thread";
243 webServer
->serveConnection(client
);
245 catch(PDNSException
&e
) {
246 SLOG(g_log
<<Logger::Error
<<"PDNSException while serving a connection in main webserver thread: "<<e
.reason
<<endl
,
247 webServer
->d_slog
->error(Logr::Error
, e
.reason
, msg
, "exception", Logging::Loggable("PDNSException")));
249 catch(std::exception
&e
) {
250 SLOG(g_log
<<Logger::Error
<<"STL Exception while serving a connection in main webserver thread: "<<e
.what()<<endl
,
251 webServer
->d_slog
->error(Logr::Error
, e
.what(), msg
, "exception", Logging::Loggable("std::exception")));
254 SLOG(g_log
<<Logger::Error
<<"Unknown exception while serving a connection in main webserver thread"<<endl
,
255 webServer
->d_slog
->info(Logr::Error
, msg
));
260 void WebServer::handleRequest(HttpRequest
& req
, HttpResponse
& resp
) const
262 // set default headers
263 resp
.headers
["Content-Type"] = "text/html; charset=utf-8";
266 auto log
= req
.d_slog
->withValues("urlpath", Logging::Loggable(req
.url
.path
));
271 SLOG(g_log
<<Logger::Debug
<<req
.logprefix
<<"Incomplete request" << endl
,
272 d_slog
->info(Logr::Debug
, "Incomplete request"));
273 throw HttpBadRequestException();
275 SLOG(g_log
<<Logger::Debug
<<req
.logprefix
<<"Handling request \"" << req
.url
.path
<< "\"" << endl
,
276 log
->info(Logr::Debug
, "Handling request"));
278 YaHTTP::strstr_map_t::iterator header
;
280 if ((header
= req
.headers
.find("accept")) != req
.headers
.end()) {
281 // yaml wins over json, json wins over html
282 if (header
->second
.find("application/x-yaml") != std::string::npos
) {
283 req
.accept_yaml
= true;
284 } else if (header
->second
.find("text/x-yaml") != std::string::npos
) {
285 req
.accept_yaml
= true;
286 } else if (header
->second
.find("application/json") != std::string::npos
) {
287 req
.accept_json
= true;
288 } else if (header
->second
.find("text/html") != std::string::npos
) {
289 req
.accept_html
= true;
293 YaHTTP::THandlerFunction handler
;
294 if (!YaHTTP::Router::Route(&req
, handler
)) {
295 SLOG(g_log
<<Logger::Debug
<<req
.logprefix
<<"No route found for \"" << req
.url
.path
<< "\"" << endl
,
296 log
->info(Logr::Debug
, "No route found"));
297 throw HttpNotFoundException();
300 const string msg
= "HTTP ISE Exception";
302 handler(&req
, &resp
);
303 SLOG(g_log
<<Logger::Debug
<<req
.logprefix
<<"Result for \"" << req
.url
.path
<< "\": " << resp
.status
<< ", body length: " << resp
.body
.size() << endl
,
304 log
->info(Logr::Debug
, "Result", "status", Logging::Loggable(resp
.status
), "bodyLength", Logging::Loggable(resp
.body
.size())));
306 catch(HttpException
&) {
309 catch(PDNSException
&e
) {
310 SLOG(g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": Exception: " << e
.reason
<< endl
,
311 log
->error(Logr::Error
, e
.reason
, msg
, "exception", Logging::Loggable("PDNSException")));
312 throw HttpInternalServerErrorException();
314 catch(std::exception
&e
) {
315 SLOG(g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": STL Exception: " << e
.what() << endl
,
316 log
->error(Logr::Error
, e
.what(), msg
, "exception", Logging::Loggable("std::exception")));
317 throw HttpInternalServerErrorException();
320 SLOG(g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": Unknown Exception" << endl
,
321 log
->info(Logr::Error
, msg
));
322 throw HttpInternalServerErrorException();
325 catch(HttpException
&e
) {
328 // An HttpException does not initialize d_slog
333 // TODO rm this logline?
334 SLOG(g_log
<<Logger::Debug
<<req
.logprefix
<<"Error result for \"" << req
.url
.path
<< "\": " << resp
.status
<< endl
,
335 d_slog
->error(Logr::Debug
, resp
.status
, "Error result", "urlpath", Logging::Loggable(req
.url
.path
)));
336 string what
= YaHTTP::Utility::status2text(resp
.status
);
337 if (req
.accept_json
) {
338 resp
.headers
["Content-Type"] = "application/json";
339 if (resp
.body
.empty()) {
340 resp
.setErrorResult(what
, resp
.status
);
342 } else if (req
.accept_html
) {
343 resp
.headers
["Content-Type"] = "text/html; charset=utf-8";
344 resp
.body
= "<!html><title>" + what
+ "</title><h1>" + what
+ "</h1>";
346 resp
.headers
["Content-Type"] = "text/plain; charset=utf-8";
351 // always set these headers
352 resp
.headers
["Connection"] = "close";
354 if (req
.method
== "HEAD") {
357 resp
.headers
["Content-Length"] = std::to_string(resp
.body
.size());
362 // Helper to log key-value maps used by YaHTTP
364 std::string
Logging::IterLoggable
<YaHTTP::strstr_map_t::const_iterator
>::to_string() const
366 std::ostringstream oss
;
368 for (auto i
= _t1
; i
!= _t2
; i
++) {
375 oss
<< i
->first
<< ": " << i
->second
;
381 void WebServer::logRequest(const HttpRequest
& req
, [[maybe_unused
]] const ComboAddress
& remote
) const {
382 if (d_loglevel
>= WebServer::LogLevel::Detailed
) {
384 if (!g_slogStructured
) {
386 auto logprefix
= req
.logprefix
;
387 g_log
<<Logger::Notice
<<logprefix
<<"Request details:"<<endl
;
390 for (const auto& r
: req
.getvars
) {
393 g_log
<<Logger::Notice
<<logprefix
<<" GET params:"<<endl
;
395 g_log
<<Logger::Notice
<<logprefix
<<" "<<r
.first
<<": "<<r
.second
<<endl
;
399 for (const auto& r
: req
.postvars
) {
402 g_log
<<Logger::Notice
<<logprefix
<<" POST params:"<<endl
;
404 g_log
<<Logger::Notice
<<logprefix
<<" "<<r
.first
<<": "<<r
.second
<<endl
;
408 for (const auto& h
: req
.headers
) {
411 g_log
<<Logger::Notice
<<logprefix
<<" Headers:"<<endl
;
413 g_log
<<Logger::Notice
<<logprefix
<<" "<<h
.first
<<": "<<h
.second
<<endl
;
416 if (req
.body
.empty()) {
417 g_log
<<Logger::Notice
<<logprefix
<<" No body"<<endl
;
419 g_log
<<Logger::Notice
<<logprefix
<<" Full body: "<<endl
;
420 g_log
<<Logger::Notice
<<logprefix
<<" "<<req
.body
<<endl
;
425 req
.d_slog
->info(Logr::Info
, "Request details", "getParams", Logging::IterLoggable(req
.getvars
.cbegin(), req
.getvars
.cend()),
426 "postParams", Logging::IterLoggable(req
.postvars
.cbegin(), req
.postvars
.cend()),
427 "body", Logging::Loggable(req
.body
),
428 "address", Logging::Loggable(remote
));
434 void WebServer::logResponse(const HttpResponse
& resp
, const ComboAddress
& /* remote */, const string
& logprefix
) const {
435 if (d_loglevel
>= WebServer::LogLevel::Detailed
) {
437 if (!g_slogStructured
) {
439 g_log
<<Logger::Notice
<<logprefix
<<"Response details:"<<endl
;
441 for (const auto& h
: resp
.headers
) {
444 g_log
<<Logger::Notice
<<logprefix
<<" Headers:"<<endl
;
446 g_log
<<Logger::Notice
<<logprefix
<<" "<<h
.first
<<": "<<h
.second
<<endl
;
448 if (resp
.body
.empty()) {
449 g_log
<<Logger::Notice
<<logprefix
<<" No body"<<endl
;
451 g_log
<<Logger::Notice
<<logprefix
<<" Full body: "<<endl
;
452 g_log
<<Logger::Notice
<<logprefix
<<" "<<resp
.body
<<endl
;
457 resp
.d_slog
->info(Logr::Info
, "Response details", "headers", Logging::IterLoggable(resp
.headers
.cbegin(), resp
.headers
.cend()),
458 "body", Logging::Loggable(resp
.body
));
464 void WebServer::serveConnection(const std::shared_ptr
<Socket
>& client
) const {
465 const auto unique
= getUniqueID();
466 const string logprefix
= d_logprefix
+ to_string(unique
) + " ";
468 HttpRequest
req(logprefix
);
472 auto log
= d_slog
->withValues("uniqueid", Logging::Loggable(to_string(unique
)));
476 resp
.max_response_size
=d_maxbodysize
;
481 YaHTTP::AsyncRequestLoader yarl
;
482 yarl
.initialize(&req
);
483 req
.max_request_size
=d_maxbodysize
;
485 client
->setNonBlocking();
488 while(!req
.complete
) {
491 bytes
= client
->readWithTimeout(buf
, sizeof(buf
), timeout
);
493 string data
= string(buf
, bytes
);
494 req
.complete
= yarl
.feed(data
);
501 } catch (YaHTTP::ParseError
&e
) {
502 // request stays incomplete
503 SLOG(g_log
<<Logger::Warning
<<logprefix
<<"Unable to parse request: "<<e
.what()<<endl
,
504 d_slog
->error(Logr::Warning
, e
.what(), "Unable to parse request"));
507 // Uses of `remote` below guarded by d_loglevel
508 if (d_loglevel
> WebServer::LogLevel::None
) {
509 client
->getRemote(remote
);
512 logRequest(req
, remote
);
514 WebServer::handleRequest(req
, resp
);
519 logResponse(resp
, remote
, logprefix
);
521 client
->writenWithTimeout(reply
.c_str(), reply
.size(), timeout
);
523 catch(PDNSException
&e
) {
524 SLOG(g_log
<<Logger::Error
<<logprefix
<<"HTTP Exception: "<<e
.reason
<<endl
,
525 d_slog
->error(Logr::Error
, e
.reason
, "HTTP Exception", "exception", Logging::Loggable("PDNSException")));
527 catch(std::exception
&e
) {
528 if(strstr(e
.what(), "timeout")==nullptr)
529 SLOG(g_log
<<Logger::Error
<<logprefix
<<"HTTP STL Exception: "<<e
.what()<<endl
,
530 d_slog
->error(Logr::Error
, e
.what(), "HTTP Exception", "exception", Logging::Loggable("std::exception")));
533 SLOG(g_log
<<Logger::Error
<<logprefix
<<"Unknown exception"<<endl
,
534 d_slog
->info(Logr::Error
, "HTTP Exception"));
537 if (d_loglevel
>= WebServer::LogLevel::Normal
) {
538 SLOG(g_log
<<Logger::Notice
<<logprefix
<<remote
<<" \""<<req
.method
<<" "<<YaHTTP::Utility::encodeURL(req
.url
.path
)<<" HTTP/"<<req
.versionStr(req
.version
)<<"\" "<<resp
.status
<<" "<<reply
.size()<<endl
,
539 d_slog
->info(Logr::Info
, "Request", "remote", Logging::Loggable(remote
), "method", Logging::Loggable(req
.method
),
540 "urlpath", Logging::Loggable(req
.url
.path
), "HTTPVersion", Logging::Loggable(req
.versionStr(req
.version
)),
541 "status", Logging::Loggable(resp
.status
), "respsize", Logging::Loggable(reply
.size())));
545 WebServer::WebServer(string listenaddress
, int port
) :
546 d_listenaddress(std::move(listenaddress
)),
549 d_maxbodysize(2*1024*1024)
553 void WebServer::bind()
556 d_server
= createServer();
557 SLOG(g_log
<<Logger::Warning
<<d_logprefix
<<"Listening for HTTP requests on "<<d_server
->d_local
.toStringWithPort()<<endl
,
558 d_slog
->info(Logr::Info
, "Listening for HTTP requests", "address", Logging::Loggable(d_server
->d_local
)));
560 catch(NetworkError
&e
) {
561 SLOG(g_log
<<Logger::Error
<<d_logprefix
<<"Listening on HTTP socket failed: "<<e
.what()<<endl
,
562 d_slog
->error(Logr::Error
, e
.what(), "Listening on HTTP socket failed", "exception", Logging::Loggable("NetworkError")));
571 const string msg
= "Exception in main webserver thread";
574 const string acceptmsg
= "Exception while accepting a connection in main webserver thread";
576 auto client
= d_server
->accept();
580 if (client
->acl(d_acl
)) {
581 std::thread
webHandler(WebServerConnectionThreadStart
, this, client
);
585 if (client
->getRemote(remote
))
586 g_log
<<Logger::Error
<<d_logprefix
<<"Webserver closing socket: remote ("<< remote
.toString() <<") does not match the set ACL("<<d_acl
.toString()<<")"<<endl
;
589 catch(PDNSException
&e
) {
590 SLOG(g_log
<<Logger::Error
<<d_logprefix
<<"PDNSException while accepting a connection in main webserver thread: "<<e
.reason
<<endl
,
591 d_slog
->error(Logr::Error
, e
.reason
, acceptmsg
, Logging::Loggable("PDNSException")));
593 catch(std::exception
&e
) {
594 SLOG(g_log
<<Logger::Error
<<d_logprefix
<<"STL Exception while accepting a connection in main webserver thread: "<<e
.what()<<endl
,
595 d_slog
->error(Logr::Error
, e
.what(), acceptmsg
, Logging::Loggable("std::exception")));
598 SLOG(g_log
<<Logger::Error
<<d_logprefix
<<"Unknown exception while accepting a connection in main webserver thread"<<endl
,
599 d_slog
->info(Logr::Error
, msg
));
603 catch(PDNSException
&e
) {
604 SLOG(g_log
<<Logger::Error
<<d_logprefix
<<"PDNSException in main webserver thread: "<<e
.reason
<<endl
,
605 d_slog
->error(Logr::Error
, e
.reason
, msg
, Logging::Loggable("PDNSException")));
607 catch(std::exception
&e
) {
608 SLOG(g_log
<<Logger::Error
<<d_logprefix
<<"STL Exception in main webserver thread: "<<e
.what()<<endl
,
609 d_slog
->error(Logr::Error
, e
.what(), msg
, Logging::Loggable("std::exception")));
612 SLOG(g_log
<<Logger::Error
<<d_logprefix
<<"Unknown exception in main webserver thread"<<endl
,
613 d_slog
->info(Logr::Error
, msg
));