]>
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> |
12c86877 BH |
29 | #include <vector> |
30 | #include "logger.hh" | |
31 | #include <stdio.h> | |
32 | #include "dns.hh" | |
2db9c30e | 33 | #include "base64.hh" |
33196945 | 34 | #include "json.hh" |
69e7f117 | 35 | #include "arguments.hh" |
583ea80d | 36 | #include <yahttp/router.hpp> |
12c86877 | 37 | |
5938c49f CH |
38 | json11::Json HttpRequest::json() |
39 | { | |
40 | string err; | |
41 | if(this->body.empty()) { | |
42 | L<<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 | L<<Logger::Debug<<"HTTP: parsing of JSON document failed:" << err << endl; | |
48 | throw HttpBadRequestException(); | |
49 | } | |
50 | return doc; | |
51 | } | |
52 | ||
bbef8f04 CH |
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 | ||
5938c49f CH |
84 | void HttpResponse::setBody(const json11::Json& document) |
85 | { | |
86 | document.dump(this->body); | |
87 | } | |
88 | ||
dd079764 | 89 | void HttpResponse::setErrorResult(const std::string& message, const int status_) |
692829aa CH |
90 | { |
91 | setBody(json11::Json::object { { "error", message } }); | |
dd079764 | 92 | this->status = status_; |
692829aa CH |
93 | } |
94 | ||
dd079764 | 95 | void HttpResponse::setSuccessResult(const std::string& message, const int status_) |
692829aa CH |
96 | { |
97 | setBody(json11::Json::object { { "result", message } }); | |
dd079764 | 98 | this->status = status_; |
692829aa CH |
99 | } |
100 | ||
bbef8f04 | 101 | static void bareHandlerWrapper(WebServer::HandlerFunction handler, YaHTTP::Request* req, YaHTTP::Response* resp) |
12c86877 | 102 | { |
583ea80d CH |
103 | // wrapper to convert from YaHTTP::* to our subclasses |
104 | handler(static_cast<HttpRequest*>(req), static_cast<HttpResponse*>(resp)); | |
105 | } | |
232f0877 | 106 | |
bbef8f04 | 107 | void WebServer::registerBareHandler(const string& url, HandlerFunction handler) |
583ea80d | 108 | { |
bbef8f04 | 109 | YaHTTP::THandlerFunction f = boost::bind(&bareHandlerWrapper, handler, _1, _2); |
583ea80d | 110 | YaHTTP::Router::Any(url, f); |
232f0877 CH |
111 | } |
112 | ||
7f7481be AT |
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 | ||
583ea80d | 127 | static void apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp) { |
d07bf7ff | 128 | const string& api_key = arg()["api-key"]; |
7f7481be AT |
129 | |
130 | if (optionsHandler(req, resp)) return; | |
131 | ||
132 | resp->headers["access-control-allow-origin"] = "*"; | |
133 | ||
bbef8f04 | 134 | if (api_key.empty()) { |
53255086 PL |
135 | L<<Logger::Error<<"HTTP API Request \"" << req->url.path << "\": Authentication failed, API Key missing in config" << endl; |
136 | throw HttpUnauthorizedException("X-API-Key"); | |
bbef8f04 | 137 | } |
c89f8cd0 | 138 | bool auth_ok = req->compareHeader("x-api-key", api_key) || req->getvars["api-key"]==api_key; |
139 | ||
bbef8f04 | 140 | if (!auth_ok) { |
53255086 PL |
141 | L<<Logger::Error<<"HTTP Request \"" << req->url.path << "\": Authentication by API Key failed" << endl; |
142 | throw HttpUnauthorizedException("X-API-Key"); | |
bbef8f04 CH |
143 | } |
144 | ||
3ae143b0 CH |
145 | resp->headers["Content-Type"] = "application/json"; |
146 | ||
7fe2a2dc CH |
147 | // security headers |
148 | resp->headers["X-Content-Type-Options"] = "nosniff"; | |
149 | resp->headers["X-Frame-Options"] = "deny"; | |
150 | resp->headers["X-Permitted-Cross-Domain-Policies"] = "none"; | |
151 | resp->headers["X-XSS-Protection"] = "1; mode=block"; | |
152 | resp->headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'"; | |
3ae143b0 | 153 | |
583ea80d | 154 | req->getvars.erase("_"); // jQuery cache buster |
3ae143b0 CH |
155 | |
156 | try { | |
583ea80d | 157 | resp->status = 200; |
3ae143b0 CH |
158 | handler(req, resp); |
159 | } catch (ApiException &e) { | |
692829aa | 160 | resp->setErrorResult(e.what(), 422); |
6ec5e728 CH |
161 | return; |
162 | } catch (JsonException &e) { | |
692829aa | 163 | resp->setErrorResult(e.what(), 422); |
3ae143b0 CH |
164 | return; |
165 | } | |
166 | ||
37663c3b CH |
167 | if (resp->status == 204) { |
168 | // No Content -> no Content-Type. | |
169 | resp->headers.erase("Content-Type"); | |
170 | } | |
3ae143b0 CH |
171 | } |
172 | ||
173 | void WebServer::registerApiHandler(const string& url, HandlerFunction handler) { | |
174 | HandlerFunction f = boost::bind(&apiWrapper, handler, _1, _2); | |
bbef8f04 CH |
175 | registerBareHandler(url, f); |
176 | } | |
177 | ||
178 | static void webWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp) { | |
179 | const string& web_password = arg()["webserver-password"]; | |
7f7481be | 180 | |
bbef8f04 CH |
181 | if (!web_password.empty()) { |
182 | bool auth_ok = req->compareAuthorization(web_password); | |
183 | if (!auth_ok) { | |
184 | L<<Logger::Debug<<"HTTP Request \"" << req->url.path << "\": Web Authentication failed" << endl; | |
53255086 | 185 | throw HttpUnauthorizedException("Basic"); |
bbef8f04 CH |
186 | } |
187 | } | |
188 | ||
189 | handler(req, resp); | |
190 | } | |
191 | ||
192 | void WebServer::registerWebHandler(const string& url, HandlerFunction handler) { | |
193 | HandlerFunction f = boost::bind(&webWrapper, handler, _1, _2); | |
194 | registerBareHandler(url, f); | |
3ae143b0 CH |
195 | } |
196 | ||
b184a9dc RG |
197 | static void *WebServerConnectionThreadStart(const WebServer* webServer, std::shared_ptr<Socket> client) { |
198 | webServer->serveConnection(client); | |
d4c53d8c | 199 | return nullptr; |
232f0877 CH |
200 | } |
201 | ||
b184a9dc | 202 | void WebServer::handleRequest(HttpRequest& req, HttpResponse& resp) const |
80d59cd1 | 203 | { |
80d59cd1 CH |
204 | // set default headers |
205 | resp.headers["Content-Type"] = "text/html; charset=utf-8"; | |
33196945 | 206 | |
12c86877 | 207 | try { |
825fa717 | 208 | if (!req.complete) { |
583ea80d | 209 | L<<Logger::Debug<<"HTTP: Incomplete request" << endl; |
825fa717 CH |
210 | throw HttpBadRequestException(); |
211 | } | |
212 | ||
213 | L<<Logger::Debug<<"HTTP: Handling request \"" << req.url.path << "\"" << endl; | |
214 | ||
80d59cd1 CH |
215 | YaHTTP::strstr_map_t::iterator header; |
216 | ||
217 | if ((header = req.headers.find("accept")) != req.headers.end()) { | |
218 | // json wins over html | |
219 | if (header->second.find("application/json") != std::string::npos) { | |
220 | req.accept_json = true; | |
221 | } else if (header->second.find("text/html") != std::string::npos) { | |
222 | req.accept_html = true; | |
223 | } | |
12c86877 BH |
224 | } |
225 | ||
583ea80d CH |
226 | YaHTTP::THandlerFunction handler; |
227 | if (!YaHTTP::Router::Route(&req, handler)) { | |
51bd0969 | 228 | L<<Logger::Debug<<"HTTP: No route found for \"" << req.url.path << "\"" << endl; |
33196945 | 229 | throw HttpNotFoundException(); |
12c86877 | 230 | } |
12c86877 | 231 | |
0f67eeda | 232 | try { |
583ea80d | 233 | handler(&req, &resp); |
51bd0969 | 234 | L<<Logger::Debug<<"HTTP: Result for \"" << req.url.path << "\": " << resp.status << ", body length: " << resp.body.size() << endl; |
0f67eeda | 235 | } |
4d706054 | 236 | catch(HttpException&) { |
583ea80d CH |
237 | throw; |
238 | } | |
0f67eeda CH |
239 | catch(PDNSException &e) { |
240 | L<<Logger::Error<<"HTTP ISE for \""<< req.url.path << "\": Exception: " << e.reason << endl; | |
241 | throw HttpInternalServerErrorException(); | |
242 | } | |
243 | catch(std::exception &e) { | |
244 | L<<Logger::Error<<"HTTP ISE for \""<< req.url.path << "\": STL Exception: " << e.what() << endl; | |
245 | throw HttpInternalServerErrorException(); | |
246 | } | |
247 | catch(...) { | |
248 | L<<Logger::Error<<"HTTP ISE for \""<< req.url.path << "\": Unknown Exception" << endl; | |
249 | throw HttpInternalServerErrorException(); | |
250 | } | |
12c86877 | 251 | } |
33196945 | 252 | catch(HttpException &e) { |
80d59cd1 | 253 | resp = e.response(); |
51bd0969 | 254 | L<<Logger::Debug<<"HTTP: Error result for \"" << req.url.path << "\": " << resp.status << endl; |
80d59cd1 | 255 | string what = YaHTTP::Utility::status2text(resp.status); |
02c04144 | 256 | if(req.accept_html) { |
80d59cd1 CH |
257 | resp.headers["Content-Type"] = "text/html; charset=utf-8"; |
258 | resp.body = "<!html><title>" + what + "</title><h1>" + what + "</h1>"; | |
02c04144 | 259 | } else if (req.accept_json) { |
80d59cd1 | 260 | resp.headers["Content-Type"] = "application/json"; |
692829aa | 261 | resp.setErrorResult(what, resp.status); |
33196945 | 262 | } else { |
80d59cd1 CH |
263 | resp.headers["Content-Type"] = "text/plain; charset=utf-8"; |
264 | resp.body = what; | |
33196945 | 265 | } |
12c86877 | 266 | } |
33196945 | 267 | |
80d59cd1 | 268 | // always set these headers |
41ea0e50 | 269 | resp.headers["Server"] = "PowerDNS/" VERSION; |
80d59cd1 CH |
270 | resp.headers["Connection"] = "close"; |
271 | ||
ac9908c2 CH |
272 | if (req.method == "HEAD") { |
273 | resp.body = ""; | |
274 | } else { | |
335da0ba | 275 | resp.headers["Content-Length"] = std::to_string(resp.body.size()); |
ac9908c2 | 276 | } |
80d59cd1 CH |
277 | } |
278 | ||
b184a9dc | 279 | void WebServer::serveConnection(std::shared_ptr<Socket> client) const |
80d59cd1 CH |
280 | try { |
281 | HttpRequest req; | |
583ea80d CH |
282 | YaHTTP::AsyncRequestLoader yarl; |
283 | yarl.initialize(&req); | |
825fa717 CH |
284 | int timeout = 5; |
285 | client->setNonBlocking(); | |
80d59cd1 | 286 | |
80d59cd1 | 287 | try { |
825fa717 | 288 | while(!req.complete) { |
80d59cd1 CH |
289 | int bytes; |
290 | char buf[1024]; | |
825fa717 CH |
291 | bytes = client->readWithTimeout(buf, sizeof(buf), timeout); |
292 | if (bytes > 0) { | |
80d59cd1 | 293 | string data = string(buf, bytes); |
825fa717 CH |
294 | req.complete = yarl.feed(data); |
295 | } else { | |
296 | // read error OR EOF | |
297 | break; | |
80d59cd1 CH |
298 | } |
299 | } | |
583ea80d | 300 | yarl.finalize(); |
80d59cd1 | 301 | } catch (YaHTTP::ParseError &e) { |
825fa717 | 302 | // request stays incomplete |
80d59cd1 CH |
303 | } |
304 | ||
34c513f9 RG |
305 | HttpResponse resp; |
306 | WebServer::handleRequest(req, resp); | |
80d59cd1 CH |
307 | ostringstream ss; |
308 | resp.write(ss); | |
825fa717 CH |
309 | string reply = ss.str(); |
310 | ||
311 | client->writenWithTimeout(reply.c_str(), reply.size(), timeout); | |
33196945 CH |
312 | } |
313 | catch(PDNSException &e) { | |
825fa717 | 314 | L<<Logger::Error<<"HTTP Exception: "<<e.reason<<endl; |
33196945 CH |
315 | } |
316 | catch(std::exception &e) { | |
7d7d776e | 317 | if(strstr(e.what(), "timeout")==0) |
318 | L<<Logger::Error<<"HTTP STL Exception: "<<e.what()<<endl; | |
33196945 CH |
319 | } |
320 | catch(...) { | |
825fa717 | 321 | L<<Logger::Error<<"HTTP: Unknown exception"<<endl; |
33196945 CH |
322 | } |
323 | ||
690984d4 | 324 | WebServer::WebServer(const string &listenaddress, int port) : d_server(nullptr) |
12c86877 BH |
325 | { |
326 | d_listenaddress=listenaddress; | |
327 | d_port=port; | |
825fa717 CH |
328 | } |
329 | ||
330 | void WebServer::bind() | |
331 | { | |
96d299db | 332 | try { |
825fa717 CH |
333 | d_server = createServer(); |
334 | L<<Logger::Warning<<"Listening for HTTP requests on "<<d_server->d_local.toStringWithPort()<<endl; | |
96d299db | 335 | } |
825fa717 CH |
336 | catch(NetworkError &e) { |
337 | L<<Logger::Error<<"Listening on HTTP socket failed: "<<e.what()<<endl; | |
690984d4 | 338 | d_server = nullptr; |
96d299db | 339 | } |
12c86877 BH |
340 | } |
341 | ||
342 | void WebServer::go() | |
343 | { | |
96d299db BH |
344 | if(!d_server) |
345 | return; | |
12c86877 | 346 | try { |
69e7f117 KM |
347 | NetmaskGroup acl; |
348 | acl.toMasks(::arg()["webserver-allow-from"]); | |
349 | ||
3ae143b0 | 350 | while(true) { |
17d60ab0 | 351 | try { |
b184a9dc | 352 | auto client = d_server->accept(); |
8a781bb5 RG |
353 | if (!client) { |
354 | continue; | |
355 | } | |
b184a9dc RG |
356 | if (client->acl(acl)) { |
357 | std::thread webHandler(WebServerConnectionThreadStart, this, client); | |
d4c53d8c | 358 | webHandler.detach(); |
17d60ab0 RG |
359 | } else { |
360 | ComboAddress remote; | |
b184a9dc | 361 | if (client->getRemote(remote)) |
17d60ab0 | 362 | L<<Logger::Error<<"Webserver closing socket: remote ("<< remote.toString() <<") does not match 'webserver-allow-from'"<<endl; |
17d60ab0 RG |
363 | } |
364 | } | |
365 | catch(PDNSException &e) { | |
366 | L<<Logger::Error<<"PDNSException while accepting a connection in main webserver thread: "<<e.reason<<endl; | |
17d60ab0 RG |
367 | } |
368 | catch(std::exception &e) { | |
369 | L<<Logger::Error<<"STL Exception while accepting a connection in main webserver thread: "<<e.what()<<endl; | |
17d60ab0 RG |
370 | } |
371 | catch(...) { | |
372 | L<<Logger::Error<<"Unknown exception while accepting a connection in main webserver thread"<<endl; | |
69e7f117 | 373 | } |
12c86877 BH |
374 | } |
375 | } | |
69e7f117 KM |
376 | catch(PDNSException &e) { |
377 | L<<Logger::Error<<"PDNSException in main webserver thread: "<<e.reason<<endl; | |
378 | } | |
adc10f99 | 379 | catch(std::exception &e) { |
12c86877 BH |
380 | L<<Logger::Error<<"STL Exception in main webserver thread: "<<e.what()<<endl; |
381 | } | |
382 | catch(...) { | |
383 | L<<Logger::Error<<"Unknown exception in main webserver thread"<<endl; | |
384 | } | |
5bd2ea7b | 385 | _exit(1); |
3ae143b0 | 386 | } |