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 static void apiWrapper(WebServer::HandlerFunction handler
, HttpRequest
* req
, HttpResponse
* resp
, const string
&apikey
) {
129 if (optionsHandler(req
, resp
)) return;
131 resp
->headers
["access-control-allow-origin"] = "*";
133 if (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");
137 bool auth_ok
= req
->compareHeader("x-api-key", apikey
) || req
->getvars
["api-key"] == apikey
;
140 g_log
<<Logger::Error
<<req
->logprefix
<<"HTTP Request \"" << req
->url
.path
<< "\": Authentication by API Key failed" << endl
;
141 throw HttpUnauthorizedException("X-API-Key");
144 resp
->headers
["Content-Type"] = "application/json";
147 resp
->headers
["X-Content-Type-Options"] = "nosniff";
148 resp
->headers
["X-Frame-Options"] = "deny";
149 resp
->headers
["X-Permitted-Cross-Domain-Policies"] = "none";
150 resp
->headers
["X-XSS-Protection"] = "1; mode=block";
151 resp
->headers
["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'";
153 req
->getvars
.erase("_"); // jQuery cache buster
158 } catch (ApiException
&e
) {
159 resp
->setErrorResult(e
.what(), 422);
161 } catch (JsonException
&e
) {
162 resp
->setErrorResult(e
.what(), 422);
166 if (resp
->status
== 204) {
167 // No Content -> no Content-Type.
168 resp
->headers
.erase("Content-Type");
172 void WebServer::registerApiHandler(const string
& url
, HandlerFunction handler
) {
173 HandlerFunction f
= boost::bind(&apiWrapper
, handler
, _1
, _2
, d_apikey
);
174 registerBareHandler(url
, f
);
175 d_registerApiHandlerCalled
= true;
178 static void webWrapper(WebServer::HandlerFunction handler
, HttpRequest
* req
, HttpResponse
* resp
, const string
&password
) {
179 if (!password
.empty()) {
180 bool auth_ok
= req
->compareAuthorization(password
);
182 g_log
<<Logger::Debug
<<req
->logprefix
<<"HTTP Request \"" << req
->url
.path
<< "\": Web Authentication failed" << endl
;
183 throw HttpUnauthorizedException("Basic");
190 void WebServer::registerWebHandler(const string
& url
, HandlerFunction handler
) {
191 HandlerFunction f
= boost::bind(&webWrapper
, handler
, _1
, _2
, d_webserverPassword
);
192 registerBareHandler(url
, f
);
195 static void *WebServerConnectionThreadStart(const WebServer
* webServer
, std::shared_ptr
<Socket
> client
) {
196 setThreadName("pdns-r/webhndlr");
197 webServer
->serveConnection(client
);
201 void WebServer::handleRequest(HttpRequest
& req
, HttpResponse
& resp
) const
203 // set default headers
204 resp
.headers
["Content-Type"] = "text/html; charset=utf-8";
208 g_log
<<Logger::Debug
<<req
.logprefix
<<"Incomplete request" << endl
;
209 throw HttpBadRequestException();
212 g_log
<<Logger::Debug
<<req
.logprefix
<<"Handling request \"" << req
.url
.path
<< "\"" << endl
;
214 YaHTTP::strstr_map_t::iterator header
;
216 if ((header
= req
.headers
.find("accept")) != req
.headers
.end()) {
217 // json wins over html
218 if (header
->second
.find("application/json") != std::string::npos
) {
219 req
.accept_json
= true;
220 } else if (header
->second
.find("text/html") != std::string::npos
) {
221 req
.accept_html
= true;
225 YaHTTP::THandlerFunction handler
;
226 if (!YaHTTP::Router::Route(&req
, handler
)) {
227 g_log
<<Logger::Debug
<<req
.logprefix
<<"No route found for \"" << req
.url
.path
<< "\"" << endl
;
228 throw HttpNotFoundException();
232 handler(&req
, &resp
);
233 g_log
<<Logger::Debug
<<req
.logprefix
<<"Result for \"" << req
.url
.path
<< "\": " << resp
.status
<< ", body length: " << resp
.body
.size() << endl
;
235 catch(HttpException
&) {
238 catch(PDNSException
&e
) {
239 g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": Exception: " << e
.reason
<< endl
;
240 throw HttpInternalServerErrorException();
242 catch(std::exception
&e
) {
243 g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": STL Exception: " << e
.what() << endl
;
244 throw HttpInternalServerErrorException();
247 g_log
<<Logger::Error
<<req
.logprefix
<<"HTTP ISE for \""<< req
.url
.path
<< "\": Unknown Exception" << endl
;
248 throw HttpInternalServerErrorException();
251 catch(HttpException
&e
) {
253 // TODO rm this logline?
254 g_log
<<Logger::Debug
<<req
.logprefix
<<"Error result for \"" << req
.url
.path
<< "\": " << resp
.status
<< endl
;
255 string what
= YaHTTP::Utility::status2text(resp
.status
);
256 if(req
.accept_html
) {
257 resp
.headers
["Content-Type"] = "text/html; charset=utf-8";
258 resp
.body
= "<!html><title>" + what
+ "</title><h1>" + what
+ "</h1>";
259 } else if (req
.accept_json
) {
260 resp
.headers
["Content-Type"] = "application/json";
261 if (resp
.body
.empty()) {
262 resp
.setErrorResult(what
, resp
.status
);
265 resp
.headers
["Content-Type"] = "text/plain; charset=utf-8";
270 // always set these headers
271 resp
.headers
["Server"] = "PowerDNS/" VERSION
;
272 resp
.headers
["Connection"] = "close";
274 if (req
.method
== "HEAD") {
277 resp
.headers
["Content-Length"] = std::to_string(resp
.body
.size());
281 void WebServer::logRequest(const HttpRequest
& req
, const ComboAddress
& remote
) const {
282 if (d_loglevel
>= WebServer::LogLevel::Detailed
) {
283 auto logprefix
= req
.logprefix
;
284 g_log
<<Logger::Notice
<<logprefix
<<"Request details:"<<endl
;
287 for (const auto& r
: req
.getvars
) {
290 g_log
<<Logger::Notice
<<logprefix
<<" GET params:"<<endl
;
292 g_log
<<Logger::Notice
<<logprefix
<<" "<<r
.first
<<": "<<r
.second
<<endl
;
296 for (const auto& r
: req
.postvars
) {
299 g_log
<<Logger::Notice
<<logprefix
<<" POST params:"<<endl
;
301 g_log
<<Logger::Notice
<<logprefix
<<" "<<r
.first
<<": "<<r
.second
<<endl
;
305 for (const auto& h
: req
.headers
) {
308 g_log
<<Logger::Notice
<<logprefix
<<" Headers:"<<endl
;
310 g_log
<<Logger::Notice
<<logprefix
<<" "<<h
.first
<<": "<<h
.second
<<endl
;
313 if (req
.body
.empty()) {
314 g_log
<<Logger::Notice
<<logprefix
<<" No body"<<endl
;
316 g_log
<<Logger::Notice
<<logprefix
<<" Full body: "<<endl
;
317 g_log
<<Logger::Notice
<<logprefix
<<" "<<req
.body
<<endl
;
322 void WebServer::logResponse(const HttpResponse
& resp
, const ComboAddress
& remote
, const string
& logprefix
) const {
323 if (d_loglevel
>= WebServer::LogLevel::Detailed
) {
324 g_log
<<Logger::Notice
<<logprefix
<<"Response details:"<<endl
;
326 for (const auto& h
: resp
.headers
) {
329 g_log
<<Logger::Notice
<<logprefix
<<" Headers:"<<endl
;
331 g_log
<<Logger::Notice
<<logprefix
<<" "<<h
.first
<<": "<<h
.second
<<endl
;
333 if (resp
.body
.empty()) {
334 g_log
<<Logger::Notice
<<logprefix
<<" No body"<<endl
;
336 g_log
<<Logger::Notice
<<logprefix
<<" Full body: "<<endl
;
337 g_log
<<Logger::Notice
<<logprefix
<<" "<<resp
.body
<<endl
;
342 void WebServer::serveConnection(std::shared_ptr
<Socket
> client
) const {
343 const string logprefix
= d_logprefix
+ to_string(getUniqueID()) + " ";
345 HttpRequest
req(logprefix
);
351 YaHTTP::AsyncRequestLoader yarl
;
352 yarl
.initialize(&req
);
354 client
->setNonBlocking();
357 while(!req
.complete
) {
360 bytes
= client
->readWithTimeout(buf
, sizeof(buf
), timeout
);
362 string data
= string(buf
, bytes
);
363 req
.complete
= yarl
.feed(data
);
370 } catch (YaHTTP::ParseError
&e
) {
371 // request stays incomplete
372 g_log
<<Logger::Warning
<<logprefix
<<"Unable to parse request: "<<e
.what()<<endl
;
375 if (d_loglevel
>= WebServer::LogLevel::None
) {
376 client
->getRemote(remote
);
379 logRequest(req
, remote
);
381 WebServer::handleRequest(req
, resp
);
386 logResponse(resp
, remote
, logprefix
);
388 client
->writenWithTimeout(reply
.c_str(), reply
.size(), timeout
);
390 catch(PDNSException
&e
) {
391 g_log
<<Logger::Error
<<logprefix
<<"HTTP Exception: "<<e
.reason
<<endl
;
393 catch(std::exception
&e
) {
394 if(strstr(e
.what(), "timeout")==0)
395 g_log
<<Logger::Error
<<logprefix
<<"HTTP STL Exception: "<<e
.what()<<endl
;
398 g_log
<<Logger::Error
<<logprefix
<<"Unknown exception"<<endl
;
401 if (d_loglevel
>= WebServer::LogLevel::Normal
) {
402 g_log
<<Logger::Notice
<<logprefix
<<remote
<<" \""<<req
.method
<<" "<<req
.url
.path
<<" HTTP/"<<req
.versionStr(req
.version
)<<"\" "<<resp
.status
<<" "<<reply
.size()<<endl
;
406 WebServer::WebServer(const string
&listenaddress
, int port
) :
407 d_listenaddress(listenaddress
),
413 void WebServer::bind()
416 d_server
= createServer();
417 g_log
<<Logger::Warning
<<d_logprefix
<<"Listening for HTTP requests on "<<d_server
->d_local
.toStringWithPort()<<endl
;
419 catch(NetworkError
&e
) {
420 g_log
<<Logger::Error
<<d_logprefix
<<"Listening on HTTP socket failed: "<<e
.what()<<endl
;
432 auto client
= d_server
->accept();
436 if (client
->acl(d_acl
)) {
437 std::thread
webHandler(WebServerConnectionThreadStart
, this, client
);
441 if (client
->getRemote(remote
))
442 g_log
<<Logger::Error
<<d_logprefix
<<"Webserver closing socket: remote ("<< remote
.toString() <<") does not match the set ACL("<<d_acl
.toString()<<")"<<endl
;
445 catch(PDNSException
&e
) {
446 g_log
<<Logger::Error
<<d_logprefix
<<"PDNSException while accepting a connection in main webserver thread: "<<e
.reason
<<endl
;
448 catch(std::exception
&e
) {
449 g_log
<<Logger::Error
<<d_logprefix
<<"STL Exception while accepting a connection in main webserver thread: "<<e
.what()<<endl
;
452 g_log
<<Logger::Error
<<d_logprefix
<<"Unknown exception while accepting a connection in main webserver thread"<<endl
;
456 catch(PDNSException
&e
) {
457 g_log
<<Logger::Error
<<d_logprefix
<<"PDNSException in main webserver thread: "<<e
.reason
<<endl
;
459 catch(std::exception
&e
) {
460 g_log
<<Logger::Error
<<d_logprefix
<<"STL Exception in main webserver thread: "<<e
.what()<<endl
;
463 g_log
<<Logger::Error
<<d_logprefix
<<"Unknown exception in main webserver thread"<<endl
;