]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/ws-auth.cc
Merge pull request #4985 from rgacogne/write-single-syscall
[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 276 }
bea69e32 277 else if(S.ringExists(req->getvars["ring"]))
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
ce846be6 305static Json::object getZoneInfo(const DomainInfo& di, DNSSECKeeper *dk) {
290a083d 306 string zoneId = apiZoneNameToId(di.zone);
62a9a74c
CH
307 return Json::object {
308 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
309 { "id", zoneId },
16e25450 310 { "url", "/api/v1/servers/localhost/zones/" + zoneId },
62a9a74c
CH
311 { "name", di.zone.toString() },
312 { "kind", di.getKindString() },
ce846be6 313 { "dnssec", dk->isSecuredZone(di.zone) },
62a9a74c
CH
314 { "account", di.account },
315 { "masters", di.masters },
316 { "serial", (double)di.serial },
317 { "notified_serial", (double)di.notified_serial },
318 { "last_check", (double)di.last_check }
319 };
c04b5870
CH
320}
321
290a083d 322static void fillZone(const DNSName& zonename, HttpResponse* resp) {
1abb81f4 323 UeberBackend B;
1abb81f4 324 DomainInfo di;
73301d73 325 if(!B.getDomainInfo(zonename, di))
290a083d 326 throw ApiException("Could not find domain '"+zonename.toString()+"'");
1abb81f4 327
adef67eb 328 DNSSECKeeper dk(&B);
ce846be6 329 Json::object doc = getZoneInfo(di, &dk);
62a9a74c 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()) {
b43f5903 384 if (cit == comments.end() || (rit != records.end() && (cit->qname.toString() <= rit->qname.toString() || cit->qtype < rit->qtype || 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 474 // verify that there's a zone for the PTR
27c0050c 475 DNSPacket fakePacket(false);
1f68b185
CH
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.
955cbfd0 773 * Case 2: the backend returns false on removal. An error occurred.
60b0a236 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>
1ec08a2b 792 * "algo" : "key generation algorithm "name|number" as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
997cab68
BZ
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
955cbfd0 907 * Case 3: the backend returns false on de/activation. An error occurred.
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;
33e6c3e9 1029 bool have_zone_ns = false;
f63168e6
CH
1030 vector<DNSResourceRecord> new_records;
1031 vector<Comment> new_comments;
1032 vector<DNSResourceRecord> new_ptrs;
0f0e73fe 1033
6754ef71
CH
1034 if (rrsets.is_array()) {
1035 for (const auto& rrset : rrsets.array_items()) {
1036 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
1037 apiCheckQNameAllowedCharacters(qname.toString());
1038 QType qtype;
1039 qtype = stringFromJson(rrset, "type");
1040 if (qtype.getCode() == 0) {
1041 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1042 }
1043 if (rrset["records"].is_array()) {
1044 int ttl = intFromJson(rrset, "ttl");
1045 gatherRecords(rrset, qname, qtype, ttl, new_records, new_ptrs);
1046 }
1047 if (rrset["comments"].is_array()) {
1048 gatherComments(rrset, qname, qtype, new_comments);
1049 }
1050 }
0f0e73fe 1051 } else if (zonestring != "") {
1f68b185 1052 gatherRecordsFromZone(zonestring, new_records, zonename);
0f0e73fe
MS
1053 }
1054
1f68b185 1055 for(auto& rr : new_records) {
1d6b70f9 1056 if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
561434a6 1057 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": Name is out of zone");
cb9b5901 1058 apiCheckQNameAllowedCharacters(rr.qname.toString());
f63168e6 1059
1d6b70f9 1060 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
f63168e6 1061 have_soa = true;
a6448d95 1062 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
1063 // fixup dots after serializeSOAData/increaseSOARecord
1064 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
f63168e6 1065 }
33e6c3e9
CH
1066 if (rr.qtype.getCode() == QType::NS && rr.qname==zonename) {
1067 have_zone_ns = true;
1068 }
f63168e6 1069 }
f7bfeb30
CH
1070
1071 // synthesize RRs as needed
1072 DNSResourceRecord autorr;
1d6b70f9 1073 autorr.qname = zonename;
f7bfeb30
CH
1074 autorr.auth = 1;
1075 autorr.ttl = ::arg().asNum("default-ttl");
e2dba705 1076
4de11a54 1077 if (!have_soa && zonekind != DomainInfo::Slave) {
f63168e6 1078 // synthesize a SOA record so the zone "really" exists
1d6b70f9
CH
1079 string soa = (boost::format("%s %s %lu")
1080 % ::arg()["default-soa-name"]
1081 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename).toString() : ::arg()["default-soa-mail"])
1f68b185 1082 % document["serial"].int_value()
1d6b70f9 1083 ).str();
f63168e6 1084 SOAData sd;
1d6b70f9 1085 fillSOAData(soa, sd); // fills out default values for us
f7bfeb30 1086 autorr.qtype = "SOA";
1d6b70f9 1087 autorr.content = serializeSOAData(sd);
f7bfeb30 1088 increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
1089 // fixup dots after serializeSOAData/increaseSOARecord
1090 autorr.content = makeBackendRecordContent(autorr.qtype, autorr.content);
f7bfeb30 1091 new_records.push_back(autorr);
f63168e6
CH
1092 }
1093
1094 // create NS records if nameservers are given
1f68b185
CH
1095 for (auto value : nameservers.array_items()) {
1096 string nameserver = value.string_value();
1097 if (nameserver.empty())
1098 throw ApiException("Nameservers must be non-empty strings");
1099 if (!isCanonical(nameserver))
1100 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
1101 try {
1102 // ensure the name parses
8f955653 1103 autorr.content = DNSName(nameserver).toStringRootDot();
1f68b185
CH
1104 } catch (...) {
1105 throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
4bdff352 1106 }
1f68b185
CH
1107 autorr.qtype = "NS";
1108 new_records.push_back(autorr);
33e6c3e9
CH
1109 if (have_zone_ns) {
1110 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1111 }
e2dba705
CH
1112 }
1113
f63168e6 1114 // no going back after this
1d6b70f9
CH
1115 if(!B.createDomain(zonename))
1116 throw ApiException("Creating domain '"+zonename.toString()+"' failed");
f63168e6 1117
1d6b70f9
CH
1118 if(!B.getDomainInfo(zonename, di))
1119 throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");
f63168e6 1120
9440a9f0
CH
1121 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1122 if (!soa_edit_api_kind.empty()) {
1123 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
1124 }
1125
1d6b70f9 1126 di.backend->startTransaction(zonename, di.id);
f63168e6 1127
abb873ee 1128 for(auto rr : new_records) {
f63168e6 1129 rr.domain_id = di.id;
e2dba705
CH
1130 di.backend->feedRecord(rr);
1131 }
1d6b70f9 1132 for(Comment& c : new_comments) {
f63168e6
CH
1133 c.domain_id = di.id;
1134 di.backend->feedComment(c);
1135 }
e2dba705 1136
1d6b70f9 1137 updateDomainSettingsFromDocument(di, zonename, document);
e2dba705 1138
f63168e6
CH
1139 di.backend->commitTransaction();
1140
3fe7c7d6
CH
1141 storeChangedPTRs(B, new_ptrs);
1142
1d6b70f9 1143 fillZone(zonename, resp);
64a36f0d 1144 resp->status = 201;
e2dba705
CH
1145 return;
1146 }
1147
c67bf8c5
CH
1148 if(req->method != "GET")
1149 throw HttpMethodNotAllowedException();
1150
c67bf8c5 1151 vector<DomainInfo> domains;
cea26350 1152 B.getAllDomains(&domains, true); // incl. disabled
c67bf8c5 1153
62a9a74c
CH
1154 Json::array doc;
1155 for(const DomainInfo& di : domains) {
ce846be6 1156 doc.push_back(getZoneInfo(di, &dk));
c67bf8c5 1157 }
669822d0 1158 resp->setBody(doc);
c67bf8c5
CH
1159}
1160
05776d2f 1161static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
290a083d 1162 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
05776d2f 1163
d07bf7ff 1164 if(req->method == "PUT" && !::arg().mustDo("api-readonly")) {
7c0ba3d2
CH
1165 // update domain settings
1166 UeberBackend B;
1167 DomainInfo di;
1168 if(!B.getDomainInfo(zonename, di))
290a083d 1169 throw ApiException("Could not find domain '"+zonename.toString()+"'");
7c0ba3d2 1170
1f68b185 1171 updateDomainSettingsFromDocument(di, zonename, req->json());
7c0ba3d2 1172
f0e76cee
CH
1173 resp->body = "";
1174 resp->status = 204; // No Content, but indicate success
7c0ba3d2
CH
1175 return;
1176 }
d07bf7ff 1177 else if(req->method == "DELETE" && !::arg().mustDo("api-readonly")) {
a462a01d
CH
1178 // delete domain
1179 UeberBackend B;
1180 DomainInfo di;
1181 if(!B.getDomainInfo(zonename, di))
290a083d 1182 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a462a01d
CH
1183
1184 if(!di.backend->deleteDomain(zonename))
290a083d 1185 throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");
a462a01d
CH
1186
1187 // empty body on success
1188 resp->body = "";
37663c3b 1189 resp->status = 204; // No Content: declare that the zone is gone now
a462a01d 1190 return;
d07bf7ff 1191 } else if (req->method == "PATCH" && !::arg().mustDo("api-readonly")) {
d708640f 1192 patchZone(req, resp);
6cc98ddf
CH
1193 return;
1194 } else if (req->method == "GET") {
1195 fillZone(zonename, resp);
1196 return;
a462a01d 1197 }
7c0ba3d2 1198
6cc98ddf 1199 throw HttpMethodNotAllowedException();
05776d2f
CH
1200}
1201
a83004d3 1202static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
290a083d 1203 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a83004d3
CH
1204
1205 if(req->method != "GET")
1206 throw HttpMethodNotAllowedException();
1207
1208 ostringstream ss;
1209
1210 UeberBackend B;
1211 DomainInfo di;
1212 if(!B.getDomainInfo(zonename, di))
290a083d 1213 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a83004d3
CH
1214
1215 DNSResourceRecord rr;
1216 SOAData sd;
1217 di.backend->list(zonename, di.id);
1218 while(di.backend->get(rr)) {
1219 if (!rr.qtype.getCode())
1220 continue; // skip empty non-terminals
1221
a83004d3 1222 ss <<
675fa24c 1223 rr.qname.toString() << "\t" <<
a83004d3
CH
1224 rr.ttl << "\t" <<
1225 rr.qtype.getName() << "\t" <<
1d6b70f9 1226 makeApiRecordContent(rr.qtype, rr.content) <<
a83004d3
CH
1227 endl;
1228 }
1229
1230 if (req->accept_json) {
41873e7c 1231 resp->setBody(Json::object { { "zone", ss.str() } });
a83004d3
CH
1232 } else {
1233 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
1234 resp->body = ss.str();
1235 }
1236}
1237
a426cb89 1238static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
290a083d 1239 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89
CH
1240
1241 if(req->method != "PUT")
1242 throw HttpMethodNotAllowedException();
1243
1244 UeberBackend B;
1245 DomainInfo di;
1246 if(!B.getDomainInfo(zonename, di))
290a083d 1247 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a426cb89
CH
1248
1249 if(di.masters.empty())
290a083d 1250 throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");
a426cb89
CH
1251
1252 random_shuffle(di.masters.begin(), di.masters.end());
1253 Communicator.addSuckRequest(zonename, di.masters.front());
692829aa 1254 resp->setSuccessResult("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front());
a426cb89
CH
1255}
1256
1257static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
290a083d 1258 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89
CH
1259
1260 if(req->method != "PUT")
1261 throw HttpMethodNotAllowedException();
1262
1263 UeberBackend B;
1264 DomainInfo di;
1265 if(!B.getDomainInfo(zonename, di))
290a083d 1266 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a426cb89
CH
1267
1268 if(!Communicator.notifyDomain(zonename))
1269 throw ApiException("Failed to add to the queue - see server log");
1270
692829aa 1271 resp->setSuccessResult("Notification queued");
a426cb89
CH
1272}
1273
d1587ceb
CH
1274static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
1275 if (rr.qtype.getCode() == QType::A) {
1276 uint32_t ip;
1277 if (!IpToU32(rr.content, &ip)) {
1278 throw ApiException("PTR: Invalid IP address given");
1279 }
1d6b70f9 1280 ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
d1587ceb
CH
1281 % ((ip >> 24) & 0xff)
1282 % ((ip >> 16) & 0xff)
1283 % ((ip >> 8) & 0xff)
1284 % ((ip ) & 0xff)
1d6b70f9 1285 ).str());
d1587ceb
CH
1286 } else if (rr.qtype.getCode() == QType::AAAA) {
1287 ComboAddress ca(rr.content);
5fb3aa58 1288 char buf[3];
d1587ceb 1289 ostringstream ss;
5fb3aa58
CH
1290 for (int octet = 0; octet < 16; ++octet) {
1291 if (snprintf(buf, sizeof(buf), "%02x", ca.sin6.sin6_addr.s6_addr[octet]) != (sizeof(buf)-1)) {
1292 // this should be impossible: no byte should give more than two digits in hex format
1293 throw PDNSException("Formatting IPv6 address failed");
1294 }
1295 ss << buf[0] << '.' << buf[1] << '.';
d1587ceb 1296 }
5fb3aa58
CH
1297 string tmp = ss.str();
1298 tmp.resize(tmp.size()-1); // remove last dot
1299 // reverse and append arpa domain
1d6b70f9 1300 ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
d1587ceb 1301 } else {
675fa24c 1302 throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
d1587ceb
CH
1303 }
1304
1305 ptr->qtype = "PTR";
1306 ptr->ttl = rr.ttl;
1307 ptr->disabled = rr.disabled;
8f955653 1308 ptr->content = rr.qname.toStringRootDot();
d1587ceb
CH
1309}
1310
995473c8
CH
1311static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs) {
1312 for(const DNSResourceRecord& rr : new_ptrs) {
27c0050c 1313 DNSPacket fakePacket(false);
995473c8
CH
1314 SOAData sd;
1315 sd.db = (DNSBackend *)-1; // getAuth() cache bypass
1316 fakePacket.qtype = QType::PTR;
1317
1318 if (!B.getAuth(&fakePacket, &sd, rr.qname))
1319 throw ApiException("Could not find domain for PTR '"+rr.qname.toString()+"' requested for '"+rr.content+"' (while saving)");
1320
1321 string soa_edit_api_kind;
1322 string soa_edit_kind;
1323 bool soa_changed = false;
1324 DNSResourceRecord soarr;
1325 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT-API", soa_edit_api_kind);
1326 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT", soa_edit_kind);
1327 if (!soa_edit_api_kind.empty()) {
1328 soarr.qname = sd.qname;
1329 soarr.content = serializeSOAData(sd);
1330 soarr.qtype = "SOA";
1331 soarr.domain_id = sd.domain_id;
1332 soarr.auth = 1;
1333 soarr.ttl = sd.ttl;
1334 increaseSOARecord(soarr, soa_edit_api_kind, soa_edit_kind);
1335 // fixup dots after serializeSOAData/increaseSOARecord
1336 soarr.content = makeBackendRecordContent(soarr.qtype, soarr.content);
1337 soa_changed = true;
1338 }
1339
1340 sd.db->startTransaction(sd.qname);
1341 if (!sd.db->replaceRRSet(sd.domain_id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1342 sd.db->abortTransaction();
1343 throw ApiException("PTR-Hosting backend for "+rr.qname.toString()+"/"+rr.qtype.getName()+" does not support editing records.");
1344 }
1345
1346 if (soa_changed) {
1347 sd.db->replaceRRSet(sd.domain_id, soarr.qname, soarr.qtype, vector<DNSResourceRecord>(1, soarr));
1348 }
1349
1350 sd.db->commitTransaction();
1351 PC.purgeExact(rr.qname);
1352 }
1353}
1354
d708640f 1355static void patchZone(HttpRequest* req, HttpResponse* resp) {
b3905a3d
CH
1356 UeberBackend B;
1357 DomainInfo di;
290a083d 1358 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
d708640f 1359 if (!B.getDomainInfo(zonename, di))
290a083d 1360 throw ApiException("Could not find domain '"+zonename.toString()+"'");
b3905a3d 1361
f63168e6
CH
1362 vector<DNSResourceRecord> new_records;
1363 vector<Comment> new_comments;
d708640f
CH
1364 vector<DNSResourceRecord> new_ptrs;
1365
1f68b185 1366 Json document = req->json();
b3905a3d 1367
1f68b185
CH
1368 auto rrsets = document["rrsets"];
1369 if (!rrsets.is_array())
d708640f 1370 throw ApiException("No rrsets given in update request");
b3905a3d 1371
d708640f 1372 di.backend->startTransaction(zonename);
6cc98ddf 1373
d708640f 1374 try {
d29d5db7 1375 string soa_edit_api_kind;
a6448d95 1376 string soa_edit_kind;
d29d5db7 1377 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
a6448d95 1378 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
d29d5db7
CH
1379 bool soa_edit_done = false;
1380
6754ef71
CH
1381 for (const auto& rrset : rrsets.array_items()) {
1382 string changetype = toUpper(stringFromJson(rrset, "changetype"));
c576d0c5 1383 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
cb9b5901 1384 apiCheckQNameAllowedCharacters(qname.toString());
6754ef71 1385 QType qtype;
d708640f 1386 qtype = stringFromJson(rrset, "type");
6754ef71
CH
1387 if (qtype.getCode() == 0) {
1388 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1389 }
d708640f 1390
d708640f 1391 if (changetype == "DELETE") {
b7f21ab1 1392 // delete all matching qname/qtype RRs (and, implicitly comments).
d708640f
CH
1393 if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
1394 throw ApiException("Hosting backend does not support editing records.");
6cc98ddf 1395 }
d708640f
CH
1396 }
1397 else if (changetype == "REPLACE") {
1d6b70f9 1398 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
e325f20c 1399 if (!qname.isPartOf(zonename) && qname != zonename)
edda67a2 1400 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
34df6ecc 1401
6754ef71
CH
1402 bool replace_records = rrset["records"].is_array();
1403 bool replace_comments = rrset["comments"].is_array();
f63168e6 1404
6754ef71
CH
1405 if (!replace_records && !replace_comments) {
1406 throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.getName());
1407 }
f63168e6 1408
6754ef71
CH
1409 new_records.clear();
1410 new_comments.clear();
f63168e6 1411
6754ef71
CH
1412 if (replace_records) {
1413 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1414 int ttl = intFromJson(rrset, "ttl");
1415 // new_ptrs is merged.
1416 gatherRecords(rrset, qname, qtype, ttl, new_records, new_ptrs);
1417
1418 for(DNSResourceRecord& rr : new_records) {
1419 rr.domain_id = di.id;
1420 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
1421 soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1422 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
1423 }
d708640f 1424 }
6cc98ddf
CH
1425 }
1426
6754ef71
CH
1427 if (replace_comments) {
1428 gatherComments(rrset, qname, qtype, new_comments);
f63168e6 1429
6754ef71
CH
1430 for(Comment& c : new_comments) {
1431 c.domain_id = di.id;
1432 }
d708640f 1433 }
b3905a3d 1434
d708640f
CH
1435 if (replace_records) {
1436 if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
1437 throw ApiException("Hosting backend does not support editing records.");
1438 }
1439 }
1440 if (replace_comments) {
1441 if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
1442 throw ApiException("Hosting backend does not support editing comments.");
1443 }
1444 }
6cc98ddf 1445 }
d708640f
CH
1446 else
1447 throw ApiException("Changetype not understood");
6cc98ddf 1448 }
d29d5db7
CH
1449
1450 // edit SOA (if needed)
1451 if (!soa_edit_api_kind.empty() && !soa_edit_done) {
1452 SOAData sd;
1453 if (!B.getSOA(zonename, sd))
290a083d 1454 throw ApiException("No SOA found for domain '"+zonename.toString()+"'");
d29d5db7
CH
1455
1456 DNSResourceRecord rr;
1457 rr.qname = zonename;
1458 rr.content = serializeSOAData(sd);
1459 rr.qtype = "SOA";
1460 rr.domain_id = di.id;
1461 rr.auth = 1;
1462 rr.ttl = sd.ttl;
a6448d95 1463 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
1464 // fixup dots after serializeSOAData/increaseSOARecord
1465 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
d29d5db7
CH
1466
1467 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1468 throw ApiException("Hosting backend does not support editing records.");
1469 }
1470 }
1471
d708640f
CH
1472 } catch(...) {
1473 di.backend->abortTransaction();
1474 throw;
1475 }
1476 di.backend->commitTransaction();
b3905a3d 1477
be9d7339 1478 PC.purgeExact(zonename);
d1587ceb 1479
d708640f 1480 // now the PTRs
995473c8 1481 storeChangedPTRs(B, new_ptrs);
b3905a3d 1482
f0e76cee
CH
1483 resp->body = "";
1484 resp->status = 204; // No Content, but indicate success
1485 return;
b3905a3d
CH
1486}
1487
b1902fab
CH
1488static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
1489 if(req->method != "GET")
1490 throw HttpMethodNotAllowedException();
1491
583ea80d 1492 string q = req->getvars["q"];
720ed2bd
AT
1493 string sMax = req->getvars["max"];
1494 int maxEnts = 100;
1495 int ents = 0;
1496
b1902fab
CH
1497 if (q.empty())
1498 throw ApiException("Query q can't be blank");
606c8752 1499 if (!sMax.empty())
335da0ba 1500 maxEnts = std::stoi(sMax);
720ed2bd
AT
1501 if (maxEnts < 1)
1502 throw ApiException("Maximum entries must be larger than 0");
b1902fab 1503
720ed2bd 1504 SimpleMatch sm(q,true);
b1902fab 1505 UeberBackend B;
b1902fab 1506 vector<DomainInfo> domains;
720ed2bd
AT
1507 vector<DNSResourceRecord> result_rr;
1508 vector<Comment> result_c;
1d6b70f9
CH
1509 map<int,DomainInfo> zoneIdZone;
1510 map<int,DomainInfo>::iterator val;
00963dea 1511 Json::array doc;
b1902fab 1512
720ed2bd 1513 B.getAllDomains(&domains, true);
d2d194a9 1514
720ed2bd 1515 for(const DomainInfo di: domains)
1d6b70f9 1516 {
720ed2bd 1517 if (ents < maxEnts && sm.match(di.zone)) {
00963dea
CH
1518 doc.push_back(Json::object {
1519 { "object_type", "zone" },
1520 { "zone_id", apiZoneNameToId(di.zone) },
1521 { "name", di.zone.toString() }
1522 });
720ed2bd 1523 ents++;
b1902fab 1524 }
1d6b70f9 1525 zoneIdZone[di.id] = di; // populate cache
720ed2bd 1526 }
b1902fab 1527
720ed2bd
AT
1528 if (B.searchRecords(q, maxEnts, result_rr))
1529 {
1530 for(const DNSResourceRecord& rr: result_rr)
1531 {
7cbc5255
CH
1532 if (!rr.qtype.getCode())
1533 continue; // skip empty non-terminals
1534
00963dea
CH
1535 auto object = Json::object {
1536 { "object_type", "record" },
1537 { "name", rr.qname.toString() },
1538 { "type", rr.qtype.getName() },
1539 { "ttl", (double)rr.ttl },
1540 { "disabled", rr.disabled },
1541 { "content", makeApiRecordContent(rr.qtype, rr.content) }
1542 };
720ed2bd 1543 if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
00963dea
CH
1544 object["zone_id"] = apiZoneNameToId(val->second.zone);
1545 object["zone"] = val->second.zone.toString();
720ed2bd 1546 }
00963dea 1547 doc.push_back(object);
b1902fab 1548 }
720ed2bd 1549 }
b1902fab 1550
720ed2bd
AT
1551 if (B.searchComments(q, maxEnts, result_c))
1552 {
1553 for(const Comment &c: result_c)
1554 {
00963dea
CH
1555 auto object = Json::object {
1556 { "object_type", "comment" },
25dcc05f 1557 { "name", c.qname.toString() },
00963dea
CH
1558 { "content", c.content }
1559 };
720ed2bd 1560 if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
00963dea
CH
1561 object["zone_id"] = apiZoneNameToId(val->second.zone);
1562 object["zone"] = val->second.zone.toString();
720ed2bd 1563 }
00963dea 1564 doc.push_back(object);
b1902fab
CH
1565 }
1566 }
4bd3d119 1567
b1902fab
CH
1568 resp->setBody(doc);
1569}
1570
c0f6a1da 1571void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
a426cb89
CH
1572 if(req->method != "PUT")
1573 throw HttpMethodNotAllowedException();
80d59cd1 1574
c0f6a1da
CH
1575 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
1576
c0f6a1da 1577 int count = PC.purgeExact(canon);
f682752a
CH
1578 resp->setBody(Json::object {
1579 { "count", count },
1580 { "result", "Flushed cache." }
1581 });
ddc84d12
CH
1582}
1583
dea47634 1584void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
c67bf8c5 1585{
80d59cd1
CH
1586 resp->headers["Cache-Control"] = "max-age=86400";
1587 resp->headers["Content-Type"] = "text/css";
c67bf8c5 1588
1071abdd 1589 ostringstream ret;
1071abdd
CH
1590 ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
1591 ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
1592 ret<<"a { color: #0959c2; }"<<endl;
1593 ret<<"a:hover { color: #3B8EC8; }"<<endl;
1594 ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
1595 ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
1596 ret<<".row:after { clear: both; }"<<endl;
1597 ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
1598 ret<<".all { width: 100%; }"<<endl;
1599 ret<<".headl { width: 60%; }"<<endl;
1600 ret<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
1601 ret<<"background-image: url(data:image/png;base64,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);";
1602 ret<<" width: 154px; height: 20px; }"<<endl;
1603 ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
1604 ret<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
1605 ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
1606 ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
1607 ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
1608 ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
1609 ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
1610 ret<<"table.data tr:hover { background: white; }"<<endl;
1611 ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
1612 ret<<".resetring {float: right; }"<<endl;
1613 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;
1614 ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
1615 ret<<".resizering {float: right;}"<<endl;
80d59cd1 1616 resp->body = ret.str();
c146576d 1617 resp->status = 200;
1071abdd
CH
1618}
1619
dea47634 1620void AuthWebServer::webThread()
12c86877
BH
1621{
1622 try {
479e0976 1623 if(::arg().mustDo("api")) {
c0f6a1da 1624 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
46d06a12 1625 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
46d06a12
PL
1626 d_ws->registerApiHandler("/api/v1/servers/localhost/search-log", &apiServerSearchLog);
1627 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
1628 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
1629 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
1630 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
1631 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
1632 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
24e11043
CJ
1633 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind);
1634 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata);
46d06a12
PL
1635 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
1636 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
1637 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
1638 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
1639 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
9e6d2033 1640 d_ws->registerApiHandler("/api", &apiDiscovery);
c67bf8c5 1641 }
536ab56f
CH
1642 if (::arg().mustDo("webserver")) {
1643 d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
1644 d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
1645 }
96d299db 1646 d_ws->go();
12c86877
BH
1647 }
1648 catch(...) {
dea47634 1649 L<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
12c86877
BH
1650 exit(1);
1651 }
1652}