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