2 Copyright (C) 2002 - 2016 PowerDNS.COM BV
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
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.
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.
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
25 #include "dynlistener.hh"
28 #include "webserver.hh"
30 #include "packetcache.hh"
33 #include "arguments.hh"
36 #include "ueberbackend.hh"
37 #include <boost/format.hpp>
39 #include "namespaces.hh"
42 #include "dnsseckeeper.hh"
44 #include "zoneparser-tng.hh"
45 #include "common_startup.hh"
51 extern PacketCache PC
;
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
);
57 AuthWebServer::AuthWebServer()
60 d_min10
=d_min5
=d_min1
=0;
63 if(arg().mustDo("webserver")) {
64 d_ws
= new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
69 void AuthWebServer::go()
71 if(arg().mustDo("webserver"))
74 pthread_create(&d_tid
, 0, webThreadHelper
, this);
75 pthread_create(&d_tid
, 0, statThreadHelper
, this);
79 void AuthWebServer::statThread()
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"));
92 L
<<Logger::Error
<<"Webserver statThread caught an exception, dying"<<endl
;
97 void *AuthWebServer::statThreadHelper(void *p
)
99 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
101 return 0; // never reached
104 void *AuthWebServer::webThreadHelper(void *p
)
106 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
108 return 0; // never reached
111 static string
htmlescape(const string
&s
) {
113 for(string::const_iterator it
=s
.begin(); it
!=s
.end(); ++it
) {
134 void printtable(ostringstream
&ret
, const string
&ringname
, const string
&title
, int limit
=10)
138 vector
<pair
<string
,unsigned int> >ring
=S
.getRing(ringname
);
140 for(vector
<pair
<string
, unsigned int> >::const_iterator i
=ring
.begin(); i
!=ring
.end();++i
) {
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
)<<"&size="<<sizes
[i
]<<"\">"<<sizes
[i
]<<"</a> ";
156 ret
<<"("<<sizes
[i
]<<") ";
158 ret
<<"</span></div>";
160 ret
<<"<table class=\"data\">";
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
;
167 ret
<<"<tr><td colspan=3></td></tr>"<<endl
;
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
;
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
;
175 void AuthWebServer::printvars(ostringstream
&ret
)
177 ret
<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl
;
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
;
184 ret
<<"</table></div>"<<endl
;
187 void AuthWebServer::printargs(ostringstream
&ret
)
189 ret
<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl
;
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
;
197 string
AuthWebServer::makePercentage(const double& val
)
199 return (boost::format("%.01f%%") % val
).str();
202 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
204 if(!req
->getvars
["resetring"].empty()) {
205 if (S
.ringExists(req
->getvars
["resetring"]))
206 S
.resetRing(req
->getvars
["resetring"]);
208 resp
->headers
["Location"] = req
->url
.path
;
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"]));
216 resp
->headers
["Location"] = req
->url
.path
;
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
;
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"])<<"]";
234 ret
<<"</a></div>"<<endl
;
235 ret
<<"<div class=\"headr columns\"></div></div>";
236 ret
<<"<div class=\"row\"><div class=\"all columns\">";
238 time_t passed
=time(0)-s_starttime
;
241 humanDuration(passed
)<<
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()<<
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())))<<
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())))<<
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()<<
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
));
277 if(arg().mustDo("webserver-print-arguments"))
281 printtable(ret
,req
->getvars
["ring"],S
.getRingTitle(req
->getvars
["ring"]),100);
283 ret
<<"</div></div>"<<endl
;
284 ret
<<"<footer class=\"row\">"<<fullVersionString()<<"<br>© 2013 - 2016 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl
;
285 ret
<<"</body></html>"<<endl
;
287 resp
->body
= ret
.str();
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
);
297 /** "Normalize" record content for API consumers. */
298 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
) {
299 return makeRecordContent(qtype
, content
, false);
302 /** "Normalize" record content for backend storage. */
303 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
) {
304 return makeRecordContent(qtype
, content
, true);
307 static Json::object
getZoneInfo(const DomainInfo
& di
) {
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)
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
}
325 static void fillZone(const DNSName
& zonename
, HttpResponse
* resp
) {
328 if(!B
.getDomainInfo(zonename
, di
))
329 throw ApiException("Could not find domain '"+zonename
.toString()+"'");
331 Json::object doc
= getZoneInfo(di
);
332 // extra stuff getZoneInfo doesn't do for us (more expensive)
334 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
335 doc
["soa_edit_api"] = soa_edit_api
;
337 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
338 doc
["soa_edit"] = soa_edit
;
340 vector
<DNSResourceRecord
> records
;
341 vector
<Comment
> comments
;
343 // load all records + sort
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
);
352 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& a
, const DNSResourceRecord
& b
) {
353 if (a
.qname
== b
.qname
) {
354 return b
.qtype
< a
.qtype
;
356 return b
.qname
< a
.qname
;
360 // load all comments + sort
363 di
.backend
->listComments(di
.id
);
364 while(di
.backend
->getComment(comment
)) {
365 comments
.push_back(comment
);
367 sort(comments
.begin(), comments
.end(), [](const Comment
& a
, const Comment
& b
) {
368 if (a
.qname
== b
.qname
) {
369 return b
.qtype
< a
.qtype
;
371 return b
.qname
< a
.qname
;
377 Json::array rrset_records
;
378 Json::array rrset_comments
;
379 DNSName current_qname
;
382 auto rit
= records
.begin();
383 auto cit
= comments
.begin();
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
;
391 current_qname
= cit
->qname
;
392 current_qtype
= cit
->qtype
;
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
) }
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
}
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
);
420 rrset_records
.clear();
421 rrset_comments
.clear();
424 doc
["rrsets"] = rrsets
;
429 void productServerStatisticsFetch(map
<string
,string
>& out
)
431 vector
<string
> items
= S
.getEntries();
432 for(const string
& item
: items
) {
433 out
[item
] = std::to_string(S
.read(item
));
437 out
["uptime"] = std::to_string(time(0) - s_starttime
);
440 static void gatherRecords(const Json container
, const DNSName
& qname
, const QType qtype
, const int ttl
, vector
<DNSResourceRecord
>& new_records
, vector
<DNSResourceRecord
>& new_ptrs
) {
442 DNSResourceRecord rr
;
447 for(auto record
: container
["records"].array_items()) {
448 string content
= stringFromJson(record
, "content");
449 rr
.disabled
= boolFromJson(record
, "disabled");
451 // validate that the client sent something we can actually parse, and require that data to be dotted.
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
+"')");
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");
464 rr
.content
= makeBackendRecordContent(rr
.qtype
, content
);
466 catch(std::exception
& e
)
468 throw ApiException("Record "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" '"+content
+"': "+e
.what());
471 if ((rr
.qtype
.getCode() == QType::A
|| rr
.qtype
.getCode() == QType::AAAA
) &&
472 boolFromJson(record
, "set-ptr", false) == true) {
473 DNSResourceRecord ptr
;
476 // verify that there's a zone for the PTR
477 DNSPacket fakePacket
;
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
+"'");
483 ptr
.domain_id
= sd
.domain_id
;
484 new_ptrs
.push_back(ptr
);
487 new_records
.push_back(rr
);
491 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
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
);
505 static void updateDomainSettingsFromDocument(const DomainInfo
& di
, const DNSName
& zonename
, const Json document
) {
507 for(auto value
: document
["masters"].array_items()) {
508 string master
= value
.string_value();
510 throw ApiException("Master can not be an empty string");
511 zonemaster
+= master
+ " ";
514 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
515 di
.backend
->setMaster(zonename
, zonemaster
);
517 if (document
["soa_edit_api"].is_string()) {
518 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
520 if (document
["soa_edit"].is_string()) {
521 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
523 if (document
["account"].is_string()) {
524 di
.backend
->setAccount(zonename
, document
["account"].string_value());
528 static void apiZoneCryptokeys(HttpRequest
* req
, HttpResponse
* resp
) {
529 if(req
->method
!= "GET")
530 throw ApiException("Only GET is implemented");
532 bool inquireSingleKey
= false;
534 if (req
->parameters
.count("key_id")) {
535 inquireSingleKey
= true;
536 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
539 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
544 if(!B
.getDomainInfo(zonename
, di
))
545 throw HttpNotFoundException();
547 DNSSECKeeper::keyset_t keyset
=dk
.getKeys(zonename
, false);
550 for(const auto& value
: keyset
) {
551 if (inquireSingleKey
&& inquireKeyId
!= value
.second
.id
) {
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;
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() }
571 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
573 for(const int keyid
: { 1, 2, 3, 4 })
575 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
580 if (inquireSingleKey
) {
581 key
["privatekey"] = value
.first
.getKey()->convertToISC();
588 if (inquireSingleKey
) {
589 // we came here because we couldn't find the requested key.
590 throw HttpNotFoundException();
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");
600 ZoneParserTNG
zpt(zonedata
, zonename
);
604 string comment
= "Imported via the API";
607 while(zpt
.get(rr
, &comment
)) {
608 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
610 if(rr
.qtype
.getCode() == QType::SOA
)
613 new_records
.push_back(rr
);
616 catch(std::exception
& ae
) {
617 throw ApiException("An error occured while parsing the zonedata: "+string(ae
.what()));
621 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
624 if (req
->method
== "POST" && !::arg().mustDo("api-readonly")) {
626 auto document
= req
->json();
627 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
628 apiCheckNameAllowedCharacters(zonename
.toString());
630 bool exists
= B
.getDomainInfo(zonename
, di
);
632 throw ApiException("Domain '"+zonename
.toString()+"' already exists");
634 // validate 'kind' is set
635 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
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");
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)");
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();
651 soa_edit_api_kind
= "DEFAULT";
653 string soa_edit_kind
= document
["soa_edit"].string_value();
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
;
661 if (rrsets
.is_array()) {
662 for (const auto& rrset
: rrsets
.array_items()) {
663 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
664 apiCheckQNameAllowedCharacters(qname
.toString());
666 qtype
= stringFromJson(rrset
, "type");
667 if (qtype
.getCode() == 0) {
668 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
670 if (rrset
["records"].is_array()) {
671 int ttl
= intFromJson(rrset
, "ttl");
672 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
674 if (rrset
["comments"].is_array()) {
675 gatherComments(rrset
, qname
, qtype
, new_comments
);
678 } else if (zonestring
!= "") {
679 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
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());
687 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
689 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
690 // fixup dots after serializeSOAData/increaseSOARecord
691 rr
.content
= makeBackendRecordContent(rr
.qtype
, rr
.content
);
695 // synthesize RRs as needed
696 DNSResourceRecord autorr
;
697 autorr
.qname
= zonename
;
699 autorr
.ttl
= ::arg().asNum("default-ttl");
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()
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
);
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
+ "'");
726 // ensure the name parses
727 autorr
.content
= DNSName(nameserver
).toStringNoDot();
729 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
732 new_records
.push_back(autorr
);
735 // no going back after this
736 if(!B
.createDomain(zonename
))
737 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
739 if(!B
.getDomainInfo(zonename
, di
))
740 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
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
);
747 di
.backend
->startTransaction(zonename
, di
.id
);
749 for(auto rr
: new_records
) {
750 rr
.domain_id
= di
.id
;
751 di
.backend
->feedRecord(rr
);
753 for(Comment
& c
: new_comments
) {
755 di
.backend
->feedComment(c
);
758 updateDomainSettingsFromDocument(di
, zonename
, document
);
760 di
.backend
->commitTransaction();
762 fillZone(zonename
, resp
);
767 if(req
->method
!= "GET")
768 throw HttpMethodNotAllowedException();
770 vector
<DomainInfo
> domains
;
771 B
.getAllDomains(&domains
, true); // incl. disabled
774 for(const DomainInfo
& di
: domains
) {
775 doc
.push_back(getZoneInfo(di
));
780 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
781 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
783 if(req
->method
== "PUT" && !::arg().mustDo("api-readonly")) {
784 // update domain settings
787 if(!B
.getDomainInfo(zonename
, di
))
788 throw ApiException("Could not find domain '"+zonename
.toString()+"'");
790 updateDomainSettingsFromDocument(di
, zonename
, req
->json());
792 fillZone(zonename
, resp
);
795 else if(req
->method
== "DELETE" && !::arg().mustDo("api-readonly")) {
799 if(!B
.getDomainInfo(zonename
, di
))
800 throw ApiException("Could not find domain '"+zonename
.toString()+"'");
802 if(!di
.backend
->deleteDomain(zonename
))
803 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
805 // empty body on success
807 resp
->status
= 204; // No Content: declare that the zone is gone now
809 } else if (req
->method
== "PATCH" && !::arg().mustDo("api-readonly")) {
810 patchZone(req
, resp
);
812 } else if (req
->method
== "GET") {
813 fillZone(zonename
, resp
);
817 throw HttpMethodNotAllowedException();
820 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
821 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
823 if(req
->method
!= "GET")
824 throw HttpMethodNotAllowedException();
830 if(!B
.getDomainInfo(zonename
, di
))
831 throw ApiException("Could not find domain '"+zonename
.toString()+"'");
833 DNSResourceRecord rr
;
835 di
.backend
->list(zonename
, di
.id
);
836 while(di
.backend
->get(rr
)) {
837 if (!rr
.qtype
.getCode())
838 continue; // skip empty non-terminals
841 rr
.qname
.toString() << "\t" <<
843 rr
.qtype
.getName() << "\t" <<
844 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
848 if (req
->accept_json
) {
849 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
851 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
852 resp
->body
= ss
.str();
856 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
) {
857 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
859 if(req
->method
!= "PUT")
860 throw HttpMethodNotAllowedException();
864 if(!B
.getDomainInfo(zonename
, di
))
865 throw ApiException("Could not find domain '"+zonename
.toString()+"'");
867 if(di
.masters
.empty())
868 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
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());
875 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
876 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
878 if(req
->method
!= "PUT")
879 throw HttpMethodNotAllowedException();
883 if(!B
.getDomainInfo(zonename
, di
))
884 throw ApiException("Could not find domain '"+zonename
.toString()+"'");
886 if(!Communicator
.notifyDomain(zonename
))
887 throw ApiException("Failed to add to the queue - see server log");
889 resp
->setSuccessResult("Notification queued");
892 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
893 if (rr
.qtype
.getCode() == QType::A
) {
895 if (!IpToU32(rr
.content
, &ip
)) {
896 throw ApiException("PTR: Invalid IP address given");
898 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
899 % ((ip
>> 24) & 0xff)
900 % ((ip
>> 16) & 0xff)
904 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
905 ComboAddress
ca(rr
.content
);
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");
913 ss
<< buf
[0] << '.' << buf
[1] << '.';
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.");
920 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
925 ptr
->disabled
= rr
.disabled
;
926 ptr
->content
= rr
.qname
.toString();
929 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
930 for(const DNSResourceRecord
& rr
: new_ptrs
) {
931 DNSPacket fakePacket
;
933 sd
.db
= (DNSBackend
*)-1; // getAuth() cache bypass
934 fakePacket
.qtype
= QType::PTR
;
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)");
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
);
949 soarr
.domain_id
= sd
.domain_id
;
952 increaseSOARecord(soarr
, soa_edit_api_kind
, soa_edit_kind
);
953 // fixup dots after serializeSOAData/increaseSOARecord
954 soarr
.content
= makeBackendRecordContent(soarr
.qtype
, soarr
.content
);
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.");
965 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
968 sd
.db
->commitTransaction();
969 PC
.purgeExact(rr
.qname
);
973 static void patchZone(HttpRequest
* req
, HttpResponse
* resp
) {
976 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
977 if (!B
.getDomainInfo(zonename
, di
))
978 throw ApiException("Could not find domain '"+zonename
.toString()+"'");
980 vector
<DNSResourceRecord
> new_records
;
981 vector
<Comment
> new_comments
;
982 vector
<DNSResourceRecord
> new_ptrs
;
984 Json document
= req
->json();
986 auto rrsets
= document
["rrsets"];
987 if (!rrsets
.is_array())
988 throw ApiException("No rrsets given in update request");
990 di
.backend
->startTransaction(zonename
);
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;
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());
1004 qtype
= stringFromJson(rrset
, "type");
1005 if (qtype
.getCode() == 0) {
1006 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
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.");
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");
1020 bool replace_records
= rrset
["records"].is_array();
1021 bool replace_comments
= rrset
["comments"].is_array();
1023 if (!replace_records
&& !replace_comments
) {
1024 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
1027 new_records
.clear();
1028 new_comments
.clear();
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
);
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
);
1045 if (replace_comments
) {
1046 gatherComments(rrset
, qname
, qtype
, new_comments
);
1048 for(Comment
& c
: new_comments
) {
1049 c
.domain_id
= di
.id
;
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.");
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.");
1065 throw ApiException("Changetype not understood");
1068 // edit SOA (if needed)
1069 if (!soa_edit_api_kind
.empty() && !soa_edit_done
) {
1071 if (!B
.getSOA(zonename
, sd
))
1072 throw ApiException("No SOA found for domain '"+zonename
.toString()+"'");
1074 DNSResourceRecord rr
;
1075 rr
.qname
= zonename
;
1076 rr
.content
= serializeSOAData(sd
);
1078 rr
.domain_id
= di
.id
;
1081 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1082 // fixup dots after serializeSOAData/increaseSOARecord
1083 rr
.content
= makeBackendRecordContent(rr
.qtype
, rr
.content
);
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.");
1091 di
.backend
->abortTransaction();
1094 di
.backend
->commitTransaction();
1096 PC
.purgeExact(zonename
);
1099 storeChangedPTRs(B
, new_ptrs
);
1102 fillZone(zonename
, resp
);
1105 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
1106 if(req
->method
!= "GET")
1107 throw HttpMethodNotAllowedException();
1109 string q
= req
->getvars
["q"];
1110 string sMax
= req
->getvars
["max"];
1115 throw ApiException("Query q can't be blank");
1116 if (sMax
.empty() == false)
1117 maxEnts
= std::stoi(sMax
);
1119 throw ApiException("Maximum entries must be larger than 0");
1121 SimpleMatch
sm(q
,true);
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
;
1130 B
.getAllDomains(&domains
, true);
1132 for(const DomainInfo di
: domains
)
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() }
1142 zoneIdZone
[di
.id
] = di
; // populate cache
1145 if (B
.searchRecords(q
, maxEnts
, result_rr
))
1147 for(const DNSResourceRecord
& rr
: result_rr
)
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
) }
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();
1161 doc
.push_back(object
);
1165 if (B
.searchComments(q
, maxEnts
, result_c
))
1167 for(const Comment
&c
: result_c
)
1169 auto object
= Json::object
{
1170 { "object_type", "comment" },
1171 { "name", c
.qname
.toString() },
1172 { "content", c
.content
}
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();
1178 doc
.push_back(object
);
1185 void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
1186 if(req
->method
!= "PUT")
1187 throw HttpMethodNotAllowedException();
1189 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
1191 int count
= PC
.purgeExact(canon
);
1192 resp
->setBody(Json::object
{
1194 { "result", "Flushed cache." }
1198 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
1200 resp
->headers
["Cache-Control"] = "max-age=86400";
1201 resp
->headers
["Content-Type"] = "text/css";
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();
1234 void AuthWebServer::webThread()
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
);
1254 d_ws
->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction
, this, _1
, _2
));
1255 d_ws
->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction
, this, _1
, _2
));
1259 L
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;