]>
Commit | Line | Data |
---|---|---|
12c86877 BH |
1 | /* |
2 | PowerDNS Versatile Database Driven Nameserver | |
ac7ba905 | 3 | Copyright (C) 2002-2012 PowerDNS.COM BV |
12c86877 BH |
4 | |
5 | This program is free software; you can redistribute it and/or modify | |
22dc646a BH |
6 | it under the terms of the GNU General Public License version 2 |
7 | as published by the Free Software Foundation | |
f782fe38 MH |
8 | |
9 | Additionally, the license of this program contains a special | |
10 | exception which allows to distribute the program in binary form when | |
11 | it is linked against OpenSSL. | |
12c86877 BH |
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 | |
06bd9ccf | 20 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
12c86877 | 21 | */ |
731f58b8 | 22 | #include "utility.hh" |
12c86877 | 23 | #include "webserver.hh" |
12c86877 BH |
24 | #include "misc.hh" |
25 | #include <vector> | |
26 | #include "logger.hh" | |
27 | #include <stdio.h> | |
28 | #include "dns.hh" | |
2db9c30e | 29 | #include "base64.hh" |
33196945 | 30 | #include "json.hh" |
69e7f117 | 31 | #include "arguments.hh" |
583ea80d | 32 | #include <yahttp/router.hpp> |
12c86877 | 33 | |
232f0877 CH |
34 | struct connectionThreadData { |
35 | WebServer* webServer; | |
825fa717 | 36 | Socket* client; |
232f0877 | 37 | }; |
12c86877 | 38 | |
6ec5e728 CH |
39 | void HttpRequest::json(rapidjson::Document& document) |
40 | { | |
583ea80d CH |
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 | } | |
6ec5e728 | 45 | if(document.Parse<0>(this->body.c_str()).HasParseError()) { |
583ea80d | 46 | L<<Logger::Debug<<"HTTP: parsing of JSON document failed" << endl; |
6ec5e728 CH |
47 | throw HttpBadRequestException(); |
48 | } | |
49 | } | |
50 | ||
bbef8f04 CH |
51 | bool HttpRequest::compareAuthorization(const string &expected_password) |
52 | { | |
53 | // validate password | |
54 | YaHTTP::strstr_map_t::iterator header = headers.find("authorization"); | |
55 | bool auth_ok = false; | |
56 | if (header != headers.end() && toLower(header->second).find("basic ") == 0) { | |
57 | string cookie = header->second.substr(6); | |
58 | ||
59 | string plain; | |
60 | B64Decode(cookie, plain); | |
61 | ||
62 | vector<string> cparts; | |
63 | stringtok(cparts, plain, ":"); | |
64 | ||
65 | // this gets rid of terminating zeros | |
66 | auth_ok = (cparts.size()==2 && (0==strcmp(cparts[1].c_str(), expected_password.c_str()))); | |
67 | } | |
68 | return auth_ok; | |
69 | } | |
70 | ||
71 | bool HttpRequest::compareHeader(const string &header_name, const string &expected_value) | |
72 | { | |
73 | YaHTTP::strstr_map_t::iterator header = headers.find(header_name); | |
74 | if (header == headers.end()) | |
75 | return false; | |
76 | ||
77 | // this gets rid of terminating zeros | |
78 | return (0==strcmp(header->second.c_str(), expected_value.c_str())); | |
79 | } | |
80 | ||
81 | ||
669822d0 CH |
82 | void HttpResponse::setBody(rapidjson::Document& document) |
83 | { | |
84 | this->body = makeStringFromDocument(document); | |
85 | } | |
86 | ||
bbef8f04 | 87 | static void bareHandlerWrapper(WebServer::HandlerFunction handler, YaHTTP::Request* req, YaHTTP::Response* resp) |
12c86877 | 88 | { |
583ea80d CH |
89 | // wrapper to convert from YaHTTP::* to our subclasses |
90 | handler(static_cast<HttpRequest*>(req), static_cast<HttpResponse*>(resp)); | |
91 | } | |
232f0877 | 92 | |
bbef8f04 | 93 | void WebServer::registerBareHandler(const string& url, HandlerFunction handler) |
583ea80d | 94 | { |
bbef8f04 | 95 | YaHTTP::THandlerFunction f = boost::bind(&bareHandlerWrapper, handler, _1, _2); |
583ea80d | 96 | YaHTTP::Router::Any(url, f); |
232f0877 CH |
97 | } |
98 | ||
7f7481be AT |
99 | static bool optionsHandler(HttpRequest* req, HttpResponse* resp) { |
100 | if (req->method == "OPTIONS") { | |
101 | resp->headers["access-control-allow-origin"] = "*"; | |
102 | resp->headers["access-control-allow-headers"] = "Content-Type, X-API-Key"; | |
103 | resp->headers["access-control-allow-methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"; | |
104 | resp->headers["access-control-max-age"] = "3600"; | |
105 | resp->status = 200; | |
106 | resp->headers["content-type"]= "text/plain"; | |
107 | resp->body = ""; | |
108 | return true; | |
109 | } | |
110 | return false; | |
111 | } | |
112 | ||
583ea80d | 113 | static void apiWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp) { |
bbef8f04 | 114 | const string& api_key = arg()["experimental-api-key"]; |
7f7481be AT |
115 | |
116 | if (optionsHandler(req, resp)) return; | |
117 | ||
118 | resp->headers["access-control-allow-origin"] = "*"; | |
119 | ||
bbef8f04 | 120 | if (api_key.empty()) { |
53255086 PL |
121 | L<<Logger::Error<<"HTTP API Request \"" << req->url.path << "\": Authentication failed, API Key missing in config" << endl; |
122 | throw HttpUnauthorizedException("X-API-Key"); | |
bbef8f04 | 123 | } |
c89f8cd0 | 124 | bool auth_ok = req->compareHeader("x-api-key", api_key) || req->getvars["api-key"]==api_key; |
125 | ||
bbef8f04 | 126 | if (!auth_ok) { |
53255086 PL |
127 | L<<Logger::Error<<"HTTP Request \"" << req->url.path << "\": Authentication by API Key failed" << endl; |
128 | throw HttpUnauthorizedException("X-API-Key"); | |
bbef8f04 CH |
129 | } |
130 | ||
3ae143b0 CH |
131 | resp->headers["Content-Type"] = "application/json"; |
132 | ||
133 | string callback; | |
134 | ||
583ea80d CH |
135 | if(req->getvars.count("callback")) { |
136 | callback=req->getvars["callback"]; | |
137 | req->getvars.erase("callback"); | |
3ae143b0 CH |
138 | } |
139 | ||
583ea80d | 140 | req->getvars.erase("_"); // jQuery cache buster |
3ae143b0 CH |
141 | |
142 | try { | |
583ea80d | 143 | resp->status = 200; |
3ae143b0 CH |
144 | handler(req, resp); |
145 | } catch (ApiException &e) { | |
6ec5e728 CH |
146 | resp->body = returnJsonError(e.what()); |
147 | resp->status = 422; | |
148 | return; | |
149 | } catch (JsonException &e) { | |
150 | resp->body = returnJsonError(e.what()); | |
3ae143b0 CH |
151 | resp->status = 422; |
152 | return; | |
153 | } | |
154 | ||
37663c3b CH |
155 | if (resp->status == 204) { |
156 | // No Content -> no Content-Type. | |
157 | resp->headers.erase("Content-Type"); | |
158 | } | |
159 | ||
3ae143b0 CH |
160 | if(!callback.empty()) { |
161 | resp->body = callback + "(" + resp->body + ");"; | |
162 | } | |
163 | } | |
164 | ||
165 | void WebServer::registerApiHandler(const string& url, HandlerFunction handler) { | |
166 | HandlerFunction f = boost::bind(&apiWrapper, handler, _1, _2); | |
bbef8f04 CH |
167 | registerBareHandler(url, f); |
168 | } | |
169 | ||
170 | static void webWrapper(WebServer::HandlerFunction handler, HttpRequest* req, HttpResponse* resp) { | |
171 | const string& web_password = arg()["webserver-password"]; | |
7f7481be | 172 | |
bbef8f04 CH |
173 | if (!web_password.empty()) { |
174 | bool auth_ok = req->compareAuthorization(web_password); | |
175 | if (!auth_ok) { | |
176 | L<<Logger::Debug<<"HTTP Request \"" << req->url.path << "\": Web Authentication failed" << endl; | |
53255086 | 177 | throw HttpUnauthorizedException("Basic"); |
bbef8f04 CH |
178 | } |
179 | } | |
180 | ||
181 | handler(req, resp); | |
182 | } | |
183 | ||
184 | void WebServer::registerWebHandler(const string& url, HandlerFunction handler) { | |
185 | HandlerFunction f = boost::bind(&webWrapper, handler, _1, _2); | |
186 | registerBareHandler(url, f); | |
3ae143b0 CH |
187 | } |
188 | ||
232f0877 CH |
189 | static void *WebServerConnectionThreadStart(void *p) { |
190 | connectionThreadData* data = static_cast<connectionThreadData*>(p); | |
6b70b8c7 | 191 | pthread_detach(pthread_self()); |
232f0877 | 192 | data->webServer->serveConnection(data->client); |
80d59cd1 | 193 | |
825fa717 | 194 | delete data->client; // close socket |
232f0877 | 195 | delete data; |
80d59cd1 | 196 | |
232f0877 CH |
197 | return NULL; |
198 | } | |
199 | ||
80d59cd1 CH |
200 | HttpResponse WebServer::handleRequest(HttpRequest req) |
201 | { | |
583ea80d | 202 | HttpResponse resp; |
825fa717 | 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 | } |
583ea80d CH |
236 | catch(HttpException) { |
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"; |
6ec5e728 | 261 | resp.body = returnJsonError(what); |
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 { | |
275 | resp.headers["Content-Length"] = lexical_cast<string>(resp.body.size()); | |
276 | } | |
277 | ||
80d59cd1 CH |
278 | return resp; |
279 | } | |
280 | ||
825fa717 | 281 | void WebServer::serveConnection(Socket *client) |
80d59cd1 CH |
282 | try { |
283 | HttpRequest req; | |
583ea80d CH |
284 | YaHTTP::AsyncRequestLoader yarl; |
285 | yarl.initialize(&req); | |
825fa717 CH |
286 | int timeout = 5; |
287 | client->setNonBlocking(); | |
80d59cd1 | 288 | |
80d59cd1 | 289 | try { |
825fa717 | 290 | while(!req.complete) { |
80d59cd1 CH |
291 | int bytes; |
292 | char buf[1024]; | |
825fa717 CH |
293 | bytes = client->readWithTimeout(buf, sizeof(buf), timeout); |
294 | if (bytes > 0) { | |
80d59cd1 | 295 | string data = string(buf, bytes); |
825fa717 CH |
296 | req.complete = yarl.feed(data); |
297 | } else { | |
298 | // read error OR EOF | |
299 | break; | |
80d59cd1 CH |
300 | } |
301 | } | |
583ea80d | 302 | yarl.finalize(); |
80d59cd1 | 303 | } catch (YaHTTP::ParseError &e) { |
825fa717 | 304 | // request stays incomplete |
80d59cd1 CH |
305 | } |
306 | ||
307 | HttpResponse resp = WebServer::handleRequest(req); | |
308 | ostringstream ss; | |
309 | resp.write(ss); | |
825fa717 CH |
310 | string reply = ss.str(); |
311 | ||
312 | client->writenWithTimeout(reply.c_str(), reply.size(), timeout); | |
33196945 CH |
313 | } |
314 | catch(PDNSException &e) { | |
825fa717 | 315 | L<<Logger::Error<<"HTTP Exception: "<<e.reason<<endl; |
33196945 CH |
316 | } |
317 | catch(std::exception &e) { | |
7d7d776e | 318 | if(strstr(e.what(), "timeout")==0) |
319 | L<<Logger::Error<<"HTTP STL Exception: "<<e.what()<<endl; | |
33196945 CH |
320 | } |
321 | catch(...) { | |
825fa717 | 322 | L<<Logger::Error<<"HTTP: Unknown exception"<<endl; |
33196945 CH |
323 | } |
324 | ||
bbef8f04 | 325 | WebServer::WebServer(const string &listenaddress, int port) : d_server(NULL) |
12c86877 BH |
326 | { |
327 | d_listenaddress=listenaddress; | |
328 | d_port=port; | |
825fa717 CH |
329 | } |
330 | ||
331 | void WebServer::bind() | |
332 | { | |
96d299db | 333 | try { |
825fa717 CH |
334 | d_server = createServer(); |
335 | L<<Logger::Warning<<"Listening for HTTP requests on "<<d_server->d_local.toStringWithPort()<<endl; | |
96d299db | 336 | } |
825fa717 CH |
337 | catch(NetworkError &e) { |
338 | L<<Logger::Error<<"Listening on HTTP socket failed: "<<e.what()<<endl; | |
8b6348d6 | 339 | d_server = NULL; |
96d299db | 340 | } |
12c86877 BH |
341 | } |
342 | ||
343 | void WebServer::go() | |
344 | { | |
96d299db BH |
345 | if(!d_server) |
346 | return; | |
12c86877 | 347 | try { |
12c86877 | 348 | pthread_t tid; |
12c86877 | 349 | |
69e7f117 KM |
350 | NetmaskGroup acl; |
351 | acl.toMasks(::arg()["webserver-allow-from"]); | |
352 | ||
3ae143b0 | 353 | while(true) { |
825fa717 | 354 | // data and data->client will be freed by thread |
232f0877 CH |
355 | connectionThreadData *data = new connectionThreadData; |
356 | data->webServer = this; | |
3ae143b0 | 357 | data->client = d_server->accept(); |
69e7f117 KM |
358 | if (data->client->acl(acl)) { |
359 | pthread_create(&tid, 0, &WebServerConnectionThreadStart, (void *)data); | |
360 | } else { | |
f0e5e114 KM |
361 | ComboAddress remote; |
362 | if (data->client->getRemote(remote)) | |
363 | L<<Logger::Error<<"Webserver closing socket: remote ("<< remote.toString() <<") does not match 'webserver-allow-from'"<<endl; | |
69e7f117 KM |
364 | delete data->client; // close socket |
365 | delete data; | |
366 | } | |
12c86877 BH |
367 | } |
368 | } | |
69e7f117 KM |
369 | catch(PDNSException &e) { |
370 | L<<Logger::Error<<"PDNSException in main webserver thread: "<<e.reason<<endl; | |
371 | } | |
adc10f99 | 372 | catch(std::exception &e) { |
12c86877 BH |
373 | L<<Logger::Error<<"STL Exception in main webserver thread: "<<e.what()<<endl; |
374 | } | |
375 | catch(...) { | |
376 | L<<Logger::Error<<"Unknown exception in main webserver thread"<<endl; | |
377 | } | |
378 | exit(1); | |
3ae143b0 | 379 | } |