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