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