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