]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/ws-recursor.cc
Webserver: convert to new yahttp api and router
[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 return;
297 }
298
299 if(req->method != "GET")
300 throw HttpMethodNotAllowedException();
301
302 Document doc;
303 doc.SetArray();
304
305 BOOST_FOREACH(const SyncRes::domainmap_t::value_type& val, *t_sstorage->domainmap) {
306 const SyncRes::AuthDomain& zone = val.second;
307 Value jdi;
308 jdi.SetObject();
309 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
310 string zoneId = apiZoneNameToId(val.first);
311 Value jzoneid(zoneId.c_str(), doc.GetAllocator()); // copy
312 jdi.AddMember("id", jzoneid, doc.GetAllocator());
313 string url = "/servers/localhost/zones/" + zoneId;
314 Value jurl(url.c_str(), doc.GetAllocator()); // copy
315 jdi.AddMember("url", jurl, doc.GetAllocator());
316 jdi.AddMember("name", val.first.c_str(), doc.GetAllocator());
317 jdi.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator());
318 Value servers;
319 servers.SetArray();
320 BOOST_FOREACH(const ComboAddress& server, zone.d_servers) {
321 Value value(server.toStringWithPort().c_str(), doc.GetAllocator());
322 servers.PushBack(value, doc.GetAllocator());
323 }
324 jdi.AddMember("servers", servers, doc.GetAllocator());
325 bool rd = zone.d_servers.empty() ? false : zone.d_rdForward;
326 jdi.AddMember("recursion_desired", rd, doc.GetAllocator());
327 doc.PushBack(jdi, doc.GetAllocator());
328 }
329 resp->setBody(doc);
330 }
331
332 static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
333 {
334 string zonename = apiZoneIdToName(req->parameters["id"]);
335 zonename += ".";
336
337 SyncRes::domainmap_t::const_iterator iter = t_sstorage->domainmap->find(zonename);
338 if (iter == t_sstorage->domainmap->end())
339 throw ApiException("Could not find domain '"+zonename+"'");
340
341 if(req->method == "PUT" && !::arg().mustDo("experimental-api-readonly")) {
342 Document document;
343 req->json(document);
344
345 doDeleteZone(zonename);
346 doCreateZone(document);
347 reloadAuthAndForwards();
348 fillZone(stringFromJson(document, "name"), resp);
349 }
350 else if(req->method == "DELETE" && !::arg().mustDo("experimental-api-readonly")) {
351 if (!doDeleteZone(zonename)) {
352 throw ApiException("Deleting domain failed");
353 }
354
355 reloadAuthAndForwards();
356 // empty body on success
357 resp->body = "";
358 resp->status = 204; // No Content: declare that the zone is gone now
359 } else if(req->method == "GET") {
360 fillZone(zonename, resp);
361 } else {
362 throw HttpMethodNotAllowedException();
363 }
364 }
365
366 static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
367 if(req->method != "GET")
368 throw HttpMethodNotAllowedException();
369
370 string q = req->getvars["q"];
371 if (q.empty())
372 throw ApiException("Query q can't be blank");
373
374 Document doc;
375 doc.SetArray();
376
377 BOOST_FOREACH(const SyncRes::domainmap_t::value_type& val, *t_sstorage->domainmap) {
378 string zoneId = apiZoneNameToId(val.first);
379 if (pdns_ci_find(val.first, q) != string::npos) {
380 Value object;
381 object.SetObject();
382 object.AddMember("type", "zone", doc.GetAllocator());
383 Value jzoneId(zoneId.c_str(), doc.GetAllocator()); // copy
384 object.AddMember("zone_id", jzoneId, doc.GetAllocator());
385 Value jzoneName(val.first.c_str(), doc.GetAllocator()); // copy
386 object.AddMember("name", jzoneName, doc.GetAllocator());
387 doc.PushBack(object, doc.GetAllocator());
388 }
389
390 // if zone name is an exact match, don't bother with returning all records/comments in it
391 if (val.first == q) {
392 continue;
393 }
394
395 const SyncRes::AuthDomain& zone = val.second;
396
397 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type& rr, zone.d_records) {
398 if (pdns_ci_find(rr.qname, q) == string::npos && pdns_ci_find(rr.content, q) == string::npos)
399 continue;
400
401 Value object;
402 object.SetObject();
403 object.AddMember("type", "record", doc.GetAllocator());
404 Value jzoneId(zoneId.c_str(), doc.GetAllocator()); // copy
405 object.AddMember("zone_id", jzoneId, doc.GetAllocator());
406 Value jzoneName(val.first.c_str(), doc.GetAllocator()); // copy
407 object.AddMember("zone_name", jzoneName, doc.GetAllocator());
408 Value jname(rr.qname.c_str(), doc.GetAllocator()); // copy
409 object.AddMember("name", jname, doc.GetAllocator());
410 Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy
411 object.AddMember("content", jcontent, doc.GetAllocator());
412
413 doc.PushBack(object, doc.GetAllocator());
414 }
415 }
416 resp->setBody(doc);
417 }
418
419 RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
420 {
421 RecursorControlParser rcp; // inits
422
423 d_ws = new AsyncWebServer(fdm, arg()["experimental-webserver-address"], arg().asNum("experimental-webserver-port"), arg()["experimental-webserver-password"]);
424 d_ws->bind();
425
426 // legacy dispatch
427 d_ws->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat, this, _1, _2));
428 d_ws->registerApiHandler("/servers/localhost/config/allow-from", &apiServerConfigAllowFrom);
429 d_ws->registerApiHandler("/servers/localhost/config", &apiServerConfig);
430 d_ws->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog);
431 d_ws->registerApiHandler("/servers/localhost/search-data", &apiServerSearchData);
432 d_ws->registerApiHandler("/servers/localhost/statistics", &apiServerStatistics);
433 d_ws->registerApiHandler("/servers/localhost/zones/<id>", &apiServerZoneDetail);
434 d_ws->registerApiHandler("/servers/localhost/zones", &apiServerZones);
435 d_ws->registerApiHandler("/servers/localhost", &apiServerDetail);
436 d_ws->registerApiHandler("/servers", &apiServer);
437
438 d_ws->go();
439 }
440
441 void RecursorWebServer::jsonstat(HttpRequest* req, HttpResponse *resp)
442 {
443 string command;
444
445 if(req->getvars.count("command")) {
446 command = req->getvars["command"];
447 req->getvars.erase("command");
448 }
449
450 map<string, string> stats;
451 if(command == "domains") {
452 Document doc;
453 doc.SetArray();
454 BOOST_FOREACH(const SyncRes::domainmap_t::value_type& val, *t_sstorage->domainmap) {
455 Value jzone;
456 jzone.SetObject();
457
458 const SyncRes::AuthDomain& zone = val.second;
459 Value zonename(val.first.c_str(), doc.GetAllocator());
460 jzone.AddMember("name", zonename, doc.GetAllocator());
461 jzone.AddMember("type", "Zone", doc.GetAllocator());
462 jzone.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator());
463 Value servers;
464 servers.SetArray();
465 BOOST_FOREACH(const ComboAddress& server, zone.d_servers) {
466 Value value(server.toStringWithPort().c_str(), doc.GetAllocator());
467 servers.PushBack(value, doc.GetAllocator());
468 }
469 jzone.AddMember("servers", servers, doc.GetAllocator());
470 bool rdbit = zone.d_servers.empty() ? false : zone.d_rdForward;
471 jzone.AddMember("rdbit", rdbit, doc.GetAllocator());
472
473 doc.PushBack(jzone, doc.GetAllocator());
474 }
475 resp->setBody(doc);
476 return;
477 }
478 else if(command == "zone") {
479 string arg_zone = req->getvars["zone"];
480 SyncRes::domainmap_t::const_iterator ret = t_sstorage->domainmap->find(arg_zone);
481 if (ret != t_sstorage->domainmap->end()) {
482 Document doc;
483 doc.SetObject();
484 Value root;
485 root.SetObject();
486
487 const SyncRes::AuthDomain& zone = ret->second;
488 Value zonename(ret->first.c_str(), doc.GetAllocator());
489 root.AddMember("name", zonename, doc.GetAllocator());
490 root.AddMember("type", "Zone", doc.GetAllocator());
491 root.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator());
492 Value servers;
493 servers.SetArray();
494 BOOST_FOREACH(const ComboAddress& server, zone.d_servers) {
495 Value value(server.toStringWithPort().c_str(), doc.GetAllocator());
496 servers.PushBack(value, doc.GetAllocator());
497 }
498 root.AddMember("servers", servers, doc.GetAllocator());
499 bool rdbit = zone.d_servers.empty() ? false : zone.d_rdForward;
500 root.AddMember("rdbit", rdbit, doc.GetAllocator());
501
502 Value records;
503 records.SetArray();
504 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type& rr, zone.d_records) {
505 Value object;
506 object.SetObject();
507 Value jname(rr.qname.c_str(), doc.GetAllocator()); // copy
508 object.AddMember("name", jname, doc.GetAllocator());
509 Value jtype(rr.qtype.getName().c_str(), doc.GetAllocator()); // copy
510 object.AddMember("type", jtype, doc.GetAllocator());
511 object.AddMember("ttl", rr.ttl, doc.GetAllocator());
512 object.AddMember("priority", rr.priority, doc.GetAllocator());
513 Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy
514 object.AddMember("content", jcontent, doc.GetAllocator());
515 records.PushBack(object, doc.GetAllocator());
516 }
517 root.AddMember("records", records, doc.GetAllocator());
518
519 doc.AddMember("zone", root, doc.GetAllocator());
520 resp->setBody(doc);
521 return;
522 } else {
523 resp->body = returnJsonError("Could not find domain '"+arg_zone+"'");
524 return;
525 }
526 }
527 else if(command == "flush-cache") {
528 string canon=toCanonic("", req->getvars["domain"]);
529 int count = broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeCache, canon));
530 count+=broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeAndCountNegCache, canon));
531 stats["number"]=lexical_cast<string>(count);
532 resp->body = returnJsonObject(stats);
533 return;
534 }
535 else if(command == "config") {
536 vector<string> items = ::arg().list();
537 BOOST_FOREACH(const string& var, items) {
538 stats[var] = ::arg()[var];
539 }
540 resp->body = returnJsonObject(stats);
541 return;
542 }
543 else if(command == "log-grep") {
544 // legacy parameter name hack
545 req->getvars["q"] = req->getvars["needle"];
546 apiServerSearchLog(req, resp);
547 return;
548 }
549 else if(command == "stats") {
550 stats = getAllStatsMap();
551 resp->body = returnJsonObject(stats);
552 return;
553 } else {
554 resp->status = 404;
555 resp->body = returnJsonError("Not found");
556 }
557 }
558
559
560 void AsyncServerNewConnectionMT(void *p) {
561 AsyncServer *server = (AsyncServer*)p;
562 try {
563 Socket* socket = server->accept();
564 server->d_asyncNewConnectionCallback(socket);
565 delete socket;
566 } catch (NetworkError &e) {
567 // we're running in a shared process/thread, so can't just terminate/abort.
568 return;
569 }
570 }
571
572 void AsyncServer::asyncWaitForConnections(FDMultiplexer* fdm, const newconnectioncb_t& callback)
573 {
574 d_asyncNewConnectionCallback = callback;
575 fdm->addReadFD(d_server_socket.getHandle(), boost::bind(&AsyncServer::newConnection, this));
576 }
577
578 void AsyncServer::newConnection()
579 {
580 MT->makeThread(&AsyncServerNewConnectionMT, this);
581 }
582
583
584 void AsyncWebServer::serveConnection(Socket *client)
585 {
586 HttpRequest req;
587 YaHTTP::AsyncRequestLoader yarl;
588 yarl.initialize(&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 yarl.finalize();
604 } catch (YaHTTP::ParseError &e) {
605 // request stays incomplete
606 }
607
608 HttpResponse resp = handleRequest(req);
609 ostringstream ss;
610 resp.write(ss);
611 data = ss.str();
612
613 // now send the reply
614 if (asendtcp(data, client) == -1 || data.empty()) {
615 L<<Logger::Error<<"Failed sending reply to HTTP client"<<endl;
616 }
617 }
618
619 void AsyncWebServer::go() {
620 if (!d_server)
621 return;
622 ((AsyncServer*)d_server)->asyncWaitForConnections(d_fdm, boost::bind(&AsyncWebServer::serveConnection, this, _1));
623 }