]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/webserver.cc
Merge pull request #7615 from DNS-Leo/patch-1
[thirdparty/pdns.git] / pdns / webserver.cc
1 /*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
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.
8 *
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.
12 *
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.
17 *
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.
21 */
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 #include "utility.hh"
26 #include "webserver.hh"
27 #include "misc.hh"
28 #include <thread>
29 #include "threadname.hh"
30 #include <vector>
31 #include "logger.hh"
32 #include <stdio.h>
33 #include "dns.hh"
34 #include "base64.hh"
35 #include "json.hh"
36 #include "uuid-utils.hh"
37 #include <yahttp/router.hpp>
38
39 json11::Json HttpRequest::json()
40 {
41 string err;
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();
45 }
46 json11::Json doc = json11::Json::parse(this->body, err);
47 if (doc.is_null()) {
48 g_log<<Logger::Debug<<logprefix<<"parsing of JSON document failed:" << err << endl;
49 throw HttpBadRequestException();
50 }
51 return doc;
52 }
53
54 bool HttpRequest::compareAuthorization(const string &expected_password)
55 {
56 // validate password
57 YaHTTP::strstr_map_t::iterator header = headers.find("authorization");
58 bool auth_ok = false;
59 if (header != headers.end() && toLower(header->second).find("basic ") == 0) {
60 string cookie = header->second.substr(6);
61
62 string plain;
63 B64Decode(cookie, plain);
64
65 vector<string> cparts;
66 stringtok(cparts, plain, ":");
67
68 // this gets rid of terminating zeros
69 auth_ok = (cparts.size()==2 && (0==strcmp(cparts[1].c_str(), expected_password.c_str())));
70 }
71 return auth_ok;
72 }
73
74 bool HttpRequest::compareHeader(const string &header_name, const string &expected_value)
75 {
76 YaHTTP::strstr_map_t::iterator header = headers.find(header_name);
77 if (header == headers.end())
78 return false;
79
80 // this gets rid of terminating zeros
81 return (0==strcmp(header->second.c_str(), expected_value.c_str()));
82 }
83
84
85 void HttpResponse::setBody(const json11::Json& document)
86 {
87 document.dump(this->body);
88 }
89
90 void HttpResponse::setErrorResult(const std::string& message, const int status_)
91 {
92 setBody(json11::Json::object { { "error", message } });
93 this->status = status_;
94 }
95
96 void HttpResponse::setSuccessResult(const std::string& message, const int status_)
97 {
98 setBody(json11::Json::object { { "result", message } });
99 this->status = status_;
100 }
101
102 static void bareHandlerWrapper(WebServer::HandlerFunction handler, YaHTTP::Request* req, YaHTTP::Response* resp)
103 {
104 // wrapper to convert from YaHTTP::* to our subclasses
105 handler(static_cast<HttpRequest*>(req), static_cast<HttpResponse*>(resp));
106 }
107
108 void WebServer::registerBareHandler(const string& url, HandlerFunction handler)
109 {
110 YaHTTP::THandlerFunction f = boost::bind(&bareHandlerWrapper, handler, _1, _2);
111 YaHTTP::Router::Any(url, f);
112 }
113
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";
120 resp->status = 200;
121 resp->headers["content-type"]= "text/plain";
122 resp->body = "";
123 return true;
124 }
125 return false;
126 }
127
128 static void apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp, const string &apikey) {
129 if (optionsHandler(req, resp)) return;
130
131 resp->headers["access-control-allow-origin"] = "*";
132
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");
136 }
137 bool auth_ok = req->compareHeader("x-api-key", apikey) || req->getvars["api-key"] == apikey;
138
139 if (!auth_ok) {
140 g_log<<Logger::Error<<req->logprefix<<"HTTP Request \"" << req->url.path << "\": Authentication by API Key failed" << endl;
141 throw HttpUnauthorizedException("X-API-Key");
142 }
143
144 resp->headers["Content-Type"] = "application/json";
145
146 // security headers
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'";
152
153 req->getvars.erase("_"); // jQuery cache buster
154
155 try {
156 resp->status = 200;
157 handler(req, resp);
158 } catch (ApiException &e) {
159 resp->setErrorResult(e.what(), 422);
160 return;
161 } catch (JsonException &e) {
162 resp->setErrorResult(e.what(), 422);
163 return;
164 }
165
166 if (resp->status == 204) {
167 // No Content -> no Content-Type.
168 resp->headers.erase("Content-Type");
169 }
170 }
171
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;
176 }
177
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);
181 if (!auth_ok) {
182 g_log<<Logger::Debug<<req->logprefix<<"HTTP Request \"" << req->url.path << "\": Web Authentication failed" << endl;
183 throw HttpUnauthorizedException("Basic");
184 }
185 }
186
187 handler(req, resp);
188 }
189
190 void WebServer::registerWebHandler(const string& url, HandlerFunction handler) {
191 HandlerFunction f = boost::bind(&webWrapper, handler, _1, _2, d_webserverPassword);
192 registerBareHandler(url, f);
193 }
194
195 static void *WebServerConnectionThreadStart(const WebServer* webServer, std::shared_ptr<Socket> client) {
196 setThreadName("pdns-r/webhndlr");
197 webServer->serveConnection(client);
198 return nullptr;
199 }
200
201 void WebServer::handleRequest(HttpRequest& req, HttpResponse& resp) const
202 {
203 // set default headers
204 resp.headers["Content-Type"] = "text/html; charset=utf-8";
205
206 try {
207 if (!req.complete) {
208 g_log<<Logger::Debug<<req.logprefix<<"Incomplete request" << endl;
209 throw HttpBadRequestException();
210 }
211
212 g_log<<Logger::Debug<<req.logprefix<<"Handling request \"" << req.url.path << "\"" << endl;
213
214 YaHTTP::strstr_map_t::iterator header;
215
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;
222 }
223 }
224
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();
229 }
230
231 try {
232 handler(&req, &resp);
233 g_log<<Logger::Debug<<req.logprefix<<"Result for \"" << req.url.path << "\": " << resp.status << ", body length: " << resp.body.size() << endl;
234 }
235 catch(HttpException&) {
236 throw;
237 }
238 catch(PDNSException &e) {
239 g_log<<Logger::Error<<req.logprefix<<"HTTP ISE for \""<< req.url.path << "\": Exception: " << e.reason << endl;
240 throw HttpInternalServerErrorException();
241 }
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();
245 }
246 catch(...) {
247 g_log<<Logger::Error<<req.logprefix<<"HTTP ISE for \""<< req.url.path << "\": Unknown Exception" << endl;
248 throw HttpInternalServerErrorException();
249 }
250 }
251 catch(HttpException &e) {
252 resp = e.response();
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);
263 }
264 } else {
265 resp.headers["Content-Type"] = "text/plain; charset=utf-8";
266 resp.body = what;
267 }
268 }
269
270 // always set these headers
271 resp.headers["Server"] = "PowerDNS/" VERSION;
272 resp.headers["Connection"] = "close";
273
274 if (req.method == "HEAD") {
275 resp.body = "";
276 } else {
277 resp.headers["Content-Length"] = std::to_string(resp.body.size());
278 }
279 }
280
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;
285
286 bool first = true;
287 for (const auto& r : req.getvars) {
288 if (first) {
289 first = false;
290 g_log<<Logger::Notice<<logprefix<<" GET params:"<<endl;
291 }
292 g_log<<Logger::Notice<<logprefix<<" "<<r.first<<": "<<r.second<<endl;
293 }
294
295 first = true;
296 for (const auto& r : req.postvars) {
297 if (first) {
298 first = false;
299 g_log<<Logger::Notice<<logprefix<<" POST params:"<<endl;
300 }
301 g_log<<Logger::Notice<<logprefix<<" "<<r.first<<": "<<r.second<<endl;
302 }
303
304 first = true;
305 for (const auto& h : req.headers) {
306 if (first) {
307 first = false;
308 g_log<<Logger::Notice<<logprefix<<" Headers:"<<endl;
309 }
310 g_log<<Logger::Notice<<logprefix<<" "<<h.first<<": "<<h.second<<endl;
311 }
312
313 if (req.body.empty()) {
314 g_log<<Logger::Notice<<logprefix<<" No body"<<endl;
315 } else {
316 g_log<<Logger::Notice<<logprefix<<" Full body: "<<endl;
317 g_log<<Logger::Notice<<logprefix<<" "<<req.body<<endl;
318 }
319 }
320 }
321
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;
325 bool first = true;
326 for (const auto& h : resp.headers) {
327 if (first) {
328 first = false;
329 g_log<<Logger::Notice<<logprefix<<" Headers:"<<endl;
330 }
331 g_log<<Logger::Notice<<logprefix<<" "<<h.first<<": "<<h.second<<endl;
332 }
333 if (resp.body.empty()) {
334 g_log<<Logger::Notice<<logprefix<<" No body"<<endl;
335 } else {
336 g_log<<Logger::Notice<<logprefix<<" Full body: "<<endl;
337 g_log<<Logger::Notice<<logprefix<<" "<<resp.body<<endl;
338 }
339 }
340 }
341
342 void WebServer::serveConnection(std::shared_ptr<Socket> client) const {
343 const string logprefix = d_logprefix + to_string(getUniqueID()) + " ";
344
345 HttpRequest req(logprefix);
346 HttpResponse resp;
347 ComboAddress remote;
348 string reply;
349
350 try {
351 YaHTTP::AsyncRequestLoader yarl;
352 yarl.initialize(&req);
353 int timeout = 5;
354 client->setNonBlocking();
355
356 try {
357 while(!req.complete) {
358 int bytes;
359 char buf[1024];
360 bytes = client->readWithTimeout(buf, sizeof(buf), timeout);
361 if (bytes > 0) {
362 string data = string(buf, bytes);
363 req.complete = yarl.feed(data);
364 } else {
365 // read error OR EOF
366 break;
367 }
368 }
369 yarl.finalize();
370 } catch (YaHTTP::ParseError &e) {
371 // request stays incomplete
372 g_log<<Logger::Warning<<logprefix<<"Unable to parse request: "<<e.what()<<endl;
373 }
374
375 if (d_loglevel >= WebServer::LogLevel::None) {
376 client->getRemote(remote);
377 }
378
379 logRequest(req, remote);
380
381 WebServer::handleRequest(req, resp);
382 ostringstream ss;
383 resp.write(ss);
384 reply = ss.str();
385
386 logResponse(resp, remote, logprefix);
387
388 client->writenWithTimeout(reply.c_str(), reply.size(), timeout);
389 }
390 catch(PDNSException &e) {
391 g_log<<Logger::Error<<logprefix<<"HTTP Exception: "<<e.reason<<endl;
392 }
393 catch(std::exception &e) {
394 if(strstr(e.what(), "timeout")==0)
395 g_log<<Logger::Error<<logprefix<<"HTTP STL Exception: "<<e.what()<<endl;
396 }
397 catch(...) {
398 g_log<<Logger::Error<<logprefix<<"Unknown exception"<<endl;
399 }
400
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;
403 }
404 }
405
406 WebServer::WebServer(const string &listenaddress, int port) :
407 d_listenaddress(listenaddress),
408 d_port(port),
409 d_server(nullptr)
410 {
411 }
412
413 void WebServer::bind()
414 {
415 try {
416 d_server = createServer();
417 g_log<<Logger::Warning<<d_logprefix<<"Listening for HTTP requests on "<<d_server->d_local.toStringWithPort()<<endl;
418 }
419 catch(NetworkError &e) {
420 g_log<<Logger::Error<<d_logprefix<<"Listening on HTTP socket failed: "<<e.what()<<endl;
421 d_server = nullptr;
422 }
423 }
424
425 void WebServer::go()
426 {
427 if(!d_server)
428 return;
429 try {
430 while(true) {
431 try {
432 auto client = d_server->accept();
433 if (!client) {
434 continue;
435 }
436 if (client->acl(d_acl)) {
437 std::thread webHandler(WebServerConnectionThreadStart, this, client);
438 webHandler.detach();
439 } else {
440 ComboAddress remote;
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;
443 }
444 }
445 catch(PDNSException &e) {
446 g_log<<Logger::Error<<d_logprefix<<"PDNSException while accepting a connection in main webserver thread: "<<e.reason<<endl;
447 }
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;
450 }
451 catch(...) {
452 g_log<<Logger::Error<<d_logprefix<<"Unknown exception while accepting a connection in main webserver thread"<<endl;
453 }
454 }
455 }
456 catch(PDNSException &e) {
457 g_log<<Logger::Error<<d_logprefix<<"PDNSException in main webserver thread: "<<e.reason<<endl;
458 }
459 catch(std::exception &e) {
460 g_log<<Logger::Error<<d_logprefix<<"STL Exception in main webserver thread: "<<e.what()<<endl;
461 }
462 catch(...) {
463 g_log<<Logger::Error<<d_logprefix<<"Unknown exception in main webserver thread"<<endl;
464 }
465 _exit(1);
466 }