]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/webserver.cc
Merge pull request #7628 from tcely/patch-3
[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 <yahttp/router.hpp>
37
38 json11::Json HttpRequest::json()
39 {
40 string err;
41 if(this->body.empty()) {
42 g_log<<Logger::Debug<<"HTTP: JSON document expected in request body, but body was empty" << endl;
43 throw HttpBadRequestException();
44 }
45 json11::Json doc = json11::Json::parse(this->body, err);
46 if (doc.is_null()) {
47 g_log<<Logger::Debug<<"HTTP: parsing of JSON document failed:" << err << endl;
48 throw HttpBadRequestException();
49 }
50 return doc;
51 }
52
53 bool HttpRequest::compareAuthorization(const string &expected_password)
54 {
55 // validate password
56 YaHTTP::strstr_map_t::iterator header = headers.find("authorization");
57 bool auth_ok = false;
58 if (header != headers.end() && toLower(header->second).find("basic ") == 0) {
59 string cookie = header->second.substr(6);
60
61 string plain;
62 B64Decode(cookie, plain);
63
64 vector<string> cparts;
65 stringtok(cparts, plain, ":");
66
67 // this gets rid of terminating zeros
68 auth_ok = (cparts.size()==2 && (0==strcmp(cparts[1].c_str(), expected_password.c_str())));
69 }
70 return auth_ok;
71 }
72
73 bool HttpRequest::compareHeader(const string &header_name, const string &expected_value)
74 {
75 YaHTTP::strstr_map_t::iterator header = headers.find(header_name);
76 if (header == headers.end())
77 return false;
78
79 // this gets rid of terminating zeros
80 return (0==strcmp(header->second.c_str(), expected_value.c_str()));
81 }
82
83
84 void HttpResponse::setBody(const json11::Json& document)
85 {
86 document.dump(this->body);
87 }
88
89 void HttpResponse::setErrorResult(const std::string& message, const int status_)
90 {
91 setBody(json11::Json::object { { "error", message } });
92 this->status = status_;
93 }
94
95 void HttpResponse::setSuccessResult(const std::string& message, const int status_)
96 {
97 setBody(json11::Json::object { { "result", message } });
98 this->status = status_;
99 }
100
101 static void bareHandlerWrapper(WebServer::HandlerFunction handler, YaHTTP::Request* req, YaHTTP::Response* resp)
102 {
103 // wrapper to convert from YaHTTP::* to our subclasses
104 handler(static_cast<HttpRequest*>(req), static_cast<HttpResponse*>(resp));
105 }
106
107 void WebServer::registerBareHandler(const string& url, HandlerFunction handler)
108 {
109 YaHTTP::THandlerFunction f = boost::bind(&bareHandlerWrapper, handler, _1, _2);
110 YaHTTP::Router::Any(url, f);
111 }
112
113 static bool optionsHandler(HttpRequest* req, HttpResponse* resp) {
114 if (req->method == "OPTIONS") {
115 resp->headers["access-control-allow-origin"] = "*";
116 resp->headers["access-control-allow-headers"] = "Content-Type, X-API-Key";
117 resp->headers["access-control-allow-methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS";
118 resp->headers["access-control-max-age"] = "3600";
119 resp->status = 200;
120 resp->headers["content-type"]= "text/plain";
121 resp->body = "";
122 return true;
123 }
124 return false;
125 }
126
127 static void apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp, const string &apikey) {
128 if (optionsHandler(req, resp)) return;
129
130 resp->headers["access-control-allow-origin"] = "*";
131
132 if (apikey.empty()) {
133 g_log<<Logger::Error<<"HTTP API Request \"" << req->url.path << "\": Authentication failed, API Key missing in config" << endl;
134 throw HttpUnauthorizedException("X-API-Key");
135 }
136 bool auth_ok = req->compareHeader("x-api-key", apikey) || req->getvars["api-key"] == apikey;
137
138 if (!auth_ok) {
139 g_log<<Logger::Error<<"HTTP Request \"" << req->url.path << "\": Authentication by API Key failed" << endl;
140 throw HttpUnauthorizedException("X-API-Key");
141 }
142
143 resp->headers["Content-Type"] = "application/json";
144
145 // security headers
146 resp->headers["X-Content-Type-Options"] = "nosniff";
147 resp->headers["X-Frame-Options"] = "deny";
148 resp->headers["X-Permitted-Cross-Domain-Policies"] = "none";
149 resp->headers["X-XSS-Protection"] = "1; mode=block";
150 resp->headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'";
151
152 req->getvars.erase("_"); // jQuery cache buster
153
154 try {
155 resp->status = 200;
156 handler(req, resp);
157 } catch (ApiException &e) {
158 resp->setErrorResult(e.what(), 422);
159 return;
160 } catch (JsonException &e) {
161 resp->setErrorResult(e.what(), 422);
162 return;
163 }
164
165 if (resp->status == 204) {
166 // No Content -> no Content-Type.
167 resp->headers.erase("Content-Type");
168 }
169 }
170
171 void WebServer::registerApiHandler(const string& url, HandlerFunction handler) {
172 HandlerFunction f = boost::bind(&apiWrapper, handler, _1, _2, d_apikey);
173 registerBareHandler(url, f);
174 d_registerApiHandlerCalled = true;
175 }
176
177 static void webWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp, const string &password) {
178 if (!password.empty()) {
179 bool auth_ok = req->compareAuthorization(password);
180 if (!auth_ok) {
181 g_log<<Logger::Debug<<"HTTP Request \"" << req->url.path << "\": Web Authentication failed" << endl;
182 throw HttpUnauthorizedException("Basic");
183 }
184 }
185
186 handler(req, resp);
187 }
188
189 void WebServer::registerWebHandler(const string& url, HandlerFunction handler) {
190 HandlerFunction f = boost::bind(&webWrapper, handler, _1, _2, d_webserverPassword);
191 registerBareHandler(url, f);
192 }
193
194 static void *WebServerConnectionThreadStart(const WebServer* webServer, std::shared_ptr<Socket> client) {
195 setThreadName("pdns-r/webhndlr");
196 webServer->serveConnection(client);
197 return nullptr;
198 }
199
200 void WebServer::handleRequest(HttpRequest& req, HttpResponse& resp) const
201 {
202 // set default headers
203 resp.headers["Content-Type"] = "text/html; charset=utf-8";
204
205 try {
206 if (!req.complete) {
207 g_log<<Logger::Debug<<"HTTP: Incomplete request" << endl;
208 throw HttpBadRequestException();
209 }
210
211 g_log<<Logger::Debug<<"HTTP: Handling request \"" << req.url.path << "\"" << endl;
212
213 YaHTTP::strstr_map_t::iterator header;
214
215 if ((header = req.headers.find("accept")) != req.headers.end()) {
216 // json wins over html
217 if (header->second.find("application/json") != std::string::npos) {
218 req.accept_json = true;
219 } else if (header->second.find("text/html") != std::string::npos) {
220 req.accept_html = true;
221 }
222 }
223
224 YaHTTP::THandlerFunction handler;
225 if (!YaHTTP::Router::Route(&req, handler)) {
226 g_log<<Logger::Debug<<"HTTP: No route found for \"" << req.url.path << "\"" << endl;
227 throw HttpNotFoundException();
228 }
229
230 try {
231 handler(&req, &resp);
232 g_log<<Logger::Debug<<"HTTP: Result for \"" << req.url.path << "\": " << resp.status << ", body length: " << resp.body.size() << endl;
233 }
234 catch(HttpException&) {
235 throw;
236 }
237 catch(PDNSException &e) {
238 g_log<<Logger::Error<<"HTTP ISE for \""<< req.url.path << "\": Exception: " << e.reason << endl;
239 throw HttpInternalServerErrorException();
240 }
241 catch(std::exception &e) {
242 g_log<<Logger::Error<<"HTTP ISE for \""<< req.url.path << "\": STL Exception: " << e.what() << endl;
243 throw HttpInternalServerErrorException();
244 }
245 catch(...) {
246 g_log<<Logger::Error<<"HTTP ISE for \""<< req.url.path << "\": Unknown Exception" << endl;
247 throw HttpInternalServerErrorException();
248 }
249 }
250 catch(HttpException &e) {
251 resp = e.response();
252 g_log<<Logger::Debug<<"HTTP: Error result for \"" << req.url.path << "\": " << resp.status << endl;
253 string what = YaHTTP::Utility::status2text(resp.status);
254 if(req.accept_html) {
255 resp.headers["Content-Type"] = "text/html; charset=utf-8";
256 resp.body = "<!html><title>" + what + "</title><h1>" + what + "</h1>";
257 } else if (req.accept_json) {
258 resp.headers["Content-Type"] = "application/json";
259 if (resp.body.empty()) {
260 resp.setErrorResult(what, resp.status);
261 }
262 } else {
263 resp.headers["Content-Type"] = "text/plain; charset=utf-8";
264 resp.body = what;
265 }
266 }
267
268 // always set these headers
269 resp.headers["Server"] = "PowerDNS/" VERSION;
270 resp.headers["Connection"] = "close";
271
272 if (req.method == "HEAD") {
273 resp.body = "";
274 } else {
275 resp.headers["Content-Length"] = std::to_string(resp.body.size());
276 }
277 }
278
279 void WebServer::serveConnection(std::shared_ptr<Socket> client) const
280 try {
281 HttpRequest req;
282 YaHTTP::AsyncRequestLoader yarl;
283 yarl.initialize(&req);
284 int timeout = 5;
285 client->setNonBlocking();
286
287 try {
288 while(!req.complete) {
289 int bytes;
290 char buf[1024];
291 bytes = client->readWithTimeout(buf, sizeof(buf), timeout);
292 if (bytes > 0) {
293 string data = string(buf, bytes);
294 req.complete = yarl.feed(data);
295 } else {
296 // read error OR EOF
297 break;
298 }
299 }
300 yarl.finalize();
301 } catch (YaHTTP::ParseError &e) {
302 // request stays incomplete
303 }
304
305 HttpResponse resp;
306 WebServer::handleRequest(req, resp);
307 ostringstream ss;
308 resp.write(ss);
309 string reply = ss.str();
310
311 client->writenWithTimeout(reply.c_str(), reply.size(), timeout);
312 }
313 catch(PDNSException &e) {
314 g_log<<Logger::Error<<"HTTP Exception: "<<e.reason<<endl;
315 }
316 catch(std::exception &e) {
317 if(strstr(e.what(), "timeout")==0)
318 g_log<<Logger::Error<<"HTTP STL Exception: "<<e.what()<<endl;
319 }
320 catch(...) {
321 g_log<<Logger::Error<<"HTTP: Unknown exception"<<endl;
322 }
323
324 WebServer::WebServer(const string &listenaddress, int port) :
325 d_listenaddress(listenaddress),
326 d_port(port),
327 d_server(nullptr)
328 {
329 }
330
331 void WebServer::bind()
332 {
333 try {
334 d_server = createServer();
335 g_log<<Logger::Warning<<"Listening for HTTP requests on "<<d_server->d_local.toStringWithPort()<<endl;
336 }
337 catch(NetworkError &e) {
338 g_log<<Logger::Error<<"Listening on HTTP socket failed: "<<e.what()<<endl;
339 d_server = nullptr;
340 }
341 }
342
343 void WebServer::go()
344 {
345 if(!d_server)
346 return;
347 try {
348 while(true) {
349 try {
350 auto client = d_server->accept();
351 if (!client) {
352 continue;
353 }
354 if (client->acl(d_acl)) {
355 std::thread webHandler(WebServerConnectionThreadStart, this, client);
356 webHandler.detach();
357 } else {
358 ComboAddress remote;
359 if (client->getRemote(remote))
360 g_log<<Logger::Error<<"Webserver closing socket: remote ("<< remote.toString() <<") does not match the set ACL("<<d_acl.toString()<<")"<<endl;
361 }
362 }
363 catch(PDNSException &e) {
364 g_log<<Logger::Error<<"PDNSException while accepting a connection in main webserver thread: "<<e.reason<<endl;
365 }
366 catch(std::exception &e) {
367 g_log<<Logger::Error<<"STL Exception while accepting a connection in main webserver thread: "<<e.what()<<endl;
368 }
369 catch(...) {
370 g_log<<Logger::Error<<"Unknown exception while accepting a connection in main webserver thread"<<endl;
371 }
372 }
373 }
374 catch(PDNSException &e) {
375 g_log<<Logger::Error<<"PDNSException in main webserver thread: "<<e.reason<<endl;
376 }
377 catch(std::exception &e) {
378 g_log<<Logger::Error<<"STL Exception in main webserver thread: "<<e.what()<<endl;
379 }
380 catch(...) {
381 g_log<<Logger::Error<<"Unknown exception in main webserver thread"<<endl;
382 }
383 _exit(1);
384 }