]> git.ipfire.org Git - thirdparty/pdns.git/blame_incremental - pdns/ws-auth.cc
API: one UeberBackend less
[thirdparty/pdns.git] / pdns / ws-auth.cc
... / ...
CommitLineData
1/*
2 Copyright (C) 2002 - 2016 PowerDNS.COM BV
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License version 2
6 as published by the Free Software Foundation
7
8 Additionally, the license of this program contains a special
9 exception which allows to distribute the program in binary form when
10 it is linked against OpenSSL.
11
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20*/
21#ifdef HAVE_CONFIG_H
22#include "config.h"
23#endif
24#include "utility.hh"
25#include "dynlistener.hh"
26#include "ws-auth.hh"
27#include "json.hh"
28#include "webserver.hh"
29#include "logger.hh"
30#include "packetcache.hh"
31#include "statbag.hh"
32#include "misc.hh"
33#include "arguments.hh"
34#include "dns.hh"
35#include "comment.hh"
36#include "ueberbackend.hh"
37#include <boost/format.hpp>
38
39#include "namespaces.hh"
40#include "ws-api.hh"
41#include "version.hh"
42#include "dnsseckeeper.hh"
43#include <iomanip>
44#include "zoneparser-tng.hh"
45#include "common_startup.hh"
46
47
48using json11::Json;
49
50extern StatBag S;
51
52static void patchZone(HttpRequest* req, HttpResponse* resp);
53static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr);
54
55AuthWebServer::AuthWebServer()
56{
57 d_start=time(0);
58 d_min10=d_min5=d_min1=0;
59 d_ws = 0;
60 d_tid = 0;
61 if(arg().mustDo("webserver")) {
62 d_ws = new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
63 d_ws->bind();
64 }
65}
66
67void AuthWebServer::go()
68{
69 if(arg().mustDo("webserver"))
70 {
71 S.doRings();
72 pthread_create(&d_tid, 0, webThreadHelper, this);
73 pthread_create(&d_tid, 0, statThreadHelper, this);
74 }
75}
76
77void AuthWebServer::statThread()
78{
79 try {
80 for(;;) {
81 d_queries.submit(S.read("udp-queries"));
82 d_cachehits.submit(S.read("packetcache-hit"));
83 d_cachemisses.submit(S.read("packetcache-miss"));
84 d_qcachehits.submit(S.read("query-cache-hit"));
85 d_qcachemisses.submit(S.read("query-cache-miss"));
86 Utility::sleep(1);
87 }
88 }
89 catch(...) {
90 L<<Logger::Error<<"Webserver statThread caught an exception, dying"<<endl;
91 exit(1);
92 }
93}
94
95void *AuthWebServer::statThreadHelper(void *p)
96{
97 AuthWebServer *self=static_cast<AuthWebServer *>(p);
98 self->statThread();
99 return 0; // never reached
100}
101
102void *AuthWebServer::webThreadHelper(void *p)
103{
104 AuthWebServer *self=static_cast<AuthWebServer *>(p);
105 self->webThread();
106 return 0; // never reached
107}
108
109static string htmlescape(const string &s) {
110 string result;
111 for(string::const_iterator it=s.begin(); it!=s.end(); ++it) {
112 switch (*it) {
113 case '&':
114 result += "&amp;";
115 break;
116 case '<':
117 result += "&lt;";
118 break;
119 case '>':
120 result += "&gt;";
121 break;
122 case '"':
123 result += "&quot;";
124 break;
125 default:
126 result += *it;
127 }
128 }
129 return result;
130}
131
132void printtable(ostringstream &ret, const string &ringname, const string &title, int limit=10)
133{
134 int tot=0;
135 int entries=0;
136 vector<pair <string,unsigned int> >ring=S.getRing(ringname);
137
138 for(vector<pair<string, unsigned int> >::const_iterator i=ring.begin(); i!=ring.end();++i) {
139 tot+=i->second;
140 entries++;
141 }
142
143 ret<<"<div class=\"panel\">";
144 ret<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname)<<"\">Reset</a></span>"<<endl;
145 ret<<"<h2>"<<title<<"</h2>"<<endl;
146 ret<<"<div class=ringmeta>";
147 ret<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname)<<"\">Showing: Top "<<limit<<" of "<<entries<<"</a>"<<endl;
148 ret<<"<span class=resizering>Resize: ";
149 unsigned int sizes[]={10,100,500,1000,10000,500000,0};
150 for(int i=0;sizes[i];++i) {
151 if(S.getRingSize(ringname)!=sizes[i])
152 ret<<"<a href=\"?resizering="<<htmlescape(ringname)<<"&amp;size="<<sizes[i]<<"\">"<<sizes[i]<<"</a> ";
153 else
154 ret<<"("<<sizes[i]<<") ";
155 }
156 ret<<"</span></div>";
157
158 ret<<"<table class=\"data\">";
159 int printed=0;
160 int total=max(1,tot);
161 for(vector<pair<string,unsigned int> >::const_iterator i=ring.begin();limit && i!=ring.end();++i,--limit) {
162 ret<<"<tr><td>"<<htmlescape(i->first)<<"</td><td>"<<i->second<<"</td><td align=right>"<< AuthWebServer::makePercentage(i->second*100.0/total)<<"</td>"<<endl;
163 printed+=i->second;
164 }
165 ret<<"<tr><td colspan=3></td></tr>"<<endl;
166 if(printed!=tot)
167 ret<<"<tr><td><b>Rest:</b></td><td><b>"<<tot-printed<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot-printed)*100.0/total)<<"</b></td>"<<endl;
168
169 ret<<"<tr><td><b>Total:</b></td><td><b>"<<tot<<"</b></td><td align=right><b>100%</b></td>";
170 ret<<"</table></div>"<<endl;
171}
172
173void AuthWebServer::printvars(ostringstream &ret)
174{
175 ret<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl;
176
177 vector<string>entries=S.getEntries();
178 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
179 ret<<"<tr><td>"<<*i<<"</td><td>"<<S.read(*i)<<"</td><td>"<<S.getDescrip(*i)<<"</td>"<<endl;
180 }
181
182 ret<<"</table></div>"<<endl;
183}
184
185void AuthWebServer::printargs(ostringstream &ret)
186{
187 ret<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl;
188
189 vector<string>entries=arg().list();
190 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
191 ret<<"<tr><td>"<<*i<<"</td><td>"<<arg()[*i]<<"</td><td>"<<arg().getHelp(*i)<<"</td>"<<endl;
192 }
193}
194
195string AuthWebServer::makePercentage(const double& val)
196{
197 return (boost::format("%.01f%%") % val).str();
198}
199
200void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
201{
202 if(!req->getvars["resetring"].empty()) {
203 if (S.ringExists(req->getvars["resetring"]))
204 S.resetRing(req->getvars["resetring"]);
205 resp->status = 301;
206 resp->headers["Location"] = req->url.path;
207 return;
208 }
209 if(!req->getvars["resizering"].empty()){
210 int size=std::stoi(req->getvars["size"]);
211 if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000)
212 S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
213 resp->status = 301;
214 resp->headers["Location"] = req->url.path;
215 return;
216 }
217
218 ostringstream ret;
219
220 ret<<"<!DOCTYPE html>"<<endl;
221 ret<<"<html><head>"<<endl;
222 ret<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl;
223 ret<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl;
224 ret<<"</head><body>"<<endl;
225
226 ret<<"<div class=\"row\">"<<endl;
227 ret<<"<div class=\"headl columns\">";
228 ret<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION);
229 if(!arg()["config-name"].empty()) {
230 ret<<" ["<<htmlescape(arg()["config-name"])<<"]";
231 }
232 ret<<"</a></div>"<<endl;
233 ret<<"<div class=\"headr columns\"></div></div>";
234 ret<<"<div class=\"row\"><div class=\"all columns\">";
235
236 time_t passed=time(0)-s_starttime;
237
238 ret<<"<p>Uptime: "<<
239 humanDuration(passed)<<
240 "<br>"<<endl;
241
242 ret<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
243 d_queries.get1()<<", "<<
244 d_queries.get5()<<", "<<
245 d_queries.get10()<<". Max queries/second: "<<d_queries.getMax()<<
246 "<br>"<<endl;
247
248 if(d_cachemisses.get10()+d_cachehits.get10()>0)
249 ret<<"Cache hitrate, 1, 5, 10 minute averages: "<<
250 makePercentage((d_cachehits.get1()*100.0)/((d_cachehits.get1())+(d_cachemisses.get1())))<<", "<<
251 makePercentage((d_cachehits.get5()*100.0)/((d_cachehits.get5())+(d_cachemisses.get5())))<<", "<<
252 makePercentage((d_cachehits.get10()*100.0)/((d_cachehits.get10())+(d_cachemisses.get10())))<<
253 "<br>"<<endl;
254
255 if(d_qcachemisses.get10()+d_qcachehits.get10()>0)
256 ret<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
257 makePercentage((d_qcachehits.get1()*100.0)/((d_qcachehits.get1())+(d_qcachemisses.get1())))<<", "<<
258 makePercentage((d_qcachehits.get5()*100.0)/((d_qcachehits.get5())+(d_qcachemisses.get5())))<<", "<<
259 makePercentage((d_qcachehits.get10()*100.0)/((d_qcachehits.get10())+(d_qcachemisses.get10())))<<
260 "<br>"<<endl;
261
262 ret<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
263 d_qcachemisses.get1()<<", "<<
264 d_qcachemisses.get5()<<", "<<
265 d_qcachemisses.get10()<<". Max queries/second: "<<d_qcachemisses.getMax()<<
266 "<br>"<<endl;
267
268 ret<<"Total queries: "<<S.read("udp-queries")<<". Question/answer latency: "<<S.read("latency")/1000.0<<"ms</p><br>"<<endl;
269 if(req->getvars["ring"].empty()) {
270 vector<string>entries=S.listRings();
271 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i)
272 printtable(ret,*i,S.getRingTitle(*i));
273
274 printvars(ret);
275 if(arg().mustDo("webserver-print-arguments"))
276 printargs(ret);
277 }
278 else
279 printtable(ret,req->getvars["ring"],S.getRingTitle(req->getvars["ring"]),100);
280
281 ret<<"</div></div>"<<endl;
282 ret<<"<footer class=\"row\">"<<fullVersionString()<<"<br>&copy; 2013 - 2016 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl;
283 ret<<"</body></html>"<<endl;
284
285 resp->body = ret.str();
286 resp->status = 200;
287}
288
289/** Helper to build a record content as needed. */
290static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot) {
291 // noDot: for backend storage, pass true. for API users, pass false.
292 return DNSRecordContent::mastermake(qtype.getCode(), 1, content)->getZoneRepresentation(noDot);
293}
294
295/** "Normalize" record content for API consumers. */
296static inline string makeApiRecordContent(const QType& qtype, const string& content) {
297 return makeRecordContent(qtype, content, false);
298}
299
300/** "Normalize" record content for backend storage. */
301static inline string makeBackendRecordContent(const QType& qtype, const string& content) {
302 return makeRecordContent(qtype, content, true);
303}
304
305static Json::object getZoneInfo(const DomainInfo& di) {
306 DNSSECKeeper dk;
307 string zoneId = apiZoneNameToId(di.zone);
308 return Json::object {
309 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
310 { "id", zoneId },
311 { "url", "api/v1/servers/localhost/zones/" + zoneId },
312 { "name", di.zone.toString() },
313 { "kind", di.getKindString() },
314 { "dnssec", dk.isSecuredZone(di.zone) },
315 { "account", di.account },
316 { "masters", di.masters },
317 { "serial", (double)di.serial },
318 { "notified_serial", (double)di.notified_serial },
319 { "last_check", (double)di.last_check }
320 };
321}
322
323static void fillZone(const DNSName& zonename, HttpResponse* resp) {
324 UeberBackend B;
325 DomainInfo di;
326 if(!B.getDomainInfo(zonename, di))
327 throw ApiException("Could not find domain '"+zonename.toString()+"'");
328
329 Json::object doc = getZoneInfo(di);
330 // extra stuff getZoneInfo doesn't do for us (more expensive)
331 string soa_edit_api;
332 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
333 doc["soa_edit_api"] = soa_edit_api;
334 string soa_edit;
335 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
336 doc["soa_edit"] = soa_edit;
337
338 vector<DNSResourceRecord> records;
339 vector<Comment> comments;
340
341 // load all records + sort
342 {
343 DNSResourceRecord rr;
344 di.backend->list(zonename, di.id, true); // incl. disabled
345 while(di.backend->get(rr)) {
346 if (!rr.qtype.getCode())
347 continue; // skip empty non-terminals
348 records.push_back(rr);
349 }
350 sort(records.begin(), records.end(), [](const DNSResourceRecord& a, const DNSResourceRecord& b) {
351 if (a.qname == b.qname) {
352 return b.qtype < a.qtype;
353 }
354 return b.qname < a.qname;
355 });
356 }
357
358 // load all comments + sort
359 {
360 Comment comment;
361 di.backend->listComments(di.id);
362 while(di.backend->getComment(comment)) {
363 comments.push_back(comment);
364 }
365 sort(comments.begin(), comments.end(), [](const Comment& a, const Comment& b) {
366 if (a.qname == b.qname) {
367 return b.qtype < a.qtype;
368 }
369 return b.qname < a.qname;
370 });
371 }
372
373 Json::array rrsets;
374 Json::object rrset;
375 Json::array rrset_records;
376 Json::array rrset_comments;
377 DNSName current_qname;
378 QType current_qtype;
379 uint32_t ttl;
380 auto rit = records.begin();
381 auto cit = comments.begin();
382
383 while (rit != records.end() || cit != comments.end()) {
384 if (cit == comments.end() || (rit != records.end() && (cit->qname.toString() < rit->qname.toString() || cit->qtype < rit->qtype))) {
385 current_qname = rit->qname;
386 current_qtype = rit->qtype;
387 ttl = rit->ttl;
388 } else {
389 current_qname = cit->qname;
390 current_qtype = cit->qtype;
391 ttl = 0;
392 }
393
394 while(rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
395 ttl = min(ttl, rit->ttl);
396 rrset_records.push_back(Json::object {
397 { "disabled", rit->disabled },
398 { "content", makeApiRecordContent(rit->qtype, rit->content) }
399 });
400 rit++;
401 }
402 while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
403 rrset_comments.push_back(Json::object {
404 { "modified_at", (double)cit->modified_at },
405 { "account", cit->account },
406 { "content", cit->content }
407 });
408 cit++;
409 }
410
411 rrset["name"] = current_qname.toString();
412 rrset["type"] = current_qtype.getName();
413 rrset["records"] = rrset_records;
414 rrset["comments"] = rrset_comments;
415 rrset["ttl"] = (double)ttl;
416 rrsets.push_back(rrset);
417 rrset.clear();
418 rrset_records.clear();
419 rrset_comments.clear();
420 }
421
422 doc["rrsets"] = rrsets;
423
424 resp->setBody(doc);
425}
426
427void productServerStatisticsFetch(map<string,string>& out)
428{
429 vector<string> items = S.getEntries();
430 for(const string& item : items) {
431 out[item] = std::to_string(S.read(item));
432 }
433
434 // add uptime
435 out["uptime"] = std::to_string(time(0) - s_starttime);
436}
437
438static void gatherRecords(const Json container, const DNSName& qname, const QType qtype, const int ttl, vector<DNSResourceRecord>& new_records, vector<DNSResourceRecord>& new_ptrs) {
439 UeberBackend B;
440 DNSResourceRecord rr;
441 rr.qname = qname;
442 rr.qtype = qtype;
443 rr.auth = 1;
444 rr.ttl = ttl;
445 for(auto record : container["records"].array_items()) {
446 string content = stringFromJson(record, "content");
447 rr.disabled = boolFromJson(record, "disabled");
448
449 // validate that the client sent something we can actually parse, and require that data to be dotted.
450 try {
451 if (rr.qtype.getCode() != QType::AAAA) {
452 string tmp = makeApiRecordContent(rr.qtype, content);
453 if (!pdns_iequals(tmp, content)) {
454 throw std::runtime_error("Not in expected format (parsed as '"+tmp+"')");
455 }
456 } else {
457 struct in6_addr tmpbuf;
458 if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
459 throw std::runtime_error("Invalid IPv6 address");
460 }
461 }
462 rr.content = makeBackendRecordContent(rr.qtype, content);
463 }
464 catch(std::exception& e)
465 {
466 throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.getName()+" '"+content+"': "+e.what());
467 }
468
469 if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) &&
470 boolFromJson(record, "set-ptr", false) == true) {
471 DNSResourceRecord ptr;
472 makePtr(rr, &ptr);
473
474 // verify that there's a zone for the PTR
475 DNSPacket fakePacket;
476 SOAData sd;
477 fakePacket.qtype = QType::PTR;
478 if (!B.getAuth(&fakePacket, &sd, ptr.qname))
479 throw ApiException("Could not find domain for PTR '"+ptr.qname.toString()+"' requested for '"+ptr.content+"'");
480
481 ptr.domain_id = sd.domain_id;
482 new_ptrs.push_back(ptr);
483 }
484
485 new_records.push_back(rr);
486 }
487}
488
489static void gatherComments(const Json container, const DNSName& qname, const QType qtype, vector<Comment>& new_comments) {
490 Comment c;
491 c.qname = qname;
492 c.qtype = qtype;
493
494 time_t now = time(0);
495 for (auto comment : container["comments"].array_items()) {
496 c.modified_at = intFromJson(comment, "modified_at", now);
497 c.content = stringFromJson(comment, "content");
498 c.account = stringFromJson(comment, "account");
499 new_comments.push_back(c);
500 }
501}
502
503static void updateDomainSettingsFromDocument(const DomainInfo& di, const DNSName& zonename, const Json document) {
504 string zonemaster;
505 for(auto value : document["masters"].array_items()) {
506 string master = value.string_value();
507 if (master.empty())
508 throw ApiException("Master can not be an empty string");
509 zonemaster += master + " ";
510 }
511
512 di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
513 di.backend->setMaster(zonename, zonemaster);
514
515 if (document["soa_edit_api"].is_string()) {
516 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
517 }
518 if (document["soa_edit"].is_string()) {
519 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
520 }
521 if (document["account"].is_string()) {
522 di.backend->setAccount(zonename, document["account"].string_value());
523 }
524}
525
526static void apiZoneCryptokeys(HttpRequest* req, HttpResponse* resp) {
527 if(req->method != "GET")
528 throw ApiException("Only GET is implemented");
529
530 bool inquireSingleKey = false;
531 int inquireKeyId;
532 if (req->parameters.count("key_id")) {
533 inquireSingleKey = true;
534 inquireKeyId = std::stoi(req->parameters["key_id"]);
535 }
536
537 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
538
539 UeberBackend B;
540 DNSSECKeeper dk(&B);
541 DomainInfo di;
542 if(!B.getDomainInfo(zonename, di))
543 throw HttpNotFoundException();
544
545 DNSSECKeeper::keyset_t keyset=dk.getKeys(zonename, false);
546
547 Json::array doc;
548 for(const auto& value : keyset) {
549 if (inquireSingleKey && inquireKeyId != value.second.id) {
550 continue;
551 }
552
553 string keyType;
554 switch(value.second.keyType){
555 case DNSSECKeeper::KSK: keyType="ksk"; break;
556 case DNSSECKeeper::ZSK: keyType="zsk"; break;
557 case DNSSECKeeper::CSK: keyType="csk"; break;
558 }
559
560 Json::object key {
561 { "type", "Cryptokey" },
562 { "id", (int)value.second.id },
563 { "active", value.second.active },
564 { "keytype", keyType },
565 { "flags", (uint16_t)value.first.d_flags },
566 { "dnskey", value.first.getDNSKEY().getZoneRepresentation() }
567 };
568
569 if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
570 Json::array dses;
571 for(const int keyid : { 1, 2, 3, 4 })
572 try {
573 dses.push_back(makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation());
574 } catch (...) {}
575 key["ds"] = dses;
576 }
577
578 if (inquireSingleKey) {
579 key["privatekey"] = value.first.getKey()->convertToISC();
580 resp->setBody(key);
581 return;
582 }
583 doc.push_back(key);
584 }
585
586 if (inquireSingleKey) {
587 // we came here because we couldn't find the requested key.
588 throw HttpNotFoundException();
589 }
590 resp->setBody(doc);
591}
592
593static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, DNSName zonename) {
594 DNSResourceRecord rr;
595 vector<string> zonedata;
596 stringtok(zonedata, zonestring, "\r\n");
597
598 ZoneParserTNG zpt(zonedata, zonename);
599
600 bool seenSOA=false;
601
602 string comment = "Imported via the API";
603
604 try {
605 while(zpt.get(rr, &comment)) {
606 if(seenSOA && rr.qtype.getCode() == QType::SOA)
607 continue;
608 if(rr.qtype.getCode() == QType::SOA)
609 seenSOA=true;
610
611 new_records.push_back(rr);
612 }
613 }
614 catch(std::exception& ae) {
615 throw ApiException("An error occured while parsing the zonedata: "+string(ae.what()));
616 }
617}
618
619static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
620 UeberBackend B;
621 DNSSECKeeper dk(&B);
622 if (req->method == "POST" && !::arg().mustDo("api-readonly")) {
623 DomainInfo di;
624 auto document = req->json();
625 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
626 apiCheckNameAllowedCharacters(zonename.toString());
627
628 bool exists = B.getDomainInfo(zonename, di);
629 if(exists)
630 throw ApiException("Domain '"+zonename.toString()+"' already exists");
631
632 // validate 'kind' is set
633 DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
634
635 string zonestring = document["zone"].string_value();
636 auto rrsets = document["rrsets"];
637 if (rrsets.is_array() && zonestring != "")
638 throw ApiException("You cannot give rrsets AND zone data as text");
639
640 auto nameservers = document["nameservers"];
641 if (!nameservers.is_array() && zonekind != DomainInfo::Slave)
642 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
643
644 string soa_edit_api_kind;
645 if (document["soa_edit_api"].is_string()) {
646 soa_edit_api_kind = document["soa_edit_api"].string_value();
647 }
648 else {
649 soa_edit_api_kind = "DEFAULT";
650 }
651 string soa_edit_kind = document["soa_edit"].string_value();
652
653 // if records/comments are given, load and check them
654 bool have_soa = false;
655 vector<DNSResourceRecord> new_records;
656 vector<Comment> new_comments;
657 vector<DNSResourceRecord> new_ptrs;
658
659 if (rrsets.is_array()) {
660 for (const auto& rrset : rrsets.array_items()) {
661 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
662 apiCheckQNameAllowedCharacters(qname.toString());
663 QType qtype;
664 qtype = stringFromJson(rrset, "type");
665 if (qtype.getCode() == 0) {
666 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
667 }
668 if (rrset["records"].is_array()) {
669 int ttl = intFromJson(rrset, "ttl");
670 gatherRecords(rrset, qname, qtype, ttl, new_records, new_ptrs);
671 }
672 if (rrset["comments"].is_array()) {
673 gatherComments(rrset, qname, qtype, new_comments);
674 }
675 }
676 } else if (zonestring != "") {
677 gatherRecordsFromZone(zonestring, new_records, zonename);
678 }
679
680 for(auto& rr : new_records) {
681 if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
682 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": Name is out of zone");
683 apiCheckQNameAllowedCharacters(rr.qname.toString());
684
685 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
686 have_soa = true;
687 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
688 // fixup dots after serializeSOAData/increaseSOARecord
689 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
690 }
691 }
692
693 // synthesize RRs as needed
694 DNSResourceRecord autorr;
695 autorr.qname = zonename;
696 autorr.auth = 1;
697 autorr.ttl = ::arg().asNum("default-ttl");
698
699 if (!have_soa && zonekind != DomainInfo::Slave) {
700 // synthesize a SOA record so the zone "really" exists
701 string soa = (boost::format("%s %s %lu")
702 % ::arg()["default-soa-name"]
703 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename).toString() : ::arg()["default-soa-mail"])
704 % document["serial"].int_value()
705 ).str();
706 SOAData sd;
707 fillSOAData(soa, sd); // fills out default values for us
708 autorr.qtype = "SOA";
709 autorr.content = serializeSOAData(sd);
710 increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
711 // fixup dots after serializeSOAData/increaseSOARecord
712 autorr.content = makeBackendRecordContent(autorr.qtype, autorr.content);
713 new_records.push_back(autorr);
714 }
715
716 // create NS records if nameservers are given
717 for (auto value : nameservers.array_items()) {
718 string nameserver = value.string_value();
719 if (nameserver.empty())
720 throw ApiException("Nameservers must be non-empty strings");
721 if (!isCanonical(nameserver))
722 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
723 try {
724 // ensure the name parses
725 autorr.content = DNSName(nameserver).toStringNoDot();
726 } catch (...) {
727 throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
728 }
729 autorr.qtype = "NS";
730 new_records.push_back(autorr);
731 }
732
733 // no going back after this
734 if(!B.createDomain(zonename))
735 throw ApiException("Creating domain '"+zonename.toString()+"' failed");
736
737 if(!B.getDomainInfo(zonename, di))
738 throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");
739
740 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
741 if (!soa_edit_api_kind.empty()) {
742 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
743 }
744
745 di.backend->startTransaction(zonename, di.id);
746
747 for(auto rr : new_records) {
748 rr.domain_id = di.id;
749 di.backend->feedRecord(rr);
750 }
751 for(Comment& c : new_comments) {
752 c.domain_id = di.id;
753 di.backend->feedComment(c);
754 }
755
756 updateDomainSettingsFromDocument(di, zonename, document);
757
758 di.backend->commitTransaction();
759
760 fillZone(zonename, resp);
761 resp->status = 201;
762 return;
763 }
764
765 if(req->method != "GET")
766 throw HttpMethodNotAllowedException();
767
768 vector<DomainInfo> domains;
769 B.getAllDomains(&domains, true); // incl. disabled
770
771 Json::array doc;
772 for(const DomainInfo& di : domains) {
773 doc.push_back(getZoneInfo(di));
774 }
775 resp->setBody(doc);
776}
777
778static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
779 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
780
781 if(req->method == "PUT" && !::arg().mustDo("api-readonly")) {
782 // update domain settings
783 UeberBackend B;
784 DomainInfo di;
785 if(!B.getDomainInfo(zonename, di))
786 throw ApiException("Could not find domain '"+zonename.toString()+"'");
787
788 updateDomainSettingsFromDocument(di, zonename, req->json());
789
790 fillZone(zonename, resp);
791 return;
792 }
793 else if(req->method == "DELETE" && !::arg().mustDo("api-readonly")) {
794 // delete domain
795 UeberBackend B;
796 DomainInfo di;
797 if(!B.getDomainInfo(zonename, di))
798 throw ApiException("Could not find domain '"+zonename.toString()+"'");
799
800 if(!di.backend->deleteDomain(zonename))
801 throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");
802
803 // empty body on success
804 resp->body = "";
805 resp->status = 204; // No Content: declare that the zone is gone now
806 return;
807 } else if (req->method == "PATCH" && !::arg().mustDo("api-readonly")) {
808 patchZone(req, resp);
809 return;
810 } else if (req->method == "GET") {
811 fillZone(zonename, resp);
812 return;
813 }
814
815 throw HttpMethodNotAllowedException();
816}
817
818static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
819 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
820
821 if(req->method != "GET")
822 throw HttpMethodNotAllowedException();
823
824 ostringstream ss;
825
826 UeberBackend B;
827 DomainInfo di;
828 if(!B.getDomainInfo(zonename, di))
829 throw ApiException("Could not find domain '"+zonename.toString()+"'");
830
831 DNSResourceRecord rr;
832 SOAData sd;
833 di.backend->list(zonename, di.id);
834 while(di.backend->get(rr)) {
835 if (!rr.qtype.getCode())
836 continue; // skip empty non-terminals
837
838 ss <<
839 rr.qname.toString() << "\t" <<
840 rr.ttl << "\t" <<
841 rr.qtype.getName() << "\t" <<
842 makeApiRecordContent(rr.qtype, rr.content) <<
843 endl;
844 }
845
846 if (req->accept_json) {
847 resp->setBody(Json::object { { "zone", ss.str() } });
848 } else {
849 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
850 resp->body = ss.str();
851 }
852}
853
854static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
855 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
856
857 if(req->method != "PUT")
858 throw HttpMethodNotAllowedException();
859
860 UeberBackend B;
861 DomainInfo di;
862 if(!B.getDomainInfo(zonename, di))
863 throw ApiException("Could not find domain '"+zonename.toString()+"'");
864
865 if(di.masters.empty())
866 throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");
867
868 random_shuffle(di.masters.begin(), di.masters.end());
869 Communicator.addSuckRequest(zonename, di.masters.front());
870 resp->setSuccessResult("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front());
871}
872
873static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
874 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
875
876 if(req->method != "PUT")
877 throw HttpMethodNotAllowedException();
878
879 UeberBackend B;
880 DomainInfo di;
881 if(!B.getDomainInfo(zonename, di))
882 throw ApiException("Could not find domain '"+zonename.toString()+"'");
883
884 if(!Communicator.notifyDomain(zonename))
885 throw ApiException("Failed to add to the queue - see server log");
886
887 resp->setSuccessResult("Notification queued");
888}
889
890static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
891 if (rr.qtype.getCode() == QType::A) {
892 uint32_t ip;
893 if (!IpToU32(rr.content, &ip)) {
894 throw ApiException("PTR: Invalid IP address given");
895 }
896 ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
897 % ((ip >> 24) & 0xff)
898 % ((ip >> 16) & 0xff)
899 % ((ip >> 8) & 0xff)
900 % ((ip ) & 0xff)
901 ).str());
902 } else if (rr.qtype.getCode() == QType::AAAA) {
903 ComboAddress ca(rr.content);
904 char buf[3];
905 ostringstream ss;
906 for (int octet = 0; octet < 16; ++octet) {
907 if (snprintf(buf, sizeof(buf), "%02x", ca.sin6.sin6_addr.s6_addr[octet]) != (sizeof(buf)-1)) {
908 // this should be impossible: no byte should give more than two digits in hex format
909 throw PDNSException("Formatting IPv6 address failed");
910 }
911 ss << buf[0] << '.' << buf[1] << '.';
912 }
913 string tmp = ss.str();
914 tmp.resize(tmp.size()-1); // remove last dot
915 // reverse and append arpa domain
916 ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
917 } else {
918 throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
919 }
920
921 ptr->qtype = "PTR";
922 ptr->ttl = rr.ttl;
923 ptr->disabled = rr.disabled;
924 ptr->content = rr.qname.toString();
925}
926
927static void patchZone(HttpRequest* req, HttpResponse* resp) {
928 UeberBackend B;
929 DomainInfo di;
930 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
931 if (!B.getDomainInfo(zonename, di))
932 throw ApiException("Could not find domain '"+zonename.toString()+"'");
933
934 vector<DNSResourceRecord> new_records;
935 vector<Comment> new_comments;
936 vector<DNSResourceRecord> new_ptrs;
937
938 Json document = req->json();
939
940 auto rrsets = document["rrsets"];
941 if (!rrsets.is_array())
942 throw ApiException("No rrsets given in update request");
943
944 di.backend->startTransaction(zonename);
945
946 try {
947 string soa_edit_api_kind;
948 string soa_edit_kind;
949 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
950 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
951 bool soa_edit_done = false;
952
953 for (const auto& rrset : rrsets.array_items()) {
954 string changetype = toUpper(stringFromJson(rrset, "changetype"));
955 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
956 apiCheckQNameAllowedCharacters(qname.toString());
957 QType qtype;
958 qtype = stringFromJson(rrset, "type");
959 if (qtype.getCode() == 0) {
960 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
961 }
962
963 if (changetype == "DELETE") {
964 // delete all matching qname/qtype RRs (and, implictly comments).
965 if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
966 throw ApiException("Hosting backend does not support editing records.");
967 }
968 }
969 else if (changetype == "REPLACE") {
970 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
971 if (!qname.isPartOf(zonename) && qname != zonename)
972 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
973
974 bool replace_records = rrset["records"].is_array();
975 bool replace_comments = rrset["comments"].is_array();
976
977 if (!replace_records && !replace_comments) {
978 throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.getName());
979 }
980
981 new_records.clear();
982 new_comments.clear();
983
984 if (replace_records) {
985 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
986 int ttl = intFromJson(rrset, "ttl");
987 // new_ptrs is merged.
988 gatherRecords(rrset, qname, qtype, ttl, new_records, new_ptrs);
989
990 for(DNSResourceRecord& rr : new_records) {
991 rr.domain_id = di.id;
992 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
993 soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
994 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
995 }
996 }
997 }
998
999 if (replace_comments) {
1000 gatherComments(rrset, qname, qtype, new_comments);
1001
1002 for(Comment& c : new_comments) {
1003 c.domain_id = di.id;
1004 }
1005 }
1006
1007 if (replace_records) {
1008 if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
1009 throw ApiException("Hosting backend does not support editing records.");
1010 }
1011 }
1012 if (replace_comments) {
1013 if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
1014 throw ApiException("Hosting backend does not support editing comments.");
1015 }
1016 }
1017 }
1018 else
1019 throw ApiException("Changetype not understood");
1020 }
1021
1022 // edit SOA (if needed)
1023 if (!soa_edit_api_kind.empty() && !soa_edit_done) {
1024 SOAData sd;
1025 if (!B.getSOA(zonename, sd))
1026 throw ApiException("No SOA found for domain '"+zonename.toString()+"'");
1027
1028 DNSResourceRecord rr;
1029 rr.qname = zonename;
1030 rr.content = serializeSOAData(sd);
1031 rr.qtype = "SOA";
1032 rr.domain_id = di.id;
1033 rr.auth = 1;
1034 rr.ttl = sd.ttl;
1035 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1036 // fixup dots after serializeSOAData/increaseSOARecord
1037 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
1038
1039 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1040 throw ApiException("Hosting backend does not support editing records.");
1041 }
1042 }
1043
1044 } catch(...) {
1045 di.backend->abortTransaction();
1046 throw;
1047 }
1048 di.backend->commitTransaction();
1049
1050 extern PacketCache PC;
1051 PC.purgeExact(zonename);
1052
1053 // now the PTRs
1054 for(const DNSResourceRecord& rr : new_ptrs) {
1055 DNSPacket fakePacket;
1056 SOAData sd;
1057 sd.db = (DNSBackend *)-1; // getAuth() cache bypass
1058 fakePacket.qtype = QType::PTR;
1059
1060 if (!B.getAuth(&fakePacket, &sd, rr.qname))
1061 throw ApiException("Could not find domain for PTR '"+rr.qname.toString()+"' requested for '"+rr.content+"' (while saving)");
1062
1063 string soa_edit_api_kind;
1064 string soa_edit_kind;
1065 bool soa_changed = false;
1066 DNSResourceRecord soarr;
1067 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT-API", soa_edit_api_kind);
1068 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT", soa_edit_kind);
1069 if (!soa_edit_api_kind.empty()) {
1070 soarr.qname = sd.qname;
1071 soarr.content = serializeSOAData(sd);
1072 soarr.qtype = "SOA";
1073 soarr.domain_id = sd.domain_id;
1074 soarr.auth = 1;
1075 soarr.ttl = sd.ttl;
1076 increaseSOARecord(soarr, soa_edit_api_kind, soa_edit_kind);
1077 // fixup dots after serializeSOAData/increaseSOARecord
1078 soarr.content = makeBackendRecordContent(soarr.qtype, soarr.content);
1079 soa_changed = true;
1080 }
1081
1082 sd.db->startTransaction(sd.qname);
1083 if (!sd.db->replaceRRSet(sd.domain_id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1084 sd.db->abortTransaction();
1085 throw ApiException("PTR-Hosting backend for "+rr.qname.toString()+"/"+rr.qtype.getName()+" does not support editing records.");
1086 }
1087
1088 if (soa_changed) {
1089 sd.db->replaceRRSet(sd.domain_id, soarr.qname, soarr.qtype, vector<DNSResourceRecord>(1, soarr));
1090 }
1091
1092 sd.db->commitTransaction();
1093 PC.purgeExact(rr.qname);
1094 }
1095
1096 // success
1097 fillZone(zonename, resp);
1098}
1099
1100static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
1101 if(req->method != "GET")
1102 throw HttpMethodNotAllowedException();
1103
1104 string q = req->getvars["q"];
1105 string sMax = req->getvars["max"];
1106 int maxEnts = 100;
1107 int ents = 0;
1108
1109 if (q.empty())
1110 throw ApiException("Query q can't be blank");
1111 if (sMax.empty() == false)
1112 maxEnts = std::stoi(sMax);
1113 if (maxEnts < 1)
1114 throw ApiException("Maximum entries must be larger than 0");
1115
1116 SimpleMatch sm(q,true);
1117 UeberBackend B;
1118 vector<DomainInfo> domains;
1119 vector<DNSResourceRecord> result_rr;
1120 vector<Comment> result_c;
1121 map<int,DomainInfo> zoneIdZone;
1122 map<int,DomainInfo>::iterator val;
1123 Json::array doc;
1124
1125 B.getAllDomains(&domains, true);
1126
1127 for(const DomainInfo di: domains)
1128 {
1129 if (ents < maxEnts && sm.match(di.zone)) {
1130 doc.push_back(Json::object {
1131 { "object_type", "zone" },
1132 { "zone_id", apiZoneNameToId(di.zone) },
1133 { "name", di.zone.toString() }
1134 });
1135 ents++;
1136 }
1137 zoneIdZone[di.id] = di; // populate cache
1138 }
1139
1140 if (B.searchRecords(q, maxEnts, result_rr))
1141 {
1142 for(const DNSResourceRecord& rr: result_rr)
1143 {
1144 auto object = Json::object {
1145 { "object_type", "record" },
1146 { "name", rr.qname.toString() },
1147 { "type", rr.qtype.getName() },
1148 { "ttl", (double)rr.ttl },
1149 { "disabled", rr.disabled },
1150 { "content", makeApiRecordContent(rr.qtype, rr.content) }
1151 };
1152 if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
1153 object["zone_id"] = apiZoneNameToId(val->second.zone);
1154 object["zone"] = val->second.zone.toString();
1155 }
1156 doc.push_back(object);
1157 }
1158 }
1159
1160 if (B.searchComments(q, maxEnts, result_c))
1161 {
1162 for(const Comment &c: result_c)
1163 {
1164 auto object = Json::object {
1165 { "object_type", "comment" },
1166 { "name", c.qname.toString() },
1167 { "content", c.content }
1168 };
1169 if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
1170 object["zone_id"] = apiZoneNameToId(val->second.zone);
1171 object["zone"] = val->second.zone.toString();
1172 }
1173 doc.push_back(object);
1174 }
1175 }
1176
1177 resp->setBody(doc);
1178}
1179
1180void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
1181 if(req->method != "PUT")
1182 throw HttpMethodNotAllowedException();
1183
1184 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
1185
1186 extern PacketCache PC;
1187 int count = PC.purgeExact(canon);
1188 resp->setBody(Json::object {
1189 { "count", count },
1190 { "result", "Flushed cache." }
1191 });
1192}
1193
1194void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
1195{
1196 resp->headers["Cache-Control"] = "max-age=86400";
1197 resp->headers["Content-Type"] = "text/css";
1198
1199 ostringstream ret;
1200 ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
1201 ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
1202 ret<<"a { color: #0959c2; }"<<endl;
1203 ret<<"a:hover { color: #3B8EC8; }"<<endl;
1204 ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
1205 ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
1206 ret<<".row:after { clear: both; }"<<endl;
1207 ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
1208 ret<<".all { width: 100%; }"<<endl;
1209 ret<<".headl { width: 60%; }"<<endl;
1210 ret<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
1211 ret<<"background-image: url(data:image/png;base64,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);";
1212 ret<<" width: 154px; height: 20px; }"<<endl;
1213 ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
1214 ret<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
1215 ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
1216 ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
1217 ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
1218 ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
1219 ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
1220 ret<<"table.data tr:hover { background: white; }"<<endl;
1221 ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
1222 ret<<".resetring {float: right; }"<<endl;
1223 ret<<".resetring i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA/klEQVQY01XPP04UUBgE8N/33vd2XZUWEuzYuMZEG4KFCQn2NhA4AIewAOMBPIG2xhNYeAcKGqkNCdmYlVBZGBIT4FHsbuE0U8xk/kAbqm9TOfI/nicfhmwgDNhvylUT58kxCp4l31L8SfH9IetJ2ev6PwyIwyZWsdb11/gbTK55Co+r8rmJaRPTFJcpZil+pTit7C5awMpA+Zpi1sRFE9MqflYOloYCjY2uP8EdYiGU4CVGUBubxKfOOLjrtOBmzvEilbVb/aQWvhRl0unBZVXe4XdnK+bprwqnhoyTsyZ+JG8Wk0apfExxlcp7PFruXH8gdxamWB4cyW2sIO4BG3czIp78jUIAAAAASUVORK5CYII=); width: 10px; height: 10px; margin-right: 2px; display: inline-block; background-repeat: no-repeat; }"<<endl;
1224 ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
1225 ret<<".resizering {float: right;}"<<endl;
1226 resp->body = ret.str();
1227 resp->status = 200;
1228}
1229
1230void AuthWebServer::webThread()
1231{
1232 try {
1233 if(::arg().mustDo("api")) {
1234 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
1235 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
1236 d_ws->registerApiHandler("/api/v1/servers/localhost/search-log", &apiServerSearchLog);
1237 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
1238 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
1239 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
1240 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
1241 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
1242 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
1243 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
1244 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
1245 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
1246 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
1247 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
1248 d_ws->registerApiHandler("/api", &apiDiscovery);
1249 }
1250 d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
1251 d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
1252 d_ws->go();
1253 }
1254 catch(...) {
1255 L<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
1256 exit(1);
1257 }
1258}