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