]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/ws-recursor.cc
Send status 201 on create
[thirdparty/pdns.git] / pdns / ws-recursor.cc
1 /*
2 PowerDNS Versatile Database Driven Nameserver
3 Copyright (C) 2003 - 2014 PowerDNS.COM BV
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License version 2
7 as published by the Free Software Foundation
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.
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 St, Fifth Floor, Boston, MA 02110-1301 USA
21 */
22 #include "ws-recursor.hh"
23 #include "json.hh"
24 #include <boost/foreach.hpp>
25 #include <string>
26 #include "namespaces.hh"
27 #include <iostream>
28 #include "iputils.hh"
29 #include "rec_channel.hh"
30 #include "arguments.hh"
31 #include "misc.hh"
32 #include "syncres.hh"
33 #include "rapidjson/document.h"
34 #include "rapidjson/stringbuffer.h"
35 #include "rapidjson/writer.h"
36 #include "webserver.hh"
37 #include "ws-api.hh"
38 #include "logger.hh"
39
40 extern __thread FDMultiplexer* t_fdm;
41
42 using namespace rapidjson;
43
44 void productServerStatisticsFetch(map<string,string>& out)
45 {
46 map<string,string> stats = getAllStatsMap();
47 out.swap(stats);
48 }
49
50 static void apiWriteConfigFile(const string& filebasename, const string& content)
51 {
52 if (::arg()["experimental-api-config-dir"].empty()) {
53 throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
54 }
55
56 string filename = ::arg()["experimental-api-config-dir"] + "/" + filebasename + ".conf";
57 ofstream ofconf(filename.c_str());
58 if (!ofconf) {
59 throw ApiException("Could not open config fragment file '"+filename+"' for writing: "+stringerror());
60 }
61 ofconf << "# Generated by pdns-recursor REST API, DO NOT EDIT" << endl;
62 ofconf << content << endl;
63 ofconf.close();
64 }
65
66 static void apiServerConfigAllowFrom(HttpRequest* req, HttpResponse* resp)
67 {
68 if (req->method == "PUT" && !::arg().mustDo("experimental-api-readonly")) {
69 Document document;
70 req->json(document);
71 const Value &jlist = document["value"];
72
73 if (!document.IsObject()) {
74 throw ApiException("Supplied JSON must be an object");
75 }
76
77 if (!jlist.IsArray()) {
78 throw ApiException("'value' must be an array");
79 }
80
81 for (SizeType i = 0; i < jlist.Size(); ++i) {
82 try {
83 Netmask(jlist[i].GetString());
84 } catch (NetmaskException &e) {
85 throw ApiException(e.reason);
86 }
87 }
88
89 ostringstream ss;
90
91 // Clear allow-from-file if set, so our changes take effect
92 ss << "allow-from-file=" << endl;
93
94 // Clear allow-from, and provide a "parent" value
95 ss << "allow-from=" << endl;
96 for (SizeType i = 0; i < jlist.Size(); ++i) {
97 ss << "allow-from+=" << jlist[i].GetString() << endl;
98 }
99
100 apiWriteConfigFile("allow-from", ss.str());
101
102 parseACLs();
103
104 // fall through to GET
105 } else if (req->method != "GET") {
106 throw HttpMethodNotAllowedException();
107 }
108
109 // Return currently configured ACLs
110 Document document;
111 document.SetObject();
112
113 Value jlist;
114 jlist.SetArray();
115
116 vector<string> entries;
117 t_allowFrom->toStringVector(&entries);
118
119 BOOST_FOREACH(const string& entry, entries) {
120 Value jentry(entry.c_str(), document.GetAllocator()); // copy
121 jlist.PushBack(jentry, document.GetAllocator());
122 }
123
124 document.AddMember("name", "allow-from", document.GetAllocator());
125 document.AddMember("value", jlist, document.GetAllocator());
126
127 resp->setBody(document);
128 }
129
130 static void fillZone(const string& zonename, HttpResponse* resp)
131 {
132 SyncRes::domainmap_t::const_iterator iter = t_sstorage->domainmap->find(zonename);
133 if (iter == t_sstorage->domainmap->end())
134 throw ApiException("Could not find domain '"+zonename+"'");
135
136 Document doc;
137 doc.SetObject();
138
139 const SyncRes::AuthDomain& zone = iter->second;
140
141 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
142 string zoneId = apiZoneNameToId(iter->first);
143 Value jzoneid(zoneId.c_str(), doc.GetAllocator()); // copy
144 doc.AddMember("id", jzoneid, doc.GetAllocator());
145 string url = "/servers/localhost/zones/" + zoneId;
146 Value jurl(url.c_str(), doc.GetAllocator()); // copy
147 doc.AddMember("url", jurl, doc.GetAllocator());
148 doc.AddMember("name", iter->first.c_str(), doc.GetAllocator());
149 doc.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator());
150 Value servers;
151 servers.SetArray();
152 BOOST_FOREACH(const ComboAddress& server, zone.d_servers) {
153 Value value(server.toStringWithPort().c_str(), doc.GetAllocator());
154 servers.PushBack(value, doc.GetAllocator());
155 }
156 doc.AddMember("servers", servers, doc.GetAllocator());
157 bool rd = zone.d_servers.empty() ? false : zone.d_rdForward;
158 doc.AddMember("recursion_desired", rd, doc.GetAllocator());
159
160 Value records;
161 records.SetArray();
162 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type& rr, zone.d_records) {
163 Value object;
164 object.SetObject();
165 Value jname(rr.qname.c_str(), doc.GetAllocator()); // copy
166 object.AddMember("name", jname, doc.GetAllocator());
167 Value jtype(rr.qtype.getName().c_str(), doc.GetAllocator()); // copy
168 object.AddMember("type", jtype, doc.GetAllocator());
169 object.AddMember("ttl", rr.ttl, doc.GetAllocator());
170 object.AddMember("priority", rr.priority, doc.GetAllocator());
171 Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy
172 object.AddMember("content", jcontent, doc.GetAllocator());
173 records.PushBack(object, doc.GetAllocator());
174 }
175 doc.AddMember("records", records, doc.GetAllocator());
176
177 resp->setBody(doc);
178 }
179
180 static void doCreateZone(const Value& document)
181 {
182 if (::arg()["experimental-api-config-dir"].empty()) {
183 throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
184 }
185
186 string zonename = stringFromJson(document, "name");
187 // TODO: better validation of zonename - apiZoneNameToId takes care of escaping / however
188 if(zonename.empty())
189 throw ApiException("Zone name empty");
190
191 if (zonename[zonename.size()-1] != '.') {
192 zonename += ".";
193 }
194
195 string singleIPTarget = stringFromJson(document, "single_target_ip", "");
196 string kind = toUpper(stringFromJson(document, "kind"));
197 bool rd = boolFromJson(document, "recursion_desired");
198 string confbasename = "zone-" + apiZoneNameToId(zonename);
199
200 if (kind == "NATIVE") {
201 if (rd)
202 throw ApiException("kind=Native and recursion_desired are mutually exclusive");
203 if(!singleIPTarget.empty()) {
204 try {
205 ComboAddress rem(singleIPTarget);
206 if(rem.sin4.sin_family != AF_INET)
207 throw ApiException("");
208 singleIPTarget = rem.toString();
209 }
210 catch(...) {
211 throw ApiException("Single IP target '"+singleIPTarget+"' is invalid");
212 }
213 }
214 string zonefilename = ::arg()["experimental-api-config-dir"] + "/" + confbasename + ".zone";
215 ofstream ofzone(zonefilename.c_str());
216 if (!ofzone) {
217 throw ApiException("Could not open '"+zonefilename+"' for writing: "+stringerror());
218 }
219 ofzone << "; Generated by pdns-recursor REST API, DO NOT EDIT" << endl;
220 ofzone << zonename << "\tIN\tSOA\tlocal.zone.\thostmaster."<<zonename<<" 1 1 1 1 1" << endl;
221 if(!singleIPTarget.empty()) {
222 ofzone <<zonename << "\t3600\tIN\tA\t"<<singleIPTarget<<endl;
223 ofzone <<"*."<<zonename << "\t3600\tIN\tA\t"<<singleIPTarget<<endl;
224 }
225 ofzone.close();
226
227 apiWriteConfigFile(confbasename, "auth-zones+=" + zonename + "=" + zonefilename);
228 } else if (kind == "FORWARDED") {
229 const Value &servers = document["servers"];
230 if (kind == "FORWARDED" && (!servers.IsArray() || servers.Size() == 0))
231 throw ApiException("Need at least one upstream server when forwarding");
232
233 string serverlist;
234 if (servers.IsArray()) {
235 for (SizeType i = 0; i < servers.Size(); ++i) {
236 if (!serverlist.empty()) {
237 serverlist += ";";
238 }
239 serverlist += servers[i].GetString();
240 }
241 }
242
243 if (rd) {
244 apiWriteConfigFile(confbasename, "forward-zones-recurse+=" + zonename + "=" + serverlist);
245 } else {
246 apiWriteConfigFile(confbasename, "forward-zones+=" + zonename + "=" + serverlist);
247 }
248 } else {
249 throw ApiException("invalid kind");
250 }
251 }
252
253 static bool doDeleteZone(const string& zonename)
254 {
255 if (::arg()["experimental-api-config-dir"].empty()) {
256 throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
257 }
258
259 string filename;
260
261 // this one must exist
262 filename = ::arg()["experimental-api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".conf";
263 if (unlink(filename.c_str()) != 0) {
264 return false;
265 }
266
267 // .zone file is optional
268 filename = ::arg()["experimental-api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".zone";
269 unlink(filename.c_str());
270
271 return true;
272 }
273
274 static void apiServerZones(HttpRequest* req, HttpResponse* resp)
275 {
276 if (req->method == "POST" && !::arg().mustDo("experimental-api-readonly")) {
277 if (::arg()["experimental-api-config-dir"].empty()) {
278 throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
279 }
280
281 Document document;
282 req->json(document);
283
284 string zonename = stringFromJson(document, "name");
285 if (zonename[zonename.size()-1] != '.') {
286 zonename += ".";
287 }
288
289 SyncRes::domainmap_t::const_iterator iter = t_sstorage->domainmap->find(zonename);
290 if (iter != t_sstorage->domainmap->end())
291 throw ApiException("Zone already exists");
292
293 doCreateZone(document);
294 reloadAuthAndForwards();
295 fillZone(zonename, resp);
296 resp->status = 201;
297 return;
298 }
299
300 if(req->method != "GET")
301 throw HttpMethodNotAllowedException();
302
303 Document doc;
304 doc.SetArray();
305
306 BOOST_FOREACH(const SyncRes::domainmap_t::value_type& val, *t_sstorage->domainmap) {
307 const SyncRes::AuthDomain& zone = val.second;
308 Value jdi;
309 jdi.SetObject();
310 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
311 string zoneId = apiZoneNameToId(val.first);
312 Value jzoneid(zoneId.c_str(), doc.GetAllocator()); // copy
313 jdi.AddMember("id", jzoneid, doc.GetAllocator());
314 string url = "/servers/localhost/zones/" + zoneId;
315 Value jurl(url.c_str(), doc.GetAllocator()); // copy
316 jdi.AddMember("url", jurl, doc.GetAllocator());
317 jdi.AddMember("name", val.first.c_str(), doc.GetAllocator());
318 jdi.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator());
319 Value servers;
320 servers.SetArray();
321 BOOST_FOREACH(const ComboAddress& server, zone.d_servers) {
322 Value value(server.toStringWithPort().c_str(), doc.GetAllocator());
323 servers.PushBack(value, doc.GetAllocator());
324 }
325 jdi.AddMember("servers", servers, doc.GetAllocator());
326 bool rd = zone.d_servers.empty() ? false : zone.d_rdForward;
327 jdi.AddMember("recursion_desired", rd, doc.GetAllocator());
328 doc.PushBack(jdi, doc.GetAllocator());
329 }
330 resp->setBody(doc);
331 }
332
333 static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
334 {
335 string zonename = apiZoneIdToName(req->path_parameters["id"]);
336 zonename += ".";
337
338 SyncRes::domainmap_t::const_iterator iter = t_sstorage->domainmap->find(zonename);
339 if (iter == t_sstorage->domainmap->end())
340 throw ApiException("Could not find domain '"+zonename+"'");
341
342 if(req->method == "PUT" && !::arg().mustDo("experimental-api-readonly")) {
343 Document document;
344 req->json(document);
345
346 doDeleteZone(zonename);
347 doCreateZone(document);
348 reloadAuthAndForwards();
349 fillZone(stringFromJson(document, "name"), resp);
350 }
351 else if(req->method == "DELETE" && !::arg().mustDo("experimental-api-readonly")) {
352 if (!doDeleteZone(zonename)) {
353 throw ApiException("Deleting domain failed");
354 }
355
356 reloadAuthAndForwards();
357 // empty body on success
358 resp->body = "";
359 resp->status = 204; // No Content: declare that the zone is gone now
360 } else if(req->method == "GET") {
361 fillZone(zonename, resp);
362 } else {
363 throw HttpMethodNotAllowedException();
364 }
365 }
366
367 static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
368 if(req->method != "GET")
369 throw HttpMethodNotAllowedException();
370
371 string q = req->parameters["q"];
372 if (q.empty())
373 throw ApiException("Query q can't be blank");
374
375 Document doc;
376 doc.SetArray();
377
378 BOOST_FOREACH(const SyncRes::domainmap_t::value_type& val, *t_sstorage->domainmap) {
379 string zoneId = apiZoneNameToId(val.first);
380 if (pdns_ci_find(val.first, q) != string::npos) {
381 Value object;
382 object.SetObject();
383 object.AddMember("type", "zone", doc.GetAllocator());
384 Value jzoneId(zoneId.c_str(), doc.GetAllocator()); // copy
385 object.AddMember("zone_id", jzoneId, doc.GetAllocator());
386 Value jzoneName(val.first.c_str(), doc.GetAllocator()); // copy
387 object.AddMember("name", jzoneName, doc.GetAllocator());
388 doc.PushBack(object, doc.GetAllocator());
389 }
390
391 // if zone name is an exact match, don't bother with returning all records/comments in it
392 if (val.first == q) {
393 continue;
394 }
395
396 const SyncRes::AuthDomain& zone = val.second;
397
398 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type& rr, zone.d_records) {
399 if (pdns_ci_find(rr.qname, q) == string::npos && pdns_ci_find(rr.content, q) == string::npos)
400 continue;
401
402 Value object;
403 object.SetObject();
404 object.AddMember("type", "record", doc.GetAllocator());
405 Value jzoneId(zoneId.c_str(), doc.GetAllocator()); // copy
406 object.AddMember("zone_id", jzoneId, doc.GetAllocator());
407 Value jzoneName(val.first.c_str(), doc.GetAllocator()); // copy
408 object.AddMember("zone_name", jzoneName, doc.GetAllocator());
409 Value jname(rr.qname.c_str(), doc.GetAllocator()); // copy
410 object.AddMember("name", jname, doc.GetAllocator());
411 Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy
412 object.AddMember("content", jcontent, doc.GetAllocator());
413
414 doc.PushBack(object, doc.GetAllocator());
415 }
416 }
417 resp->setBody(doc);
418 }
419
420 RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
421 {
422 RecursorControlParser rcp; // inits
423
424 d_ws = new AsyncWebServer(fdm, arg()["experimental-webserver-address"], arg().asNum("experimental-webserver-port"), arg()["experimental-webserver-password"]);
425 d_ws->bind();
426
427 // legacy dispatch
428 d_ws->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat, this, _1, _2));
429 d_ws->registerApiHandler("/servers/localhost/config/allow-from", &apiServerConfigAllowFrom);
430 d_ws->registerApiHandler("/servers/localhost/config", &apiServerConfig);
431 d_ws->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog);
432 d_ws->registerApiHandler("/servers/localhost/search-data", &apiServerSearchData);
433 d_ws->registerApiHandler("/servers/localhost/statistics", &apiServerStatistics);
434 d_ws->registerApiHandler("/servers/localhost/zones/<id>", &apiServerZoneDetail);
435 d_ws->registerApiHandler("/servers/localhost/zones", &apiServerZones);
436 d_ws->registerApiHandler("/servers/localhost", &apiServerDetail);
437 d_ws->registerApiHandler("/servers", &apiServer);
438
439 d_ws->go();
440 }
441
442 void RecursorWebServer::jsonstat(HttpRequest* req, HttpResponse *resp)
443 {
444 string command;
445
446 if(req->parameters.count("command")) {
447 command = req->parameters["command"];
448 req->parameters.erase("command");
449 }
450
451 map<string, string> stats;
452 if(command == "domains") {
453 Document doc;
454 doc.SetArray();
455 BOOST_FOREACH(const SyncRes::domainmap_t::value_type& val, *t_sstorage->domainmap) {
456 Value jzone;
457 jzone.SetObject();
458
459 const SyncRes::AuthDomain& zone = val.second;
460 Value zonename(val.first.c_str(), doc.GetAllocator());
461 jzone.AddMember("name", zonename, doc.GetAllocator());
462 jzone.AddMember("type", "Zone", doc.GetAllocator());
463 jzone.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator());
464 Value servers;
465 servers.SetArray();
466 BOOST_FOREACH(const ComboAddress& server, zone.d_servers) {
467 Value value(server.toStringWithPort().c_str(), doc.GetAllocator());
468 servers.PushBack(value, doc.GetAllocator());
469 }
470 jzone.AddMember("servers", servers, doc.GetAllocator());
471 bool rdbit = zone.d_servers.empty() ? false : zone.d_rdForward;
472 jzone.AddMember("rdbit", rdbit, doc.GetAllocator());
473
474 doc.PushBack(jzone, doc.GetAllocator());
475 }
476 resp->setBody(doc);
477 return;
478 }
479 else if(command == "zone") {
480 string arg_zone = req->parameters["zone"];
481 SyncRes::domainmap_t::const_iterator ret = t_sstorage->domainmap->find(arg_zone);
482 if (ret != t_sstorage->domainmap->end()) {
483 Document doc;
484 doc.SetObject();
485 Value root;
486 root.SetObject();
487
488 const SyncRes::AuthDomain& zone = ret->second;
489 Value zonename(ret->first.c_str(), doc.GetAllocator());
490 root.AddMember("name", zonename, doc.GetAllocator());
491 root.AddMember("type", "Zone", doc.GetAllocator());
492 root.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator());
493 Value servers;
494 servers.SetArray();
495 BOOST_FOREACH(const ComboAddress& server, zone.d_servers) {
496 Value value(server.toStringWithPort().c_str(), doc.GetAllocator());
497 servers.PushBack(value, doc.GetAllocator());
498 }
499 root.AddMember("servers", servers, doc.GetAllocator());
500 bool rdbit = zone.d_servers.empty() ? false : zone.d_rdForward;
501 root.AddMember("rdbit", rdbit, doc.GetAllocator());
502
503 Value records;
504 records.SetArray();
505 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type& rr, zone.d_records) {
506 Value object;
507 object.SetObject();
508 Value jname(rr.qname.c_str(), doc.GetAllocator()); // copy
509 object.AddMember("name", jname, doc.GetAllocator());
510 Value jtype(rr.qtype.getName().c_str(), doc.GetAllocator()); // copy
511 object.AddMember("type", jtype, doc.GetAllocator());
512 object.AddMember("ttl", rr.ttl, doc.GetAllocator());
513 object.AddMember("priority", rr.priority, doc.GetAllocator());
514 Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy
515 object.AddMember("content", jcontent, doc.GetAllocator());
516 records.PushBack(object, doc.GetAllocator());
517 }
518 root.AddMember("records", records, doc.GetAllocator());
519
520 doc.AddMember("zone", root, doc.GetAllocator());
521 resp->setBody(doc);
522 return;
523 } else {
524 resp->body = returnJsonError("Could not find domain '"+arg_zone+"'");
525 return;
526 }
527 }
528 else if(command == "flush-cache") {
529 string canon=toCanonic("", req->parameters["domain"]);
530 int count = broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeCache, canon));
531 count+=broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeAndCountNegCache, canon));
532 stats["number"]=lexical_cast<string>(count);
533 resp->body = returnJsonObject(stats);
534 return;
535 }
536 else if(command == "config") {
537 vector<string> items = ::arg().list();
538 BOOST_FOREACH(const string& var, items) {
539 stats[var] = ::arg()[var];
540 }
541 resp->body = returnJsonObject(stats);
542 return;
543 }
544 else if(command == "log-grep") {
545 // legacy parameter name hack
546 req->parameters["q"] = req->parameters["needle"];
547 apiServerSearchLog(req, resp);
548 return;
549 }
550 else if(command == "stats") {
551 stats = getAllStatsMap();
552 resp->body = returnJsonObject(stats);
553 return;
554 } else {
555 resp->status = 404;
556 resp->body = returnJsonError("Not found");
557 }
558 }
559
560
561 void AsyncServerNewConnectionMT(void *p) {
562 AsyncServer *server = (AsyncServer*)p;
563 try {
564 Socket* socket = server->accept();
565 server->d_asyncNewConnectionCallback(socket);
566 delete socket;
567 } catch (NetworkError &e) {
568 // we're running in a shared process/thread, so can't just terminate/abort.
569 return;
570 }
571 }
572
573 void AsyncServer::asyncWaitForConnections(FDMultiplexer* fdm, const newconnectioncb_t& callback)
574 {
575 d_asyncNewConnectionCallback = callback;
576 fdm->addReadFD(d_server_socket.getHandle(), boost::bind(&AsyncServer::newConnection, this));
577 }
578
579 void AsyncServer::newConnection()
580 {
581 MT->makeThread(&AsyncServerNewConnectionMT, this);
582 }
583
584
585 void AsyncWebServer::serveConnection(Socket *client)
586 {
587 HttpRequest req;
588 YaHTTP::AsyncRequestLoader yarl(&req);
589 client->setNonBlocking();
590
591 string data;
592 try {
593 while(!req.complete) {
594 data.clear();
595 int bytes = arecvtcp(data, 16384, client, true);
596 if (bytes > 0) {
597 req.complete = yarl.feed(data);
598 } else {
599 // read error OR EOF
600 break;
601 }
602 }
603 } catch (YaHTTP::ParseError &e) {
604 // request stays incomplete
605 }
606
607 HttpResponse resp = handleRequest(req);
608 ostringstream ss;
609 resp.write(ss);
610 data = ss.str();
611
612 // now send the reply
613 if (asendtcp(data, client) == -1 || data.empty()) {
614 L<<Logger::Error<<"Failed sending reply to HTTP client"<<endl;
615 }
616 }
617
618 void AsyncWebServer::go() {
619 if (!d_server)
620 return;
621 ((AsyncServer*)d_server)->asyncWaitForConnections(d_fdm, boost::bind(&AsyncWebServer::serveConnection, this, _1));
622 }