]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/ws-auth.cc
Merge pull request #8096 from mind04/pdns-notify-db-queries
[thirdparty/pdns.git] / pdns / ws-auth.cc
CommitLineData
12c86877 1/*
6edbf68a
PL
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
870a0fe4
AT
22#ifdef HAVE_CONFIG_H
23#include "config.h"
24#endif
9054d8a4 25#include "utility.hh"
d267d1bf 26#include "dynlistener.hh"
2470b36e 27#include "ws-auth.hh"
e611a06c 28#include "json.hh"
12c86877
BH
29#include "webserver.hh"
30#include "logger.hh"
31#include "statbag.hh"
32#include "misc.hh"
6f16f97c 33#include "base64.hh"
12c86877
BH
34#include "arguments.hh"
35#include "dns.hh"
6cc98ddf 36#include "comment.hh"
e611a06c 37#include "ueberbackend.hh"
dcc65f25 38#include <boost/format.hpp>
fa8fd4d2 39
9ac4a7c6 40#include "namespaces.hh"
6ec5e728 41#include "ws-api.hh"
ba1a571d 42#include "version.hh"
d29d5db7 43#include "dnsseckeeper.hh"
3c3c006b 44#include <iomanip>
0f0e73fe 45#include "zoneparser-tng.hh"
a426cb89 46#include "common_startup.hh"
bf269e28 47#include "auth-caches.hh"
519f5484 48#include "threadname.hh"
ac5298aa 49#include "tsigutils.hh"
8537b9f0 50
24afabad 51using json11::Json;
12c86877
BH
52
53extern StatBag S;
54
311cb9a2 55static void patchZone(UeberBackend& B, HttpRequest* req, HttpResponse* resp);
995473c8 56static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs);
f63168e6
CH
57static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr);
58
646bcd7d
CH
59// QTypes that MUST NOT have multiple records of the same type in a given RRset.
60static const std::set<uint16_t> onlyOneEntryTypes = { QType::CNAME, QType::DNAME, QType::SOA };
61// QTypes that MUST NOT be used with any other QType on the same name.
5e73d266 62static const std::set<uint16_t> exclusiveEntryTypes = { QType::CNAME };
646bcd7d 63
8a70e507 64AuthWebServer::AuthWebServer() :
eace2c24
RG
65 d_tid(0),
66 d_start(time(nullptr)),
8a70e507
CHB
67 d_min10(0),
68 d_min5(0),
eace2c24 69 d_min1(0)
12c86877 70{
536ab56f 71 if(arg().mustDo("webserver") || arg().mustDo("api")) {
bbef8f04 72 d_ws = new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
29997a3c 73 d_ws->setApiKey(arg()["api-key"]);
0c3b088c 74 d_ws->setPassword(arg()["webserver-password"]);
8ca656a8 75 d_ws->setLogLevel(arg()["webserver-loglevel"]);
0010aefa
PL
76
77 NetmaskGroup acl;
78 acl.toMasks(::arg()["webserver-allow-from"]);
79 d_ws->setACL(acl);
80
214b034e
PD
81 d_ws->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));
82
825fa717
CH
83 d_ws->bind();
84 }
12c86877
BH
85}
86
dea47634 87void AuthWebServer::go()
12c86877 88{
536ab56f
CH
89 S.doRings();
90 pthread_create(&d_tid, 0, webThreadHelper, this);
91 pthread_create(&d_tid, 0, statThreadHelper, this);
12c86877
BH
92}
93
dea47634 94void AuthWebServer::statThread()
12c86877
BH
95{
96 try {
519f5484 97 setThreadName("pdns/statHelper");
12c86877
BH
98 for(;;) {
99 d_queries.submit(S.read("udp-queries"));
100 d_cachehits.submit(S.read("packetcache-hit"));
101 d_cachemisses.submit(S.read("packetcache-miss"));
102 d_qcachehits.submit(S.read("query-cache-hit"));
103 d_qcachemisses.submit(S.read("query-cache-miss"));
104 Utility::sleep(1);
105 }
106 }
107 catch(...) {
e6a9dde5 108 g_log<<Logger::Error<<"Webserver statThread caught an exception, dying"<<endl;
5bd2ea7b 109 _exit(1);
12c86877
BH
110 }
111}
112
dea47634 113void *AuthWebServer::statThreadHelper(void *p)
12c86877 114{
dea47634
CH
115 AuthWebServer *self=static_cast<AuthWebServer *>(p);
116 self->statThread();
12c86877
BH
117 return 0; // never reached
118}
119
dea47634 120void *AuthWebServer::webThreadHelper(void *p)
12c86877 121{
dea47634
CH
122 AuthWebServer *self=static_cast<AuthWebServer *>(p);
123 self->webThread();
12c86877
BH
124 return 0; // never reached
125}
126
9f3fdaa0
CH
127static string htmlescape(const string &s) {
128 string result;
129 for(string::const_iterator it=s.begin(); it!=s.end(); ++it) {
130 switch (*it) {
131 case '&':
c86a96f9 132 result += "&amp;";
9f3fdaa0
CH
133 break;
134 case '<':
135 result += "&lt;";
136 break;
137 case '>':
138 result += "&gt;";
139 break;
c7f59d62
PL
140 case '"':
141 result += "&quot;";
142 break;
9f3fdaa0
CH
143 default:
144 result += *it;
145 }
146 }
147 return result;
148}
149
12c86877
BH
150void printtable(ostringstream &ret, const string &ringname, const string &title, int limit=10)
151{
152 int tot=0;
153 int entries=0;
101b5d5d 154 vector<pair <string,unsigned int> >ring=S.getRing(ringname);
12c86877 155
1071abdd 156 for(vector<pair<string, unsigned int> >::const_iterator i=ring.begin(); i!=ring.end();++i) {
12c86877
BH
157 tot+=i->second;
158 entries++;
159 }
160
1071abdd 161 ret<<"<div class=\"panel\">";
c7f59d62 162 ret<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname)<<"\">Reset</a></span>"<<endl;
1071abdd
CH
163 ret<<"<h2>"<<title<<"</h2>"<<endl;
164 ret<<"<div class=ringmeta>";
c7f59d62 165 ret<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname)<<"\">Showing: Top "<<limit<<" of "<<entries<<"</a>"<<endl;
1071abdd 166 ret<<"<span class=resizering>Resize: ";
bb3c3f50 167 unsigned int sizes[]={10,100,500,1000,10000,500000,0};
12c86877
BH
168 for(int i=0;sizes[i];++i) {
169 if(S.getRingSize(ringname)!=sizes[i])
c7f59d62 170 ret<<"<a href=\"?resizering="<<htmlescape(ringname)<<"&amp;size="<<sizes[i]<<"\">"<<sizes[i]<<"</a> ";
12c86877
BH
171 else
172 ret<<"("<<sizes[i]<<") ";
173 }
1071abdd 174 ret<<"</span></div>";
12c86877 175
1071abdd 176 ret<<"<table class=\"data\">";
12c86877 177 int printed=0;
f5cb7e61 178 int total=max(1,tot);
bb3c3f50 179 for(vector<pair<string,unsigned int> >::const_iterator i=ring.begin();limit && i!=ring.end();++i,--limit) {
dea47634 180 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
181 printed+=i->second;
182 }
183 ret<<"<tr><td colspan=3></td></tr>"<<endl;
184 if(printed!=tot)
dea47634 185 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 186
e2a77e08 187 ret<<"<tr><td><b>Total:</b></td><td><b>"<<tot<<"</b></td><td align=right><b>100%</b></td>";
1071abdd 188 ret<<"</table></div>"<<endl;
12c86877
BH
189}
190
dea47634 191void AuthWebServer::printvars(ostringstream &ret)
12c86877 192{
1071abdd 193 ret<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl;
12c86877
BH
194
195 vector<string>entries=S.getEntries();
196 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
197 ret<<"<tr><td>"<<*i<<"</td><td>"<<S.read(*i)<<"</td><td>"<<S.getDescrip(*i)<<"</td>"<<endl;
198 }
e2a77e08 199
1071abdd 200 ret<<"</table></div>"<<endl;
12c86877
BH
201}
202
dea47634 203void AuthWebServer::printargs(ostringstream &ret)
12c86877 204{
e2a77e08 205 ret<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl;
12c86877
BH
206
207 vector<string>entries=arg().list();
208 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
209 ret<<"<tr><td>"<<*i<<"</td><td>"<<arg()[*i]<<"</td><td>"<<arg().getHelp(*i)<<"</td>"<<endl;
210 }
211}
212
dea47634 213string AuthWebServer::makePercentage(const double& val)
b6f57093
BH
214{
215 return (boost::format("%.01f%%") % val).str();
216}
217
dea47634 218void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
12c86877 219{
583ea80d
CH
220 if(!req->getvars["resetring"].empty()) {
221 if (S.ringExists(req->getvars["resetring"]))
222 S.resetRing(req->getvars["resetring"]);
d7b8730e 223 resp->status = 302;
0665b7e6 224 resp->headers["Location"] = req->url.path;
80d59cd1 225 return;
12c86877 226 }
583ea80d 227 if(!req->getvars["resizering"].empty()){
335da0ba 228 int size=std::stoi(req->getvars["size"]);
583ea80d 229 if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000)
335da0ba 230 S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
d7b8730e 231 resp->status = 302;
0665b7e6 232 resp->headers["Location"] = req->url.path;
80d59cd1 233 return;
12c86877
BH
234 }
235
236 ostringstream ret;
237
1071abdd
CH
238 ret<<"<!DOCTYPE html>"<<endl;
239 ret<<"<html><head>"<<endl;
240 ret<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl;
241 ret<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl;
242 ret<<"</head><body>"<<endl;
243
244 ret<<"<div class=\"row\">"<<endl;
245 ret<<"<div class=\"headl columns\">";
a1caa8b8 246 ret<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION);
1071abdd 247 if(!arg()["config-name"].empty()) {
a1caa8b8 248 ret<<" ["<<htmlescape(arg()["config-name"])<<"]";
1071abdd
CH
249 }
250 ret<<"</a></div>"<<endl;
251 ret<<"<div class=\"headr columns\"></div></div>";
252 ret<<"<div class=\"row\"><div class=\"all columns\">";
12c86877
BH
253
254 time_t passed=time(0)-s_starttime;
255
e2a77e08
KM
256 ret<<"<p>Uptime: "<<
257 humanDuration(passed)<<
258 "<br>"<<endl;
12c86877 259
395b07ea 260 ret<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
3e1cd1f4 261 (int)d_queries.get1()<<", "<<
262 (int)d_queries.get5()<<", "<<
263 (int)d_queries.get10()<<". Max queries/second: "<<(int)d_queries.getMax()<<
12c86877 264 "<br>"<<endl;
1d6b70f9 265
f6154a3b 266 if(d_cachemisses.get10()+d_cachehits.get10()>0)
b6f57093 267 ret<<"Cache hitrate, 1, 5, 10 minute averages: "<<
f6154a3b
CH
268 makePercentage((d_cachehits.get1()*100.0)/((d_cachehits.get1())+(d_cachemisses.get1())))<<", "<<
269 makePercentage((d_cachehits.get5()*100.0)/((d_cachehits.get5())+(d_cachemisses.get5())))<<", "<<
270 makePercentage((d_cachehits.get10()*100.0)/((d_cachehits.get10())+(d_cachemisses.get10())))<<
b6f57093 271 "<br>"<<endl;
12c86877 272
f6154a3b 273 if(d_qcachemisses.get10()+d_qcachehits.get10()>0)
395b07ea 274 ret<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
f6154a3b
CH
275 makePercentage((d_qcachehits.get1()*100.0)/((d_qcachehits.get1())+(d_qcachemisses.get1())))<<", "<<
276 makePercentage((d_qcachehits.get5()*100.0)/((d_qcachehits.get5())+(d_qcachemisses.get5())))<<", "<<
277 makePercentage((d_qcachehits.get10()*100.0)/((d_qcachehits.get10())+(d_qcachemisses.get10())))<<
b6f57093 278 "<br>"<<endl;
12c86877 279
395b07ea 280 ret<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
3e1cd1f4 281 (int)d_qcachemisses.get1()<<", "<<
282 (int)d_qcachemisses.get5()<<", "<<
283 (int)d_qcachemisses.get10()<<". Max queries/second: "<<(int)d_qcachemisses.getMax()<<
12c86877
BH
284 "<br>"<<endl;
285
1071abdd 286 ret<<"Total queries: "<<S.read("udp-queries")<<". Question/answer latency: "<<S.read("latency")/1000.0<<"ms</p><br>"<<endl;
583ea80d 287 if(req->getvars["ring"].empty()) {
eb029b8e
PL
288 auto entries = S.listRings();
289 for(const auto &i: entries) {
290 printtable(ret, i, S.getRingTitle(i));
291 }
12c86877 292
f6154a3b 293 printvars(ret);
12c86877 294 if(arg().mustDo("webserver-print-arguments"))
f6154a3b 295 printargs(ret);
12c86877 296 }
bea69e32 297 else if(S.ringExists(req->getvars["ring"]))
583ea80d 298 printtable(ret,req->getvars["ring"],S.getRingTitle(req->getvars["ring"]),100);
12c86877 299
1071abdd 300 ret<<"</div></div>"<<endl;
620e780a 301 ret<<"<footer class=\"row\">"<<fullVersionString()<<"<br>&copy; 2013 - 2019 <a href=\"https://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl;
12c86877
BH
302 ret<<"</body></html>"<<endl;
303
80d59cd1 304 resp->body = ret.str();
61f5d289 305 resp->status = 200;
12c86877
BH
306}
307
1d6b70f9
CH
308/** Helper to build a record content as needed. */
309static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot) {
310 // noDot: for backend storage, pass true. for API users, pass false.
32122aab 311 auto drc = DNSRecordContent::mastermake(qtype.getCode(), QClass::IN, content);
7fe1a82b 312 return drc->getZoneRepresentation(noDot);
1d6b70f9
CH
313}
314
315/** "Normalize" record content for API consumers. */
316static inline string makeApiRecordContent(const QType& qtype, const string& content) {
317 return makeRecordContent(qtype, content, false);
318}
319
320/** "Normalize" record content for backend storage. */
321static inline string makeBackendRecordContent(const QType& qtype, const string& content) {
322 return makeRecordContent(qtype, content, true);
323}
324
07a32d18 325static Json::object getZoneInfo(const DomainInfo& di, DNSSECKeeper* dk) {
290a083d 326 string zoneId = apiZoneNameToId(di.zone);
d622042f 327 vector<string> masters;
328 for(const auto& m : di.masters)
329 masters.push_back(m.toStringWithPortExcept(53));
24ded6cc 330
07a32d18 331 auto obj = Json::object {
62a9a74c
CH
332 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
333 { "id", zoneId },
16e25450 334 { "url", "/api/v1/servers/localhost/zones/" + zoneId },
62a9a74c
CH
335 { "name", di.zone.toString() },
336 { "kind", di.getKindString() },
62a9a74c 337 { "account", di.account },
d622042f 338 { "masters", masters },
62a9a74c
CH
339 { "serial", (double)di.serial },
340 { "notified_serial", (double)di.notified_serial },
341 { "last_check", (double)di.last_check }
342 };
07a32d18
CH
343 if (dk) {
344 obj["dnssec"] = dk->isSecuredZone(di.zone);
345 obj["edited_serial"] = (double)calculateEditSOA(di.serial, *dk, di.zone);
346 }
347 return obj;
c04b5870
CH
348}
349
986e4858
PL
350static bool shouldDoRRSets(HttpRequest* req) {
351 if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true")
352 return true;
353 if (req->getvars["rrsets"] == "false")
354 return false;
355 throw ApiException("'rrsets' request parameter value '"+req->getvars["rrsets"]+"' is not supported");
356}
357
311cb9a2 358static void fillZone(UeberBackend& B, const DNSName& zonename, HttpResponse* resp, bool doRRSets) {
1abb81f4 359 DomainInfo di;
77bfe8de
PL
360 if(!B.getDomainInfo(zonename, di)) {
361 throw HttpNotFoundException();
362 }
1abb81f4 363
adef67eb 364 DNSSECKeeper dk(&B);
ce846be6 365 Json::object doc = getZoneInfo(di, &dk);
62a9a74c 366 // extra stuff getZoneInfo doesn't do for us (more expensive)
d29d5db7
CH
367 string soa_edit_api;
368 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
62a9a74c 369 doc["soa_edit_api"] = soa_edit_api;
6bb25159
MS
370 string soa_edit;
371 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
62a9a74c 372 doc["soa_edit"] = soa_edit;
986e4858
PL
373 string nsec3param;
374 di.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param);
375 doc["nsec3param"] = nsec3param;
376 string nsec3narrow;
377 bool nsec3narrowbool = false;
378 di.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow);
379 if (nsec3narrow == "1")
380 nsec3narrowbool = true;
381 doc["nsec3narrow"] = nsec3narrowbool;
0ce25a64 382 doc["dnssec"] = dk.isSecuredZone(zonename);
986e4858
PL
383
384 string api_rectify;
385 di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
386 doc["api_rectify"] = (api_rectify == "1");
387
eecc9ed5
PL
388 // TSIG
389 vector<string> tsig_master, tsig_slave;
390 di.backend->getDomainMetadata(zonename, "TSIG-ALLOW-AXFR", tsig_master);
391 di.backend->getDomainMetadata(zonename, "AXFR-MASTER-TSIG", tsig_slave);
392
393 Json::array tsig_master_keys;
394 for (const auto& keyname : tsig_master) {
395 tsig_master_keys.push_back(apiZoneNameToId(DNSName(keyname)));
396 }
397 doc["master_tsig_key_ids"] = tsig_master_keys;
398
399 Json::array tsig_slave_keys;
400 for (const auto& keyname : tsig_slave) {
401 tsig_slave_keys.push_back(apiZoneNameToId(DNSName(keyname)));
402 }
403 doc["slave_tsig_key_ids"] = tsig_slave_keys;
404
986e4858
PL
405 if (doRRSets) {
406 vector<DNSResourceRecord> records;
407 vector<Comment> comments;
408
409 // load all records + sort
410 {
411 DNSResourceRecord rr;
412 di.backend->list(zonename, di.id, true); // incl. disabled
413 while(di.backend->get(rr)) {
414 if (!rr.qtype.getCode())
415 continue; // skip empty non-terminals
416 records.push_back(rr);
417 }
418 sort(records.begin(), records.end(), [](const DNSResourceRecord& a, const DNSResourceRecord& b) {
f2d6dcc0
RG
419 /* if you ever want to update this comparison function,
420 please be aware that you will also need to update the conditions in the code merging
421 the records and comments below */
986e4858
PL
422 if (a.qname == b.qname) {
423 return b.qtype < a.qtype;
424 }
425 return b.qname < a.qname;
426 });
6754ef71 427 }
6754ef71 428
986e4858
PL
429 // load all comments + sort
430 {
431 Comment comment;
432 di.backend->listComments(di.id);
433 while(di.backend->getComment(comment)) {
434 comments.push_back(comment);
435 }
436 sort(comments.begin(), comments.end(), [](const Comment& a, const Comment& b) {
f2d6dcc0
RG
437 /* if you ever want to update this comparison function,
438 please be aware that you will also need to update the conditions in the code merging
439 the records and comments below */
986e4858
PL
440 if (a.qname == b.qname) {
441 return b.qtype < a.qtype;
442 }
443 return b.qname < a.qname;
444 });
6754ef71 445 }
6754ef71 446
986e4858
PL
447 Json::array rrsets;
448 Json::object rrset;
449 Json::array rrset_records;
450 Json::array rrset_comments;
451 DNSName current_qname;
452 QType current_qtype;
453 uint32_t ttl;
454 auto rit = records.begin();
455 auto cit = comments.begin();
456
457 while (rit != records.end() || cit != comments.end()) {
5481c77c
PD
458 // if you think this should be rit < cit instead of cit < rit, note the b < a instead of a < b in the sort comparison functions above
459 if (cit == comments.end() || (rit != records.end() && (rit->qname == cit->qname ? (cit->qtype < rit->qtype || cit->qtype == rit->qtype) : cit->qname < rit->qname))) {
986e4858
PL
460 current_qname = rit->qname;
461 current_qtype = rit->qtype;
462 ttl = rit->ttl;
463 } else {
464 current_qname = cit->qname;
465 current_qtype = cit->qtype;
466 ttl = 0;
467 }
6754ef71 468
986e4858
PL
469 while(rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
470 ttl = min(ttl, rit->ttl);
471 rrset_records.push_back(Json::object {
472 { "disabled", rit->disabled },
473 { "content", makeApiRecordContent(rit->qtype, rit->content) }
474 });
475 rit++;
476 }
477 while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
478 rrset_comments.push_back(Json::object {
479 { "modified_at", (double)cit->modified_at },
480 { "account", cit->account },
481 { "content", cit->content }
482 });
483 cit++;
484 }
485
486 rrset["name"] = current_qname.toString();
487 rrset["type"] = current_qtype.getName();
488 rrset["records"] = rrset_records;
489 rrset["comments"] = rrset_comments;
490 rrset["ttl"] = (double)ttl;
491 rrsets.push_back(rrset);
492 rrset.clear();
493 rrset_records.clear();
494 rrset_comments.clear();
6754ef71
CH
495 }
496
986e4858 497 doc["rrsets"] = rrsets;
6754ef71
CH
498 }
499
669822d0 500 resp->setBody(doc);
1abb81f4
CH
501}
502
6ec5e728
CH
503void productServerStatisticsFetch(map<string,string>& out)
504{
a45303b8 505 vector<string> items = S.getEntries();
ff05fd12 506 for(const string& item : items) {
335da0ba 507 out[item] = std::to_string(S.read(item));
a45303b8
CH
508 }
509
510 // add uptime
335da0ba 511 out["uptime"] = std::to_string(time(0) - s_starttime);
c67bf8c5
CH
512}
513
5376a5d7
RG
514boost::optional<uint64_t> productServerStatisticsFetch(const std::string& name)
515{
516 try {
517 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
518 return S.read(name);
519 }
520 catch(...) {
521 return boost::none;
522 }
523}
524
24ded6cc
CHB
525static void validateGatheredRRType(const DNSResourceRecord& rr) {
526 if (rr.qtype.getCode() == QType::OPT || rr.qtype.getCode() == QType::TSIG) {
527 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": invalid type given");
528 }
529}
530
311cb9a2 531static void gatherRecords(UeberBackend& B, const string& logprefix, const Json container, const DNSName& qname, const QType qtype, const int ttl, vector<DNSResourceRecord>& new_records, vector<DNSResourceRecord>& new_ptrs) {
f63168e6 532 DNSResourceRecord rr;
6754ef71
CH
533 rr.qname = qname;
534 rr.qtype = qtype;
535 rr.auth = 1;
536 rr.ttl = ttl;
24ded6cc
CHB
537
538 validateGatheredRRType(rr);
7f201325 539 const auto& items = container["records"].array_items();
7f201325 540 for(const auto& record : items) {
1f68b185 541 string content = stringFromJson(record, "content");
70aad565
PL
542 rr.disabled = false;
543 if(!record["disabled"].is_null()) {
544 rr.disabled = boolFromJson(record, "disabled");
545 }
1f68b185 546
1f68b185
CH
547 // validate that the client sent something we can actually parse, and require that data to be dotted.
548 try {
549 if (rr.qtype.getCode() != QType::AAAA) {
550 string tmp = makeApiRecordContent(rr.qtype, content);
551 if (!pdns_iequals(tmp, content)) {
552 throw std::runtime_error("Not in expected format (parsed as '"+tmp+"')");
553 }
554 } else {
555 struct in6_addr tmpbuf;
556 if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
557 throw std::runtime_error("Invalid IPv6 address");
1e5b9ab9 558 }
f63168e6 559 }
1f68b185
CH
560 rr.content = makeBackendRecordContent(rr.qtype, content);
561 }
562 catch(std::exception& e)
563 {
564 throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.getName()+" '"+content+"': "+e.what());
565 }
f63168e6 566
1f68b185
CH
567 if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) &&
568 boolFromJson(record, "set-ptr", false) == true) {
3a0f6fdd
CH
569
570 g_log<<Logger::Warning<<logprefix<<"API call uses deprecated set-ptr feature, please remove it"<<endl;
571
1f68b185
CH
572 DNSResourceRecord ptr;
573 makePtr(rr, &ptr);
f63168e6 574
1f68b185 575 // verify that there's a zone for the PTR
1f68b185 576 SOAData sd;
cec52de6 577 if (!B.getAuth(ptr.qname, QType(QType::PTR), &sd, false))
1f68b185 578 throw ApiException("Could not find domain for PTR '"+ptr.qname.toString()+"' requested for '"+ptr.content+"'");
f63168e6 579
1f68b185
CH
580 ptr.domain_id = sd.domain_id;
581 new_ptrs.push_back(ptr);
f63168e6 582 }
1f68b185
CH
583
584 new_records.push_back(rr);
f63168e6
CH
585 }
586}
587
6754ef71 588static void gatherComments(const Json container, const DNSName& qname, const QType qtype, vector<Comment>& new_comments) {
f63168e6 589 Comment c;
6754ef71
CH
590 c.qname = qname;
591 c.qtype = qtype;
f63168e6
CH
592
593 time_t now = time(0);
1f68b185 594 for (auto comment : container["comments"].array_items()) {
1f68b185
CH
595 c.modified_at = intFromJson(comment, "modified_at", now);
596 c.content = stringFromJson(comment, "content");
597 c.account = stringFromJson(comment, "account");
598 new_comments.push_back(c);
f63168e6
CH
599 }
600}
6cc98ddf 601
986e4858
PL
602static void checkDefaultDNSSECAlgos() {
603 int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
604 int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
605 int k_size = arg().asNum("default-ksk-size");
606 int z_size = arg().asNum("default-zsk-size");
607
608 // Sanity check DNSSEC parameters
609 if (::arg()["default-zsk-algorithm"] != "") {
610 if (k_algo == -1)
611 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
612 else if (k_algo <= 10 && k_size == 0)
613 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
614 }
615
616 if (::arg()["default-zsk-algorithm"] != "") {
617 if (z_algo == -1)
618 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
619 else if (z_algo <= 10 && z_size == 0)
620 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
621 }
622}
623
89a7e706
PL
624static void throwUnableToSecure(const DNSName& zonename) {
625 throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
626 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
627}
628
f43646f5 629static void updateDomainSettingsFromDocument(UeberBackend& B, const DomainInfo& di, const DNSName& zonename, const Json document, bool rectifyTransaction=true) {
49570562 630 vector<string> zonemaster;
986e4858 631 bool shouldRectify = false;
1f68b185
CH
632 for(auto value : document["masters"].array_items()) {
633 string master = value.string_value();
634 if (master.empty())
635 throw ApiException("Master can not be an empty string");
27efa4be
PL
636 try {
637 ComboAddress m(master);
638 } catch (const PDNSException &e) {
639 throw ApiException("Master (" + master + ") is not an IP address: " + e.reason);
640 }
49570562 641 zonemaster.push_back(master);
bb9fd223
CH
642 }
643
49570562
KD
644 if (zonemaster.size()) {
645 di.backend->setMaster(zonename, boost::join(zonemaster, ","));
986e4858
PL
646 }
647 if (document["kind"].is_string()) {
648 di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
649 }
1f68b185
CH
650 if (document["soa_edit_api"].is_string()) {
651 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
d29d5db7 652 }
1f68b185
CH
653 if (document["soa_edit"].is_string()) {
654 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
6bb25159 655 }
c00908f1
PL
656 try {
657 bool api_rectify = boolFromJson(document, "api_rectify");
658 di.backend->setDomainMetadataOne(zonename, "API-RECTIFY", api_rectify ? "1" : "0");
986e4858 659 }
819861fa 660 catch (const JsonException&) {}
c00908f1 661
1f68b185
CH
662 if (document["account"].is_string()) {
663 di.backend->setAccount(zonename, document["account"].string_value());
79532aa7 664 }
986e4858
PL
665
666 DNSSECKeeper dk(&B);
667 bool dnssecInJSON = false;
668 bool dnssecDocVal = false;
669
670 try {
671 dnssecDocVal = boolFromJson(document, "dnssec");
672 dnssecInJSON = true;
673 }
819861fa 674 catch (const JsonException&) {}
986e4858
PL
675
676 bool isDNSSECZone = dk.isSecuredZone(zonename);
677
678 if (dnssecInJSON) {
679 if (dnssecDocVal) {
680 if (!isDNSSECZone) {
681 checkDefaultDNSSECAlgos();
682
683 int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
684 int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
685 int k_size = arg().asNum("default-ksk-size");
686 int z_size = arg().asNum("default-zsk-size");
687
688 if (k_algo != -1) {
689 int64_t id;
690 if (!dk.addKey(zonename, true, k_algo, id, k_size)) {
89a7e706 691 throwUnableToSecure(zonename);
986e4858
PL
692 }
693 }
694
695 if (z_algo != -1) {
696 int64_t id;
697 if (!dk.addKey(zonename, false, z_algo, id, z_size)) {
89a7e706 698 throwUnableToSecure(zonename);
986e4858
PL
699 }
700 }
701
702 // Used later for NSEC3PARAM
703 isDNSSECZone = dk.isSecuredZone(zonename);
704
705 if (!isDNSSECZone) {
89a7e706 706 throwUnableToSecure(zonename);
986e4858
PL
707 }
708 shouldRectify = true;
709 }
710 } else {
711 // "dnssec": false in json
712 if (isDNSSECZone) {
cbe8b186
PL
713 string info, error;
714 if (!dk.unSecureZone(zonename, error, info)) {
715 throw ApiException("Error while un-securing zone '"+ zonename.toString()+"': " + error);
716 }
717 isDNSSECZone = dk.isSecuredZone(zonename);
718 if (isDNSSECZone) {
719 throw ApiException("Unable to un-secure zone '"+ zonename.toString()+"'");
720 }
721 shouldRectify = true;
986e4858
PL
722 }
723 }
724 }
725
726 if(document["nsec3param"].string_value().length() > 0) {
727 shouldRectify = true;
728 NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
729 string error_msg = "";
730 if (!isDNSSECZone) {
731 throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"', but zone is not DNSSEC secured.");
732 }
733 if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
734 throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
735 }
736 if (!dk.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) {
737 throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() +
738 "' passed our basic sanity checks, but cannot be used with the current backend.");
739 }
740 }
741
a843c67e
KM
742 if (shouldRectify && !dk.isPresigned(zonename)) {
743 // Rectify
744 string api_rectify;
745 di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
b8cd24cc
SH
746 if (api_rectify.empty()) {
747 if (::arg().mustDo("default-api-rectify")) {
748 api_rectify = "1";
749 }
750 }
a843c67e
KM
751 if (api_rectify == "1") {
752 string info;
753 string error_msg;
f43646f5 754 if (!dk.rectifyZone(zonename, error_msg, info, rectifyTransaction)) {
a843c67e
KM
755 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
756 }
757 }
758
759 // Increase serial
760 string soa_edit_api_kind;
761 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
762 if (!soa_edit_api_kind.empty()) {
763 SOAData sd;
76e1255a 764 if (!B.getSOAUncached(zonename, sd))
a843c67e
KM
765 return;
766
767 string soa_edit_kind;
768 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
769
770 DNSResourceRecord rr;
771 if (makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, rr)) {
772 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
773 throw ApiException("Hosting backend does not support editing records.");
774 }
775 }
a843c67e 776 }
986e4858 777 }
1096d493
PL
778
779 if (!document["master_tsig_key_ids"].is_null()) {
780 vector<string> metadata;
781 for(auto value : document["master_tsig_key_ids"].array_items()) {
dc30b8fd 782 auto keyname(apiZoneIdToName(value.string_value()));
3deb283e
RG
783 DNSName keyAlgo;
784 string keyContent;
dc30b8fd
PL
785 B.getTSIGKey(keyname, &keyAlgo, &keyContent);
786 if (keyAlgo.empty() || keyContent.empty()) {
787 throw ApiException("A TSIG key with the name '"+keyname.toLogString()+"' does not exist");
788 }
789 metadata.push_back(keyname.toString());
1096d493
PL
790 }
791 if (!di.backend->setDomainMetadata(zonename, "TSIG-ALLOW-AXFR", metadata)) {
b69468fc 792 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename.toLogString() + "'");
1096d493
PL
793 }
794 }
795 if (!document["slave_tsig_key_ids"].is_null()) {
796 vector<string> metadata;
797 for(auto value : document["slave_tsig_key_ids"].array_items()) {
dc30b8fd 798 auto keyname(apiZoneIdToName(value.string_value()));
3deb283e
RG
799 DNSName keyAlgo;
800 string keyContent;
dc30b8fd
PL
801 B.getTSIGKey(keyname, &keyAlgo, &keyContent);
802 if (keyAlgo.empty() || keyContent.empty()) {
803 throw ApiException("A TSIG key with the name '"+keyname.toLogString()+"' does not exist");
804 }
805 metadata.push_back(keyname.toString());
1096d493
PL
806 }
807 if (!di.backend->setDomainMetadata(zonename, "AXFR-MASTER-TSIG", metadata)) {
b69468fc 808 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename.toLogString() + "'");
1096d493
PL
809 }
810 }
bb9fd223
CH
811}
812
24e11043
CJ
813static bool isValidMetadataKind(const string& kind, bool readonly) {
814 static vector<string> builtinOptions {
815 "ALLOW-AXFR-FROM",
816 "AXFR-SOURCE",
817 "ALLOW-DNSUPDATE-FROM",
818 "TSIG-ALLOW-DNSUPDATE",
819 "FORWARD-DNSUPDATE",
820 "SOA-EDIT-DNSUPDATE",
4c5b6925 821 "NOTIFY-DNSUPDATE",
24e11043
CJ
822 "ALSO-NOTIFY",
823 "AXFR-MASTER-TSIG",
824 "GSS-ALLOW-AXFR-PRINCIPAL",
825 "GSS-ACCEPTOR-PRINCIPAL",
826 "IXFR",
827 "LUA-AXFR-SCRIPT",
828 "NSEC3NARROW",
829 "NSEC3PARAM",
830 "PRESIGNED",
831 "PUBLISH-CDNSKEY",
832 "PUBLISH-CDS",
c750c26e 833 "SLAVE-RENOTIFY",
24e11043
CJ
834 "SOA-EDIT",
835 "TSIG-ALLOW-AXFR",
836 "TSIG-ALLOW-DNSUPDATE"
837 };
838
839 // the following options do not allow modifications via API
840 static vector<string> protectedOptions {
986e4858 841 "API-RECTIFY",
2fabf5ce 842 "AXFR-MASTER-TSIG",
24e11043
CJ
843 "NSEC3NARROW",
844 "NSEC3PARAM",
845 "PRESIGNED",
2fabf5ce
PL
846 "LUA-AXFR-SCRIPT",
847 "TSIG-ALLOW-AXFR"
24e11043
CJ
848 };
849
9ac4e6d5
PL
850 if (kind.find("X-") == 0)
851 return true;
852
24e11043
CJ
853 bool found = false;
854
d8043c73 855 for (const string& s : builtinOptions) {
24e11043 856 if (kind == s) {
d8043c73 857 for (const string& s2 : protectedOptions) {
24e11043
CJ
858 if (!readonly && s == s2)
859 return false;
860 }
861 found = true;
862 break;
863 }
864 }
865
866 return found;
867}
868
869static void apiZoneMetadata(HttpRequest* req, HttpResponse *resp) {
870 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
d38e81e6 871
24e11043 872 UeberBackend B;
d38e81e6 873 DomainInfo di;
77bfe8de
PL
874 if (!B.getDomainInfo(zonename, di)) {
875 throw HttpNotFoundException();
876 }
24e11043
CJ
877
878 if (req->method == "GET") {
879 map<string, vector<string> > md;
880 Json::array document;
881
882 if (!B.getAllDomainMetadata(zonename, md))
883 throw HttpNotFoundException();
884
885 for (const auto& i : md) {
886 Json::array entries;
887 for (string j : i.second)
888 entries.push_back(j);
889
890 Json::object key {
891 { "type", "Metadata" },
892 { "kind", i.first },
893 { "metadata", entries }
894 };
895
896 document.push_back(key);
897 }
898
899 resp->setBody(document);
2054afbb 900 } else if (req->method == "POST") {
24e11043
CJ
901 auto document = req->json();
902 string kind;
903 vector<string> entries;
904
905 try {
906 kind = stringFromJson(document, "kind");
819861fa 907 } catch (const JsonException&) {
24e11043
CJ
908 throw ApiException("kind is not specified or not a string");
909 }
910
911 if (!isValidMetadataKind(kind, false))
912 throw ApiException("Unsupported metadata kind '" + kind + "'");
913
914 vector<string> vecMetadata;
c6720e79
CJ
915
916 if (!B.getDomainMetadata(zonename, kind, vecMetadata))
917 throw ApiException("Could not retrieve metadata entries for domain '" +
918 zonename.toString() + "'");
919
24e11043
CJ
920 auto& metadata = document["metadata"];
921 if (!metadata.is_array())
922 throw ApiException("metadata is not specified or not an array");
923
924 for (const auto& i : metadata.array_items()) {
925 if (!i.is_string())
926 throw ApiException("metadata must be strings");
c6720e79
CJ
927 else if (std::find(vecMetadata.cbegin(),
928 vecMetadata.cend(),
929 i.string_value()) == vecMetadata.cend()) {
930 vecMetadata.push_back(i.string_value());
931 }
24e11043
CJ
932 }
933
934 if (!B.setDomainMetadata(zonename, kind, vecMetadata))
c6720e79
CJ
935 throw ApiException("Could not update metadata entries for domain '" +
936 zonename.toString() + "'");
937
938 Json::array respMetadata;
939 for (const string& s : vecMetadata)
940 respMetadata.push_back(s);
941
942 Json::object key {
943 { "type", "Metadata" },
944 { "kind", document["kind"] },
945 { "metadata", respMetadata }
946 };
24e11043 947
24e11043 948 resp->status = 201;
c6720e79 949 resp->setBody(key);
24e11043
CJ
950 } else
951 throw HttpMethodNotAllowedException();
952}
953
954static void apiZoneMetadataKind(HttpRequest* req, HttpResponse* resp) {
955 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
d38e81e6 956
24e11043 957 UeberBackend B;
d38e81e6 958 DomainInfo di;
77bfe8de
PL
959 if (!B.getDomainInfo(zonename, di)) {
960 throw HttpNotFoundException();
961 }
d38e81e6
PL
962
963 string kind = req->parameters["kind"];
24e11043
CJ
964
965 if (req->method == "GET") {
966 vector<string> metadata;
967 Json::object document;
968 Json::array entries;
969
970 if (!B.getDomainMetadata(zonename, kind, metadata))
971 throw HttpNotFoundException();
972 else if (!isValidMetadataKind(kind, true))
973 throw ApiException("Unsupported metadata kind '" + kind + "'");
974
975 document["type"] = "Metadata";
976 document["kind"] = kind;
977
978 for (const string& i : metadata)
979 entries.push_back(i);
980
981 document["metadata"] = entries;
982 resp->setBody(document);
2054afbb 983 } else if (req->method == "PUT") {
24e11043
CJ
984 auto document = req->json();
985
986 if (!isValidMetadataKind(kind, false))
987 throw ApiException("Unsupported metadata kind '" + kind + "'");
988
989 vector<string> vecMetadata;
990 auto& metadata = document["metadata"];
991 if (!metadata.is_array())
992 throw ApiException("metadata is not specified or not an array");
993
994 for (const auto& i : metadata.array_items()) {
995 if (!i.is_string())
996 throw ApiException("metadata must be strings");
997 vecMetadata.push_back(i.string_value());
998 }
999
1000 if (!B.setDomainMetadata(zonename, kind, vecMetadata))
1001 throw ApiException("Could not update metadata entries for domain '" + zonename.toString() + "'");
1002
1003 Json::object key {
1004 { "type", "Metadata" },
1005 { "kind", kind },
1006 { "metadata", metadata }
1007 };
1008
1009 resp->setBody(key);
2054afbb 1010 } else if (req->method == "DELETE") {
24e11043
CJ
1011 if (!isValidMetadataKind(kind, false))
1012 throw ApiException("Unsupported metadata kind '" + kind + "'");
1013
1014 vector<string> md; // an empty vector will do it
1015 if (!B.setDomainMetadata(zonename, kind, md))
1016 throw ApiException("Could not delete metadata for domain '" + zonename.toString() + "' (" + kind + ")");
1017 } else
1018 throw HttpMethodNotAllowedException();
1019}
1020
71de13d7
PL
1021// Throws 404 if the key with inquireKeyId does not exist
1022static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename, int inquireKeyId, DNSSECKeeper *dk) {
1023 DNSSECKeeper::keyset_t keyset=dk->getKeys(zonename, false);
1024 bool found = false;
1025 for(const auto& value : keyset) {
1026 if (value.second.id == (unsigned) inquireKeyId) {
1027 found = true;
1028 break;
1029 }
1030 }
1031 if (!found) {
1032 throw HttpNotFoundException();
1033 }
1034}
1035
60b0a236
BZ
1036static void apiZoneCryptokeysGET(DNSName zonename, int inquireKeyId, HttpResponse *resp, DNSSECKeeper *dk) {
1037 DNSSECKeeper::keyset_t keyset=dk->getKeys(zonename, false);
4b7f120a 1038
997cab68
BZ
1039 bool inquireSingleKey = inquireKeyId >= 0;
1040
24afabad 1041 Json::array doc;
29704f66 1042 for(const auto& value : keyset) {
997cab68 1043 if (inquireSingleKey && (unsigned)inquireKeyId != value.second.id) {
29704f66 1044 continue;
38809e97 1045 }
24afabad 1046
b6bd795c 1047 string keyType;
60b0a236 1048 switch (value.second.keyType) {
b6bd795c
PL
1049 case DNSSECKeeper::KSK: keyType="ksk"; break;
1050 case DNSSECKeeper::ZSK: keyType="zsk"; break;
1051 case DNSSECKeeper::CSK: keyType="csk"; break;
1052 }
1053
24afabad 1054 Json::object key {
997cab68
BZ
1055 { "type", "Cryptokey" },
1056 { "id", (int)value.second.id },
1057 { "active", value.second.active },
33918299 1058 { "published", value.second.published },
997cab68
BZ
1059 { "keytype", keyType },
1060 { "flags", (uint16_t)value.first.d_flags },
5d9c6182
PL
1061 { "dnskey", value.first.getDNSKEY().getZoneRepresentation() },
1062 { "algorithm", DNSSECKeeper::algorithm2name(value.first.d_algorithm) },
1063 { "bits", value.first.getKey()->getBits() }
24afabad
CH
1064 };
1065
b6bd795c 1066 if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
24afabad 1067 Json::array dses;
690b86b7 1068 for(const uint8_t keyid : { DNSSECKeeper::DIGEST_SHA1, DNSSECKeeper::DIGEST_SHA256, DNSSECKeeper::DIGEST_GOST, DNSSECKeeper::DIGEST_SHA384 })
997cab68
BZ
1069 try {
1070 dses.push_back(makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation());
1071 } catch (...) {}
24afabad 1072 key["ds"] = dses;
4b7f120a 1073 }
29704f66
CH
1074
1075 if (inquireSingleKey) {
1076 key["privatekey"] = value.first.getKey()->convertToISC();
1077 resp->setBody(key);
1078 return;
1079 }
24afabad 1080 doc.push_back(key);
4b7f120a
MS
1081 }
1082
29704f66
CH
1083 if (inquireSingleKey) {
1084 // we came here because we couldn't find the requested key.
1085 throw HttpNotFoundException();
1086 }
4b7f120a 1087 resp->setBody(doc);
997cab68
BZ
1088
1089}
1090
1091/*
1092 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1093 * It deletes a key from :zone_name specified by :cryptokey_id.
1094 * Server Answers:
60b0a236 1095 * Case 1: the backend returns true on removal. This means the key is gone.
75191dc4 1096 * The server returns 204 No Content, no body.
955cbfd0 1097 * Case 2: the backend returns false on removal. An error occurred.
75191dc4
PL
1098 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1099 * Case 3: the key or zone does not exist.
1100 * The server returns 404 Not Found
997cab68 1101 * */
60b0a236
BZ
1102static void apiZoneCryptokeysDELETE(DNSName zonename, int inquireKeyId, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
1103 if (dk->removeKey(zonename, inquireKeyId)) {
1104 resp->body = "";
75191dc4 1105 resp->status = 204;
997cab68
BZ
1106 } else {
1107 resp->setErrorResult("Could not DELETE " + req->parameters["key_id"], 422);
1108 }
1109}
1110
1111/*
1112 * This method adds a key to a zone by generate it or content parameter.
1113 * Parameter:
1114 * {
5d9c6182 1115 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
997cab68
BZ
1116 * "keytype" : "ksk|zsk" <string>
1117 * "active" : "true|false" <value>
5d9c6182 1118 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
997cab68
BZ
1119 * "bits" : number of bits <int>
1120 * }
1121 *
1122 * Response:
1123 * Case 1: keytype isn't ksk|zsk
1124 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
60b0a236
BZ
1125 * Case 2: 'bits' must be a positive integer value.
1126 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
5d9c6182 1127 * Case 3: The "algorithm" isn't supported
997cab68 1128 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
60b0a236 1129 * Case 4: Algorithm <= 10 and no bits were passed
997cab68 1130 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
60b0a236
BZ
1131 * Case 5: The wrong keysize was passed
1132 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1133 * Case 6: If the server cant guess the keysize
1134 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1135 * Case 7: The key-creation failed
997cab68 1136 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
60b0a236
BZ
1137 * Case 8: The key in content has the wrong format
1138 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1139 * Case 9: The wrong combination of fields is submitted
1140 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1141 * Case 10: No content and everything was fine
1142 * The server returns 201 Created and all public data about the new cryptokey
1143 * Case 11: With specified content
1144 * The server returns 201 Created and all public data about the added cryptokey
997cab68
BZ
1145 */
1146
60b0a236 1147static void apiZoneCryptokeysPOST(DNSName zonename, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
997cab68 1148 auto document = req->json();
5d9c6182
PL
1149 string privatekey_fieldname = "privatekey";
1150 auto privatekey = document["privatekey"];
1151 if (privatekey.is_null()) {
1152 // Fallback to the old "content" behaviour
1153 privatekey = document["content"];
1154 privatekey_fieldname = "content";
1155 }
997cab68 1156 bool active = boolFromJson(document, "active", false);
33918299 1157 bool published = boolFromJson(document, "published", true);
997cab68 1158 bool keyOrZone;
60b0a236 1159
eefd15b3 1160 if (stringFromJson(document, "keytype") == "ksk" || stringFromJson(document, "keytype") == "csk") {
997cab68
BZ
1161 keyOrZone = true;
1162 } else if (stringFromJson(document, "keytype") == "zsk") {
1163 keyOrZone = false;
1164 } else {
1165 throw ApiException("Invalid keytype " + stringFromJson(document, "keytype"));
1166 }
1167
b727e19b 1168 int64_t insertedId = -1;
997cab68 1169
5d9c6182 1170 if (privatekey.is_null()) {
43215ca6 1171 int bits = keyOrZone ? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
60b0a236
BZ
1172 auto docbits = document["bits"];
1173 if (!docbits.is_null()) {
1174 if (!docbits.is_number() || (fmod(docbits.number_value(), 1.0) != 0) || docbits.int_value() < 0) {
1175 throw ApiException("'bits' must be a positive integer value");
1176 } else {
1177 bits = docbits.int_value();
1178 }
1179 }
43215ca6 1180 int algorithm = DNSSECKeeper::shorthand2algorithm(keyOrZone ? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
5d9c6182 1181 auto providedAlgo = document["algorithm"];
997cab68 1182 if (providedAlgo.is_string()) {
60b0a236
BZ
1183 algorithm = DNSSECKeeper::shorthand2algorithm(providedAlgo.string_value());
1184 if (algorithm == -1)
997cab68 1185 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
997cab68
BZ
1186 } else if (providedAlgo.is_number()) {
1187 algorithm = providedAlgo.int_value();
60b0a236
BZ
1188 } else if (!providedAlgo.is_null()) {
1189 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
997cab68
BZ
1190 }
1191
60b0a236 1192 try {
33918299 1193 if (!dk->addKey(zonename, keyOrZone, algorithm, insertedId, bits, active, published)) {
b727e19b
RG
1194 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1195 }
60b0a236 1196 } catch (std::runtime_error& error) {
997cab68
BZ
1197 throw ApiException(error.what());
1198 }
997cab68
BZ
1199 if (insertedId < 0)
1200 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
5d9c6182
PL
1201 } else if (document["bits"].is_null() && document["algorithm"].is_null()) {
1202 auto keyData = stringFromJson(document, privatekey_fieldname);
997cab68
BZ
1203 DNSKEYRecordContent dkrc;
1204 DNSSECPrivateKey dpk;
60b0a236 1205 try {
997cab68
BZ
1206 shared_ptr<DNSCryptoKeyEngine> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc, keyData));
1207 dpk.d_algorithm = dkrc.d_algorithm;
5d9c6182 1208 // TODO remove in 4.2.0
902c4e9c
CH
1209 if(dpk.d_algorithm == DNSSECKeeper::RSASHA1NSEC3SHA1)
1210 dpk.d_algorithm = DNSSECKeeper::RSASHA1;
997cab68
BZ
1211
1212 if (keyOrZone)
1213 dpk.d_flags = 257;
1214 else
1215 dpk.d_flags = 256;
1216
1217 dpk.setKey(dke);
997cab68 1218 }
60b0a236
BZ
1219 catch (std::runtime_error& error) {
1220 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1221 } try {
33918299 1222 if (!dk->addKey(zonename, dpk,insertedId, active, published)) {
b727e19b
RG
1223 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1224 }
60b0a236 1225 } catch (std::runtime_error& error) {
997cab68
BZ
1226 throw ApiException(error.what());
1227 }
1228 if (insertedId < 0)
1229 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
60b0a236 1230 } else {
5d9c6182 1231 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
997cab68 1232 }
60b0a236 1233 apiZoneCryptokeysGET(zonename, insertedId, resp, dk);
997cab68 1234 resp->status = 201;
60b0a236 1235}
997cab68
BZ
1236
1237/*
1238 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1239 * It de/activates a key from :zone_name specified by :cryptokey_id.
1240 * Server Answers:
60b0a236 1241 * Case 1: invalid JSON data
997cab68 1242 * The server returns 400 Bad Request
60b0a236
BZ
1243 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1244 * The server returns 204 No Content
955cbfd0 1245 * Case 3: the backend returns false on de/activation. An error occurred.
997cab68
BZ
1246 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1247 * */
60b0a236 1248static void apiZoneCryptokeysPUT(DNSName zonename, int inquireKeyId, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
997cab68
BZ
1249 //throws an exception if the Body is empty
1250 auto document = req->json();
1251 //throws an exception if the key does not exist or is not a bool
1252 bool active = boolFromJson(document, "active");
33918299 1253 bool published = boolFromJson(document, "published", true);
60b0a236
BZ
1254 if (active) {
1255 if (!dk->activateKey(zonename, inquireKeyId)) {
997cab68
BZ
1256 resp->setErrorResult("Could not activate Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
1257 return;
1258 }
1259 } else {
60b0a236 1260 if (!dk->deactivateKey(zonename, inquireKeyId)) {
997cab68
BZ
1261 resp->setErrorResult("Could not deactivate Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
1262 return;
1263 }
1264 }
33918299
RG
1265
1266 if (published) {
1267 if (!dk->publishKey(zonename, inquireKeyId)) {
1268 resp->setErrorResult("Could not publish Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
1269 return;
1270 }
1271 } else {
1272 if (!dk->unpublishKey(zonename, inquireKeyId)) {
1273 resp->setErrorResult("Could not unpublish Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
1274 return;
1275 }
1276 }
1277
60b0a236
BZ
1278 resp->body = "";
1279 resp->status = 204;
1280 return;
997cab68
BZ
1281}
1282
1283/*
1284 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1285 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1286 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1287 * */
1288static void apiZoneCryptokeys(HttpRequest *req, HttpResponse *resp) {
1289 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1290
60b0a236
BZ
1291 UeberBackend B;
1292 DNSSECKeeper dk(&B);
1293 DomainInfo di;
77bfe8de
PL
1294 if (!B.getDomainInfo(zonename, di)) {
1295 throw HttpNotFoundException();
1296 }
60b0a236 1297
997cab68
BZ
1298 int inquireKeyId = -1;
1299 if (req->parameters.count("key_id")) {
1300 inquireKeyId = std::stoi(req->parameters["key_id"]);
71de13d7 1301 apiZoneCryptoKeysCheckKeyExists(zonename, inquireKeyId, &dk);
997cab68
BZ
1302 }
1303
1304 if (req->method == "GET") {
60b0a236 1305 apiZoneCryptokeysGET(zonename, inquireKeyId, resp, &dk);
2054afbb 1306 } else if (req->method == "DELETE") {
60b0a236
BZ
1307 if (inquireKeyId == -1)
1308 throw HttpBadRequestException();
1309 apiZoneCryptokeysDELETE(zonename, inquireKeyId, req, resp, &dk);
2054afbb 1310 } else if (req->method == "POST") {
60b0a236 1311 apiZoneCryptokeysPOST(zonename, req, resp, &dk);
2054afbb 1312 } else if (req->method == "PUT") {
60b0a236
BZ
1313 if (inquireKeyId == -1)
1314 throw HttpBadRequestException();
1315 apiZoneCryptokeysPUT(zonename, inquireKeyId, req, resp, &dk);
997cab68
BZ
1316 } else {
1317 throw HttpMethodNotAllowedException(); //Returns method not allowed
1318 }
4b7f120a
MS
1319}
1320
1f68b185 1321static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, DNSName zonename) {
0f0e73fe
MS
1322 DNSResourceRecord rr;
1323 vector<string> zonedata;
1f68b185 1324 stringtok(zonedata, zonestring, "\r\n");
0f0e73fe
MS
1325
1326 ZoneParserTNG zpt(zonedata, zonename);
ba3d53d1 1327 zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
0f0e73fe
MS
1328
1329 bool seenSOA=false;
1330
1331 string comment = "Imported via the API";
1332
1333 try {
1334 while(zpt.get(rr, &comment)) {
1335 if(seenSOA && rr.qtype.getCode() == QType::SOA)
1336 continue;
1337 if(rr.qtype.getCode() == QType::SOA)
1338 seenSOA=true;
24ded6cc 1339 validateGatheredRRType(rr);
0f0e73fe 1340
0f0e73fe
MS
1341 new_records.push_back(rr);
1342 }
1343 }
1344 catch(std::exception& ae) {
1af62161 1345 throw ApiException("An error occurred while parsing the zonedata: "+string(ae.what()));
0f0e73fe
MS
1346 }
1347}
1348
646bcd7d 1349/** Throws ApiException if records which violate RRset contraints are present.
e3675a8a 1350 * NOTE: sorts records in-place.
646bcd7d
CH
1351 *
1352 * Constraints being checked:
1353 * *) no exact duplicates
1354 * *) no duplicates for QTypes that can only be present once per RRset
1355 * *) hostnames are hostnames
e3675a8a 1356 */
646bcd7d 1357static void checkNewRecords(vector<DNSResourceRecord>& records) {
e3675a8a
CH
1358 sort(records.begin(), records.end(),
1359 [](const DNSResourceRecord& rec_a, const DNSResourceRecord& rec_b) -> bool {
f2d6dcc0
RG
1360 /* we need _strict_ weak ordering */
1361 return std::tie(rec_a.qname, rec_a.qtype, rec_a.content) < std::tie(rec_b.qname, rec_b.qtype, rec_b.content);
e3675a8a
CH
1362 }
1363 );
646bcd7d 1364
e3675a8a
CH
1365 DNSResourceRecord previous;
1366 for(const auto& rec : records) {
646bcd7d
CH
1367 if (previous.qname == rec.qname) {
1368 if (previous.qtype == rec.qtype) {
1369 if (onlyOneEntryTypes.count(rec.qtype.getCode()) != 0) {
1370 throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName()+" has more than one record");
1371 }
1372 if (previous.content == rec.content) {
1373 throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.getName() + " with content \"" + rec.content + "\"");
1374 }
1375 } else if (exclusiveEntryTypes.count(rec.qtype.getCode()) != 0 || exclusiveEntryTypes.count(previous.qtype.getCode()) != 0) {
1376 throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName()+": Conflicts with another RRset");
1377 }
1378 }
1379
1380 // Check if the DNSNames that should be hostnames, are hostnames
1381 try {
1382 checkHostnameCorrectness(rec);
1383 } catch (const std::exception& e) {
1384 throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName() + " " + e.what());
e3675a8a 1385 }
646bcd7d 1386
e3675a8a
CH
1387 previous = rec;
1388 }
1389}
1390
6f16f97c
PL
1391static void checkTSIGKey(UeberBackend& B, const DNSName& keyname, const DNSName& algo, const string& content) {
1392 DNSName algoFromDB;
1393 string contentFromDB;
1394 B.getTSIGKey(keyname, &algoFromDB, &contentFromDB);
1395 if (!contentFromDB.empty() || !algoFromDB.empty()) {
f7b99555 1396 throw HttpConflictException("A TSIG key with the name '"+keyname.toLogString()+"' already exists");
fa07a3cf
PL
1397 }
1398
6f16f97c
PL
1399 TSIGHashEnum the;
1400 if (!getTSIGHashEnum(algo, the)) {
1401 throw ApiException("Unknown TSIG algorithm: " + algo.toLogString());
1402 }
fa07a3cf 1403
6f16f97c
PL
1404 string b64out;
1405 if (B64Decode(content, b64out) == -1) {
1406 throw ApiException("TSIG content '" + content + "' cannot be base64-decoded");
fa07a3cf 1407 }
6f16f97c 1408}
fa07a3cf 1409
6f16f97c
PL
1410static Json::object makeJSONTSIGKey(const DNSName& keyname, const DNSName& algo, const string& content) {
1411 Json::object tsigkey = {
1412 { "name", keyname.toStringNoDot() },
1413 { "id", apiZoneNameToId(keyname) },
1414 { "algorithm", algo.toStringNoDot() },
1415 { "key", content },
1416 { "type", "TSIGKey" }
1417 };
1418 return tsigkey;
1419}
fa07a3cf 1420
519a6288
PL
1421static Json::object makeJSONTSIGKey(const struct TSIGKey& key, bool doContent=true) {
1422 return makeJSONTSIGKey(key.name, key.algorithm, doContent ? key.key : "");
6f16f97c
PL
1423}
1424
f711ef76 1425static void apiServerTSIGKeys(HttpRequest* req, HttpResponse* resp) {
6f16f97c
PL
1426 UeberBackend B;
1427 if (req->method == "GET") {
1428 vector<struct TSIGKey> keys;
1429
1430 if (!B.getTSIGKeys(keys)) {
8204102e 1431 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
6f16f97c
PL
1432 }
1433
1434 Json::array doc;
1435
1436 for(const auto &key : keys) {
519a6288 1437 doc.push_back(makeJSONTSIGKey(key, false));
6f16f97c
PL
1438 }
1439 resp->setBody(doc);
9c69eb3f 1440 } else if (req->method == "POST") {
6f16f97c
PL
1441 auto document = req->json();
1442 DNSName keyname(stringFromJson(document, "name"));
1443 DNSName algo(stringFromJson(document, "algorithm"));
8204102e 1444 string content = document["key"].string_value();
6f16f97c
PL
1445
1446 if (content.empty()) {
ac5298aa
PL
1447 try {
1448 content = makeTSIGKey(algo);
1449 } catch (const PDNSException& e) {
1450 throw HttpBadRequestException(e.reason);
6f16f97c 1451 }
6f16f97c
PL
1452 }
1453
f7b99555 1454 // Will throw an ApiException or HttpConflictException on error
6f16f97c
PL
1455 checkTSIGKey(B, keyname, algo, content);
1456
1457 if(!B.setTSIGKey(keyname, algo, content)) {
8204102e 1458 throw HttpInternalServerErrorException("Unable to add TSIG key");
6f16f97c
PL
1459 }
1460
1461 resp->status = 201;
1462 resp->setBody(makeJSONTSIGKey(keyname, algo, content));
81c39bc6
PL
1463 } else {
1464 throw HttpMethodNotAllowedException();
6f16f97c 1465 }
fa07a3cf
PL
1466}
1467
f711ef76 1468static void apiServerTSIGKeyDetail(HttpRequest* req, HttpResponse* resp) {
fa07a3cf 1469 UeberBackend B;
81c39bc6 1470 DNSName keyname = apiZoneIdToName(req->parameters["id"]);
fa07a3cf
PL
1471 DNSName algo;
1472 string content;
1473
1474 if (!B.getTSIGKey(keyname, &algo, &content)) {
8204102e 1475 throw HttpNotFoundException("TSIG key with name '"+keyname.toLogString()+"' not found");
fa07a3cf
PL
1476 }
1477
81c39bc6
PL
1478 struct TSIGKey tsk;
1479 tsk.name = keyname;
1480 tsk.algorithm = algo;
1481 tsk.key = content;
1482
1483 if (req->method == "GET") {
1484 resp->setBody(makeJSONTSIGKey(tsk));
9c69eb3f 1485 } else if (req->method == "PUT") {
097370b2
PL
1486 json11::Json document;
1487 if (!req->body.empty()) {
1488 document = req->json();
1489 }
81c39bc6
PL
1490 if (document["name"].is_string()) {
1491 tsk.name = DNSName(document["name"].string_value());
1492 }
1493 if (document["algorithm"].is_string()) {
1494 tsk.algorithm = DNSName(document["algorithm"].string_value());
1495
1496 TSIGHashEnum the;
1497 if (!getTSIGHashEnum(tsk.algorithm, the)) {
1498 throw ApiException("Unknown TSIG algorithm: " + tsk.algorithm.toLogString());
1499 }
1500 }
1501 if (document["key"].is_string()) {
1502 string new_content = document["key"].string_value();
1503 string decoded;
1504 if (B64Decode(new_content, decoded) == -1) {
1505 throw ApiException("Can not base64 decode key content '" + new_content + "'");
1506 }
1507 tsk.key = new_content;
1508 }
1509 if (!B.setTSIGKey(tsk.name, tsk.algorithm, tsk.key)) {
8204102e 1510 throw HttpInternalServerErrorException("Unable to save TSIG Key");
81c39bc6
PL
1511 }
1512 if (tsk.name != keyname) {
1513 // Remove the old key
1514 if (!B.deleteTSIGKey(keyname)) {
8204102e 1515 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
81c39bc6
PL
1516 }
1517 }
1518 resp->setBody(makeJSONTSIGKey(tsk));
9c69eb3f 1519 } else if (req->method == "DELETE") {
81c39bc6 1520 if (!B.deleteTSIGKey(keyname)) {
8204102e 1521 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
81c39bc6
PL
1522 } else {
1523 resp->body = "";
1524 resp->status = 204;
1525 }
1526 } else {
1527 throw HttpMethodNotAllowedException();
1528 }
fa07a3cf
PL
1529}
1530
80d59cd1 1531static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
e2dba705 1532 UeberBackend B;
53942520 1533 DNSSECKeeper dk(&B);
2054afbb 1534 if (req->method == "POST") {
e2dba705 1535 DomainInfo di;
1f68b185 1536 auto document = req->json();
c576d0c5 1537 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
1d6b70f9 1538 apiCheckNameAllowedCharacters(zonename.toString());
e3675a8a 1539 zonename.makeUsLowerCase();
4ebf78b1 1540
1d6b70f9 1541 bool exists = B.getDomainInfo(zonename, di);
e2dba705 1542 if(exists)
331d3062 1543 throw HttpConflictException();
e2dba705 1544
bb9fd223 1545 // validate 'kind' is set
4bdff352 1546 DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
bb9fd223 1547
6754ef71
CH
1548 string zonestring = document["zone"].string_value();
1549 auto rrsets = document["rrsets"];
1550 if (rrsets.is_array() && zonestring != "")
1551 throw ApiException("You cannot give rrsets AND zone data as text");
0f0e73fe 1552
1f68b185 1553 auto nameservers = document["nameservers"];
6fbecaef
PL
1554 if (!nameservers.is_null() && !nameservers.is_array() && zonekind != DomainInfo::Slave)
1555 throw ApiException("Nameservers is not a list");
e2dba705 1556
f63168e6 1557 string soa_edit_api_kind;
1f68b185
CH
1558 if (document["soa_edit_api"].is_string()) {
1559 soa_edit_api_kind = document["soa_edit_api"].string_value();
a6448d95
CH
1560 }
1561 else {
1562 soa_edit_api_kind = "DEFAULT";
1563 }
1f68b185 1564 string soa_edit_kind = document["soa_edit"].string_value();
e90b4e38 1565
f63168e6
CH
1566 // if records/comments are given, load and check them
1567 bool have_soa = false;
33e6c3e9 1568 bool have_zone_ns = false;
f63168e6
CH
1569 vector<DNSResourceRecord> new_records;
1570 vector<Comment> new_comments;
1571 vector<DNSResourceRecord> new_ptrs;
0f0e73fe 1572
6754ef71
CH
1573 if (rrsets.is_array()) {
1574 for (const auto& rrset : rrsets.array_items()) {
1575 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
1576 apiCheckQNameAllowedCharacters(qname.toString());
1577 QType qtype;
1578 qtype = stringFromJson(rrset, "type");
1579 if (qtype.getCode() == 0) {
1580 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1581 }
1582 if (rrset["records"].is_array()) {
1583 int ttl = intFromJson(rrset, "ttl");
311cb9a2 1584 gatherRecords(B, req->logprefix, rrset, qname, qtype, ttl, new_records, new_ptrs);
6754ef71
CH
1585 }
1586 if (rrset["comments"].is_array()) {
1587 gatherComments(rrset, qname, qtype, new_comments);
1588 }
1589 }
0f0e73fe 1590 } else if (zonestring != "") {
1f68b185 1591 gatherRecordsFromZone(zonestring, new_records, zonename);
0f0e73fe
MS
1592 }
1593
1f68b185 1594 for(auto& rr : new_records) {
e3675a8a 1595 rr.qname.makeUsLowerCase();
1d6b70f9 1596 if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
561434a6 1597 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": Name is out of zone");
cb9b5901 1598 apiCheckQNameAllowedCharacters(rr.qname.toString());
f63168e6 1599
1d6b70f9 1600 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
f63168e6 1601 have_soa = true;
a6448d95 1602 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
f63168e6 1603 }
33e6c3e9
CH
1604 if (rr.qtype.getCode() == QType::NS && rr.qname==zonename) {
1605 have_zone_ns = true;
1606 }
f63168e6 1607 }
f7bfeb30
CH
1608
1609 // synthesize RRs as needed
1610 DNSResourceRecord autorr;
1d6b70f9 1611 autorr.qname = zonename;
f7bfeb30
CH
1612 autorr.auth = 1;
1613 autorr.ttl = ::arg().asNum("default-ttl");
e2dba705 1614
4de11a54 1615 if (!have_soa && zonekind != DomainInfo::Slave) {
f63168e6 1616 // synthesize a SOA record so the zone "really" exists
13f9e280 1617 string soa = (boost::format("%s %s %ul")
1d6b70f9
CH
1618 % ::arg()["default-soa-name"]
1619 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename).toString() : ::arg()["default-soa-mail"])
1f68b185 1620 % document["serial"].int_value()
1d6b70f9 1621 ).str();
f63168e6 1622 SOAData sd;
1d6b70f9 1623 fillSOAData(soa, sd); // fills out default values for us
13f9e280
CH
1624 autorr.qtype = QType::SOA;
1625 autorr.content = makeSOAContent(sd)->getZoneRepresentation(true);
f7bfeb30
CH
1626 increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
1627 new_records.push_back(autorr);
f63168e6
CH
1628 }
1629
1630 // create NS records if nameservers are given
1f68b185
CH
1631 for (auto value : nameservers.array_items()) {
1632 string nameserver = value.string_value();
1633 if (nameserver.empty())
1634 throw ApiException("Nameservers must be non-empty strings");
1635 if (!isCanonical(nameserver))
1636 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
1637 try {
1638 // ensure the name parses
8f955653 1639 autorr.content = DNSName(nameserver).toStringRootDot();
1f68b185
CH
1640 } catch (...) {
1641 throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
4bdff352 1642 }
13f9e280 1643 autorr.qtype = QType::NS;
1f68b185 1644 new_records.push_back(autorr);
33e6c3e9
CH
1645 if (have_zone_ns) {
1646 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1647 }
e2dba705
CH
1648 }
1649
646bcd7d 1650 checkNewRecords(new_records);
e3675a8a 1651
986e4858
PL
1652 if (boolFromJson(document, "dnssec", false)) {
1653 checkDefaultDNSSECAlgos();
1654
1655 if(document["nsec3param"].string_value().length() > 0) {
1656 NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
1657 string error_msg = "";
1658 if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
1659 throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
1660 }
1661 }
1662 }
1663
f63168e6 1664 // no going back after this
1d6b70f9
CH
1665 if(!B.createDomain(zonename))
1666 throw ApiException("Creating domain '"+zonename.toString()+"' failed");
f63168e6 1667
1d6b70f9
CH
1668 if(!B.getDomainInfo(zonename, di))
1669 throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");
f63168e6 1670
f43646f5
PD
1671 di.backend->startTransaction(zonename, di.id);
1672
9440a9f0
CH
1673 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1674 if (!soa_edit_api_kind.empty()) {
1675 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
1676 }
1677
abb873ee 1678 for(auto rr : new_records) {
f63168e6 1679 rr.domain_id = di.id;
c9b43446 1680 di.backend->feedRecord(rr, DNSName());
e2dba705 1681 }
1d6b70f9 1682 for(Comment& c : new_comments) {
f63168e6
CH
1683 c.domain_id = di.id;
1684 di.backend->feedComment(c);
1685 }
e2dba705 1686
f43646f5 1687 updateDomainSettingsFromDocument(B, di, zonename, document, false);
e2dba705 1688
f63168e6
CH
1689 di.backend->commitTransaction();
1690
3fe7c7d6
CH
1691 storeChangedPTRs(B, new_ptrs);
1692
311cb9a2 1693 fillZone(B, zonename, resp, shouldDoRRSets(req));
64a36f0d 1694 resp->status = 201;
e2dba705
CH
1695 return;
1696 }
1697
c67bf8c5
CH
1698 if(req->method != "GET")
1699 throw HttpMethodNotAllowedException();
1700
c67bf8c5 1701 vector<DomainInfo> domains;
e543cc8f 1702
37e01df4
CH
1703 if (req->getvars.count("zone")) {
1704 string zone = req->getvars["zone"];
e543cc8f
CH
1705 apiCheckNameAllowedCharacters(zone);
1706 DNSName zonename = apiNameToDNSName(zone);
1707 zonename.makeUsLowerCase();
1708 DomainInfo di;
1709 if (B.getDomainInfo(zonename, di)) {
1710 domains.push_back(di);
1711 }
1712 } else {
72abd9ef
PL
1713 try {
1714 B.getAllDomains(&domains, true); // incl. disabled
1715 } catch(const PDNSException &e) {
1716 throw HttpInternalServerErrorException("Could not retrieve all domain information: " + e.reason);
1717 }
e543cc8f 1718 }
c67bf8c5 1719
07a32d18
CH
1720 bool with_dnssec = true;
1721 if (req->getvars.count("dnssec")) {
1722 // can send ?dnssec=false to improve performance.
1723 string dnssec_flag = req->getvars["dnssec"];
1724 if (dnssec_flag == "false") {
1725 with_dnssec = false;
1726 }
1727 }
1728
62a9a74c
CH
1729 Json::array doc;
1730 for(const DomainInfo& di : domains) {
07a32d18 1731 doc.push_back(getZoneInfo(di, with_dnssec ? &dk : nullptr));
c67bf8c5 1732 }
669822d0 1733 resp->setBody(doc);
c67bf8c5
CH
1734}
1735
05776d2f 1736static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
290a083d 1737 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
05776d2f 1738
77bfe8de
PL
1739 UeberBackend B;
1740 DomainInfo di;
27efa4be
PL
1741 try {
1742 if (!B.getDomainInfo(zonename, di)) {
1743 throw HttpNotFoundException();
1744 }
1745 } catch(const PDNSException &e) {
1746 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e.reason);
77bfe8de
PL
1747 }
1748
2054afbb 1749 if(req->method == "PUT") {
7c0ba3d2 1750 // update domain settings
7c0ba3d2 1751
f43646f5
PD
1752 di.backend->startTransaction(zonename, -1);
1753 updateDomainSettingsFromDocument(B, di, zonename, req->json(), false);
1754 di.backend->commitTransaction();
7c0ba3d2 1755
f0e76cee
CH
1756 resp->body = "";
1757 resp->status = 204; // No Content, but indicate success
7c0ba3d2
CH
1758 return;
1759 }
2054afbb 1760 else if(req->method == "DELETE") {
a462a01d 1761 // delete domain
a462a01d 1762 if(!di.backend->deleteDomain(zonename))
290a083d 1763 throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");
a462a01d 1764
5a4fa722 1765 // clear caches
40b3959a 1766 DNSSECKeeper::clearCaches(zonename);
318434b7 1767 purgeAuthCaches(zonename.toString() + "$");
5a4fa722 1768
a462a01d
CH
1769 // empty body on success
1770 resp->body = "";
37663c3b 1771 resp->status = 204; // No Content: declare that the zone is gone now
a462a01d 1772 return;
2054afbb 1773 } else if (req->method == "PATCH") {
311cb9a2 1774 patchZone(B, req, resp);
6cc98ddf
CH
1775 return;
1776 } else if (req->method == "GET") {
311cb9a2 1777 fillZone(B, zonename, resp, shouldDoRRSets(req));
6cc98ddf 1778 return;
a462a01d 1779 }
6cc98ddf 1780 throw HttpMethodNotAllowedException();
05776d2f
CH
1781}
1782
a83004d3 1783static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
290a083d 1784 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a83004d3
CH
1785
1786 if(req->method != "GET")
1787 throw HttpMethodNotAllowedException();
1788
1789 ostringstream ss;
1790
1791 UeberBackend B;
1792 DomainInfo di;
77bfe8de
PL
1793 if (!B.getDomainInfo(zonename, di)) {
1794 throw HttpNotFoundException();
1795 }
a83004d3
CH
1796
1797 DNSResourceRecord rr;
1798 SOAData sd;
1799 di.backend->list(zonename, di.id);
1800 while(di.backend->get(rr)) {
1801 if (!rr.qtype.getCode())
1802 continue; // skip empty non-terminals
1803
a83004d3 1804 ss <<
675fa24c 1805 rr.qname.toString() << "\t" <<
a83004d3 1806 rr.ttl << "\t" <<
dc420da6 1807 "IN" << "\t" <<
a83004d3 1808 rr.qtype.getName() << "\t" <<
1d6b70f9 1809 makeApiRecordContent(rr.qtype, rr.content) <<
a83004d3
CH
1810 endl;
1811 }
1812
1813 if (req->accept_json) {
41873e7c 1814 resp->setBody(Json::object { { "zone", ss.str() } });
a83004d3
CH
1815 } else {
1816 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
1817 resp->body = ss.str();
1818 }
1819}
1820
a426cb89 1821static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
290a083d 1822 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89 1823
2054afbb 1824 if(req->method != "PUT")
a426cb89
CH
1825 throw HttpMethodNotAllowedException();
1826
1827 UeberBackend B;
1828 DomainInfo di;
77bfe8de
PL
1829 if (!B.getDomainInfo(zonename, di)) {
1830 throw HttpNotFoundException();
1831 }
a426cb89
CH
1832
1833 if(di.masters.empty())
290a083d 1834 throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");
a426cb89
CH
1835
1836 random_shuffle(di.masters.begin(), di.masters.end());
1837 Communicator.addSuckRequest(zonename, di.masters.front());
9b0f144f 1838 resp->setSuccessResult("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front().toLogString());
a426cb89
CH
1839}
1840
1841static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
290a083d 1842 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89 1843
2054afbb 1844 if(req->method != "PUT")
a426cb89
CH
1845 throw HttpMethodNotAllowedException();
1846
1847 UeberBackend B;
1848 DomainInfo di;
77bfe8de
PL
1849 if (!B.getDomainInfo(zonename, di)) {
1850 throw HttpNotFoundException();
1851 }
a426cb89 1852
3497eb18 1853 if(!Communicator.notifyDomain(zonename, &B))
a426cb89
CH
1854 throw ApiException("Failed to add to the queue - see server log");
1855
692829aa 1856 resp->setSuccessResult("Notification queued");
a426cb89
CH
1857}
1858
4bc8379e
PL
1859static void apiServerZoneRectify(HttpRequest* req, HttpResponse* resp) {
1860 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1861
1862 if(req->method != "PUT")
1863 throw HttpMethodNotAllowedException();
1864
1865 UeberBackend B;
1866 DomainInfo di;
77bfe8de
PL
1867 if (!B.getDomainInfo(zonename, di)) {
1868 throw HttpNotFoundException();
1869 }
4bc8379e
PL
1870
1871 DNSSECKeeper dk(&B);
1872
1873 if (!dk.isSecuredZone(zonename))
1874 throw ApiException("Zone '" + zonename.toString() + "' is not DNSSEC signed, not rectifying.");
1875
1876 if (di.kind == DomainInfo::Slave)
1877 throw ApiException("Zone '" + zonename.toString() + "' is a slave zone, not rectifying.");
1878
1879 string error_msg = "";
59102608
RG
1880 string info;
1881 if (!dk.rectifyZone(zonename, error_msg, info, true))
4bc8379e
PL
1882 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
1883
1884 resp->setSuccessResult("Rectified");
1885}
1886
d1587ceb
CH
1887static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
1888 if (rr.qtype.getCode() == QType::A) {
1889 uint32_t ip;
1890 if (!IpToU32(rr.content, &ip)) {
1891 throw ApiException("PTR: Invalid IP address given");
1892 }
1d6b70f9 1893 ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
d1587ceb
CH
1894 % ((ip >> 24) & 0xff)
1895 % ((ip >> 16) & 0xff)
1896 % ((ip >> 8) & 0xff)
1897 % ((ip ) & 0xff)
1d6b70f9 1898 ).str());
d1587ceb
CH
1899 } else if (rr.qtype.getCode() == QType::AAAA) {
1900 ComboAddress ca(rr.content);
5fb3aa58 1901 char buf[3];
d1587ceb 1902 ostringstream ss;
5fb3aa58
CH
1903 for (int octet = 0; octet < 16; ++octet) {
1904 if (snprintf(buf, sizeof(buf), "%02x", ca.sin6.sin6_addr.s6_addr[octet]) != (sizeof(buf)-1)) {
1905 // this should be impossible: no byte should give more than two digits in hex format
1906 throw PDNSException("Formatting IPv6 address failed");
1907 }
1908 ss << buf[0] << '.' << buf[1] << '.';
d1587ceb 1909 }
5fb3aa58
CH
1910 string tmp = ss.str();
1911 tmp.resize(tmp.size()-1); // remove last dot
1912 // reverse and append arpa domain
1d6b70f9 1913 ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
d1587ceb 1914 } else {
675fa24c 1915 throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
d1587ceb
CH
1916 }
1917
1918 ptr->qtype = "PTR";
1919 ptr->ttl = rr.ttl;
1920 ptr->disabled = rr.disabled;
8f955653 1921 ptr->content = rr.qname.toStringRootDot();
d1587ceb
CH
1922}
1923
995473c8
CH
1924static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs) {
1925 for(const DNSResourceRecord& rr : new_ptrs) {
995473c8 1926 SOAData sd;
cec52de6 1927 if (!B.getAuth(rr.qname, QType(QType::PTR), &sd, false))
995473c8
CH
1928 throw ApiException("Could not find domain for PTR '"+rr.qname.toString()+"' requested for '"+rr.content+"' (while saving)");
1929
1930 string soa_edit_api_kind;
1931 string soa_edit_kind;
1932 bool soa_changed = false;
1933 DNSResourceRecord soarr;
1934 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT-API", soa_edit_api_kind);
1935 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT", soa_edit_kind);
1936 if (!soa_edit_api_kind.empty()) {
13f9e280 1937 soa_changed = makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, soarr);
995473c8
CH
1938 }
1939
1940 sd.db->startTransaction(sd.qname);
1941 if (!sd.db->replaceRRSet(sd.domain_id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1942 sd.db->abortTransaction();
1943 throw ApiException("PTR-Hosting backend for "+rr.qname.toString()+"/"+rr.qtype.getName()+" does not support editing records.");
1944 }
1945
1946 if (soa_changed) {
1947 sd.db->replaceRRSet(sd.domain_id, soarr.qname, soarr.qtype, vector<DNSResourceRecord>(1, soarr));
1948 }
1949
1950 sd.db->commitTransaction();
bf269e28 1951 purgeAuthCachesExact(rr.qname);
995473c8
CH
1952 }
1953}
1954
311cb9a2 1955static void patchZone(UeberBackend& B, HttpRequest* req, HttpResponse* resp) {
a41a789a
KM
1956 bool zone_disabled;
1957 SOAData sd;
b3905a3d 1958 DomainInfo di;
290a083d 1959 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
77bfe8de
PL
1960 if (!B.getDomainInfo(zonename, di)) {
1961 throw HttpNotFoundException();
1962 }
b3905a3d 1963
f63168e6
CH
1964 vector<DNSResourceRecord> new_records;
1965 vector<Comment> new_comments;
d708640f
CH
1966 vector<DNSResourceRecord> new_ptrs;
1967
1f68b185 1968 Json document = req->json();
b3905a3d 1969
1f68b185
CH
1970 auto rrsets = document["rrsets"];
1971 if (!rrsets.is_array())
d708640f 1972 throw ApiException("No rrsets given in update request");
b3905a3d 1973
d708640f 1974 di.backend->startTransaction(zonename);
6cc98ddf 1975
d708640f 1976 try {
d29d5db7 1977 string soa_edit_api_kind;
a6448d95 1978 string soa_edit_kind;
d29d5db7 1979 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
a6448d95 1980 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
d29d5db7
CH
1981 bool soa_edit_done = false;
1982
2b048cc7
PD
1983 set<pair<DNSName, QType>> seen;
1984
6754ef71
CH
1985 for (const auto& rrset : rrsets.array_items()) {
1986 string changetype = toUpper(stringFromJson(rrset, "changetype"));
c576d0c5 1987 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
cb9b5901 1988 apiCheckQNameAllowedCharacters(qname.toString());
6754ef71 1989 QType qtype;
d708640f 1990 qtype = stringFromJson(rrset, "type");
6754ef71
CH
1991 if (qtype.getCode() == 0) {
1992 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1993 }
d708640f 1994
2b048cc7
PD
1995 if(seen.count({qname, qtype}))
1996 {
3216c7db 1997 throw ApiException("Duplicate RRset "+qname.toString()+" IN "+qtype.getName());
2b048cc7
PD
1998 }
1999 seen.insert({qname, qtype});
d708640f 2000
d708640f 2001 if (changetype == "DELETE") {
b7f21ab1 2002 // delete all matching qname/qtype RRs (and, implicitly comments).
d708640f
CH
2003 if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
2004 throw ApiException("Hosting backend does not support editing records.");
6cc98ddf 2005 }
d708640f
CH
2006 }
2007 else if (changetype == "REPLACE") {
1d6b70f9 2008 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
e325f20c 2009 if (!qname.isPartOf(zonename) && qname != zonename)
edda67a2 2010 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
34df6ecc 2011
6754ef71
CH
2012 bool replace_records = rrset["records"].is_array();
2013 bool replace_comments = rrset["comments"].is_array();
f63168e6 2014
6754ef71
CH
2015 if (!replace_records && !replace_comments) {
2016 throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.getName());
2017 }
f63168e6 2018
6754ef71
CH
2019 new_records.clear();
2020 new_comments.clear();
f63168e6 2021
6754ef71
CH
2022 if (replace_records) {
2023 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
2024 int ttl = intFromJson(rrset, "ttl");
2025 // new_ptrs is merged.
311cb9a2 2026 gatherRecords(B, req->logprefix, rrset, qname, qtype, ttl, new_records, new_ptrs);
6754ef71
CH
2027
2028 for(DNSResourceRecord& rr : new_records) {
2029 rr.domain_id = di.id;
2030 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
2031 soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
6754ef71 2032 }
d708640f 2033 }
646bcd7d 2034 checkNewRecords(new_records);
6cc98ddf
CH
2035 }
2036
6754ef71
CH
2037 if (replace_comments) {
2038 gatherComments(rrset, qname, qtype, new_comments);
f63168e6 2039
6754ef71
CH
2040 for(Comment& c : new_comments) {
2041 c.domain_id = di.id;
2042 }
d708640f 2043 }
b3905a3d 2044
d708640f 2045 if (replace_records) {
03b1cc25 2046 bool ent_present = false;
5e73d266
AT
2047 bool dname_seen = false, ns_seen = false;
2048
162fcac6 2049 di.backend->lookup(QType(QType::ANY), qname, di.id);
8560f36a
CH
2050 DNSResourceRecord rr;
2051 while (di.backend->get(rr)) {
6096ad14 2052 if (rr.qtype.getCode() == QType::ENT) {
03b1cc25 2053 ent_present = true;
002c4fe1
RG
2054 /* that's fine, we will override it */
2055 continue;
03b1cc25 2056 }
5e73d266
AT
2057 if (qtype == QType::DNAME || rr.qtype == QType::DNAME)
2058 dname_seen = true;
2059 if (qtype == QType::NS || rr.qtype == QType::NS)
2060 ns_seen = true;
646bcd7d
CH
2061 if (qtype.getCode() != rr.qtype.getCode()
2062 && (exclusiveEntryTypes.count(qtype.getCode()) != 0
2063 || exclusiveEntryTypes.count(rr.qtype.getCode()) != 0)) {
e4e8ca30
PD
2064
2065 // leave database handle in a consistent state
2066 while (di.backend->get(rr))
2067 ;
2068
646bcd7d 2069 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Conflicts with pre-existing RRset");
8560f36a
CH
2070 }
2071 }
2072
5e73d266
AT
2073 if (dname_seen && ns_seen && qname != zonename) {
2074 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Cannot have both NS and DNAME except in zone apex");
2075 }
03b1cc25
CH
2076 if (!new_records.empty() && ent_present) {
2077 QType qt_ent{0};
2078 if (!di.backend->replaceRRSet(di.id, qname, qt_ent, new_records)) {
2079 throw ApiException("Hosting backend does not support editing records.");
2080 }
2081 }
d708640f
CH
2082 if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
2083 throw ApiException("Hosting backend does not support editing records.");
2084 }
2085 }
2086 if (replace_comments) {
2087 if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
2088 throw ApiException("Hosting backend does not support editing comments.");
2089 }
2090 }
6cc98ddf 2091 }
d708640f
CH
2092 else
2093 throw ApiException("Changetype not understood");
6cc98ddf 2094 }
d29d5db7 2095
a41a789a 2096 zone_disabled = (!B.getSOAUncached(zonename, sd));
d29d5db7 2097
a41a789a
KM
2098 // edit SOA (if needed)
2099 if (!zone_disabled && !soa_edit_api_kind.empty() && !soa_edit_done) {
d29d5db7 2100 DNSResourceRecord rr;
13f9e280
CH
2101 if (makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, rr)) {
2102 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
2103 throw ApiException("Hosting backend does not support editing records.");
2104 }
d29d5db7 2105 }
3ae63ca8 2106
478de03b
KW
2107 // return old and new serials in headers
2108 resp->headers["X-PDNS-Old-Serial"] = std::to_string(sd.serial);
3ae63ca8 2109 fillSOAData(rr.content, sd);
478de03b 2110 resp->headers["X-PDNS-New-Serial"] = std::to_string(sd.serial);
d29d5db7
CH
2111 }
2112
d708640f
CH
2113 } catch(...) {
2114 di.backend->abortTransaction();
2115 throw;
2116 }
986e4858 2117
0ca59ad9 2118 // Rectify
b0486ea5 2119 DNSSECKeeper dk(&B);
a41a789a 2120 if (!zone_disabled && !dk.isPresigned(zonename)) {
0ca59ad9
KM
2121 string api_rectify;
2122 if (!di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify) && ::arg().mustDo("default-api-rectify")) {
2123 api_rectify = "1";
2124 }
2125 if (api_rectify == "1") {
2126 string info;
2127 string error_msg;
2128 if (!dk.rectifyZone(zonename, error_msg, info, false)) {
2129 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
2130 }
2131 }
986e4858
PL
2132 }
2133
d708640f 2134 di.backend->commitTransaction();
b3905a3d 2135
27e14380 2136 purgeAuthCaches(zonename.toString() + "$");
d1587ceb 2137
d708640f 2138 // now the PTRs
995473c8 2139 storeChangedPTRs(B, new_ptrs);
b3905a3d 2140
f0e76cee
CH
2141 resp->body = "";
2142 resp->status = 204; // No Content, but indicate success
2143 return;
b3905a3d
CH
2144}
2145
b1902fab
CH
2146static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
2147 if(req->method != "GET")
2148 throw HttpMethodNotAllowedException();
2149
583ea80d 2150 string q = req->getvars["q"];
720ed2bd 2151 string sMax = req->getvars["max"];
0bd91de6 2152 string sObjectType = req->getvars["object_type"];
45250285 2153
720ed2bd
AT
2154 int maxEnts = 100;
2155 int ents = 0;
2156
45250285 2157 // the following types of data can be searched for using the api
0bd91de6 2158 enum class ObjectType
45250285
JE
2159 {
2160 ALL,
2161 ZONE,
2162 RECORD,
2163 COMMENT
0bd91de6 2164 } objectType;
45250285 2165
b1902fab
CH
2166 if (q.empty())
2167 throw ApiException("Query q can't be blank");
606c8752 2168 if (!sMax.empty())
335da0ba 2169 maxEnts = std::stoi(sMax);
720ed2bd
AT
2170 if (maxEnts < 1)
2171 throw ApiException("Maximum entries must be larger than 0");
b1902fab 2172
0bd91de6
CH
2173 if (sObjectType.empty())
2174 objectType = ObjectType::ALL;
2175 else if (sObjectType == "all")
2176 objectType = ObjectType::ALL;
2177 else if (sObjectType == "zone")
2178 objectType = ObjectType::ZONE;
2179 else if (sObjectType == "record")
2180 objectType = ObjectType::RECORD;
2181 else if (sObjectType == "comment")
2182 objectType = ObjectType::COMMENT;
45250285 2183 else
0bd91de6 2184 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
45250285 2185
720ed2bd 2186 SimpleMatch sm(q,true);
b1902fab 2187 UeberBackend B;
b1902fab 2188 vector<DomainInfo> domains;
720ed2bd
AT
2189 vector<DNSResourceRecord> result_rr;
2190 vector<Comment> result_c;
1d6b70f9
CH
2191 map<int,DomainInfo> zoneIdZone;
2192 map<int,DomainInfo>::iterator val;
00963dea 2193 Json::array doc;
b1902fab 2194
720ed2bd 2195 B.getAllDomains(&domains, true);
d2d194a9 2196
720ed2bd 2197 for(const DomainInfo di: domains)
1d6b70f9 2198 {
0bd91de6 2199 if ((objectType == ObjectType::ALL || objectType == ObjectType::ZONE) && ents < maxEnts && sm.match(di.zone)) {
00963dea
CH
2200 doc.push_back(Json::object {
2201 { "object_type", "zone" },
2202 { "zone_id", apiZoneNameToId(di.zone) },
2203 { "name", di.zone.toString() }
2204 });
720ed2bd 2205 ents++;
b1902fab 2206 }
1d6b70f9 2207 zoneIdZone[di.id] = di; // populate cache
720ed2bd 2208 }
b1902fab 2209
0bd91de6 2210 if ((objectType == ObjectType::ALL || objectType == ObjectType::RECORD) && B.searchRecords(q, maxEnts, result_rr))
720ed2bd
AT
2211 {
2212 for(const DNSResourceRecord& rr: result_rr)
2213 {
7cbc5255
CH
2214 if (!rr.qtype.getCode())
2215 continue; // skip empty non-terminals
2216
00963dea
CH
2217 auto object = Json::object {
2218 { "object_type", "record" },
2219 { "name", rr.qname.toString() },
2220 { "type", rr.qtype.getName() },
2221 { "ttl", (double)rr.ttl },
2222 { "disabled", rr.disabled },
2223 { "content", makeApiRecordContent(rr.qtype, rr.content) }
2224 };
720ed2bd 2225 if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
00963dea
CH
2226 object["zone_id"] = apiZoneNameToId(val->second.zone);
2227 object["zone"] = val->second.zone.toString();
720ed2bd 2228 }
00963dea 2229 doc.push_back(object);
b1902fab 2230 }
720ed2bd 2231 }
b1902fab 2232
0bd91de6 2233 if ((objectType == ObjectType::ALL || objectType == ObjectType::COMMENT) && B.searchComments(q, maxEnts, result_c))
720ed2bd
AT
2234 {
2235 for(const Comment &c: result_c)
2236 {
00963dea
CH
2237 auto object = Json::object {
2238 { "object_type", "comment" },
25dcc05f 2239 { "name", c.qname.toString() },
00963dea
CH
2240 { "content", c.content }
2241 };
720ed2bd 2242 if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
00963dea
CH
2243 object["zone_id"] = apiZoneNameToId(val->second.zone);
2244 object["zone"] = val->second.zone.toString();
720ed2bd 2245 }
00963dea 2246 doc.push_back(object);
b1902fab
CH
2247 }
2248 }
4bd3d119 2249
b1902fab
CH
2250 resp->setBody(doc);
2251}
2252
c0f6a1da 2253void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
2054afbb 2254 if(req->method != "PUT")
a426cb89 2255 throw HttpMethodNotAllowedException();
80d59cd1 2256
c0f6a1da
CH
2257 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
2258
bf269e28 2259 uint64_t count = purgeAuthCachesExact(canon);
f682752a 2260 resp->setBody(Json::object {
bf269e28
RG
2261 { "count", (int) count },
2262 { "result", "Flushed cache." }
f682752a 2263 });
ddc84d12
CH
2264}
2265
dea47634 2266void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
c67bf8c5 2267{
80d59cd1
CH
2268 resp->headers["Cache-Control"] = "max-age=86400";
2269 resp->headers["Content-Type"] = "text/css";
c67bf8c5 2270
1071abdd 2271 ostringstream ret;
1071abdd
CH
2272 ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
2273 ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
2274 ret<<"a { color: #0959c2; }"<<endl;
2275 ret<<"a:hover { color: #3B8EC8; }"<<endl;
2276 ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
2277 ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
2278 ret<<".row:after { clear: both; }"<<endl;
2279 ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
2280 ret<<".all { width: 100%; }"<<endl;
2281 ret<<".headl { width: 60%; }"<<endl;
2282 ret<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2283 ret<<"background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJoAAAAUCAYAAAB1RSS/AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACtgAAArYBAHIqtQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABBTSURBVGiBtVp7cFRVmv9u3763b7/f9It00iFACBohgCEyQYgKI49CLV3cWaoEZBcfo2shu7KOtZbjrqOuVQtVWFuOrPqPRU3NgOIDlkgyJEYJwUAqjzEJedFJupN0p9/v+9o/mtve7r790HF+VbeSPue7555zz+98z4ucOXNmgWVZBH4AK5PJGIPBQBqNxpTNZkthGMZCCUxMTBCDg4PyiYkJWTQaRc1mc7Kuri7a1NQU4ssxDAOffPKJAQCynvnII494ESTddO3aNaXT6SS4TplMRj/44IM+7ndXV5dqfn5ewh9306ZNQZqmobu7W11qri0tLX6tVkv19vYqpqampPw+BEFYtVpNGQwG0mKxpJYsWUIKjTE6OiodGBhQ8NcgkUgYjUZDORyOhM1mSxV6fjAYFF+6dEnLb9NoNOR9990X4H53dHSovV4vzpfZvn27T6FQ0Py2sbExorOzU+N2uwmWZUGv15N33nlnuLGxMZy7byyVQEJ//nd9Yuz/lJR/HBdrHSlJ9baIuuV1L4LJ8/Y49pc/KcJX39WRC4MEgskY3Lourmn5rQdbckfe2ijfOBZo+40xNXtNysR9KLZkdVK+9oBf0fBkCABA3NraamTZwjxSKpXUAw884G1paQkUIty5c+f0Fy5cWMIfx+l0Snt6ejTt7e26AwcOuKxWawoAQCQSQW9vr3pxcTHrJTY3Nwe5Tb18+bJ2bGxMzvWhKMpu27bNj6IoCwDQ1tamd7lcRM79genpaaK1tdVQcDG3sXbt2rBWq6X6+/sV3d3d2mKyy5cvj+7cudO7atWqGL99bGxMWuxZOp0utX37du+9994b5A4Qh2AwiObei6Ioe/fdd4eVSiUNAHD16lX1+Pi4nC+zadOmIJ9oZ8+eNeTu3/T0tLSvr0/V3d0dPXr0qJNrZ+KL6MKpjZWUbyxzQMmFIYJcGCISw5+qjE9+M4UqLJmx/RdeWBK+elKfGTjuR+OhWSxx86JS/9D/zsrufDzMdSXGv5J5/vBYBZuKiLi25HS3LDndLUuMX1IYHjvtynQUQjgcFp89e9b8zjvv2BmGyepjWRbeffdd2/nz55cUIqvT6ZSeOHHC7vf7xVyb3W6P58rNzc1liOfxeLJISNM04na7Me63z+fD+P1SqZQupHn+Wty8eVN+4sSJyv7+fnlp6R/g8/nw06dPW0+ePLmUJEmklDxN08iVK1dU5Y7f0dGhvnjxYkElQVFU1jP9Xz5j4pMsSzYwifvPPWnhfsdHPpdnkYwHlk4ivi9/baFDM2IAACYZEi1++qSVTzI+YkN/VEe++726JNE4TE1Nyc6cOWPkt3322Wf6/v7+ki8nEAhgH3zwQWYhDoejINGSyaQoFAphuf2zs7MSAIBIJIImEgmU32ez2RLlruOngGVZ+Oijj6w+n09cWjobg4ODyg8//NBSWhLgu+++K4toJEkin376qancObBkFIl/f7bo2ImxC0om5kUBACK9pzTFZJlEAI0O/kEJABAf+UJOh115+8VH5MZHGkGimc3mRK66BwBoa2szBAIBMUB6w1tbW415QgUwOjqqGB4elgIA1NTU5BGN02IulwsXOqUul0sCADA/P5+3qIqKip+NaARBMBiGMbnt0Wg0z68qF729vepr164pS8k5nU7ZwsJC0U0DAOjp6VHGYjE0t10kEgmqt5TrOwIYqqRWTbmuSQAASM9fiFKy5Fx/Wnaur7Ss53tC8IQ+/fTTM/F4HH3rrbcc/E1nWRYmJyeJtWvXRr7++mt1rnoGANi6devipk2bgsePH7dHIpGs8Ts7O7W1tbXxqqqqJIZhLN+keDweDADA7XbjuWPebpcAACwsLOT1V1VVFSSayWRKvvLKK5P8tmLBTVNTk//hhx/2vv/++5aBgYEsLeB0OqWF7gMAsFqtiYqKivj169c1ueaytbVVv2HDhnChewHS7/fKlSuqPXv2LBaTyw1gAABqa2sjhw4dck1PT0vOnz9v4O+NWFNdlluBqispAABUYSEp/6TgPmRkVba0rGppybFRpZksaDodDkeioqIiT/M4nU4JAMDIyEiez1JTUxN9/PHHFyoqKpJbtmzx5faPj4/LANKOr9VqzRqbi7D4vhof8/PzOMAPhMyZa948OSAIAjiOs/xLSFvzIZFImO3bt+fNn9OqhaDRaMiDBw/Obd26NY8oTqdTWmhtfPT29paMmkOhUJ6CkEgkjFKppOvq6mIvvviis76+PkNqVF1BiQ21yWJjoiobiRlWpQAACMeWaKk5EMu2RQEAiOr7YyBCi2YliMrN0aI+Wjwez+vn/KOZmZk8lbl69eoI97+QeQwEAhgXFFRVVWX1+/1+nGVZyE1bcPB6vRKWZSE35JdKpbTJZCp4qiiKQmZmZnDuEiKqEITWTtN0SfMDALBjx45FiUSSZ35HRkaKakQAgPn5ecnU1FRRQuv1+rz0Qn9/v+ry5ctqgPTh2rFjR9ZB0e78Hzcgedb2NhDQ7vq9C24fQNXm3/gww8qCxJTX/4OfcGyJAwBgS+pSqo3/XFADo0oLqdn2lkeQaAzDIB0dHWqPx5O3YK1WSzIMA7lmEQDAaDSSQv/zEQwGUQCA6urqLKJRFIV4PB6MH3GqVCqS3z83N4cvLi5mEaVUIOD1evHXX399GXedOnXKWkweIJ3r++abb/IcYqPRWDA3xodUKmWEyMCZ/1IolQvMfXcAabN7+vRp68cff2wS8nElVVvihl99cQtV27PmhapspOHvzzmJ5Tsy6RtELGGX7G+7JV2xIysHiqAYq/rFv3h0e96f57drHnjTo2n57TwiJrIOl6SyOWo6cPmWiNAwgj7am2++6Ugmk4IkrK2tjUWjUVRoMXK5PJOHkclkdJ4AAESjURQAYPny5YKRJ59odXV1EX6ea2ZmRpKbf/s5AwEAgO+//17+8ssv1/j9/jzNt3HjxmC542g0GjI318etXQgoirKcxrx+/brKYDAUJPW6desiFy5ciM/MzORpyM7OTl04HEYPHz7synURiJpfxizPj4+T8/0S0jOEiw2rUrh5TRJE+TRAFWba+KvPZung9Hxy9iohwpUMvnRjQkSo8zQ1ICJQbX7Zp2h8LpCa7ZEwUY8Yt21IiHXLMopCkEyFSFZZWRmz2+0FVSqXUL39v6AM5yTr9XpKrVZnab2RkRFZKpXKPHvlypUxvuM+PT0tCQaDWW+lWCDwUzA3N0cIkay2tjbS0tLiL3ccoYNWzPRWVVXFcBxnAACCwSAmRCIOCILA/v373QqFghLqv3Hjhrq9vb1gioIFBNLFoLI8gbKBILdHRNi8ocvOC6nVavLw4cOzAAAKhYJGEARytRo/5A6Hw4JMk8lkmRNht9vjAwMDmU0dGhril3TAbDanDAZD0u12EwAAw8PDCoZhspZQLBD4KRBa17Zt27wPPfSQVyQqO+0IQumHQloeIB0Jr169Onzjxg01QOHDzqGioiJ55MiRW8ePH68UCg6+/PJLY0tLS4Cv1RJjF2W+z5+2UEFnxiqgKhup2/muW7pyV1YAQEfmUN9n/2SOj57PRN4IirHKphe86q2vLSIozktHMBDq+p0u3PkfRpZKZOYtqWyOavd86BZrlxWOOjMTQVH2jjvuCL/wwgtOvV5PAaQ3QyqV5r20SCSSebmhUEiQaCqVKnNfLkk4QnEwmUyk2WzOaNDp6emsU14qEABIO87Hjh2b5K79+/e7i8kLVS0UCgXF19blINfEAwCoVCpBDcShsbExVKw/FzabLXXs2LFJIT81Go2K+YFPYqpDuvDx7ko+yQAA6NAs5jn9sD1+84KMa2OpJLLw0X2VfJIBALA0iYS6/svoO/ePWcni4KWXjKH2V0x8kgEAJG99Lfd8uLmSSfiFj+j999/v3bt3r/vgwYMzb7zxxthzzz03w9UqOVit1rzFjY6OZiY7NDSUl/4gCIIxmUyZcZYtW1ZQG0mlUloul9Nmszkjn1sCK6cigGEY63A4EtxlsViKOvQOhyOm0WiyyNve3q4vN+IESKeAhKJnISeej/r6+ijfzy2Evr4+Oad19Xo9dejQoVkhbev1ejNE83/xjAXYfPcqDRZ8nz9lhdtjhjr/U0d6RwoGLtH+j7WJyctSAADSM4SHu/9bsFwFAECHXVjwq381ChKtubk50NLSEmhsbAxrNBrBU7hixYq8XMvg4KByamqKmJubw7799ts8H6GqqirGV+XV1dWJQppCq9WSAABWq7WgT/hzBwIAaW3d0NCQpVkCgQDW1dVVVnnI5XLhp06dsuW24zjO1NTUFJ0viqJsfX19Sa3W09Ojfu+996xcCkapVNIoiuaxyGAwkAAAdHBaXIw4AGnNRnqHcQCAxOTlknXdxHirHAAgOXFJBkzxQ5ic6pD/6Nodh9uRT1YxPRaLoW+//XaVWCxmhXyMe+65J8D/jeM4a7FYEkKOL5ceWLp0aUGiVVZWliSax+PBX3rppRp+27PPPjtdLKhpamoKtre3Z53Sr776yrB58+a8LzH4GB4eVr722muCpaaGhoYgQRCFVEoGGzduDF65cqVkqevGjRvqgYEBld1uj8/NzUlIMtsNwnGc4VJMlH+yrNwhFbglxoyrUnTEXVKeDs2K039nSstG5rDyvdscLF26NNnQ0JAX7tM0jQiRzGQyJdevXx/Jba+srBQ0J3q9ngRIBwRisVhQ65UTCNA0jQQCAYx/CZXO+LDb7UmLxZJFYo/Hg1+9erVovTLXtHMgCILevXt30bISh5UrV8ZzTXchUBSFTExMyIQCj7q6ugh3KHDbugSIhN8hHxLb+iQAAGasK+2SmOvTsuY1pWWNqxI/mWgAAI8++uiCTqcrmcTEMIzZt2+fW8hMFvJbuNMoEokEM+FSqZQ2m81/k0+DAADWr1+fZ8IuXrxY8lu3XKAoyu7bt8/NmbFSEDLdPxYSiYTZu3dvJqmKYHJWturhomNKa34ZFskMNACAYt2hQDFZEaGh5XfsDQMAECt2R1Glreja5GsOBP4qoul0Ouro0aO3TCZTQTOkUqnII0eO3FqxYoUgoYRKVQAA/ISl0Ph/60+Dmpqa8syky+Ui+vr6yv4uTavVks8///ytUsV0oWf/GHk+pFIp/cQTT8zqdLos31q36+S8WFcjuE9iTVVK99CpTDQuXbk7qmz8taAGRlAJq9t50o2qllIAACKJitHu+cCF4ApBdS5d/XdB+fqnguLq6upobm4Kx/GyQ3m9Xk+9+uqrk21tbZquri6t1+vFWZYFi8WSdDgcsV27di1qtdqCYb3ZbCZra2sjueaW/yl0XV1dNBwOZ/mT/KIxB6VSSTkcjlhuey44X8lkMqVy5TmC6/V6qrGx0Z8bPY6OjsrWrFkT1el0ec9CUZRVqVSUWq2mqqur4xs2bAgL+XQSiYTJvZcf9Njt9uRdd90Vys2PcQnd5ubmAMMwcPPmTXk0GhUDpCsRVVVVsccee2yBS0PxIZLqacszfZPBP7+qj4+1Kilf+lNuYtkDEU3La3mfcmsfPL4gqfxFrJxPuYll22Kmp/omgpf+zZia7ZEyCT+KGVcn5WsP+uUNh0IAAP8PaQRnE4MgdzkAAAAASUVORK5CYII=);";
2284 ret<<" width: 154px; height: 20px; }"<<endl;
2285 ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
2286 ret<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
2287 ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
2288 ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
2289 ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
2290 ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
2291 ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
2292 ret<<"table.data tr:hover { background: white; }"<<endl;
2293 ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
2294 ret<<".resetring {float: right; }"<<endl;
2295 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;
2296 ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
2297 ret<<".resizering {float: right;}"<<endl;
80d59cd1 2298 resp->body = ret.str();
c146576d 2299 resp->status = 200;
1071abdd
CH
2300}
2301
dea47634 2302void AuthWebServer::webThread()
12c86877
BH
2303{
2304 try {
519f5484 2305 setThreadName("pdns/webserver");
479e0976 2306 if(::arg().mustDo("api")) {
c0f6a1da 2307 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
46d06a12 2308 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
46d06a12
PL
2309 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
2310 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
f711ef76
PL
2311 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail);
2312 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys);
46d06a12
PL
2313 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
2314 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
2315 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
2316 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
24e11043
CJ
2317 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind);
2318 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata);
46d06a12 2319 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
4bc8379e 2320 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify);
46d06a12
PL
2321 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
2322 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
2323 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
2324 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
9e6d2033 2325 d_ws->registerApiHandler("/api", &apiDiscovery);
c67bf8c5 2326 }
536ab56f
CH
2327 if (::arg().mustDo("webserver")) {
2328 d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
2329 d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
2330 }
96d299db 2331 d_ws->go();
12c86877
BH
2332 }
2333 catch(...) {
e6a9dde5 2334 g_log<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
5bd2ea7b 2335 _exit(1);
12c86877
BH
2336 }
2337}