]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/ws-auth.cc
Merge pull request #4530 from paddg/patch-3
[thirdparty/pdns.git] / pdns / ws-auth.cc
CommitLineData
12c86877 1/*
32cb6fd4 2 Copyright (C) 2002 - 2016 PowerDNS.COM BV
12c86877
BH
3
4 This program is free software; you can redistribute it and/or modify
6ec5e728 5 it under the terms of the GNU General Public License version 2
9054d8a4 6 as published by the Free Software Foundation
12c86877 7
f782fe38
MH
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
12c86877
BH
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
06bd9ccf 19 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
9054d8a4 20*/
870a0fe4
AT
21#ifdef HAVE_CONFIG_H
22#include "config.h"
23#endif
9054d8a4 24#include "utility.hh"
d267d1bf 25#include "dynlistener.hh"
2470b36e 26#include "ws-auth.hh"
e611a06c 27#include "json.hh"
12c86877
BH
28#include "webserver.hh"
29#include "logger.hh"
e611a06c 30#include "packetcache.hh"
12c86877
BH
31#include "statbag.hh"
32#include "misc.hh"
33#include "arguments.hh"
34#include "dns.hh"
6cc98ddf 35#include "comment.hh"
e611a06c 36#include "ueberbackend.hh"
dcc65f25 37#include <boost/format.hpp>
fa8fd4d2 38
9ac4a7c6 39#include "namespaces.hh"
6ec5e728 40#include "ws-api.hh"
ba1a571d 41#include "version.hh"
d29d5db7 42#include "dnsseckeeper.hh"
3c3c006b 43#include <iomanip>
0f0e73fe 44#include "zoneparser-tng.hh"
a426cb89 45#include "common_startup.hh"
3c3c006b 46
8537b9f0 47
24afabad 48using json11::Json;
12c86877
BH
49
50extern StatBag S;
995473c8 51extern PacketCache PC;
12c86877 52
f63168e6 53static void patchZone(HttpRequest* req, HttpResponse* resp);
995473c8 54static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs);
f63168e6
CH
55static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr);
56
dea47634 57AuthWebServer::AuthWebServer()
12c86877
BH
58{
59 d_start=time(0);
96d299db 60 d_min10=d_min5=d_min1=0;
c81c2ea8 61 d_ws = 0;
f17c93b4 62 d_tid = 0;
536ab56f 63 if(arg().mustDo("webserver") || arg().mustDo("api")) {
bbef8f04 64 d_ws = new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
825fa717
CH
65 d_ws->bind();
66 }
12c86877
BH
67}
68
dea47634 69void AuthWebServer::go()
12c86877 70{
536ab56f
CH
71 S.doRings();
72 pthread_create(&d_tid, 0, webThreadHelper, this);
73 pthread_create(&d_tid, 0, statThreadHelper, this);
12c86877
BH
74}
75
dea47634 76void AuthWebServer::statThread()
12c86877
BH
77{
78 try {
79 for(;;) {
80 d_queries.submit(S.read("udp-queries"));
81 d_cachehits.submit(S.read("packetcache-hit"));
82 d_cachemisses.submit(S.read("packetcache-miss"));
83 d_qcachehits.submit(S.read("query-cache-hit"));
84 d_qcachemisses.submit(S.read("query-cache-miss"));
85 Utility::sleep(1);
86 }
87 }
88 catch(...) {
89 L<<Logger::Error<<"Webserver statThread caught an exception, dying"<<endl;
90 exit(1);
91 }
92}
93
dea47634 94void *AuthWebServer::statThreadHelper(void *p)
12c86877 95{
dea47634
CH
96 AuthWebServer *self=static_cast<AuthWebServer *>(p);
97 self->statThread();
12c86877
BH
98 return 0; // never reached
99}
100
dea47634 101void *AuthWebServer::webThreadHelper(void *p)
12c86877 102{
dea47634
CH
103 AuthWebServer *self=static_cast<AuthWebServer *>(p);
104 self->webThread();
12c86877
BH
105 return 0; // never reached
106}
107
9f3fdaa0
CH
108static string htmlescape(const string &s) {
109 string result;
110 for(string::const_iterator it=s.begin(); it!=s.end(); ++it) {
111 switch (*it) {
112 case '&':
c86a96f9 113 result += "&amp;";
9f3fdaa0
CH
114 break;
115 case '<':
116 result += "&lt;";
117 break;
118 case '>':
119 result += "&gt;";
120 break;
c7f59d62
PL
121 case '"':
122 result += "&quot;";
123 break;
9f3fdaa0
CH
124 default:
125 result += *it;
126 }
127 }
128 return result;
129}
130
12c86877
BH
131void printtable(ostringstream &ret, const string &ringname, const string &title, int limit=10)
132{
133 int tot=0;
134 int entries=0;
101b5d5d 135 vector<pair <string,unsigned int> >ring=S.getRing(ringname);
12c86877 136
1071abdd 137 for(vector<pair<string, unsigned int> >::const_iterator i=ring.begin(); i!=ring.end();++i) {
12c86877
BH
138 tot+=i->second;
139 entries++;
140 }
141
1071abdd 142 ret<<"<div class=\"panel\">";
c7f59d62 143 ret<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname)<<"\">Reset</a></span>"<<endl;
1071abdd
CH
144 ret<<"<h2>"<<title<<"</h2>"<<endl;
145 ret<<"<div class=ringmeta>";
c7f59d62 146 ret<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname)<<"\">Showing: Top "<<limit<<" of "<<entries<<"</a>"<<endl;
1071abdd 147 ret<<"<span class=resizering>Resize: ";
bb3c3f50 148 unsigned int sizes[]={10,100,500,1000,10000,500000,0};
12c86877
BH
149 for(int i=0;sizes[i];++i) {
150 if(S.getRingSize(ringname)!=sizes[i])
c7f59d62 151 ret<<"<a href=\"?resizering="<<htmlescape(ringname)<<"&amp;size="<<sizes[i]<<"\">"<<sizes[i]<<"</a> ";
12c86877
BH
152 else
153 ret<<"("<<sizes[i]<<") ";
154 }
1071abdd 155 ret<<"</span></div>";
12c86877 156
1071abdd 157 ret<<"<table class=\"data\">";
12c86877 158 int printed=0;
f5cb7e61 159 int total=max(1,tot);
bb3c3f50 160 for(vector<pair<string,unsigned int> >::const_iterator i=ring.begin();limit && i!=ring.end();++i,--limit) {
dea47634 161 ret<<"<tr><td>"<<htmlescape(i->first)<<"</td><td>"<<i->second<<"</td><td align=right>"<< AuthWebServer::makePercentage(i->second*100.0/total)<<"</td>"<<endl;
12c86877
BH
162 printed+=i->second;
163 }
164 ret<<"<tr><td colspan=3></td></tr>"<<endl;
165 if(printed!=tot)
dea47634 166 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;
12c86877 167
e2a77e08 168 ret<<"<tr><td><b>Total:</b></td><td><b>"<<tot<<"</b></td><td align=right><b>100%</b></td>";
1071abdd 169 ret<<"</table></div>"<<endl;
12c86877
BH
170}
171
dea47634 172void AuthWebServer::printvars(ostringstream &ret)
12c86877 173{
1071abdd 174 ret<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl;
12c86877
BH
175
176 vector<string>entries=S.getEntries();
177 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
178 ret<<"<tr><td>"<<*i<<"</td><td>"<<S.read(*i)<<"</td><td>"<<S.getDescrip(*i)<<"</td>"<<endl;
179 }
e2a77e08 180
1071abdd 181 ret<<"</table></div>"<<endl;
12c86877
BH
182}
183
dea47634 184void AuthWebServer::printargs(ostringstream &ret)
12c86877 185{
e2a77e08 186 ret<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl;
12c86877
BH
187
188 vector<string>entries=arg().list();
189 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
190 ret<<"<tr><td>"<<*i<<"</td><td>"<<arg()[*i]<<"</td><td>"<<arg().getHelp(*i)<<"</td>"<<endl;
191 }
192}
193
dea47634 194string AuthWebServer::makePercentage(const double& val)
b6f57093
BH
195{
196 return (boost::format("%.01f%%") % val).str();
197}
198
dea47634 199void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
12c86877 200{
583ea80d
CH
201 if(!req->getvars["resetring"].empty()) {
202 if (S.ringExists(req->getvars["resetring"]))
203 S.resetRing(req->getvars["resetring"]);
80d59cd1 204 resp->status = 301;
0665b7e6 205 resp->headers["Location"] = req->url.path;
80d59cd1 206 return;
12c86877 207 }
583ea80d 208 if(!req->getvars["resizering"].empty()){
335da0ba 209 int size=std::stoi(req->getvars["size"]);
583ea80d 210 if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000)
335da0ba 211 S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
80d59cd1 212 resp->status = 301;
0665b7e6 213 resp->headers["Location"] = req->url.path;
80d59cd1 214 return;
12c86877
BH
215 }
216
217 ostringstream ret;
218
1071abdd
CH
219 ret<<"<!DOCTYPE html>"<<endl;
220 ret<<"<html><head>"<<endl;
221 ret<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl;
222 ret<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl;
223 ret<<"</head><body>"<<endl;
224
225 ret<<"<div class=\"row\">"<<endl;
226 ret<<"<div class=\"headl columns\">";
a1caa8b8 227 ret<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION);
1071abdd 228 if(!arg()["config-name"].empty()) {
a1caa8b8 229 ret<<" ["<<htmlescape(arg()["config-name"])<<"]";
1071abdd
CH
230 }
231 ret<<"</a></div>"<<endl;
232 ret<<"<div class=\"headr columns\"></div></div>";
233 ret<<"<div class=\"row\"><div class=\"all columns\">";
12c86877
BH
234
235 time_t passed=time(0)-s_starttime;
236
e2a77e08
KM
237 ret<<"<p>Uptime: "<<
238 humanDuration(passed)<<
239 "<br>"<<endl;
12c86877 240
395b07ea 241 ret<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
f6154a3b
CH
242 d_queries.get1()<<", "<<
243 d_queries.get5()<<", "<<
244 d_queries.get10()<<". Max queries/second: "<<d_queries.getMax()<<
12c86877 245 "<br>"<<endl;
1d6b70f9 246
f6154a3b 247 if(d_cachemisses.get10()+d_cachehits.get10()>0)
b6f57093 248 ret<<"Cache hitrate, 1, 5, 10 minute averages: "<<
f6154a3b
CH
249 makePercentage((d_cachehits.get1()*100.0)/((d_cachehits.get1())+(d_cachemisses.get1())))<<", "<<
250 makePercentage((d_cachehits.get5()*100.0)/((d_cachehits.get5())+(d_cachemisses.get5())))<<", "<<
251 makePercentage((d_cachehits.get10()*100.0)/((d_cachehits.get10())+(d_cachemisses.get10())))<<
b6f57093 252 "<br>"<<endl;
12c86877 253
f6154a3b 254 if(d_qcachemisses.get10()+d_qcachehits.get10()>0)
395b07ea 255 ret<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
f6154a3b
CH
256 makePercentage((d_qcachehits.get1()*100.0)/((d_qcachehits.get1())+(d_qcachemisses.get1())))<<", "<<
257 makePercentage((d_qcachehits.get5()*100.0)/((d_qcachehits.get5())+(d_qcachemisses.get5())))<<", "<<
258 makePercentage((d_qcachehits.get10()*100.0)/((d_qcachehits.get10())+(d_qcachemisses.get10())))<<
b6f57093 259 "<br>"<<endl;
12c86877 260
395b07ea 261 ret<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
f6154a3b
CH
262 d_qcachemisses.get1()<<", "<<
263 d_qcachemisses.get5()<<", "<<
264 d_qcachemisses.get10()<<". Max queries/second: "<<d_qcachemisses.getMax()<<
12c86877
BH
265 "<br>"<<endl;
266
1071abdd 267 ret<<"Total queries: "<<S.read("udp-queries")<<". Question/answer latency: "<<S.read("latency")/1000.0<<"ms</p><br>"<<endl;
583ea80d 268 if(req->getvars["ring"].empty()) {
12c86877
BH
269 vector<string>entries=S.listRings();
270 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i)
271 printtable(ret,*i,S.getRingTitle(*i));
272
f6154a3b 273 printvars(ret);
12c86877 274 if(arg().mustDo("webserver-print-arguments"))
f6154a3b 275 printargs(ret);
12c86877
BH
276 }
277 else
583ea80d 278 printtable(ret,req->getvars["ring"],S.getRingTitle(req->getvars["ring"]),100);
12c86877 279
1071abdd 280 ret<<"</div></div>"<<endl;
32cb6fd4 281 ret<<"<footer class=\"row\">"<<fullVersionString()<<"<br>&copy; 2013 - 2016 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl;
12c86877
BH
282 ret<<"</body></html>"<<endl;
283
80d59cd1 284 resp->body = ret.str();
61f5d289 285 resp->status = 200;
12c86877
BH
286}
287
1d6b70f9
CH
288/** Helper to build a record content as needed. */
289static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot) {
290 // noDot: for backend storage, pass true. for API users, pass false.
7fe1a82b 291 std::unique_ptr<DNSRecordContent> drc(DNSRecordContent::mastermake(qtype.getCode(), 1, content));
292 return drc->getZoneRepresentation(noDot);
1d6b70f9
CH
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
62a9a74c 305static Json::object getZoneInfo(const DomainInfo& di) {
c04b5870 306 DNSSECKeeper dk;
290a083d 307 string zoneId = apiZoneNameToId(di.zone);
62a9a74c
CH
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 };
c04b5870
CH
321}
322
290a083d 323static void fillZone(const DNSName& zonename, HttpResponse* resp) {
1abb81f4 324 UeberBackend B;
1abb81f4 325 DomainInfo di;
73301d73 326 if(!B.getDomainInfo(zonename, di))
290a083d 327 throw ApiException("Could not find domain '"+zonename.toString()+"'");
1abb81f4 328
62a9a74c
CH
329 Json::object doc = getZoneInfo(di);
330 // extra stuff getZoneInfo doesn't do for us (more expensive)
d29d5db7
CH
331 string soa_edit_api;
332 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
62a9a74c 333 doc["soa_edit_api"] = soa_edit_api;
6bb25159
MS
334 string soa_edit;
335 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
62a9a74c 336 doc["soa_edit"] = soa_edit;
1abb81f4 337
6754ef71
CH
338 vector<DNSResourceRecord> records;
339 vector<Comment> comments;
1abb81f4 340
6754ef71
CH
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()) {
dedd3fc8 384 if (cit == comments.end() || (rit != records.end() && (cit->qname.toString() < rit->qname.toString() || cit->qtype < rit->qtype))) {
6754ef71
CH
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;
6cc98ddf 423
669822d0 424 resp->setBody(doc);
1abb81f4
CH
425}
426
6ec5e728
CH
427void productServerStatisticsFetch(map<string,string>& out)
428{
a45303b8 429 vector<string> items = S.getEntries();
ff05fd12 430 for(const string& item : items) {
335da0ba 431 out[item] = std::to_string(S.read(item));
a45303b8
CH
432 }
433
434 // add uptime
335da0ba 435 out["uptime"] = std::to_string(time(0) - s_starttime);
c67bf8c5
CH
436}
437
6754ef71 438static void gatherRecords(const Json container, const DNSName& qname, const QType qtype, const int ttl, vector<DNSResourceRecord>& new_records, vector<DNSResourceRecord>& new_ptrs) {
f63168e6
CH
439 UeberBackend B;
440 DNSResourceRecord rr;
6754ef71
CH
441 rr.qname = qname;
442 rr.qtype = qtype;
443 rr.auth = 1;
444 rr.ttl = ttl;
1f68b185 445 for(auto record : container["records"].array_items()) {
1f68b185 446 string content = stringFromJson(record, "content");
1f68b185
CH
447 rr.disabled = boolFromJson(record, "disabled");
448
1f68b185
CH
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");
1e5b9ab9 460 }
f63168e6 461 }
1f68b185
CH
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 }
f63168e6 468
1f68b185
CH
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);
f63168e6 473
1f68b185
CH
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+"'");
f63168e6 480
1f68b185
CH
481 ptr.domain_id = sd.domain_id;
482 new_ptrs.push_back(ptr);
f63168e6 483 }
1f68b185
CH
484
485 new_records.push_back(rr);
f63168e6
CH
486 }
487}
488
6754ef71 489static void gatherComments(const Json container, const DNSName& qname, const QType qtype, vector<Comment>& new_comments) {
f63168e6 490 Comment c;
6754ef71
CH
491 c.qname = qname;
492 c.qtype = qtype;
f63168e6
CH
493
494 time_t now = time(0);
1f68b185 495 for (auto comment : container["comments"].array_items()) {
1f68b185
CH
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);
f63168e6
CH
500 }
501}
6cc98ddf 502
1f68b185
CH
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 + " ";
bb9fd223
CH
510 }
511
512 di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
1f68b185 513 di.backend->setMaster(zonename, zonemaster);
d29d5db7 514
1f68b185
CH
515 if (document["soa_edit_api"].is_string()) {
516 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
d29d5db7 517 }
1f68b185
CH
518 if (document["soa_edit"].is_string()) {
519 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
6bb25159 520 }
1f68b185
CH
521 if (document["account"].is_string()) {
522 di.backend->setAccount(zonename, document["account"].string_value());
79532aa7 523 }
bb9fd223
CH
524}
525
24e11043
CJ
526static bool isValidMetadataKind(const string& kind, bool readonly) {
527 static vector<string> builtinOptions {
528 "ALLOW-AXFR-FROM",
529 "AXFR-SOURCE",
530 "ALLOW-DNSUPDATE-FROM",
531 "TSIG-ALLOW-DNSUPDATE",
532 "FORWARD-DNSUPDATE",
533 "SOA-EDIT-DNSUPDATE",
534 "ALSO-NOTIFY",
535 "AXFR-MASTER-TSIG",
536 "GSS-ALLOW-AXFR-PRINCIPAL",
537 "GSS-ACCEPTOR-PRINCIPAL",
538 "IXFR",
539 "LUA-AXFR-SCRIPT",
540 "NSEC3NARROW",
541 "NSEC3PARAM",
542 "PRESIGNED",
543 "PUBLISH-CDNSKEY",
544 "PUBLISH-CDS",
545 "SOA-EDIT",
546 "TSIG-ALLOW-AXFR",
547 "TSIG-ALLOW-DNSUPDATE"
548 };
549
550 // the following options do not allow modifications via API
551 static vector<string> protectedOptions {
552 "NSEC3NARROW",
553 "NSEC3PARAM",
554 "PRESIGNED",
555 "LUA-AXFR-SCRIPT"
556 };
557
558 bool found = false;
559
d8043c73 560 for (const string& s : builtinOptions) {
24e11043 561 if (kind == s) {
d8043c73 562 for (const string& s2 : protectedOptions) {
24e11043
CJ
563 if (!readonly && s == s2)
564 return false;
565 }
566 found = true;
567 break;
568 }
569 }
570
571 return found;
572}
573
574static void apiZoneMetadata(HttpRequest* req, HttpResponse *resp) {
575 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
576 UeberBackend B;
577
578 if (req->method == "GET") {
579 map<string, vector<string> > md;
580 Json::array document;
581
582 if (!B.getAllDomainMetadata(zonename, md))
583 throw HttpNotFoundException();
584
585 for (const auto& i : md) {
586 Json::array entries;
587 for (string j : i.second)
588 entries.push_back(j);
589
590 Json::object key {
591 { "type", "Metadata" },
592 { "kind", i.first },
593 { "metadata", entries }
594 };
595
596 document.push_back(key);
597 }
598
599 resp->setBody(document);
600 } else if (req->method == "POST" && !::arg().mustDo("api-readonly")) {
601 auto document = req->json();
602 string kind;
603 vector<string> entries;
604
605 try {
606 kind = stringFromJson(document, "kind");
607 } catch (JsonException) {
608 throw ApiException("kind is not specified or not a string");
609 }
610
611 if (!isValidMetadataKind(kind, false))
612 throw ApiException("Unsupported metadata kind '" + kind + "'");
613
614 vector<string> vecMetadata;
c6720e79
CJ
615
616 if (!B.getDomainMetadata(zonename, kind, vecMetadata))
617 throw ApiException("Could not retrieve metadata entries for domain '" +
618 zonename.toString() + "'");
619
24e11043
CJ
620 auto& metadata = document["metadata"];
621 if (!metadata.is_array())
622 throw ApiException("metadata is not specified or not an array");
623
624 for (const auto& i : metadata.array_items()) {
625 if (!i.is_string())
626 throw ApiException("metadata must be strings");
c6720e79
CJ
627 else if (std::find(vecMetadata.cbegin(),
628 vecMetadata.cend(),
629 i.string_value()) == vecMetadata.cend()) {
630 vecMetadata.push_back(i.string_value());
631 }
24e11043
CJ
632 }
633
634 if (!B.setDomainMetadata(zonename, kind, vecMetadata))
c6720e79
CJ
635 throw ApiException("Could not update metadata entries for domain '" +
636 zonename.toString() + "'");
637
638 Json::array respMetadata;
639 for (const string& s : vecMetadata)
640 respMetadata.push_back(s);
641
642 Json::object key {
643 { "type", "Metadata" },
644 { "kind", document["kind"] },
645 { "metadata", respMetadata }
646 };
24e11043 647
24e11043 648 resp->status = 201;
c6720e79 649 resp->setBody(key);
24e11043
CJ
650 } else
651 throw HttpMethodNotAllowedException();
652}
653
654static void apiZoneMetadataKind(HttpRequest* req, HttpResponse* resp) {
655 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
656 string kind = req->parameters["kind"];
657 UeberBackend B;
658
659 if (req->method == "GET") {
660 vector<string> metadata;
661 Json::object document;
662 Json::array entries;
663
664 if (!B.getDomainMetadata(zonename, kind, metadata))
665 throw HttpNotFoundException();
666 else if (!isValidMetadataKind(kind, true))
667 throw ApiException("Unsupported metadata kind '" + kind + "'");
668
669 document["type"] = "Metadata";
670 document["kind"] = kind;
671
672 for (const string& i : metadata)
673 entries.push_back(i);
674
675 document["metadata"] = entries;
676 resp->setBody(document);
677 } else if (req->method == "PUT" && !::arg().mustDo("api-readonly")) {
678 auto document = req->json();
679
680 if (!isValidMetadataKind(kind, false))
681 throw ApiException("Unsupported metadata kind '" + kind + "'");
682
683 vector<string> vecMetadata;
684 auto& metadata = document["metadata"];
685 if (!metadata.is_array())
686 throw ApiException("metadata is not specified or not an array");
687
688 for (const auto& i : metadata.array_items()) {
689 if (!i.is_string())
690 throw ApiException("metadata must be strings");
691 vecMetadata.push_back(i.string_value());
692 }
693
694 if (!B.setDomainMetadata(zonename, kind, vecMetadata))
695 throw ApiException("Could not update metadata entries for domain '" + zonename.toString() + "'");
696
697 Json::object key {
698 { "type", "Metadata" },
699 { "kind", kind },
700 { "metadata", metadata }
701 };
702
703 resp->setBody(key);
704 } else if (req->method == "DELETE" && !::arg().mustDo("api-readonly")) {
705 if (!isValidMetadataKind(kind, false))
706 throw ApiException("Unsupported metadata kind '" + kind + "'");
707
708 vector<string> md; // an empty vector will do it
709 if (!B.setDomainMetadata(zonename, kind, md))
710 throw ApiException("Could not delete metadata for domain '" + zonename.toString() + "' (" + kind + ")");
711 } else
712 throw HttpMethodNotAllowedException();
713}
714
60b0a236
BZ
715static void apiZoneCryptokeysGET(DNSName zonename, int inquireKeyId, HttpResponse *resp, DNSSECKeeper *dk) {
716 DNSSECKeeper::keyset_t keyset=dk->getKeys(zonename, false);
4b7f120a 717
997cab68
BZ
718 bool inquireSingleKey = inquireKeyId >= 0;
719
24afabad 720 Json::array doc;
29704f66 721 for(const auto& value : keyset) {
997cab68 722 if (inquireSingleKey && (unsigned)inquireKeyId != value.second.id) {
29704f66 723 continue;
38809e97 724 }
24afabad 725
b6bd795c 726 string keyType;
60b0a236 727 switch (value.second.keyType) {
b6bd795c
PL
728 case DNSSECKeeper::KSK: keyType="ksk"; break;
729 case DNSSECKeeper::ZSK: keyType="zsk"; break;
730 case DNSSECKeeper::CSK: keyType="csk"; break;
731 }
732
24afabad 733 Json::object key {
997cab68
BZ
734 { "type", "Cryptokey" },
735 { "id", (int)value.second.id },
736 { "active", value.second.active },
737 { "keytype", keyType },
738 { "flags", (uint16_t)value.first.d_flags },
739 { "dnskey", value.first.getDNSKEY().getZoneRepresentation() }
24afabad
CH
740 };
741
b6bd795c 742 if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
24afabad
CH
743 Json::array dses;
744 for(const int keyid : { 1, 2, 3, 4 })
997cab68
BZ
745 try {
746 dses.push_back(makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation());
747 } catch (...) {}
24afabad 748 key["ds"] = dses;
4b7f120a 749 }
29704f66
CH
750
751 if (inquireSingleKey) {
752 key["privatekey"] = value.first.getKey()->convertToISC();
753 resp->setBody(key);
754 return;
755 }
24afabad 756 doc.push_back(key);
4b7f120a
MS
757 }
758
29704f66
CH
759 if (inquireSingleKey) {
760 // we came here because we couldn't find the requested key.
761 throw HttpNotFoundException();
762 }
4b7f120a 763 resp->setBody(doc);
997cab68
BZ
764
765}
766
767/*
768 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
769 * It deletes a key from :zone_name specified by :cryptokey_id.
770 * Server Answers:
60b0a236
BZ
771 * Case 1: the backend returns true on removal. This means the key is gone.
772 * The server returns 200 OK, no body.
773 * Case 2: the backend returns false on removal. An error occoured.
774 * The sever returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
997cab68 775 * */
60b0a236
BZ
776static void apiZoneCryptokeysDELETE(DNSName zonename, int inquireKeyId, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
777 if (dk->removeKey(zonename, inquireKeyId)) {
778 resp->body = "";
779 resp->status = 200;
997cab68
BZ
780 } else {
781 resp->setErrorResult("Could not DELETE " + req->parameters["key_id"], 422);
782 }
783}
784
785/*
786 * This method adds a key to a zone by generate it or content parameter.
787 * Parameter:
788 * {
789 * "content" : "key The format used is compatible with BIND and NSD/LDNS" <string>
790 * "keytype" : "ksk|zsk" <string>
791 * "active" : "true|false" <value>
792 * "algo" : "key generation algorithim "name|number" as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
793 * "bits" : number of bits <int>
794 * }
795 *
796 * Response:
797 * Case 1: keytype isn't ksk|zsk
798 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
60b0a236
BZ
799 * Case 2: 'bits' must be a positive integer value.
800 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
801 * Case 3: The "algo" isn't supported
997cab68 802 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
60b0a236 803 * Case 4: Algorithm <= 10 and no bits were passed
997cab68 804 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
60b0a236
BZ
805 * Case 5: The wrong keysize was passed
806 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
807 * Case 6: If the server cant guess the keysize
808 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
809 * Case 7: The key-creation failed
997cab68 810 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
60b0a236
BZ
811 * Case 8: The key in content has the wrong format
812 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
813 * Case 9: The wrong combination of fields is submitted
814 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
815 * Case 10: No content and everything was fine
816 * The server returns 201 Created and all public data about the new cryptokey
817 * Case 11: With specified content
818 * The server returns 201 Created and all public data about the added cryptokey
997cab68
BZ
819 */
820
60b0a236 821static void apiZoneCryptokeysPOST(DNSName zonename, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
997cab68
BZ
822 auto document = req->json();
823 auto content = document["content"];
997cab68 824 bool active = boolFromJson(document, "active", false);
997cab68 825 bool keyOrZone;
60b0a236 826
997cab68
BZ
827 if (stringFromJson(document, "keytype") == "ksk") {
828 keyOrZone = true;
829 } else if (stringFromJson(document, "keytype") == "zsk") {
830 keyOrZone = false;
831 } else {
832 throw ApiException("Invalid keytype " + stringFromJson(document, "keytype"));
833 }
834
60b0a236 835 int64_t insertedId;
997cab68 836
997cab68 837 if (content.is_null()) {
60b0a236
BZ
838 int bits = 0;
839 auto docbits = document["bits"];
840 if (!docbits.is_null()) {
841 if (!docbits.is_number() || (fmod(docbits.number_value(), 1.0) != 0) || docbits.int_value() < 0) {
842 throw ApiException("'bits' must be a positive integer value");
843 } else {
844 bits = docbits.int_value();
845 }
846 }
997cab68
BZ
847 int algorithm = 13; // ecdsa256
848 auto providedAlgo = document["algo"];
849 if (providedAlgo.is_string()) {
60b0a236
BZ
850 algorithm = DNSSECKeeper::shorthand2algorithm(providedAlgo.string_value());
851 if (algorithm == -1)
997cab68 852 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
997cab68
BZ
853 } else if (providedAlgo.is_number()) {
854 algorithm = providedAlgo.int_value();
60b0a236
BZ
855 } else if (!providedAlgo.is_null()) {
856 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
997cab68
BZ
857 }
858
60b0a236
BZ
859 try {
860 dk->addKey(zonename, keyOrZone, algorithm, insertedId, bits, active);
861 } catch (std::runtime_error& error) {
997cab68
BZ
862 throw ApiException(error.what());
863 }
997cab68
BZ
864 if (insertedId < 0)
865 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
60b0a236 866 } else if (document["bits"].is_null() && document["algo"].is_null()) {
997cab68
BZ
867 auto keyData = stringFromJson(document, "content");
868 DNSKEYRecordContent dkrc;
869 DNSSECPrivateKey dpk;
60b0a236 870 try {
997cab68
BZ
871 shared_ptr<DNSCryptoKeyEngine> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc, keyData));
872 dpk.d_algorithm = dkrc.d_algorithm;
873 if(dpk.d_algorithm == 7)
874 dpk.d_algorithm = 5;
875
876 if (keyOrZone)
877 dpk.d_flags = 257;
878 else
879 dpk.d_flags = 256;
880
881 dpk.setKey(dke);
997cab68 882 }
60b0a236
BZ
883 catch (std::runtime_error& error) {
884 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
885 } try {
886 dk->addKey(zonename, dpk,insertedId, active);
887 } catch (std::runtime_error& error) {
997cab68
BZ
888 throw ApiException(error.what());
889 }
890 if (insertedId < 0)
891 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
60b0a236
BZ
892 } else {
893 throw ApiException("Either you submit just the 'content' field or you leave 'content' empty and submit the other fields.");
997cab68 894 }
60b0a236 895 apiZoneCryptokeysGET(zonename, insertedId, resp, dk);
997cab68 896 resp->status = 201;
60b0a236 897}
997cab68
BZ
898
899/*
900 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
901 * It de/activates a key from :zone_name specified by :cryptokey_id.
902 * Server Answers:
60b0a236 903 * Case 1: invalid JSON data
997cab68 904 * The server returns 400 Bad Request
60b0a236
BZ
905 * Case 2: the backend returns true on de/activation. This means the key is de/active.
906 * The server returns 204 No Content
907 * Case 3: the backend returns false on de/activation. An error occoured.
997cab68
BZ
908 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
909 * */
60b0a236 910static void apiZoneCryptokeysPUT(DNSName zonename, int inquireKeyId, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
997cab68
BZ
911 //throws an exception if the Body is empty
912 auto document = req->json();
913 //throws an exception if the key does not exist or is not a bool
914 bool active = boolFromJson(document, "active");
60b0a236
BZ
915 if (active) {
916 if (!dk->activateKey(zonename, inquireKeyId)) {
997cab68
BZ
917 resp->setErrorResult("Could not activate Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
918 return;
919 }
920 } else {
60b0a236 921 if (!dk->deactivateKey(zonename, inquireKeyId)) {
997cab68
BZ
922 resp->setErrorResult("Could not deactivate Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
923 return;
924 }
925 }
60b0a236
BZ
926 resp->body = "";
927 resp->status = 204;
928 return;
997cab68
BZ
929}
930
931/*
932 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
933 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
934 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
935 * */
936static void apiZoneCryptokeys(HttpRequest *req, HttpResponse *resp) {
937 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
938
60b0a236
BZ
939 UeberBackend B;
940 DNSSECKeeper dk(&B);
941 DomainInfo di;
942 if (!B.getDomainInfo(zonename, di))
943 throw HttpBadRequestException();
944
997cab68
BZ
945 int inquireKeyId = -1;
946 if (req->parameters.count("key_id")) {
947 inquireKeyId = std::stoi(req->parameters["key_id"]);
948 }
949
950 if (req->method == "GET") {
60b0a236 951 apiZoneCryptokeysGET(zonename, inquireKeyId, resp, &dk);
997cab68 952 } else if (req->method == "DELETE") {
60b0a236
BZ
953 if (inquireKeyId == -1)
954 throw HttpBadRequestException();
955 apiZoneCryptokeysDELETE(zonename, inquireKeyId, req, resp, &dk);
997cab68 956 } else if (req->method == "POST") {
60b0a236
BZ
957 apiZoneCryptokeysPOST(zonename, req, resp, &dk);
958 } else if (req->method == "PUT") {
959 if (inquireKeyId == -1)
960 throw HttpBadRequestException();
961 apiZoneCryptokeysPUT(zonename, inquireKeyId, req, resp, &dk);
997cab68
BZ
962 } else {
963 throw HttpMethodNotAllowedException(); //Returns method not allowed
964 }
4b7f120a
MS
965}
966
1f68b185 967static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, DNSName zonename) {
0f0e73fe
MS
968 DNSResourceRecord rr;
969 vector<string> zonedata;
1f68b185 970 stringtok(zonedata, zonestring, "\r\n");
0f0e73fe
MS
971
972 ZoneParserTNG zpt(zonedata, zonename);
973
974 bool seenSOA=false;
975
976 string comment = "Imported via the API";
977
978 try {
979 while(zpt.get(rr, &comment)) {
980 if(seenSOA && rr.qtype.getCode() == QType::SOA)
981 continue;
982 if(rr.qtype.getCode() == QType::SOA)
983 seenSOA=true;
984
0f0e73fe
MS
985 new_records.push_back(rr);
986 }
987 }
988 catch(std::exception& ae) {
1af62161 989 throw ApiException("An error occurred while parsing the zonedata: "+string(ae.what()));
0f0e73fe
MS
990 }
991}
992
80d59cd1 993static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
e2dba705 994 UeberBackend B;
53942520 995 DNSSECKeeper dk(&B);
d07bf7ff 996 if (req->method == "POST" && !::arg().mustDo("api-readonly")) {
e2dba705 997 DomainInfo di;
1f68b185 998 auto document = req->json();
c576d0c5 999 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
1d6b70f9 1000 apiCheckNameAllowedCharacters(zonename.toString());
4ebf78b1 1001
1d6b70f9 1002 bool exists = B.getDomainInfo(zonename, di);
e2dba705 1003 if(exists)
1d6b70f9 1004 throw ApiException("Domain '"+zonename.toString()+"' already exists");
e2dba705 1005
bb9fd223 1006 // validate 'kind' is set
4bdff352 1007 DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
bb9fd223 1008
6754ef71
CH
1009 string zonestring = document["zone"].string_value();
1010 auto rrsets = document["rrsets"];
1011 if (rrsets.is_array() && zonestring != "")
1012 throw ApiException("You cannot give rrsets AND zone data as text");
0f0e73fe 1013
1f68b185
CH
1014 auto nameservers = document["nameservers"];
1015 if (!nameservers.is_array() && zonekind != DomainInfo::Slave)
f63168e6 1016 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
e2dba705 1017
f63168e6 1018 string soa_edit_api_kind;
1f68b185
CH
1019 if (document["soa_edit_api"].is_string()) {
1020 soa_edit_api_kind = document["soa_edit_api"].string_value();
a6448d95
CH
1021 }
1022 else {
1023 soa_edit_api_kind = "DEFAULT";
1024 }
1f68b185 1025 string soa_edit_kind = document["soa_edit"].string_value();
e90b4e38 1026
f63168e6
CH
1027 // if records/comments are given, load and check them
1028 bool have_soa = false;
1029 vector<DNSResourceRecord> new_records;
1030 vector<Comment> new_comments;
1031 vector<DNSResourceRecord> new_ptrs;
0f0e73fe 1032
6754ef71
CH
1033 if (rrsets.is_array()) {
1034 for (const auto& rrset : rrsets.array_items()) {
1035 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
1036 apiCheckQNameAllowedCharacters(qname.toString());
1037 QType qtype;
1038 qtype = stringFromJson(rrset, "type");
1039 if (qtype.getCode() == 0) {
1040 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1041 }
1042 if (rrset["records"].is_array()) {
1043 int ttl = intFromJson(rrset, "ttl");
1044 gatherRecords(rrset, qname, qtype, ttl, new_records, new_ptrs);
1045 }
1046 if (rrset["comments"].is_array()) {
1047 gatherComments(rrset, qname, qtype, new_comments);
1048 }
1049 }
0f0e73fe 1050 } else if (zonestring != "") {
1f68b185 1051 gatherRecordsFromZone(zonestring, new_records, zonename);
0f0e73fe
MS
1052 }
1053
1f68b185 1054 for(auto& rr : new_records) {
1d6b70f9 1055 if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
561434a6 1056 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": Name is out of zone");
cb9b5901 1057 apiCheckQNameAllowedCharacters(rr.qname.toString());
f63168e6 1058
1d6b70f9 1059 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
f63168e6 1060 have_soa = true;
a6448d95 1061 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
1062 // fixup dots after serializeSOAData/increaseSOARecord
1063 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
f63168e6
CH
1064 }
1065 }
f7bfeb30
CH
1066
1067 // synthesize RRs as needed
1068 DNSResourceRecord autorr;
1d6b70f9 1069 autorr.qname = zonename;
f7bfeb30
CH
1070 autorr.auth = 1;
1071 autorr.ttl = ::arg().asNum("default-ttl");
e2dba705 1072
4de11a54 1073 if (!have_soa && zonekind != DomainInfo::Slave) {
f63168e6 1074 // synthesize a SOA record so the zone "really" exists
1d6b70f9
CH
1075 string soa = (boost::format("%s %s %lu")
1076 % ::arg()["default-soa-name"]
1077 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename).toString() : ::arg()["default-soa-mail"])
1f68b185 1078 % document["serial"].int_value()
1d6b70f9 1079 ).str();
f63168e6 1080 SOAData sd;
1d6b70f9 1081 fillSOAData(soa, sd); // fills out default values for us
f7bfeb30 1082 autorr.qtype = "SOA";
1d6b70f9 1083 autorr.content = serializeSOAData(sd);
f7bfeb30 1084 increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
1085 // fixup dots after serializeSOAData/increaseSOARecord
1086 autorr.content = makeBackendRecordContent(autorr.qtype, autorr.content);
f7bfeb30 1087 new_records.push_back(autorr);
f63168e6
CH
1088 }
1089
1090 // create NS records if nameservers are given
1f68b185
CH
1091 for (auto value : nameservers.array_items()) {
1092 string nameserver = value.string_value();
1093 if (nameserver.empty())
1094 throw ApiException("Nameservers must be non-empty strings");
1095 if (!isCanonical(nameserver))
1096 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
1097 try {
1098 // ensure the name parses
1099 autorr.content = DNSName(nameserver).toStringNoDot();
1100 } catch (...) {
1101 throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
4bdff352 1102 }
1f68b185
CH
1103 autorr.qtype = "NS";
1104 new_records.push_back(autorr);
e2dba705
CH
1105 }
1106
f63168e6 1107 // no going back after this
1d6b70f9
CH
1108 if(!B.createDomain(zonename))
1109 throw ApiException("Creating domain '"+zonename.toString()+"' failed");
f63168e6 1110
1d6b70f9
CH
1111 if(!B.getDomainInfo(zonename, di))
1112 throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");
f63168e6 1113
9440a9f0
CH
1114 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1115 if (!soa_edit_api_kind.empty()) {
1116 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
1117 }
1118
1d6b70f9 1119 di.backend->startTransaction(zonename, di.id);
f63168e6 1120
abb873ee 1121 for(auto rr : new_records) {
f63168e6 1122 rr.domain_id = di.id;
e2dba705
CH
1123 di.backend->feedRecord(rr);
1124 }
1d6b70f9 1125 for(Comment& c : new_comments) {
f63168e6
CH
1126 c.domain_id = di.id;
1127 di.backend->feedComment(c);
1128 }
e2dba705 1129
1d6b70f9 1130 updateDomainSettingsFromDocument(di, zonename, document);
e2dba705 1131
f63168e6
CH
1132 di.backend->commitTransaction();
1133
3fe7c7d6
CH
1134 storeChangedPTRs(B, new_ptrs);
1135
1d6b70f9 1136 fillZone(zonename, resp);
64a36f0d 1137 resp->status = 201;
e2dba705
CH
1138 return;
1139 }
1140
c67bf8c5
CH
1141 if(req->method != "GET")
1142 throw HttpMethodNotAllowedException();
1143
c67bf8c5 1144 vector<DomainInfo> domains;
cea26350 1145 B.getAllDomains(&domains, true); // incl. disabled
c67bf8c5 1146
62a9a74c
CH
1147 Json::array doc;
1148 for(const DomainInfo& di : domains) {
1149 doc.push_back(getZoneInfo(di));
c67bf8c5 1150 }
669822d0 1151 resp->setBody(doc);
c67bf8c5
CH
1152}
1153
05776d2f 1154static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
290a083d 1155 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
05776d2f 1156
d07bf7ff 1157 if(req->method == "PUT" && !::arg().mustDo("api-readonly")) {
7c0ba3d2
CH
1158 // update domain settings
1159 UeberBackend B;
1160 DomainInfo di;
1161 if(!B.getDomainInfo(zonename, di))
290a083d 1162 throw ApiException("Could not find domain '"+zonename.toString()+"'");
7c0ba3d2 1163
1f68b185 1164 updateDomainSettingsFromDocument(di, zonename, req->json());
7c0ba3d2 1165
f0e76cee
CH
1166 resp->body = "";
1167 resp->status = 204; // No Content, but indicate success
7c0ba3d2
CH
1168 return;
1169 }
d07bf7ff 1170 else if(req->method == "DELETE" && !::arg().mustDo("api-readonly")) {
a462a01d
CH
1171 // delete domain
1172 UeberBackend B;
1173 DomainInfo di;
1174 if(!B.getDomainInfo(zonename, di))
290a083d 1175 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a462a01d
CH
1176
1177 if(!di.backend->deleteDomain(zonename))
290a083d 1178 throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");
a462a01d
CH
1179
1180 // empty body on success
1181 resp->body = "";
37663c3b 1182 resp->status = 204; // No Content: declare that the zone is gone now
a462a01d 1183 return;
d07bf7ff 1184 } else if (req->method == "PATCH" && !::arg().mustDo("api-readonly")) {
d708640f 1185 patchZone(req, resp);
6cc98ddf
CH
1186 return;
1187 } else if (req->method == "GET") {
1188 fillZone(zonename, resp);
1189 return;
a462a01d 1190 }
7c0ba3d2 1191
6cc98ddf 1192 throw HttpMethodNotAllowedException();
05776d2f
CH
1193}
1194
a83004d3 1195static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
290a083d 1196 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a83004d3
CH
1197
1198 if(req->method != "GET")
1199 throw HttpMethodNotAllowedException();
1200
1201 ostringstream ss;
1202
1203 UeberBackend B;
1204 DomainInfo di;
1205 if(!B.getDomainInfo(zonename, di))
290a083d 1206 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a83004d3
CH
1207
1208 DNSResourceRecord rr;
1209 SOAData sd;
1210 di.backend->list(zonename, di.id);
1211 while(di.backend->get(rr)) {
1212 if (!rr.qtype.getCode())
1213 continue; // skip empty non-terminals
1214
a83004d3 1215 ss <<
675fa24c 1216 rr.qname.toString() << "\t" <<
a83004d3
CH
1217 rr.ttl << "\t" <<
1218 rr.qtype.getName() << "\t" <<
1d6b70f9 1219 makeApiRecordContent(rr.qtype, rr.content) <<
a83004d3
CH
1220 endl;
1221 }
1222
1223 if (req->accept_json) {
41873e7c 1224 resp->setBody(Json::object { { "zone", ss.str() } });
a83004d3
CH
1225 } else {
1226 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
1227 resp->body = ss.str();
1228 }
1229}
1230
a426cb89 1231static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
290a083d 1232 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89
CH
1233
1234 if(req->method != "PUT")
1235 throw HttpMethodNotAllowedException();
1236
1237 UeberBackend B;
1238 DomainInfo di;
1239 if(!B.getDomainInfo(zonename, di))
290a083d 1240 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a426cb89
CH
1241
1242 if(di.masters.empty())
290a083d 1243 throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");
a426cb89
CH
1244
1245 random_shuffle(di.masters.begin(), di.masters.end());
1246 Communicator.addSuckRequest(zonename, di.masters.front());
692829aa 1247 resp->setSuccessResult("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front());
a426cb89
CH
1248}
1249
1250static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
290a083d 1251 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89
CH
1252
1253 if(req->method != "PUT")
1254 throw HttpMethodNotAllowedException();
1255
1256 UeberBackend B;
1257 DomainInfo di;
1258 if(!B.getDomainInfo(zonename, di))
290a083d 1259 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a426cb89
CH
1260
1261 if(!Communicator.notifyDomain(zonename))
1262 throw ApiException("Failed to add to the queue - see server log");
1263
692829aa 1264 resp->setSuccessResult("Notification queued");
a426cb89
CH
1265}
1266
d1587ceb
CH
1267static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
1268 if (rr.qtype.getCode() == QType::A) {
1269 uint32_t ip;
1270 if (!IpToU32(rr.content, &ip)) {
1271 throw ApiException("PTR: Invalid IP address given");
1272 }
1d6b70f9 1273 ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
d1587ceb
CH
1274 % ((ip >> 24) & 0xff)
1275 % ((ip >> 16) & 0xff)
1276 % ((ip >> 8) & 0xff)
1277 % ((ip ) & 0xff)
1d6b70f9 1278 ).str());
d1587ceb
CH
1279 } else if (rr.qtype.getCode() == QType::AAAA) {
1280 ComboAddress ca(rr.content);
5fb3aa58 1281 char buf[3];
d1587ceb 1282 ostringstream ss;
5fb3aa58
CH
1283 for (int octet = 0; octet < 16; ++octet) {
1284 if (snprintf(buf, sizeof(buf), "%02x", ca.sin6.sin6_addr.s6_addr[octet]) != (sizeof(buf)-1)) {
1285 // this should be impossible: no byte should give more than two digits in hex format
1286 throw PDNSException("Formatting IPv6 address failed");
1287 }
1288 ss << buf[0] << '.' << buf[1] << '.';
d1587ceb 1289 }
5fb3aa58
CH
1290 string tmp = ss.str();
1291 tmp.resize(tmp.size()-1); // remove last dot
1292 // reverse and append arpa domain
1d6b70f9 1293 ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
d1587ceb 1294 } else {
675fa24c 1295 throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
d1587ceb
CH
1296 }
1297
1298 ptr->qtype = "PTR";
1299 ptr->ttl = rr.ttl;
1300 ptr->disabled = rr.disabled;
675fa24c 1301 ptr->content = rr.qname.toString();
d1587ceb
CH
1302}
1303
995473c8
CH
1304static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs) {
1305 for(const DNSResourceRecord& rr : new_ptrs) {
1306 DNSPacket fakePacket;
1307 SOAData sd;
1308 sd.db = (DNSBackend *)-1; // getAuth() cache bypass
1309 fakePacket.qtype = QType::PTR;
1310
1311 if (!B.getAuth(&fakePacket, &sd, rr.qname))
1312 throw ApiException("Could not find domain for PTR '"+rr.qname.toString()+"' requested for '"+rr.content+"' (while saving)");
1313
1314 string soa_edit_api_kind;
1315 string soa_edit_kind;
1316 bool soa_changed = false;
1317 DNSResourceRecord soarr;
1318 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT-API", soa_edit_api_kind);
1319 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT", soa_edit_kind);
1320 if (!soa_edit_api_kind.empty()) {
1321 soarr.qname = sd.qname;
1322 soarr.content = serializeSOAData(sd);
1323 soarr.qtype = "SOA";
1324 soarr.domain_id = sd.domain_id;
1325 soarr.auth = 1;
1326 soarr.ttl = sd.ttl;
1327 increaseSOARecord(soarr, soa_edit_api_kind, soa_edit_kind);
1328 // fixup dots after serializeSOAData/increaseSOARecord
1329 soarr.content = makeBackendRecordContent(soarr.qtype, soarr.content);
1330 soa_changed = true;
1331 }
1332
1333 sd.db->startTransaction(sd.qname);
1334 if (!sd.db->replaceRRSet(sd.domain_id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1335 sd.db->abortTransaction();
1336 throw ApiException("PTR-Hosting backend for "+rr.qname.toString()+"/"+rr.qtype.getName()+" does not support editing records.");
1337 }
1338
1339 if (soa_changed) {
1340 sd.db->replaceRRSet(sd.domain_id, soarr.qname, soarr.qtype, vector<DNSResourceRecord>(1, soarr));
1341 }
1342
1343 sd.db->commitTransaction();
1344 PC.purgeExact(rr.qname);
1345 }
1346}
1347
d708640f 1348static void patchZone(HttpRequest* req, HttpResponse* resp) {
b3905a3d
CH
1349 UeberBackend B;
1350 DomainInfo di;
290a083d 1351 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
d708640f 1352 if (!B.getDomainInfo(zonename, di))
290a083d 1353 throw ApiException("Could not find domain '"+zonename.toString()+"'");
b3905a3d 1354
f63168e6
CH
1355 vector<DNSResourceRecord> new_records;
1356 vector<Comment> new_comments;
d708640f
CH
1357 vector<DNSResourceRecord> new_ptrs;
1358
1f68b185 1359 Json document = req->json();
b3905a3d 1360
1f68b185
CH
1361 auto rrsets = document["rrsets"];
1362 if (!rrsets.is_array())
d708640f 1363 throw ApiException("No rrsets given in update request");
b3905a3d 1364
d708640f 1365 di.backend->startTransaction(zonename);
6cc98ddf 1366
d708640f 1367 try {
d29d5db7 1368 string soa_edit_api_kind;
a6448d95 1369 string soa_edit_kind;
d29d5db7 1370 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
a6448d95 1371 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
d29d5db7
CH
1372 bool soa_edit_done = false;
1373
6754ef71
CH
1374 for (const auto& rrset : rrsets.array_items()) {
1375 string changetype = toUpper(stringFromJson(rrset, "changetype"));
c576d0c5 1376 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
cb9b5901 1377 apiCheckQNameAllowedCharacters(qname.toString());
6754ef71 1378 QType qtype;
d708640f 1379 qtype = stringFromJson(rrset, "type");
6754ef71
CH
1380 if (qtype.getCode() == 0) {
1381 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1382 }
d708640f 1383
d708640f
CH
1384 if (changetype == "DELETE") {
1385 // delete all matching qname/qtype RRs (and, implictly comments).
1386 if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
1387 throw ApiException("Hosting backend does not support editing records.");
6cc98ddf 1388 }
d708640f
CH
1389 }
1390 else if (changetype == "REPLACE") {
1d6b70f9 1391 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
e325f20c 1392 if (!qname.isPartOf(zonename) && qname != zonename)
edda67a2 1393 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
34df6ecc 1394
6754ef71
CH
1395 bool replace_records = rrset["records"].is_array();
1396 bool replace_comments = rrset["comments"].is_array();
f63168e6 1397
6754ef71
CH
1398 if (!replace_records && !replace_comments) {
1399 throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.getName());
1400 }
f63168e6 1401
6754ef71
CH
1402 new_records.clear();
1403 new_comments.clear();
f63168e6 1404
6754ef71
CH
1405 if (replace_records) {
1406 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1407 int ttl = intFromJson(rrset, "ttl");
1408 // new_ptrs is merged.
1409 gatherRecords(rrset, qname, qtype, ttl, new_records, new_ptrs);
1410
1411 for(DNSResourceRecord& rr : new_records) {
1412 rr.domain_id = di.id;
1413 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
1414 soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1415 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
1416 }
d708640f 1417 }
6cc98ddf
CH
1418 }
1419
6754ef71
CH
1420 if (replace_comments) {
1421 gatherComments(rrset, qname, qtype, new_comments);
f63168e6 1422
6754ef71
CH
1423 for(Comment& c : new_comments) {
1424 c.domain_id = di.id;
1425 }
d708640f 1426 }
b3905a3d 1427
d708640f
CH
1428 if (replace_records) {
1429 if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
1430 throw ApiException("Hosting backend does not support editing records.");
1431 }
1432 }
1433 if (replace_comments) {
1434 if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
1435 throw ApiException("Hosting backend does not support editing comments.");
1436 }
1437 }
6cc98ddf 1438 }
d708640f
CH
1439 else
1440 throw ApiException("Changetype not understood");
6cc98ddf 1441 }
d29d5db7
CH
1442
1443 // edit SOA (if needed)
1444 if (!soa_edit_api_kind.empty() && !soa_edit_done) {
1445 SOAData sd;
1446 if (!B.getSOA(zonename, sd))
290a083d 1447 throw ApiException("No SOA found for domain '"+zonename.toString()+"'");
d29d5db7
CH
1448
1449 DNSResourceRecord rr;
1450 rr.qname = zonename;
1451 rr.content = serializeSOAData(sd);
1452 rr.qtype = "SOA";
1453 rr.domain_id = di.id;
1454 rr.auth = 1;
1455 rr.ttl = sd.ttl;
a6448d95 1456 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
1457 // fixup dots after serializeSOAData/increaseSOARecord
1458 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
d29d5db7
CH
1459
1460 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1461 throw ApiException("Hosting backend does not support editing records.");
1462 }
1463 }
1464
d708640f
CH
1465 } catch(...) {
1466 di.backend->abortTransaction();
1467 throw;
1468 }
1469 di.backend->commitTransaction();
b3905a3d 1470
be9d7339 1471 PC.purgeExact(zonename);
d1587ceb 1472
d708640f 1473 // now the PTRs
995473c8 1474 storeChangedPTRs(B, new_ptrs);
b3905a3d 1475
f0e76cee
CH
1476 resp->body = "";
1477 resp->status = 204; // No Content, but indicate success
1478 return;
b3905a3d
CH
1479}
1480
b1902fab
CH
1481static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
1482 if(req->method != "GET")
1483 throw HttpMethodNotAllowedException();
1484
583ea80d 1485 string q = req->getvars["q"];
720ed2bd
AT
1486 string sMax = req->getvars["max"];
1487 int maxEnts = 100;
1488 int ents = 0;
1489
b1902fab
CH
1490 if (q.empty())
1491 throw ApiException("Query q can't be blank");
720ed2bd 1492 if (sMax.empty() == false)
335da0ba 1493 maxEnts = std::stoi(sMax);
720ed2bd
AT
1494 if (maxEnts < 1)
1495 throw ApiException("Maximum entries must be larger than 0");
b1902fab 1496
720ed2bd 1497 SimpleMatch sm(q,true);
b1902fab 1498 UeberBackend B;
b1902fab 1499 vector<DomainInfo> domains;
720ed2bd
AT
1500 vector<DNSResourceRecord> result_rr;
1501 vector<Comment> result_c;
1d6b70f9
CH
1502 map<int,DomainInfo> zoneIdZone;
1503 map<int,DomainInfo>::iterator val;
00963dea 1504 Json::array doc;
b1902fab 1505
720ed2bd 1506 B.getAllDomains(&domains, true);
d2d194a9 1507
720ed2bd 1508 for(const DomainInfo di: domains)
1d6b70f9 1509 {
720ed2bd 1510 if (ents < maxEnts && sm.match(di.zone)) {
00963dea
CH
1511 doc.push_back(Json::object {
1512 { "object_type", "zone" },
1513 { "zone_id", apiZoneNameToId(di.zone) },
1514 { "name", di.zone.toString() }
1515 });
720ed2bd 1516 ents++;
b1902fab 1517 }
1d6b70f9 1518 zoneIdZone[di.id] = di; // populate cache
720ed2bd 1519 }
b1902fab 1520
720ed2bd
AT
1521 if (B.searchRecords(q, maxEnts, result_rr))
1522 {
1523 for(const DNSResourceRecord& rr: result_rr)
1524 {
00963dea
CH
1525 auto object = Json::object {
1526 { "object_type", "record" },
1527 { "name", rr.qname.toString() },
1528 { "type", rr.qtype.getName() },
1529 { "ttl", (double)rr.ttl },
1530 { "disabled", rr.disabled },
1531 { "content", makeApiRecordContent(rr.qtype, rr.content) }
1532 };
720ed2bd 1533 if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
00963dea
CH
1534 object["zone_id"] = apiZoneNameToId(val->second.zone);
1535 object["zone"] = val->second.zone.toString();
720ed2bd 1536 }
00963dea 1537 doc.push_back(object);
b1902fab 1538 }
720ed2bd 1539 }
b1902fab 1540
720ed2bd
AT
1541 if (B.searchComments(q, maxEnts, result_c))
1542 {
1543 for(const Comment &c: result_c)
1544 {
00963dea
CH
1545 auto object = Json::object {
1546 { "object_type", "comment" },
25dcc05f 1547 { "name", c.qname.toString() },
00963dea
CH
1548 { "content", c.content }
1549 };
720ed2bd 1550 if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
00963dea
CH
1551 object["zone_id"] = apiZoneNameToId(val->second.zone);
1552 object["zone"] = val->second.zone.toString();
720ed2bd 1553 }
00963dea 1554 doc.push_back(object);
b1902fab
CH
1555 }
1556 }
4bd3d119 1557
b1902fab
CH
1558 resp->setBody(doc);
1559}
1560
c0f6a1da 1561void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
a426cb89
CH
1562 if(req->method != "PUT")
1563 throw HttpMethodNotAllowedException();
80d59cd1 1564
c0f6a1da
CH
1565 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
1566
c0f6a1da 1567 int count = PC.purgeExact(canon);
f682752a
CH
1568 resp->setBody(Json::object {
1569 { "count", count },
1570 { "result", "Flushed cache." }
1571 });
ddc84d12
CH
1572}
1573
dea47634 1574void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
c67bf8c5 1575{
80d59cd1
CH
1576 resp->headers["Cache-Control"] = "max-age=86400";
1577 resp->headers["Content-Type"] = "text/css";
c67bf8c5 1578
1071abdd 1579 ostringstream ret;
1071abdd
CH
1580 ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
1581 ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
1582 ret<<"a { color: #0959c2; }"<<endl;
1583 ret<<"a:hover { color: #3B8EC8; }"<<endl;
1584 ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
1585 ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
1586 ret<<".row:after { clear: both; }"<<endl;
1587 ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
1588 ret<<".all { width: 100%; }"<<endl;
1589 ret<<".headl { width: 60%; }"<<endl;
1590 ret<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
1591 ret<<"background-image: url(data:image/png;base64,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);";
1592 ret<<" width: 154px; height: 20px; }"<<endl;
1593 ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
1594 ret<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
1595 ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
1596 ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
1597 ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
1598 ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
1599 ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
1600 ret<<"table.data tr:hover { background: white; }"<<endl;
1601 ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
1602 ret<<".resetring {float: right; }"<<endl;
1603 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;
1604 ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
1605 ret<<".resizering {float: right;}"<<endl;
80d59cd1 1606 resp->body = ret.str();
c146576d 1607 resp->status = 200;
1071abdd
CH
1608}
1609
dea47634 1610void AuthWebServer::webThread()
12c86877
BH
1611{
1612 try {
479e0976 1613 if(::arg().mustDo("api")) {
c0f6a1da 1614 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
46d06a12 1615 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
46d06a12
PL
1616 d_ws->registerApiHandler("/api/v1/servers/localhost/search-log", &apiServerSearchLog);
1617 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
1618 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
1619 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
1620 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
1621 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
1622 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
24e11043
CJ
1623 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind);
1624 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata);
46d06a12
PL
1625 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
1626 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
1627 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
1628 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
1629 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
9e6d2033 1630 d_ws->registerApiHandler("/api", &apiDiscovery);
c67bf8c5 1631 }
536ab56f
CH
1632 if (::arg().mustDo("webserver")) {
1633 d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
1634 d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
1635 }
96d299db 1636 d_ws->go();
12c86877
BH
1637 }
1638 catch(...) {
dea47634 1639 L<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
12c86877
BH
1640 exit(1);
1641 }
1642}