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