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"
36 #include "uuid-utils.hh"
37 #include <yahttp/router.hpp>
39 json11::Json
HttpRequest::json()
42 if(this->body
.empty()) {
43 g_log
<<Logger::Debug
<<logprefix
<<"JSON document expected in request body, but body was empty" << endl
;
44 throw HttpBadRequestException();
46 json11::Json doc
= json11::Json::parse(this->body
, err
);
48 g_log
<<Logger::Debug
<<logprefix
<<"parsing of JSON document failed:" << err
<< endl
;
49 throw HttpBadRequestException();
54 bool HttpRequest::compareAuthorization(const string
&expected_password
)
57 YaHTTP::strstr_map_t::iterator header
= headers
.find("authorization");
59 if (header
!= headers
.end() && toLower(header
->second
).find("basic ") == 0) {
60 string cookie
= header
->second
.substr(6);
63 B64Decode(cookie
, plain
);
65 vector
<string
> cparts
;
66 stringtok(cparts
, plain
, ":");
68 // this gets rid of terminating zeros
69 auth_ok
= (cparts
.size()==2 && (0==strcmp(cparts
[1].c_str(), expected_password
.c_str())));
74 bool HttpRequest::compareHeader(const string
&header_name
, const string
&expected_value
)
76 YaHTTP::strstr_map_t::iterator header
= headers
.find(header_name
);
77 if (header
== headers
.end())
80 // this gets rid of terminating zeros
81 return (0==strcmp(header
->second
.c_str(), expected_value
.c_str()));
85 void HttpResponse::setBody(const json11::Json
& document
)
87 document
.dump(this->body
);
90 void HttpResponse::setErrorResult(const std::string
& message
, const int status_
)
92 setBody(json11::Json::object
{ { "error", message
} });
93 this->status
= status_
;
96 void HttpResponse::setSuccessResult(const std::string
& message
, const int status_
)
98 setBody(json11::Json::object
{ { "result", message
} });
99 this->status
= status_
;
102 static void bareHandlerWrapper(WebServer::HandlerFunction handler
, YaHTTP::Request
* req
, YaHTTP::Response
* resp
)
104 // wrapper to convert from YaHTTP::* to our subclasses
105 handler(static_cast<HttpRequest
*>(req
), static_cast<HttpResponse
*>(resp
));
108 void WebServer::registerBareHandler(const string
& url
, HandlerFunction handler
)
110 YaHTTP::THandlerFunction f
= boost::bind(&bareHandlerWrapper
, handler
, _1
, _2
);
111 YaHTTP::Router::Any(url
, f
);
114 static bool optionsHandler(HttpRequest
* req
, HttpResponse
* resp
) {
115 if (req
->method
== "OPTIONS") {
116 resp
->headers
["access-control-allow-origin"] = "*";
117 resp
->headers
["access-control-allow-headers"] = "Content-Type, X-API-Key";
118 resp
->headers
["access-control-allow-methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS";
119 resp
->headers
["access-control-max-age"] = "3600";
121 resp
->headers
["content-type"]= "text/plain";
128 void WebServer::apiWrapper(WebServer::HandlerFunction handler
, HttpRequest
* req
, HttpResponse
* resp
, bool allowPassword
) {
129 if (optionsHandler(req
, resp
)) return;
131 resp
->headers
["access-control-allow-origin"] = "*";
133 if (d_apikey
.empty()) {
134 g_log
<<Logger::Error
<<req
->logprefix
<<"HTTP API Request \"" << req
->url
.path
<< "\": Authentication failed, API Key missing in config" << endl
;
135 throw HttpUnauthorizedException("X-API-Key");
138 bool auth_ok
= req
->compareHeader("x-api-key", d_apikey
) || req
->getvars
["api-key"] == d_apikey
;
140 if (!auth_ok
&& allowPassword
) {
141 if (!d_webserverPassword
.empty()) {
142 auth_ok
= req
->compareAuthorization(d_webserverPassword
);
149 g_log
<<Logger::Error
<<req
->logprefix
<<"HTTP Request \"" << req
->url
.path
<< "\": Authentication by API Key failed" << endl
;
150 throw HttpUnauthorizedException("X-API-Key");
153 resp
->headers
["Content-Type"] = "application/json";
156 resp
->headers
["X-Content-Type-Options"] = "nosniff";
157 resp
->headers
["X-Frame-Options"] = "deny";
158 resp
->headers
["X-Permitted-Cross-Domain-Policies"] = "none";
159 resp
->headers
["X-XSS-Protection"] = "1; mode=block";
160 resp
->headers
["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'";
162 req
->getvars
.erase("_"); // jQuery cache buster
167 } catch (ApiException
&e
) {
168 resp
->setErrorResult(e
.what(), 422);
170 } catch (JsonException
&e
) {
171 resp
->setErrorResult(e
.what(), 422);
175 if (resp
->status
== 204) {
176 // No Content -> no Content-Type.
177 resp
->headers
.erase("Content-Type");
181 void WebServer::registerApiHandler(const string
& url
, HandlerFunction handler
, bool allowPassword
) {
182 HandlerFunction f
= boost::bind(&WebServer::apiWrapper
, this, handler
, _1
, _2
, allowPassword
);
183 registerBareHandler(url
, f
);
186 void WebServer::webWrapper(WebServer::HandlerFunction handler
, HttpRequest
* req
, HttpResponse
* resp
) {
187 if (!d_webserverPassword
.empty()) {
188 bool auth_ok
= req
->compareAuthorization(d_webserverPassword
);
190 g_log
<<Logger::Debug
<<req
->logprefix
<<"HTTP Request \"" << req
->url
.path
<< "\": Web Authentication failed" << endl
;
191 throw HttpUnauthorizedException("Basic");
198 void WebServer::registerWebHandler(const string
& url
, HandlerFunction handler
) {
199 HandlerFunction f
= boost::bind(&WebServer::webWrapper
, this, handler
, _1
, _2
);
200 registerBareHandler(url
, f
);
203 static void *WebServerConnectionThreadStart(const WebServer
* webServer
, std::shared_ptr
<Socket
> client
) {
204 setThreadName("pdns-r/webhndlr");
205 webServer
->serveConnection(client
);
209 void WebServer::handleRequest(HttpRequest
& req
, HttpResponse
& resp
) const
211 // set default headers
212 resp
.headers
["Content-Type"] = "text/html; charset=utf-8";
216 g_log
<<Logger::Debug
<<req
.logprefix
<<"Incomplete request" << endl
;
217 throw HttpBadRequestException();
220 g_log
<<Logger::Debug
<<req
.logprefix
<<"Handling request \"" << req
.url
.path
<< "\"" << endl
;
222 YaHTTP::strstr_map_t::iterator header
;
224 if ((header
= req
.headers
.find("accept")) != req
.headers
.end()) {
225 // json wins over html
226 if (header
->second
.find("application/json") != std::string::npos
) {
227 req
.accept_json
= true;
228 } else if (header
->second
.find("text/html") != std::string::npos
) {
229 req
.accept_html
= true;
233 YaHTTP::THandlerFunction handler
;
234 if (!YaHTTP::Router::Route(&req
, handler
)) {
235 g_log
<<Logger::Debug
<<req
.logprefix
<<"No route found for \"" << req
.url
.path
<< "\"" << endl
;
236 throw HttpNotFoundException();
240 handler(&req
, &resp
);
241 g_log
<<Logger::Debug
<<req
.logprefix
<<"Result for \"" << req
.url
.path
<< "\": " << resp
.status
<< ", body length: " << resp
.body
.size() << endl
;
243 catch(HttpException
&) {
246 catch(PDNSException
&e
) {
247 g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": Exception: " << e
.reason
<< endl
;
248 throw HttpInternalServerErrorException();
250 catch(std::exception
&e
) {
251 g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": STL Exception: " << e
.what() << endl
;
252 throw HttpInternalServerErrorException();
255 g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": Unknown Exception" << endl
;
256 throw HttpInternalServerErrorException();
259 catch(HttpException
&e
) {
261 // TODO rm this logline?
262 g_log
<<Logger::Debug
<<req
.logprefix
<<"Error result for \"" << req
.url
.path
<< "\": " << resp
.status
<< endl
;
263 string what
= YaHTTP::Utility::status2text(resp
.status
);
264 if(req
.accept_html
) {
265 resp
.headers
["Content-Type"] = "text/html; charset=utf-8";
266 resp
.body
= "<!html><title>" + what
+ "</title><h1>" + what
+ "</h1>";
267 } else if (req
.accept_json
) {
268 resp
.headers
["Content-Type"] = "application/json";
269 if (resp
.body
.empty()) {
270 resp
.setErrorResult(what
, resp
.status
);
273 resp
.headers
["Content-Type"] = "text/plain; charset=utf-8";
278 // always set these headers
279 resp
.headers
["Server"] = "PowerDNS/" VERSION
;
280 resp
.headers
["Connection"] = "close";
282 if (req
.method
== "HEAD") {
285 resp
.headers
["Content-Length"] = std::to_string(resp
.body
.size());
289 void WebServer::logRequest(const HttpRequest
& req
, const ComboAddress
& remote
) const {
290 if (d_loglevel
>= WebServer::LogLevel::Detailed
) {
291 auto logprefix
= req
.logprefix
;
292 g_log
<<Logger::Notice
<<logprefix
<<"Request details:"<<endl
;
295 for (const auto& r
: req
.getvars
) {
298 g_log
<<Logger::Notice
<<logprefix
<<" GET params:"<<endl
;
300 g_log
<<Logger::Notice
<<logprefix
<<" "<<r
.first
<<": "<<r
.second
<<endl
;
304 for (const auto& r
: req
.postvars
) {
307 g_log
<<Logger::Notice
<<logprefix
<<" POST params:"<<endl
;
309 g_log
<<Logger::Notice
<<logprefix
<<" "<<r
.first
<<": "<<r
.second
<<endl
;
313 for (const auto& h
: req
.headers
) {
316 g_log
<<Logger::Notice
<<logprefix
<<" Headers:"<<endl
;
318 g_log
<<Logger::Notice
<<logprefix
<<" "<<h
.first
<<": "<<h
.second
<<endl
;
321 if (req
.body
.empty()) {
322 g_log
<<Logger::Notice
<<logprefix
<<" No body"<<endl
;
324 g_log
<<Logger::Notice
<<logprefix
<<" Full body: "<<endl
;
325 g_log
<<Logger::Notice
<<logprefix
<<" "<<req
.body
<<endl
;
330 void WebServer::logResponse(const HttpResponse
& resp
, const ComboAddress
& remote
, const string
& logprefix
) const {
331 if (d_loglevel
>= WebServer::LogLevel::Detailed
) {
332 g_log
<<Logger::Notice
<<logprefix
<<"Response details:"<<endl
;
334 for (const auto& h
: resp
.headers
) {
337 g_log
<<Logger::Notice
<<logprefix
<<" Headers:"<<endl
;
339 g_log
<<Logger::Notice
<<logprefix
<<" "<<h
.first
<<": "<<h
.second
<<endl
;
341 if (resp
.body
.empty()) {
342 g_log
<<Logger::Notice
<<logprefix
<<" No body"<<endl
;
344 g_log
<<Logger::Notice
<<logprefix
<<" Full body: "<<endl
;
345 g_log
<<Logger::Notice
<<logprefix
<<" "<<resp
.body
<<endl
;
350 void WebServer::serveConnection(std::shared_ptr
<Socket
> client
) const {
351 const string logprefix
= d_logprefix
+ to_string(getUniqueID()) + " ";
353 HttpRequest
req(logprefix
);
359 YaHTTP::AsyncRequestLoader yarl
;
360 yarl
.initialize(&req
);
362 client
->setNonBlocking();
365 while(!req
.complete
) {
368 bytes
= client
->readWithTimeout(buf
, sizeof(buf
), timeout
);
370 string data
= string(buf
, bytes
);
371 req
.complete
= yarl
.feed(data
);
378 } catch (YaHTTP::ParseError
&e
) {
379 // request stays incomplete
380 g_log
<<Logger::Warning
<<logprefix
<<"Unable to parse request: "<<e
.what()<<endl
;
383 if (d_loglevel
>= WebServer::LogLevel::None
) {
384 client
->getRemote(remote
);
387 logRequest(req
, remote
);
389 WebServer::handleRequest(req
, resp
);
394 logResponse(resp
, remote
, logprefix
);
396 client
->writenWithTimeout(reply
.c_str(), reply
.size(), timeout
);
398 catch(PDNSException
&e
) {
399 g_log
<<Logger::Error
<<logprefix
<<"HTTP Exception: "<<e
.reason
<<endl
;
401 catch(std::exception
&e
) {
402 if(strstr(e
.what(), "timeout")==0)
403 g_log
<<Logger::Error
<<logprefix
<<"HTTP STL Exception: "<<e
.what()<<endl
;
406 g_log
<<Logger::Error
<<logprefix
<<"Unknown exception"<<endl
;
409 if (d_loglevel
>= WebServer::LogLevel::Normal
) {
410 g_log
<<Logger::Notice
<<logprefix
<<remote
<<" \""<<req
.method
<<" "<<req
.url
.path
<<" HTTP/"<<req
.versionStr(req
.version
)<<"\" "<<resp
.status
<<" "<<reply
.size()<<endl
;
414 WebServer::WebServer(const string
&listenaddress
, int port
) :
415 d_listenaddress(listenaddress
),
421 void WebServer::bind()
424 d_server
= createServer();
425 g_log
<<Logger::Warning
<<d_logprefix
<<"Listening for HTTP requests on "<<d_server
->d_local
.toStringWithPort()<<endl
;
427 catch(NetworkError
&e
) {
428 g_log
<<Logger::Error
<<d_logprefix
<<"Listening on HTTP socket failed: "<<e
.what()<<endl
;
440 auto client
= d_server
->accept();
444 if (client
->acl(d_acl
)) {
445 std::thread
webHandler(WebServerConnectionThreadStart
, this, client
);
449 if (client
->getRemote(remote
))
450 g_log
<<Logger::Error
<<d_logprefix
<<"Webserver closing socket: remote ("<< remote
.toString() <<") does not match the set ACL("<<d_acl
.toString()<<")"<<endl
;
453 catch(PDNSException
&e
) {
454 g_log
<<Logger::Error
<<d_logprefix
<<"PDNSException while accepting a connection in main webserver thread: "<<e
.reason
<<endl
;
456 catch(std::exception
&e
) {
457 g_log
<<Logger::Error
<<d_logprefix
<<"STL Exception while accepting a connection in main webserver thread: "<<e
.what()<<endl
;
460 g_log
<<Logger::Error
<<d_logprefix
<<"Unknown exception while accepting a connection in main webserver thread"<<endl
;
464 catch(PDNSException
&e
) {
465 g_log
<<Logger::Error
<<d_logprefix
<<"PDNSException in main webserver thread: "<<e
.reason
<<endl
;
467 catch(std::exception
&e
) {
468 g_log
<<Logger::Error
<<d_logprefix
<<"STL Exception in main webserver thread: "<<e
.what()<<endl
;
471 g_log
<<Logger::Error
<<d_logprefix
<<"Unknown exception in main webserver thread"<<endl
;