]>
Commit | Line | Data |
---|---|---|
12c86877 | 1 | /* |
6edbf68a PL |
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 | */ | |
870a0fe4 AT |
22 | #ifdef HAVE_CONFIG_H |
23 | #include "config.h" | |
24 | #endif | |
731f58b8 | 25 | #include "utility.hh" |
12c86877 | 26 | #include "webserver.hh" |
12c86877 | 27 | #include "misc.hh" |
d4c53d8c | 28 | #include <thread> |
519f5484 | 29 | #include "threadname.hh" |
4de01aaa | 30 | #include <utility> |
12c86877 BH |
31 | #include <vector> |
32 | #include "logger.hh" | |
33 | #include <stdio.h> | |
34 | #include "dns.hh" | |
2db9c30e | 35 | #include "base64.hh" |
33196945 | 36 | #include "json.hh" |
3f9a8002 | 37 | #include "uuid-utils.hh" |
583ea80d | 38 | #include <yahttp/router.hpp> |
35eb2fcf OM |
39 | #include <algorithm> |
40 | #include <unordered_set> | |
12c86877 | 41 | |
5938c49f CH |
42 | json11::Json HttpRequest::json() |
43 | { | |
44 | string err; | |
45 | if(this->body.empty()) { | |
6c88989a OM |
46 | SLOG(g_log<<Logger::Debug<<logprefix<<"JSON document expected in request body, but body was empty" << endl, |
47 | d_slog->info(Logr::Debug, "JSON document expected in request body, but body was empty")); | |
5938c49f CH |
48 | throw HttpBadRequestException(); |
49 | } | |
50 | json11::Json doc = json11::Json::parse(this->body, err); | |
51 | if (doc.is_null()) { | |
6c88989a OM |
52 | SLOG(g_log<<Logger::Debug<<logprefix<<"parsing of JSON document failed:" << err << endl, |
53 | d_slog->error(Logr::Debug, err, "parsing of JSON document failed")); | |
5938c49f CH |
54 | throw HttpBadRequestException(); |
55 | } | |
56 | return doc; | |
57 | } | |
58 | ||
40422f78 | 59 | bool HttpRequest::compareAuthorization(const CredentialsHolder& credentials) const |
bbef8f04 CH |
60 | { |
61 | // validate password | |
40422f78 | 62 | auto header = headers.find("authorization"); |
bbef8f04 CH |
63 | bool auth_ok = false; |
64 | if (header != headers.end() && toLower(header->second).find("basic ") == 0) { | |
65 | string cookie = header->second.substr(6); | |
66 | ||
67 | string plain; | |
68 | B64Decode(cookie, plain); | |
69 | ||
70 | vector<string> cparts; | |
71 | stringtok(cparts, plain, ":"); | |
72 | ||
40422f78 | 73 | auth_ok = (cparts.size() == 2 && credentials.matches(cparts[1].c_str())); |
bbef8f04 CH |
74 | } |
75 | return auth_ok; | |
76 | } | |
77 | ||
40422f78 | 78 | bool HttpRequest::compareHeader(const string &header_name, const string &expected_value) const |
bbef8f04 | 79 | { |
40422f78 RG |
80 | auto header = headers.find(header_name); |
81 | if (header == headers.end()) { | |
bbef8f04 | 82 | return false; |
40422f78 | 83 | } |
bbef8f04 CH |
84 | |
85 | // this gets rid of terminating zeros | |
86 | return (0==strcmp(header->second.c_str(), expected_value.c_str())); | |
87 | } | |
88 | ||
40422f78 RG |
89 | bool HttpRequest::compareHeader(const string &header_name, const CredentialsHolder& credentials) const |
90 | { | |
91 | auto header = headers.find(header_name); | |
92 | if (header == headers.end()) { | |
93 | return false; | |
94 | } | |
95 | ||
96 | return credentials.matches(header->second); | |
97 | } | |
98 | ||
917f686a KF |
99 | void HttpResponse::setPlainBody(const string& document) |
100 | { | |
101 | this->headers["Content-Type"] = "text/plain; charset=utf-8"; | |
102 | ||
103 | this->body = document; | |
104 | } | |
bbef8f04 | 105 | |
917f686a | 106 | void HttpResponse::setYamlBody(const string& document) |
5938c49f | 107 | { |
917f686a KF |
108 | this->headers["Content-Type"] = "application/x-yaml"; |
109 | ||
110 | this->body = document; | |
111 | } | |
112 | ||
113 | void HttpResponse::setJsonBody(const string& document) | |
114 | { | |
115 | this->headers["Content-Type"] = "application/json"; | |
116 | ||
117 | this->body = document; | |
118 | } | |
119 | ||
120 | void HttpResponse::setJsonBody(const json11::Json& document) | |
121 | { | |
122 | this->headers["Content-Type"] = "application/json"; | |
123 | ||
5938c49f CH |
124 | document.dump(this->body); |
125 | } | |
126 | ||
dd079764 | 127 | void HttpResponse::setErrorResult(const std::string& message, const int status_) |
692829aa | 128 | { |
917f686a | 129 | setJsonBody(json11::Json::object { { "error", message } }); |
dd079764 | 130 | this->status = status_; |
692829aa CH |
131 | } |
132 | ||
dd079764 | 133 | void HttpResponse::setSuccessResult(const std::string& message, const int status_) |
692829aa | 134 | { |
917f686a | 135 | setJsonBody(json11::Json::object { { "result", message } }); |
dd079764 | 136 | this->status = status_; |
692829aa CH |
137 | } |
138 | ||
4de01aaa | 139 | static void bareHandlerWrapper(const WebServer::HandlerFunction& handler, YaHTTP::Request* req, YaHTTP::Response* resp) |
12c86877 | 140 | { |
583ea80d CH |
141 | // wrapper to convert from YaHTTP::* to our subclasses |
142 | handler(static_cast<HttpRequest*>(req), static_cast<HttpResponse*>(resp)); | |
143 | } | |
232f0877 | 144 | |
478e1699 | 145 | void WebServer::registerBareHandler(const string& url, const HandlerFunction& handler, const std::string& method) |
583ea80d | 146 | { |
a88c05b2 | 147 | YaHTTP::THandlerFunction f = [=](YaHTTP::Request* req, YaHTTP::Response* resp){return bareHandlerWrapper(handler, req, resp);}; |
478e1699 | 148 | YaHTTP::Router::Map(method, url, std::move(f)); |
232f0877 CH |
149 | } |
150 | ||
4de01aaa | 151 | void WebServer::apiWrapper(const WebServer::HandlerFunction& handler, HttpRequest* req, HttpResponse* resp, bool allowPassword) { |
7f7481be AT |
152 | resp->headers["access-control-allow-origin"] = "*"; |
153 | ||
40422f78 | 154 | if (!d_apikey) { |
6c88989a | 155 | SLOG(g_log<<Logger::Error<<req->logprefix<<"HTTP API Request \"" << req->url.path << "\": Authentication failed, API Key missing in config" << endl, |
7ccb828a | 156 | d_slog->info(Logr::Error, "Authentication failed, API Key missing in config", "urlpath", Logging::Loggable(req->url.path))); |
53255086 | 157 | throw HttpUnauthorizedException("X-API-Key"); |
bbef8f04 | 158 | } |
7579a7b9 | 159 | |
40422f78 | 160 | bool auth_ok = req->compareHeader("x-api-key", *d_apikey) || d_apikey->matches(req->getvars["api-key"]); |
c563cbe5 CH |
161 | |
162 | if (!auth_ok && allowPassword) { | |
40422f78 RG |
163 | if (d_webserverPassword) { |
164 | auth_ok = req->compareAuthorization(*d_webserverPassword); | |
c563cbe5 CH |
165 | } else { |
166 | auth_ok = true; | |
167 | } | |
168 | } | |
169 | ||
bbef8f04 | 170 | if (!auth_ok) { |
6c88989a | 171 | SLOG(g_log<<Logger::Error<<req->logprefix<<"HTTP Request \"" << req->url.path << "\": Authentication by API Key failed" << endl, |
7ccb828a | 172 | d_slog->info(Logr::Error, "Authentication by API Key failed", "urlpath", Logging::Loggable(req->url.path))); |
53255086 | 173 | throw HttpUnauthorizedException("X-API-Key"); |
bbef8f04 CH |
174 | } |
175 | ||
7fe2a2dc CH |
176 | // security headers |
177 | resp->headers["X-Content-Type-Options"] = "nosniff"; | |
178 | resp->headers["X-Frame-Options"] = "deny"; | |
179 | resp->headers["X-Permitted-Cross-Domain-Policies"] = "none"; | |
180 | resp->headers["X-XSS-Protection"] = "1; mode=block"; | |
181 | resp->headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'"; | |
3ae143b0 | 182 | |
583ea80d | 183 | req->getvars.erase("_"); // jQuery cache buster |
3ae143b0 CH |
184 | |
185 | try { | |
583ea80d | 186 | resp->status = 200; |
3ae143b0 CH |
187 | handler(req, resp); |
188 | } catch (ApiException &e) { | |
692829aa | 189 | resp->setErrorResult(e.what(), 422); |
6ec5e728 CH |
190 | return; |
191 | } catch (JsonException &e) { | |
692829aa | 192 | resp->setErrorResult(e.what(), 422); |
3ae143b0 CH |
193 | return; |
194 | } | |
195 | ||
37663c3b CH |
196 | if (resp->status == 204) { |
197 | // No Content -> no Content-Type. | |
198 | resp->headers.erase("Content-Type"); | |
199 | } | |
3ae143b0 CH |
200 | } |
201 | ||
478e1699 | 202 | void WebServer::registerApiHandler(const string& url, const HandlerFunction& handler, const std::string& method, bool allowPassword) { |
969e4459 | 203 | auto f = [=](HttpRequest *req, HttpResponse* resp){apiWrapper(handler, req, resp, allowPassword);}; |
478e1699 | 204 | registerBareHandler(url, f, method); |
bbef8f04 CH |
205 | } |
206 | ||
4de01aaa | 207 | void WebServer::webWrapper(const WebServer::HandlerFunction& handler, HttpRequest* req, HttpResponse* resp) { |
40422f78 RG |
208 | if (d_webserverPassword) { |
209 | bool auth_ok = req->compareAuthorization(*d_webserverPassword); | |
bbef8f04 | 210 | if (!auth_ok) { |
6c88989a OM |
211 | SLOG(g_log<<Logger::Debug<<req->logprefix<<"HTTP Request \"" << req->url.path << "\": Web Authentication failed" << endl, |
212 | d_slog->info(Logr::Debug, "HTTP Request: Web Authentication failed", "urlpath", Logging::Loggable(req->url.path))); | |
53255086 | 213 | throw HttpUnauthorizedException("Basic"); |
bbef8f04 CH |
214 | } |
215 | } | |
216 | ||
217 | handler(req, resp); | |
218 | } | |
219 | ||
478e1699 | 220 | void WebServer::registerWebHandler(const string& url, const HandlerFunction& handler, const std::string& method) { |
969e4459 | 221 | auto f = [=](HttpRequest *req, HttpResponse *resp){webWrapper(handler, req, resp);}; |
478e1699 | 222 | registerBareHandler(url, f, method); |
3ae143b0 CH |
223 | } |
224 | ||
224085cc RP |
225 | static void* WebServerConnectionThreadStart(const WebServer* webServer, const std::shared_ptr<Socket>& client) |
226 | { | |
9548a664 | 227 | setThreadName("rec/webhndlr"); |
6c88989a | 228 | const std::string msg = "Exception while serving a connection in main webserver thread"; |
89a5aecc | 229 | try { |
beceefde | 230 | webServer->serveConnection(client); |
89a5aecc PD |
231 | } |
232 | catch(PDNSException &e) { | |
6c88989a OM |
233 | SLOG(g_log<<Logger::Error<<"PDNSException while serving a connection in main webserver thread: "<<e.reason<<endl, |
234 | webServer->d_slog->error(Logr::Error, e.reason, msg, "exception", Logging::Loggable("PDNSException"))); | |
89a5aecc PD |
235 | } |
236 | catch(std::exception &e) { | |
6c88989a OM |
237 | SLOG(g_log<<Logger::Error<<"STL Exception while serving a connection in main webserver thread: "<<e.what()<<endl, |
238 | webServer->d_slog->error(Logr::Error, e.what(), msg, "exception", Logging::Loggable("std::exception"))); | |
89a5aecc PD |
239 | } |
240 | catch(...) { | |
6c88989a OM |
241 | SLOG(g_log<<Logger::Error<<"Unknown exception while serving a connection in main webserver thread"<<endl, |
242 | webServer->d_slog->info(Logr::Error, msg)); | |
89a5aecc | 243 | } |
d4c53d8c | 244 | return nullptr; |
232f0877 CH |
245 | } |
246 | ||
b184a9dc | 247 | void WebServer::handleRequest(HttpRequest& req, HttpResponse& resp) const |
80d59cd1 | 248 | { |
80d59cd1 CH |
249 | // set default headers |
250 | resp.headers["Content-Type"] = "text/html; charset=utf-8"; | |
33196945 | 251 | |
3e285251 OM |
252 | #ifdef RECURSOR |
253 | auto log = req.d_slog->withValues("urlpath", Logging::Loggable(req.url.path)); | |
254 | #endif | |
255 | ||
12c86877 | 256 | try { |
825fa717 | 257 | if (!req.complete) { |
6c88989a OM |
258 | SLOG(g_log<<Logger::Debug<<req.logprefix<<"Incomplete request" << endl, |
259 | d_slog->info(Logr::Debug, "Incomplete request")); | |
825fa717 CH |
260 | throw HttpBadRequestException(); |
261 | } | |
6c88989a OM |
262 | SLOG(g_log<<Logger::Debug<<req.logprefix<<"Handling request \"" << req.url.path << "\"" << endl, |
263 | log->info(Logr::Debug, "Handling request")); | |
825fa717 | 264 | |
80d59cd1 CH |
265 | YaHTTP::strstr_map_t::iterator header; |
266 | ||
267 | if ((header = req.headers.find("accept")) != req.headers.end()) { | |
917f686a KF |
268 | // yaml wins over json, json wins over html |
269 | if (header->second.find("application/x-yaml") != std::string::npos) { | |
270 | req.accept_yaml = true; | |
271 | } else if (header->second.find("text/x-yaml") != std::string::npos) { | |
272 | req.accept_yaml = true; | |
273 | } else if (header->second.find("application/json") != std::string::npos) { | |
80d59cd1 CH |
274 | req.accept_json = true; |
275 | } else if (header->second.find("text/html") != std::string::npos) { | |
276 | req.accept_html = true; | |
277 | } | |
12c86877 BH |
278 | } |
279 | ||
583ea80d | 280 | YaHTTP::THandlerFunction handler; |
478e1699 AT |
281 | YaHTTP::RoutingResult res = YaHTTP::Router::Route(&req, handler); |
282 | ||
283 | if (res == YaHTTP::RouteNotFound) { | |
6c88989a OM |
284 | SLOG(g_log<<Logger::Debug<<req.logprefix<<"No route found for \"" << req.url.path << "\"" << endl, |
285 | log->info(Logr::Debug, "No route found")); | |
33196945 | 286 | throw HttpNotFoundException(); |
12c86877 | 287 | } |
478e1699 AT |
288 | if (res == YaHTTP::RouteNoMethod) { |
289 | throw HttpMethodNotAllowedException(); | |
290 | } | |
12c86877 | 291 | |
6c88989a | 292 | const string msg = "HTTP ISE Exception"; |
0f67eeda | 293 | try { |
583ea80d | 294 | handler(&req, &resp); |
6c88989a OM |
295 | SLOG(g_log<<Logger::Debug<<req.logprefix<<"Result for \"" << req.url.path << "\": " << resp.status << ", body length: " << resp.body.size() << endl, |
296 | log->info(Logr::Debug, "Result", "status", Logging::Loggable(resp.status), "bodyLength", Logging::Loggable(resp.body.size()))); | |
0f67eeda | 297 | } |
4d706054 | 298 | catch(HttpException&) { |
583ea80d CH |
299 | throw; |
300 | } | |
0f67eeda | 301 | catch(PDNSException &e) { |
6c88989a OM |
302 | SLOG(g_log<<Logger::Error<<req.logprefix<<"HTTP ISE for \""<< req.url.path << "\": Exception: " << e.reason << endl, |
303 | log->error(Logr::Error, e.reason, msg, "exception", Logging::Loggable("PDNSException"))); | |
0f67eeda CH |
304 | throw HttpInternalServerErrorException(); |
305 | } | |
306 | catch(std::exception &e) { | |
6c88989a OM |
307 | SLOG(g_log<<Logger::Error<<req.logprefix<<"HTTP ISE for \""<< req.url.path << "\": STL Exception: " << e.what() << endl, |
308 | log->error(Logr::Error, e.what(), msg, "exception", Logging::Loggable("std::exception"))); | |
0f67eeda CH |
309 | throw HttpInternalServerErrorException(); |
310 | } | |
311 | catch(...) { | |
6c88989a OM |
312 | SLOG(g_log<<Logger::Error<<req.logprefix<<"HTTP ISE for \""<< req.url.path << "\": Unknown Exception" << endl, |
313 | log->info(Logr::Error, msg)); | |
0f67eeda CH |
314 | throw HttpInternalServerErrorException(); |
315 | } | |
12c86877 | 316 | } |
33196945 | 317 | catch(HttpException &e) { |
80d59cd1 | 318 | resp = e.response(); |
3e285251 OM |
319 | #ifdef RECURSOR |
320 | // An HttpException does not initialize d_slog | |
321 | if (!resp.d_slog) { | |
322 | resp.setSLog(log); | |
323 | } | |
324 | #endif | |
a53cd863 | 325 | // TODO rm this logline? |
6c88989a OM |
326 | SLOG(g_log<<Logger::Debug<<req.logprefix<<"Error result for \"" << req.url.path << "\": " << resp.status << endl, |
327 | d_slog->error(Logr::Debug, resp.status, "Error result", "urlpath", Logging::Loggable(req.url.path))); | |
80d59cd1 | 328 | string what = YaHTTP::Utility::status2text(resp.status); |
917f686a | 329 | if (req.accept_json) { |
80d59cd1 | 330 | resp.headers["Content-Type"] = "application/json"; |
8204102e PL |
331 | if (resp.body.empty()) { |
332 | resp.setErrorResult(what, resp.status); | |
333 | } | |
917f686a KF |
334 | } else if (req.accept_html) { |
335 | resp.headers["Content-Type"] = "text/html; charset=utf-8"; | |
336 | resp.body = "<!html><title>" + what + "</title><h1>" + what + "</h1>"; | |
33196945 | 337 | } else { |
80d59cd1 | 338 | resp.headers["Content-Type"] = "text/plain; charset=utf-8"; |
3b45a434 | 339 | resp.body = std::move(what); |
33196945 | 340 | } |
12c86877 | 341 | } |
33196945 | 342 | |
80d59cd1 | 343 | // always set these headers |
80d59cd1 CH |
344 | resp.headers["Connection"] = "close"; |
345 | ||
ac9908c2 CH |
346 | if (req.method == "HEAD") { |
347 | resp.body = ""; | |
348 | } else { | |
335da0ba | 349 | resp.headers["Content-Length"] = std::to_string(resp.body.size()); |
ac9908c2 | 350 | } |
80d59cd1 CH |
351 | } |
352 | ||
6c88989a OM |
353 | #ifdef RECURSOR |
354 | // Helper to log key-value maps used by YaHTTP | |
355 | template<> | |
356 | std::string Logging::IterLoggable<YaHTTP::strstr_map_t::const_iterator>::to_string() const | |
357 | { | |
358 | std::ostringstream oss; | |
359 | bool first = true; | |
360 | for (auto i = _t1; i != _t2; i++) { | |
361 | if (!first) { | |
362 | oss << '\n'; | |
363 | } | |
364 | else { | |
365 | first = false; | |
366 | } | |
367 | oss << i->first << ": " << i->second; | |
368 | } | |
369 | return oss.str(); | |
370 | } | |
371 | #endif | |
372 | ||
aa87b287 | 373 | void WebServer::logRequest(const HttpRequest& req, [[maybe_unused]] const ComboAddress& remote) const { |
612ad9ec | 374 | if (d_loglevel >= WebServer::LogLevel::Detailed) { |
6c88989a OM |
375 | #ifdef RECURSOR |
376 | if (!g_slogStructured) { | |
377 | #endif | |
3b45a434 | 378 | const auto& logprefix = req.logprefix; |
6c88989a OM |
379 | g_log<<Logger::Notice<<logprefix<<"Request details:"<<endl; |
380 | ||
381 | bool first = true; | |
382 | for (const auto& r : req.getvars) { | |
383 | if (first) { | |
384 | first = false; | |
385 | g_log<<Logger::Notice<<logprefix<<" GET params:"<<endl; | |
386 | } | |
387 | g_log<<Logger::Notice<<logprefix<<" "<<r.first<<": "<<r.second<<endl; | |
612ad9ec | 388 | } |
612ad9ec | 389 | |
6c88989a OM |
390 | first = true; |
391 | for (const auto& r : req.postvars) { | |
392 | if (first) { | |
393 | first = false; | |
394 | g_log<<Logger::Notice<<logprefix<<" POST params:"<<endl; | |
395 | } | |
396 | g_log<<Logger::Notice<<logprefix<<" "<<r.first<<": "<<r.second<<endl; | |
612ad9ec | 397 | } |
612ad9ec | 398 | |
6c88989a OM |
399 | first = true; |
400 | for (const auto& h : req.headers) { | |
401 | if (first) { | |
402 | first = false; | |
403 | g_log<<Logger::Notice<<logprefix<<" Headers:"<<endl; | |
404 | } | |
405 | g_log<<Logger::Notice<<logprefix<<" "<<h.first<<": "<<h.second<<endl; | |
612ad9ec | 406 | } |
612ad9ec | 407 | |
6c88989a OM |
408 | if (req.body.empty()) { |
409 | g_log<<Logger::Notice<<logprefix<<" No body"<<endl; | |
410 | } else { | |
411 | g_log<<Logger::Notice<<logprefix<<" Full body: "<<endl; | |
412 | g_log<<Logger::Notice<<logprefix<<" "<<req.body<<endl; | |
413 | } | |
414 | #ifdef RECURSOR | |
612ad9ec | 415 | } |
6c88989a OM |
416 | else { |
417 | req.d_slog->info(Logr::Info, "Request details", "getParams", Logging::IterLoggable(req.getvars.cbegin(), req.getvars.cend()), | |
418 | "postParams", Logging::IterLoggable(req.postvars.cbegin(), req.postvars.cend()), | |
419 | "body", Logging::Loggable(req.body), | |
420 | "address", Logging::Loggable(remote)); | |
421 | } | |
422 | #endif | |
612ad9ec PL |
423 | } |
424 | } | |
425 | ||
d73de874 | 426 | void WebServer::logResponse(const HttpResponse& resp, const ComboAddress& /* remote */, const string& logprefix) const { |
612ad9ec | 427 | if (d_loglevel >= WebServer::LogLevel::Detailed) { |
6c88989a OM |
428 | #ifdef RECURSOR |
429 | if (!g_slogStructured) { | |
430 | #endif | |
431 | g_log<<Logger::Notice<<logprefix<<"Response details:"<<endl; | |
432 | bool first = true; | |
433 | for (const auto& h : resp.headers) { | |
434 | if (first) { | |
435 | first = false; | |
436 | g_log<<Logger::Notice<<logprefix<<" Headers:"<<endl; | |
437 | } | |
438 | g_log<<Logger::Notice<<logprefix<<" "<<h.first<<": "<<h.second<<endl; | |
439 | } | |
440 | if (resp.body.empty()) { | |
441 | g_log<<Logger::Notice<<logprefix<<" No body"<<endl; | |
442 | } else { | |
443 | g_log<<Logger::Notice<<logprefix<<" Full body: "<<endl; | |
444 | g_log<<Logger::Notice<<logprefix<<" "<<resp.body<<endl; | |
612ad9ec | 445 | } |
6c88989a | 446 | #ifdef RECURSOR |
612ad9ec | 447 | } |
6c88989a OM |
448 | else { |
449 | resp.d_slog->info(Logr::Info, "Response details", "headers", Logging::IterLoggable(resp.headers.cbegin(), resp.headers.cend()), | |
450 | "body", Logging::Loggable(resp.body)); | |
612ad9ec | 451 | } |
6c88989a | 452 | #endif |
612ad9ec PL |
453 | } |
454 | } | |
455 | ||
35eb2fcf OM |
456 | |
457 | struct ValidChars { | |
458 | ValidChars() | |
459 | { | |
460 | // letter may be signed, but we only pass positive values | |
461 | for (auto letter : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=") { | |
462 | set.set(letter); | |
463 | } | |
464 | } | |
465 | std::bitset<127> set; | |
466 | }; | |
467 | ||
468 | static const ValidChars validChars; | |
469 | ||
470 | static bool validURLChars(const string& str) | |
471 | { | |
472 | for (auto iter = str.begin(); iter != str.end(); ++iter) { | |
473 | if (*iter == '%') { | |
474 | ++iter; | |
475 | if (iter == str.end() || isxdigit(static_cast<unsigned char>(*iter)) == 0) { | |
476 | return false; | |
477 | } | |
478 | ++iter; | |
479 | if (iter == str.end() || isxdigit(static_cast<unsigned char>(*iter)) == 0) { | |
480 | return false; | |
481 | } | |
482 | } | |
483 | else if (static_cast<size_t>(*iter) >= validChars.set.size() || !validChars.set[*iter]) { | |
484 | return false; | |
485 | } | |
486 | } | |
487 | return true; | |
488 | } | |
489 | ||
490 | bool WebServer::validURL(const YaHTTP::URL& url) | |
491 | { | |
492 | bool isOK = true; | |
493 | isOK = isOK && validURLChars(url.protocol); | |
494 | isOK = isOK && validURLChars(url.host); | |
495 | isOK = isOK && validURLChars(url.username); | |
496 | isOK = isOK && validURLChars(url.password); | |
497 | isOK = isOK && validURLChars(url.path); | |
498 | isOK = isOK && validURLChars(url.parameters); | |
499 | isOK = isOK && validURLChars(url.anchor); | |
500 | return isOK; | |
501 | } | |
502 | ||
4de01aaa | 503 | void WebServer::serveConnection(const std::shared_ptr<Socket>& client) const { |
6c88989a OM |
504 | const auto unique = getUniqueID(); |
505 | const string logprefix = d_logprefix + to_string(unique) + " "; | |
80d59cd1 | 506 | |
9b960272 | 507 | HttpRequest req(logprefix); |
6c88989a | 508 | |
9b960272 | 509 | HttpResponse resp; |
6c88989a OM |
510 | #ifdef RECURSOR |
511 | auto log = d_slog->withValues("uniqueid", Logging::Loggable(to_string(unique))); | |
512 | req.setSLog(log); | |
513 | resp.setSLog(log); | |
514 | #endif | |
214b034e | 515 | resp.max_response_size=d_maxbodysize; |
9b960272 PL |
516 | ComboAddress remote; |
517 | string reply; | |
518 | ||
80d59cd1 | 519 | try { |
a53cd863 PL |
520 | YaHTTP::AsyncRequestLoader yarl; |
521 | yarl.initialize(&req); | |
214b034e | 522 | req.max_request_size=d_maxbodysize; |
a53cd863 PL |
523 | int timeout = 5; |
524 | client->setNonBlocking(); | |
525 | ||
526 | try { | |
527 | while(!req.complete) { | |
528 | int bytes; | |
bfde2d10 | 529 | char buf[16000]; |
a53cd863 PL |
530 | bytes = client->readWithTimeout(buf, sizeof(buf), timeout); |
531 | if (bytes > 0) { | |
532 | string data = string(buf, bytes); | |
533 | req.complete = yarl.feed(data); | |
534 | } else { | |
535 | // read error OR EOF | |
536 | break; | |
537 | } | |
538 | } | |
539 | yarl.finalize(); | |
540 | } catch (YaHTTP::ParseError &e) { | |
541 | // request stays incomplete | |
6c88989a OM |
542 | SLOG(g_log<<Logger::Warning<<logprefix<<"Unable to parse request: "<<e.what()<<endl, |
543 | d_slog->error(Logr::Warning, e.what(), "Unable to parse request")); | |
a53cd863 PL |
544 | } |
545 | ||
35eb2fcf OM |
546 | if (!validURL(req.url)) { |
547 | throw PDNSException("Received request with invalid URL"); | |
548 | } | |
7ccb828a | 549 | // Uses of `remote` below guarded by d_loglevel |
6c88989a | 550 | if (d_loglevel > WebServer::LogLevel::None) { |
a53cd863 PL |
551 | client->getRemote(remote); |
552 | } | |
553 | ||
612ad9ec | 554 | logRequest(req, remote); |
80d59cd1 | 555 | |
a53cd863 PL |
556 | WebServer::handleRequest(req, resp); |
557 | ostringstream ss; | |
558 | resp.write(ss); | |
9b960272 | 559 | reply = ss.str(); |
a53cd863 | 560 | |
612ad9ec | 561 | logResponse(resp, remote, logprefix); |
a53cd863 | 562 | |
a53cd863 | 563 | client->writenWithTimeout(reply.c_str(), reply.size(), timeout); |
b2cb8982 | 564 | } |
a53cd863 | 565 | catch(PDNSException &e) { |
6c88989a OM |
566 | SLOG(g_log<<Logger::Error<<logprefix<<"HTTP Exception: "<<e.reason<<endl, |
567 | d_slog->error(Logr::Error, e.reason, "HTTP Exception", "exception", Logging::Loggable("PDNSException"))); | |
b2cb8982 | 568 | } |
a53cd863 | 569 | catch(std::exception &e) { |
4646277d | 570 | if(strstr(e.what(), "timeout")==nullptr) |
6c88989a OM |
571 | SLOG(g_log<<Logger::Error<<logprefix<<"HTTP STL Exception: "<<e.what()<<endl, |
572 | d_slog->error(Logr::Error, e.what(), "HTTP Exception", "exception", Logging::Loggable("std::exception"))); | |
a53cd863 PL |
573 | } |
574 | catch(...) { | |
6c88989a OM |
575 | SLOG(g_log<<Logger::Error<<logprefix<<"Unknown exception"<<endl, |
576 | d_slog->info(Logr::Error, "HTTP Exception")); | |
b2cb8982 | 577 | } |
9b960272 | 578 | |
a35306f9 | 579 | if (d_loglevel >= WebServer::LogLevel::Normal) { |
26f5d605 | 580 | SLOG(g_log<<Logger::Notice<<logprefix<<remote<<" \""<<req.method<<" "<<req.url.path<<" HTTP/"<<req.versionStr(req.version)<<"\" "<<resp.status<<" "<<reply.size()<<endl, |
6c88989a OM |
581 | d_slog->info(Logr::Info, "Request", "remote", Logging::Loggable(remote), "method", Logging::Loggable(req.method), |
582 | "urlpath", Logging::Loggable(req.url.path), "HTTPVersion", Logging::Loggable(req.versionStr(req.version)), | |
583 | "status", Logging::Loggable(resp.status), "respsize", Logging::Loggable(reply.size()))); | |
9b960272 | 584 | } |
33196945 CH |
585 | } |
586 | ||
1290c1d2 RP |
587 | WebServer::WebServer(string listenaddress, int port) : |
588 | d_listenaddress(std::move(listenaddress)), | |
8a70e507 | 589 | d_port(port), |
214b034e PD |
590 | d_server(nullptr), |
591 | d_maxbodysize(2*1024*1024) | |
12c86877 | 592 | { |
fe921322 AT |
593 | YaHTTP::Router::Map("OPTIONS", "/<*url>", [](YaHTTP::Request *req, YaHTTP::Response *resp) { |
594 | // look for url in routes | |
595 | bool seen = false; | |
596 | std::vector<std::string> methods; | |
597 | for(const auto& route: YaHTTP::Router::GetRoutes()) { | |
598 | const auto& method = std::get<0>(route); | |
599 | const auto& url = std::get<1>(route); | |
600 | if (method == "OPTIONS") { | |
601 | continue; | |
602 | } | |
603 | std::map<std::string, YaHTTP::TDelim> params; | |
604 | if (YaHTTP::Router::Match(url, req->url, params)) { | |
605 | methods.push_back(method); | |
606 | seen = true; | |
607 | } | |
608 | } | |
609 | if (!seen) { | |
610 | resp->status = 404; | |
611 | resp->body = ""; | |
612 | return; | |
613 | } | |
614 | methods.emplace_back("OPTIONS"); | |
615 | resp->headers["access-control-allow-origin"] = "*"; | |
616 | resp->headers["access-control-allow-headers"] = "Content-Type, X-API-Key"; | |
617 | resp->headers["access-control-allow-methods"] = boost::algorithm::join(methods, ", "); | |
618 | resp->headers["access-control-max-age"] = "3600"; | |
619 | resp->status = 200; | |
620 | resp->headers["content-type"]= "text/plain"; | |
621 | resp->body = ""; | |
622 | }, "OptionsHandlerRoute"); | |
825fa717 CH |
623 | } |
624 | ||
625 | void WebServer::bind() | |
626 | { | |
96d299db | 627 | try { |
825fa717 | 628 | d_server = createServer(); |
6c88989a | 629 | SLOG(g_log<<Logger::Warning<<d_logprefix<<"Listening for HTTP requests on "<<d_server->d_local.toStringWithPort()<<endl, |
68dbf30c | 630 | d_slog->info(Logr::Info, "Listening for HTTP requests", "address", Logging::Loggable(d_server->d_local))); |
96d299db | 631 | } |
825fa717 | 632 | catch(NetworkError &e) { |
6c88989a OM |
633 | SLOG(g_log<<Logger::Error<<d_logprefix<<"Listening on HTTP socket failed: "<<e.what()<<endl, |
634 | d_slog->error(Logr::Error, e.what(), "Listening on HTTP socket failed", "exception", Logging::Loggable("NetworkError"))); | |
690984d4 | 635 | d_server = nullptr; |
96d299db | 636 | } |
12c86877 BH |
637 | } |
638 | ||
639 | void WebServer::go() | |
640 | { | |
96d299db BH |
641 | if(!d_server) |
642 | return; | |
6c88989a | 643 | const string msg = "Exception in main webserver thread"; |
12c86877 | 644 | try { |
3ae143b0 | 645 | while(true) { |
6c88989a | 646 | const string acceptmsg = "Exception while accepting a connection in main webserver thread"; |
17d60ab0 | 647 | try { |
b184a9dc | 648 | auto client = d_server->accept(); |
8a781bb5 RG |
649 | if (!client) { |
650 | continue; | |
651 | } | |
0010aefa | 652 | if (client->acl(d_acl)) { |
b184a9dc | 653 | std::thread webHandler(WebServerConnectionThreadStart, this, client); |
d4c53d8c | 654 | webHandler.detach(); |
17d60ab0 RG |
655 | } else { |
656 | ComboAddress remote; | |
b184a9dc | 657 | if (client->getRemote(remote)) |
a53cd863 | 658 | g_log<<Logger::Error<<d_logprefix<<"Webserver closing socket: remote ("<< remote.toString() <<") does not match the set ACL("<<d_acl.toString()<<")"<<endl; |
17d60ab0 RG |
659 | } |
660 | } | |
661 | catch(PDNSException &e) { | |
6c88989a OM |
662 | SLOG(g_log<<Logger::Error<<d_logprefix<<"PDNSException while accepting a connection in main webserver thread: "<<e.reason<<endl, |
663 | d_slog->error(Logr::Error, e.reason, acceptmsg, Logging::Loggable("PDNSException"))); | |
17d60ab0 RG |
664 | } |
665 | catch(std::exception &e) { | |
6c88989a OM |
666 | SLOG(g_log<<Logger::Error<<d_logprefix<<"STL Exception while accepting a connection in main webserver thread: "<<e.what()<<endl, |
667 | d_slog->error(Logr::Error, e.what(), acceptmsg, Logging::Loggable("std::exception"))); | |
17d60ab0 RG |
668 | } |
669 | catch(...) { | |
6c88989a OM |
670 | SLOG(g_log<<Logger::Error<<d_logprefix<<"Unknown exception while accepting a connection in main webserver thread"<<endl, |
671 | d_slog->info(Logr::Error, msg)); | |
69e7f117 | 672 | } |
12c86877 BH |
673 | } |
674 | } | |
69e7f117 | 675 | catch(PDNSException &e) { |
6c88989a OM |
676 | SLOG(g_log<<Logger::Error<<d_logprefix<<"PDNSException in main webserver thread: "<<e.reason<<endl, |
677 | d_slog->error(Logr::Error, e.reason, msg, Logging::Loggable("PDNSException"))); | |
69e7f117 | 678 | } |
adc10f99 | 679 | catch(std::exception &e) { |
6c88989a OM |
680 | SLOG(g_log<<Logger::Error<<d_logprefix<<"STL Exception in main webserver thread: "<<e.what()<<endl, |
681 | d_slog->error(Logr::Error, e.what(), msg, Logging::Loggable("std::exception"))); | |
12c86877 BH |
682 | } |
683 | catch(...) { | |
6c88989a OM |
684 | SLOG(g_log<<Logger::Error<<d_logprefix<<"Unknown exception in main webserver thread"<<endl, |
685 | d_slog->info(Logr::Error, msg)); | |
12c86877 | 686 | } |
5bd2ea7b | 687 | _exit(1); |
3ae143b0 | 688 | } |