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