]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/ws-auth.cc
API: port recursor /jsonstat to json11
[thirdparty/pdns.git] / pdns / ws-auth.cc
CommitLineData
12c86877 1/*
a426cb89 2 Copyright (C) 2002 - 2015 PowerDNS.COM BV
12c86877
BH
3
4 This program is free software; you can redistribute it and/or modify
6ec5e728 5 it under the terms of the GNU General Public License version 2
9054d8a4 6 as published by the Free Software Foundation
12c86877 7
f782fe38
MH
8 Additionally, the license of this program contains a special
9 exception which allows to distribute the program in binary form when
10 it is linked against OpenSSL.
11
12c86877
BH
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
06bd9ccf 19 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
9054d8a4 20*/
870a0fe4
AT
21#ifdef HAVE_CONFIG_H
22#include "config.h"
23#endif
9054d8a4 24#include "utility.hh"
d267d1bf 25#include "dynlistener.hh"
2470b36e 26#include "ws-auth.hh"
e611a06c 27#include "json.hh"
12c86877
BH
28#include "webserver.hh"
29#include "logger.hh"
e611a06c 30#include "packetcache.hh"
12c86877
BH
31#include "statbag.hh"
32#include "misc.hh"
33#include "arguments.hh"
34#include "dns.hh"
6cc98ddf 35#include "comment.hh"
e611a06c 36#include "ueberbackend.hh"
dcc65f25 37#include <boost/format.hpp>
fa8fd4d2 38
9ac4a7c6 39#include "namespaces.hh"
ca9fc6a1 40#include "rapidjson/document.h"
8537b9f0
BH
41#include "rapidjson/stringbuffer.h"
42#include "rapidjson/writer.h"
6ec5e728 43#include "ws-api.hh"
ba1a571d 44#include "version.hh"
d29d5db7 45#include "dnsseckeeper.hh"
3c3c006b 46#include <iomanip>
0f0e73fe 47#include "zoneparser-tng.hh"
a426cb89 48#include "common_startup.hh"
3c3c006b 49
8537b9f0
BH
50
51using namespace rapidjson;
24afabad 52using json11::Json;
12c86877
BH
53
54extern StatBag S;
55
f63168e6
CH
56static void patchZone(HttpRequest* req, HttpResponse* resp);
57static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr);
58
dea47634 59AuthWebServer::AuthWebServer()
12c86877
BH
60{
61 d_start=time(0);
96d299db 62 d_min10=d_min5=d_min1=0;
c81c2ea8 63 d_ws = 0;
f17c93b4 64 d_tid = 0;
825fa717 65 if(arg().mustDo("webserver")) {
bbef8f04 66 d_ws = new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
825fa717
CH
67 d_ws->bind();
68 }
12c86877
BH
69}
70
dea47634 71void AuthWebServer::go()
12c86877 72{
c81c2ea8
PD
73 if(arg().mustDo("webserver"))
74 {
75 S.doRings();
dea47634 76 pthread_create(&d_tid, 0, webThreadHelper, this);
c81c2ea8
PD
77 pthread_create(&d_tid, 0, statThreadHelper, this);
78 }
12c86877
BH
79}
80
dea47634 81void AuthWebServer::statThread()
12c86877
BH
82{
83 try {
84 for(;;) {
85 d_queries.submit(S.read("udp-queries"));
86 d_cachehits.submit(S.read("packetcache-hit"));
87 d_cachemisses.submit(S.read("packetcache-miss"));
88 d_qcachehits.submit(S.read("query-cache-hit"));
89 d_qcachemisses.submit(S.read("query-cache-miss"));
90 Utility::sleep(1);
91 }
92 }
93 catch(...) {
94 L<<Logger::Error<<"Webserver statThread caught an exception, dying"<<endl;
95 exit(1);
96 }
97}
98
dea47634 99void *AuthWebServer::statThreadHelper(void *p)
12c86877 100{
dea47634
CH
101 AuthWebServer *self=static_cast<AuthWebServer *>(p);
102 self->statThread();
12c86877
BH
103 return 0; // never reached
104}
105
dea47634 106void *AuthWebServer::webThreadHelper(void *p)
12c86877 107{
dea47634
CH
108 AuthWebServer *self=static_cast<AuthWebServer *>(p);
109 self->webThread();
12c86877
BH
110 return 0; // never reached
111}
112
9f3fdaa0
CH
113static string htmlescape(const string &s) {
114 string result;
115 for(string::const_iterator it=s.begin(); it!=s.end(); ++it) {
116 switch (*it) {
117 case '&':
c86a96f9 118 result += "&amp;";
9f3fdaa0
CH
119 break;
120 case '<':
121 result += "&lt;";
122 break;
123 case '>':
124 result += "&gt;";
125 break;
c7f59d62
PL
126 case '"':
127 result += "&quot;";
128 break;
9f3fdaa0
CH
129 default:
130 result += *it;
131 }
132 }
133 return result;
134}
135
12c86877
BH
136void printtable(ostringstream &ret, const string &ringname, const string &title, int limit=10)
137{
138 int tot=0;
139 int entries=0;
101b5d5d 140 vector<pair <string,unsigned int> >ring=S.getRing(ringname);
12c86877 141
1071abdd 142 for(vector<pair<string, unsigned int> >::const_iterator i=ring.begin(); i!=ring.end();++i) {
12c86877
BH
143 tot+=i->second;
144 entries++;
145 }
146
1071abdd 147 ret<<"<div class=\"panel\">";
c7f59d62 148 ret<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname)<<"\">Reset</a></span>"<<endl;
1071abdd
CH
149 ret<<"<h2>"<<title<<"</h2>"<<endl;
150 ret<<"<div class=ringmeta>";
c7f59d62 151 ret<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname)<<"\">Showing: Top "<<limit<<" of "<<entries<<"</a>"<<endl;
1071abdd 152 ret<<"<span class=resizering>Resize: ";
bb3c3f50 153 unsigned int sizes[]={10,100,500,1000,10000,500000,0};
12c86877
BH
154 for(int i=0;sizes[i];++i) {
155 if(S.getRingSize(ringname)!=sizes[i])
c7f59d62 156 ret<<"<a href=\"?resizering="<<htmlescape(ringname)<<"&amp;size="<<sizes[i]<<"\">"<<sizes[i]<<"</a> ";
12c86877
BH
157 else
158 ret<<"("<<sizes[i]<<") ";
159 }
1071abdd 160 ret<<"</span></div>";
12c86877 161
1071abdd 162 ret<<"<table class=\"data\">";
12c86877 163 int printed=0;
f5cb7e61 164 int total=max(1,tot);
bb3c3f50 165 for(vector<pair<string,unsigned int> >::const_iterator i=ring.begin();limit && i!=ring.end();++i,--limit) {
dea47634 166 ret<<"<tr><td>"<<htmlescape(i->first)<<"</td><td>"<<i->second<<"</td><td align=right>"<< AuthWebServer::makePercentage(i->second*100.0/total)<<"</td>"<<endl;
12c86877
BH
167 printed+=i->second;
168 }
169 ret<<"<tr><td colspan=3></td></tr>"<<endl;
170 if(printed!=tot)
dea47634 171 ret<<"<tr><td><b>Rest:</b></td><td><b>"<<tot-printed<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot-printed)*100.0/total)<<"</b></td>"<<endl;
12c86877 172
e2a77e08 173 ret<<"<tr><td><b>Total:</b></td><td><b>"<<tot<<"</b></td><td align=right><b>100%</b></td>";
1071abdd 174 ret<<"</table></div>"<<endl;
12c86877
BH
175}
176
dea47634 177void AuthWebServer::printvars(ostringstream &ret)
12c86877 178{
1071abdd 179 ret<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl;
12c86877
BH
180
181 vector<string>entries=S.getEntries();
182 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
183 ret<<"<tr><td>"<<*i<<"</td><td>"<<S.read(*i)<<"</td><td>"<<S.getDescrip(*i)<<"</td>"<<endl;
184 }
e2a77e08 185
1071abdd 186 ret<<"</table></div>"<<endl;
12c86877
BH
187}
188
dea47634 189void AuthWebServer::printargs(ostringstream &ret)
12c86877 190{
e2a77e08 191 ret<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl;
12c86877
BH
192
193 vector<string>entries=arg().list();
194 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
195 ret<<"<tr><td>"<<*i<<"</td><td>"<<arg()[*i]<<"</td><td>"<<arg().getHelp(*i)<<"</td>"<<endl;
196 }
197}
198
dea47634 199string AuthWebServer::makePercentage(const double& val)
b6f57093
BH
200{
201 return (boost::format("%.01f%%") % val).str();
202}
203
dea47634 204void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
12c86877 205{
583ea80d
CH
206 if(!req->getvars["resetring"].empty()) {
207 if (S.ringExists(req->getvars["resetring"]))
208 S.resetRing(req->getvars["resetring"]);
80d59cd1 209 resp->status = 301;
0665b7e6 210 resp->headers["Location"] = req->url.path;
80d59cd1 211 return;
12c86877 212 }
583ea80d 213 if(!req->getvars["resizering"].empty()){
335da0ba 214 int size=std::stoi(req->getvars["size"]);
583ea80d 215 if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000)
335da0ba 216 S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
80d59cd1 217 resp->status = 301;
0665b7e6 218 resp->headers["Location"] = req->url.path;
80d59cd1 219 return;
12c86877
BH
220 }
221
222 ostringstream ret;
223
1071abdd
CH
224 ret<<"<!DOCTYPE html>"<<endl;
225 ret<<"<html><head>"<<endl;
226 ret<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl;
227 ret<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl;
228 ret<<"</head><body>"<<endl;
229
230 ret<<"<div class=\"row\">"<<endl;
231 ret<<"<div class=\"headl columns\">";
a1caa8b8 232 ret<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION);
1071abdd 233 if(!arg()["config-name"].empty()) {
a1caa8b8 234 ret<<" ["<<htmlescape(arg()["config-name"])<<"]";
1071abdd
CH
235 }
236 ret<<"</a></div>"<<endl;
237 ret<<"<div class=\"headr columns\"></div></div>";
238 ret<<"<div class=\"row\"><div class=\"all columns\">";
12c86877
BH
239
240 time_t passed=time(0)-s_starttime;
241
e2a77e08
KM
242 ret<<"<p>Uptime: "<<
243 humanDuration(passed)<<
244 "<br>"<<endl;
12c86877 245
395b07ea 246 ret<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
f6154a3b
CH
247 d_queries.get1()<<", "<<
248 d_queries.get5()<<", "<<
249 d_queries.get10()<<". Max queries/second: "<<d_queries.getMax()<<
12c86877 250 "<br>"<<endl;
1d6b70f9 251
f6154a3b 252 if(d_cachemisses.get10()+d_cachehits.get10()>0)
b6f57093 253 ret<<"Cache hitrate, 1, 5, 10 minute averages: "<<
f6154a3b
CH
254 makePercentage((d_cachehits.get1()*100.0)/((d_cachehits.get1())+(d_cachemisses.get1())))<<", "<<
255 makePercentage((d_cachehits.get5()*100.0)/((d_cachehits.get5())+(d_cachemisses.get5())))<<", "<<
256 makePercentage((d_cachehits.get10()*100.0)/((d_cachehits.get10())+(d_cachemisses.get10())))<<
b6f57093 257 "<br>"<<endl;
12c86877 258
f6154a3b 259 if(d_qcachemisses.get10()+d_qcachehits.get10()>0)
395b07ea 260 ret<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
f6154a3b
CH
261 makePercentage((d_qcachehits.get1()*100.0)/((d_qcachehits.get1())+(d_qcachemisses.get1())))<<", "<<
262 makePercentage((d_qcachehits.get5()*100.0)/((d_qcachehits.get5())+(d_qcachemisses.get5())))<<", "<<
263 makePercentage((d_qcachehits.get10()*100.0)/((d_qcachehits.get10())+(d_qcachemisses.get10())))<<
b6f57093 264 "<br>"<<endl;
12c86877 265
395b07ea 266 ret<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
f6154a3b
CH
267 d_qcachemisses.get1()<<", "<<
268 d_qcachemisses.get5()<<", "<<
269 d_qcachemisses.get10()<<". Max queries/second: "<<d_qcachemisses.getMax()<<
12c86877
BH
270 "<br>"<<endl;
271
1071abdd 272 ret<<"Total queries: "<<S.read("udp-queries")<<". Question/answer latency: "<<S.read("latency")/1000.0<<"ms</p><br>"<<endl;
583ea80d 273 if(req->getvars["ring"].empty()) {
12c86877
BH
274 vector<string>entries=S.listRings();
275 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i)
276 printtable(ret,*i,S.getRingTitle(*i));
277
f6154a3b 278 printvars(ret);
12c86877 279 if(arg().mustDo("webserver-print-arguments"))
f6154a3b 280 printargs(ret);
12c86877
BH
281 }
282 else
583ea80d 283 printtable(ret,req->getvars["ring"],S.getRingTitle(req->getvars["ring"]),100);
12c86877 284
1071abdd 285 ret<<"</div></div>"<<endl;
e23a627e 286 ret<<"<footer class=\"row\">"<<fullVersionString()<<"<br>&copy; 2013 - 2015 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl;
12c86877
BH
287 ret<<"</body></html>"<<endl;
288
80d59cd1 289 resp->body = ret.str();
61f5d289 290 resp->status = 200;
12c86877
BH
291}
292
1d6b70f9
CH
293/** Helper to build a record content as needed. */
294static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot) {
295 // noDot: for backend storage, pass true. for API users, pass false.
296 return DNSRecordContent::mastermake(qtype.getCode(), 1, content)->getZoneRepresentation(noDot);
297}
298
299/** "Normalize" record content for API consumers. */
300static inline string makeApiRecordContent(const QType& qtype, const string& content) {
301 return makeRecordContent(qtype, content, false);
302}
303
304/** "Normalize" record content for backend storage. */
305static inline string makeBackendRecordContent(const QType& qtype, const string& content) {
306 return makeRecordContent(qtype, content, true);
307}
308
62a9a74c 309static Json::object getZoneInfo(const DomainInfo& di) {
c04b5870 310 DNSSECKeeper dk;
290a083d 311 string zoneId = apiZoneNameToId(di.zone);
62a9a74c
CH
312 return Json::object {
313 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
314 { "id", zoneId },
315 { "url", "api/v1/servers/localhost/zones/" + zoneId },
316 { "name", di.zone.toString() },
317 { "kind", di.getKindString() },
318 { "dnssec", dk.isSecuredZone(di.zone) },
319 { "account", di.account },
320 { "masters", di.masters },
321 { "serial", (double)di.serial },
322 { "notified_serial", (double)di.notified_serial },
323 { "last_check", (double)di.last_check }
324 };
c04b5870
CH
325}
326
290a083d 327static void fillZone(const DNSName& zonename, HttpResponse* resp) {
1abb81f4 328 UeberBackend B;
1abb81f4 329 DomainInfo di;
73301d73 330 if(!B.getDomainInfo(zonename, di))
290a083d 331 throw ApiException("Could not find domain '"+zonename.toString()+"'");
1abb81f4 332
62a9a74c
CH
333 Json::object doc = getZoneInfo(di);
334 // extra stuff getZoneInfo doesn't do for us (more expensive)
d29d5db7
CH
335 string soa_edit_api;
336 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
62a9a74c 337 doc["soa_edit_api"] = soa_edit_api;
6bb25159
MS
338 string soa_edit;
339 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
62a9a74c 340 doc["soa_edit"] = soa_edit;
1abb81f4 341
6cc98ddf 342 // fill records
1abb81f4 343 DNSResourceRecord rr;
62a9a74c 344 Json::array records;
cea26350 345 di.backend->list(zonename, di.id, true); // incl. disabled
3d89fc28 346 while(di.backend->get(rr)) {
1abb81f4
CH
347 if (!rr.qtype.getCode())
348 continue; // skip empty non-terminals
349
62a9a74c
CH
350 records.push_back(Json::object {
351 { "name", rr.qname.toString() },
352 { "type", rr.qtype.getName() },
353 { "ttl", (double)rr.ttl },
354 { "disabled", rr.disabled },
355 { "content", makeApiRecordContent(rr.qtype, rr.content) }
356 });
1abb81f4 357 }
62a9a74c 358 doc["records"] = records;
1abb81f4 359
6cc98ddf
CH
360 // fill comments
361 Comment comment;
62a9a74c 362 Json::array comments;
6cc98ddf
CH
363 di.backend->listComments(di.id);
364 while(di.backend->getComment(comment)) {
62a9a74c
CH
365 comments.push_back(Json::object {
366 { "name", comment.qname },
367 { "type", comment.qtype.getName() },
368 { "modified_at", (double)comment.modified_at },
369 { "account", comment.account },
370 { "content", comment.content }
371 });
6cc98ddf 372 }
62a9a74c 373 doc["comments"] = comments;
6cc98ddf 374
669822d0 375 resp->setBody(doc);
1abb81f4
CH
376}
377
6ec5e728
CH
378void productServerStatisticsFetch(map<string,string>& out)
379{
a45303b8 380 vector<string> items = S.getEntries();
ff05fd12 381 for(const string& item : items) {
335da0ba 382 out[item] = std::to_string(S.read(item));
a45303b8
CH
383 }
384
385 // add uptime
335da0ba 386 out["uptime"] = std::to_string(time(0) - s_starttime);
c67bf8c5
CH
387}
388
1f68b185 389static void gatherRecords(const Json container, vector<DNSResourceRecord>& new_records, vector<DNSResourceRecord>& new_ptrs) {
f63168e6
CH
390 UeberBackend B;
391 DNSResourceRecord rr;
1f68b185
CH
392 for(auto record : container["records"].array_items()) {
393 rr.qname = apiNameToDNSName(stringFromJson(record, "name"));
394 rr.qtype = stringFromJson(record, "type");
395 string content = stringFromJson(record, "content");
396 rr.auth = 1;
397 rr.ttl = intFromJson(record, "ttl");
398 rr.disabled = boolFromJson(record, "disabled");
399
400 if (rr.qtype.getCode() == 0) {
401 throw ApiException("Record "+rr.qname.toString()+"/"+stringFromJson(record, "type")+" is of unknown type");
402 }
24cd86ca 403
1f68b185
CH
404 // validate that the client sent something we can actually parse, and require that data to be dotted.
405 try {
406 if (rr.qtype.getCode() != QType::AAAA) {
407 string tmp = makeApiRecordContent(rr.qtype, content);
408 if (!pdns_iequals(tmp, content)) {
409 throw std::runtime_error("Not in expected format (parsed as '"+tmp+"')");
410 }
411 } else {
412 struct in6_addr tmpbuf;
413 if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
414 throw std::runtime_error("Invalid IPv6 address");
1e5b9ab9 415 }
f63168e6 416 }
1f68b185
CH
417 rr.content = makeBackendRecordContent(rr.qtype, content);
418 }
419 catch(std::exception& e)
420 {
421 throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.getName()+" '"+content+"': "+e.what());
422 }
f63168e6 423
1f68b185
CH
424 if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) &&
425 boolFromJson(record, "set-ptr", false) == true) {
426 DNSResourceRecord ptr;
427 makePtr(rr, &ptr);
f63168e6 428
1f68b185
CH
429 // verify that there's a zone for the PTR
430 DNSPacket fakePacket;
431 SOAData sd;
432 fakePacket.qtype = QType::PTR;
433 if (!B.getAuth(&fakePacket, &sd, ptr.qname))
434 throw ApiException("Could not find domain for PTR '"+ptr.qname.toString()+"' requested for '"+ptr.content+"'");
f63168e6 435
1f68b185
CH
436 ptr.domain_id = sd.domain_id;
437 new_ptrs.push_back(ptr);
f63168e6 438 }
1f68b185
CH
439
440 new_records.push_back(rr);
f63168e6
CH
441 }
442}
443
1f68b185 444static void gatherComments(const Json container, vector<Comment>& new_comments, bool use_name_type_from_container) {
f63168e6
CH
445 Comment c;
446 if (use_name_type_from_container) {
447 c.qname = stringFromJson(container, "name");
448 c.qtype = stringFromJson(container, "type");
449 }
450
451 time_t now = time(0);
1f68b185
CH
452 for (auto comment : container["comments"].array_items()) {
453 if (!use_name_type_from_container) {
454 c.qname = stringFromJson(comment, "name");
455 c.qtype = stringFromJson(comment, "type");
f63168e6 456 }
1f68b185
CH
457 c.modified_at = intFromJson(comment, "modified_at", now);
458 c.content = stringFromJson(comment, "content");
459 c.account = stringFromJson(comment, "account");
460 new_comments.push_back(c);
f63168e6
CH
461 }
462}
6cc98ddf 463
1f68b185
CH
464static void updateDomainSettingsFromDocument(const DomainInfo& di, const DNSName& zonename, const Json document) {
465 string zonemaster;
466 for(auto value : document["masters"].array_items()) {
467 string master = value.string_value();
468 if (master.empty())
469 throw ApiException("Master can not be an empty string");
470 zonemaster += master + " ";
bb9fd223
CH
471 }
472
473 di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
1f68b185 474 di.backend->setMaster(zonename, zonemaster);
d29d5db7 475
1f68b185
CH
476 if (document["soa_edit_api"].is_string()) {
477 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
d29d5db7 478 }
1f68b185
CH
479 if (document["soa_edit"].is_string()) {
480 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
6bb25159 481 }
1f68b185
CH
482 if (document["account"].is_string()) {
483 di.backend->setAccount(zonename, document["account"].string_value());
79532aa7 484 }
bb9fd223
CH
485}
486
4b7f120a
MS
487static void apiZoneCryptokeys(HttpRequest* req, HttpResponse* resp) {
488 if(req->method != "GET")
489 throw ApiException("Only GET is implemented");
490
290a083d 491 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
4b7f120a
MS
492
493 UeberBackend B;
494 DomainInfo di;
495 DNSSECKeeper dk;
496
497 if(!B.getDomainInfo(zonename, di))
290a083d 498 throw ApiException("Could not find domain '"+zonename.toString()+"'");
4b7f120a 499
d8455c78 500 DNSSECKeeper::keyset_t keyset=dk.getKeys(zonename, boost::indeterminate, false);
4b7f120a
MS
501
502 if (keyset.empty())
290a083d 503 throw ApiException("No keys for zone '"+zonename.toString()+"'");
4b7f120a 504
24afabad
CH
505 Json::array doc;
506 for(const DNSSECKeeper::keyset_t::value_type value : keyset) {
583ea80d 507 if (req->parameters.count("key_id")) {
335da0ba
AT
508 int keyid = std::stoi(req->parameters["key_id"]);
509 int curid = value.second.id;
38809e97
MS
510 if (keyid != curid)
511 continue;
512 }
24afabad
CH
513
514 Json::object key {
515 { "type", "Cryptokey" },
516 { "id", (int)value.second.id },
517 { "active", value.second.active },
518 { "keytype", value.second.keyOrZone ? "ksk" : "zsk" },
519 { "dnskey", value.first.getDNSKEY().getZoneRepresentation() }
520 };
521
583ea80d 522 if (req->parameters.count("key_id")) {
335da0ba 523 DNSSECPrivateKey dpk=dk.getKeyById(zonename, std::stoi(req->parameters["key_id"]));
24afabad 524 key["content"] = dpk.getKey()->convertToISC();
38809e97 525 }
4b7f120a
MS
526
527 if (value.second.keyOrZone) {
24afabad
CH
528 Json::array dses;
529 for(const int keyid : { 1, 2, 3, 4 })
4b7f120a 530 try {
24afabad
CH
531 dses.push_back(makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation());
532 } catch (...) {}
533 key["ds"] = dses;
4b7f120a 534 }
24afabad 535 doc.push_back(key);
4b7f120a
MS
536 }
537
538 resp->setBody(doc);
539}
540
1f68b185 541static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, DNSName zonename) {
0f0e73fe
MS
542 DNSResourceRecord rr;
543 vector<string> zonedata;
1f68b185 544 stringtok(zonedata, zonestring, "\r\n");
0f0e73fe
MS
545
546 ZoneParserTNG zpt(zonedata, zonename);
547
548 bool seenSOA=false;
549
550 string comment = "Imported via the API";
551
552 try {
553 while(zpt.get(rr, &comment)) {
554 if(seenSOA && rr.qtype.getCode() == QType::SOA)
555 continue;
556 if(rr.qtype.getCode() == QType::SOA)
557 seenSOA=true;
558
0f0e73fe
MS
559 new_records.push_back(rr);
560 }
561 }
562 catch(std::exception& ae) {
563 throw ApiException("An error occured while parsing the zonedata: "+string(ae.what()));
564 }
565}
566
80d59cd1 567static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
e2dba705 568 UeberBackend B;
559115f6 569 DNSSECKeeper dk;
d07bf7ff 570 if (req->method == "POST" && !::arg().mustDo("api-readonly")) {
e2dba705 571 DomainInfo di;
1f68b185 572 auto document = req->json();
c576d0c5 573 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
1d6b70f9 574 apiCheckNameAllowedCharacters(zonename.toString());
4ebf78b1 575
1f68b185 576 string zonestring = document["zone"].string_value();
406497f5 577
1d6b70f9 578 bool exists = B.getDomainInfo(zonename, di);
e2dba705 579 if(exists)
1d6b70f9 580 throw ApiException("Domain '"+zonename.toString()+"' already exists");
e2dba705 581
bb9fd223 582 // validate 'kind' is set
4bdff352 583 DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
bb9fd223 584
1f68b185
CH
585 auto records = document["records"];
586 if (records.is_array() && zonestring != "")
0f0e73fe
MS
587 throw ApiException("You cannot give zonedata AND records");
588
1f68b185
CH
589 auto nameservers = document["nameservers"];
590 if (!nameservers.is_array() && zonekind != DomainInfo::Slave)
f63168e6 591 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
e2dba705 592
f63168e6 593 string soa_edit_api_kind;
1f68b185
CH
594 if (document["soa_edit_api"].is_string()) {
595 soa_edit_api_kind = document["soa_edit_api"].string_value();
a6448d95
CH
596 }
597 else {
598 soa_edit_api_kind = "DEFAULT";
599 }
1f68b185 600 string soa_edit_kind = document["soa_edit"].string_value();
e90b4e38 601
f63168e6
CH
602 // if records/comments are given, load and check them
603 bool have_soa = false;
604 vector<DNSResourceRecord> new_records;
605 vector<Comment> new_comments;
606 vector<DNSResourceRecord> new_ptrs;
0f0e73fe 607
1f68b185 608 if (records.is_array()) {
0f0e73fe
MS
609 gatherRecords(document, new_records, new_ptrs);
610 } else if (zonestring != "") {
1f68b185 611 gatherRecordsFromZone(zonestring, new_records, zonename);
0f0e73fe
MS
612 }
613
f63168e6 614 gatherComments(document, new_comments, false);
e2dba705 615
1f68b185 616 for(auto& rr : new_records) {
1d6b70f9 617 if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
561434a6 618 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": Name is out of zone");
1d6b70f9 619 apiCheckNameAllowedCharacters(rr.qname.toString());
f63168e6 620
1d6b70f9 621 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
f63168e6 622 have_soa = true;
a6448d95 623 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
624 // fixup dots after serializeSOAData/increaseSOARecord
625 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
f63168e6
CH
626 }
627 }
f7bfeb30
CH
628
629 // synthesize RRs as needed
630 DNSResourceRecord autorr;
1d6b70f9 631 autorr.qname = zonename;
f7bfeb30
CH
632 autorr.auth = 1;
633 autorr.ttl = ::arg().asNum("default-ttl");
e2dba705 634
4de11a54 635 if (!have_soa && zonekind != DomainInfo::Slave) {
f63168e6 636 // synthesize a SOA record so the zone "really" exists
1d6b70f9
CH
637 string soa = (boost::format("%s %s %lu")
638 % ::arg()["default-soa-name"]
639 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename).toString() : ::arg()["default-soa-mail"])
1f68b185 640 % document["serial"].int_value()
1d6b70f9 641 ).str();
f63168e6 642 SOAData sd;
1d6b70f9 643 fillSOAData(soa, sd); // fills out default values for us
f7bfeb30 644 autorr.qtype = "SOA";
1d6b70f9 645 autorr.content = serializeSOAData(sd);
f7bfeb30 646 increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
647 // fixup dots after serializeSOAData/increaseSOARecord
648 autorr.content = makeBackendRecordContent(autorr.qtype, autorr.content);
f7bfeb30 649 new_records.push_back(autorr);
f63168e6
CH
650 }
651
652 // create NS records if nameservers are given
1f68b185
CH
653 for (auto value : nameservers.array_items()) {
654 string nameserver = value.string_value();
655 if (nameserver.empty())
656 throw ApiException("Nameservers must be non-empty strings");
657 if (!isCanonical(nameserver))
658 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
659 try {
660 // ensure the name parses
661 autorr.content = DNSName(nameserver).toStringNoDot();
662 } catch (...) {
663 throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
4bdff352 664 }
1f68b185
CH
665 autorr.qtype = "NS";
666 new_records.push_back(autorr);
e2dba705
CH
667 }
668
f63168e6 669 // no going back after this
1d6b70f9
CH
670 if(!B.createDomain(zonename))
671 throw ApiException("Creating domain '"+zonename.toString()+"' failed");
f63168e6 672
1d6b70f9
CH
673 if(!B.getDomainInfo(zonename, di))
674 throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");
f63168e6 675
1d6b70f9 676 di.backend->startTransaction(zonename, di.id);
f63168e6 677
abb873ee 678 for(auto rr : new_records) {
f63168e6 679 rr.domain_id = di.id;
e2dba705
CH
680 di.backend->feedRecord(rr);
681 }
1d6b70f9 682 for(Comment& c : new_comments) {
f63168e6
CH
683 c.domain_id = di.id;
684 di.backend->feedComment(c);
685 }
e2dba705 686
1d6b70f9 687 updateDomainSettingsFromDocument(di, zonename, document);
e2dba705 688
f63168e6
CH
689 di.backend->commitTransaction();
690
1d6b70f9 691 fillZone(zonename, resp);
64a36f0d 692 resp->status = 201;
e2dba705
CH
693 return;
694 }
695
c67bf8c5
CH
696 if(req->method != "GET")
697 throw HttpMethodNotAllowedException();
698
c67bf8c5 699 vector<DomainInfo> domains;
cea26350 700 B.getAllDomains(&domains, true); // incl. disabled
c67bf8c5 701
62a9a74c
CH
702 Json::array doc;
703 for(const DomainInfo& di : domains) {
704 doc.push_back(getZoneInfo(di));
c67bf8c5 705 }
669822d0 706 resp->setBody(doc);
c67bf8c5
CH
707}
708
05776d2f 709static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
290a083d 710 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
05776d2f 711
d07bf7ff 712 if(req->method == "PUT" && !::arg().mustDo("api-readonly")) {
7c0ba3d2
CH
713 // update domain settings
714 UeberBackend B;
715 DomainInfo di;
716 if(!B.getDomainInfo(zonename, di))
290a083d 717 throw ApiException("Could not find domain '"+zonename.toString()+"'");
7c0ba3d2 718
1f68b185 719 updateDomainSettingsFromDocument(di, zonename, req->json());
7c0ba3d2 720
669822d0 721 fillZone(zonename, resp);
7c0ba3d2
CH
722 return;
723 }
d07bf7ff 724 else if(req->method == "DELETE" && !::arg().mustDo("api-readonly")) {
a462a01d
CH
725 // delete domain
726 UeberBackend B;
727 DomainInfo di;
728 if(!B.getDomainInfo(zonename, di))
290a083d 729 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a462a01d
CH
730
731 if(!di.backend->deleteDomain(zonename))
290a083d 732 throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");
a462a01d
CH
733
734 // empty body on success
735 resp->body = "";
37663c3b 736 resp->status = 204; // No Content: declare that the zone is gone now
a462a01d 737 return;
d07bf7ff 738 } else if (req->method == "PATCH" && !::arg().mustDo("api-readonly")) {
d708640f 739 patchZone(req, resp);
6cc98ddf
CH
740 return;
741 } else if (req->method == "GET") {
742 fillZone(zonename, resp);
743 return;
a462a01d 744 }
7c0ba3d2 745
6cc98ddf 746 throw HttpMethodNotAllowedException();
05776d2f
CH
747}
748
a83004d3 749static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
290a083d 750 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a83004d3
CH
751
752 if(req->method != "GET")
753 throw HttpMethodNotAllowedException();
754
755 ostringstream ss;
756
757 UeberBackend B;
758 DomainInfo di;
759 if(!B.getDomainInfo(zonename, di))
290a083d 760 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a83004d3
CH
761
762 DNSResourceRecord rr;
763 SOAData sd;
764 di.backend->list(zonename, di.id);
765 while(di.backend->get(rr)) {
766 if (!rr.qtype.getCode())
767 continue; // skip empty non-terminals
768
a83004d3 769 ss <<
675fa24c 770 rr.qname.toString() << "\t" <<
a83004d3
CH
771 rr.ttl << "\t" <<
772 rr.qtype.getName() << "\t" <<
1d6b70f9 773 makeApiRecordContent(rr.qtype, rr.content) <<
a83004d3
CH
774 endl;
775 }
776
777 if (req->accept_json) {
41873e7c 778 resp->setBody(Json::object { { "zone", ss.str() } });
a83004d3
CH
779 } else {
780 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
781 resp->body = ss.str();
782 }
783}
784
a426cb89 785static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
290a083d 786 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89
CH
787
788 if(req->method != "PUT")
789 throw HttpMethodNotAllowedException();
790
791 UeberBackend B;
792 DomainInfo di;
793 if(!B.getDomainInfo(zonename, di))
290a083d 794 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a426cb89
CH
795
796 if(di.masters.empty())
290a083d 797 throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");
a426cb89
CH
798
799 random_shuffle(di.masters.begin(), di.masters.end());
800 Communicator.addSuckRequest(zonename, di.masters.front());
290a083d 801 resp->body = returnJsonMessage("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front());
a426cb89
CH
802}
803
804static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
290a083d 805 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89
CH
806
807 if(req->method != "PUT")
808 throw HttpMethodNotAllowedException();
809
810 UeberBackend B;
811 DomainInfo di;
812 if(!B.getDomainInfo(zonename, di))
290a083d 813 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a426cb89
CH
814
815 if(!Communicator.notifyDomain(zonename))
816 throw ApiException("Failed to add to the queue - see server log");
817
818 resp->body = returnJsonMessage("Notification queued");
819}
820
d1587ceb
CH
821static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
822 if (rr.qtype.getCode() == QType::A) {
823 uint32_t ip;
824 if (!IpToU32(rr.content, &ip)) {
825 throw ApiException("PTR: Invalid IP address given");
826 }
1d6b70f9 827 ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
d1587ceb
CH
828 % ((ip >> 24) & 0xff)
829 % ((ip >> 16) & 0xff)
830 % ((ip >> 8) & 0xff)
831 % ((ip ) & 0xff)
1d6b70f9 832 ).str());
d1587ceb
CH
833 } else if (rr.qtype.getCode() == QType::AAAA) {
834 ComboAddress ca(rr.content);
5fb3aa58 835 char buf[3];
d1587ceb 836 ostringstream ss;
5fb3aa58
CH
837 for (int octet = 0; octet < 16; ++octet) {
838 if (snprintf(buf, sizeof(buf), "%02x", ca.sin6.sin6_addr.s6_addr[octet]) != (sizeof(buf)-1)) {
839 // this should be impossible: no byte should give more than two digits in hex format
840 throw PDNSException("Formatting IPv6 address failed");
841 }
842 ss << buf[0] << '.' << buf[1] << '.';
d1587ceb 843 }
5fb3aa58
CH
844 string tmp = ss.str();
845 tmp.resize(tmp.size()-1); // remove last dot
846 // reverse and append arpa domain
1d6b70f9 847 ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
d1587ceb 848 } else {
675fa24c 849 throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
d1587ceb
CH
850 }
851
852 ptr->qtype = "PTR";
853 ptr->ttl = rr.ttl;
854 ptr->disabled = rr.disabled;
675fa24c 855 ptr->content = rr.qname.toString();
d1587ceb
CH
856}
857
d708640f 858static void patchZone(HttpRequest* req, HttpResponse* resp) {
b3905a3d
CH
859 UeberBackend B;
860 DomainInfo di;
290a083d 861 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
d708640f 862 if (!B.getDomainInfo(zonename, di))
290a083d 863 throw ApiException("Could not find domain '"+zonename.toString()+"'");
b3905a3d 864
f63168e6
CH
865 vector<DNSResourceRecord> new_records;
866 vector<Comment> new_comments;
d708640f
CH
867 vector<DNSResourceRecord> new_ptrs;
868
1f68b185 869 Json document = req->json();
b3905a3d 870
1f68b185
CH
871 auto rrsets = document["rrsets"];
872 if (!rrsets.is_array())
d708640f 873 throw ApiException("No rrsets given in update request");
b3905a3d 874
d708640f 875 di.backend->startTransaction(zonename);
6cc98ddf 876
d708640f 877 try {
d29d5db7 878 string soa_edit_api_kind;
a6448d95 879 string soa_edit_kind;
d29d5db7 880 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
a6448d95 881 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
d29d5db7
CH
882 bool soa_edit_done = false;
883
1f68b185 884 for (auto rrset : rrsets.array_items()) {
edda67a2 885 string changetype;
d708640f 886 QType qtype;
c576d0c5 887 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
1d6b70f9 888 apiCheckNameAllowedCharacters(qname.toString());
d708640f
CH
889 qtype = stringFromJson(rrset, "type");
890 changetype = toUpper(stringFromJson(rrset, "changetype"));
891
d708640f
CH
892 if (changetype == "DELETE") {
893 // delete all matching qname/qtype RRs (and, implictly comments).
894 if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
895 throw ApiException("Hosting backend does not support editing records.");
6cc98ddf 896 }
d708640f
CH
897 }
898 else if (changetype == "REPLACE") {
1d6b70f9 899 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
e325f20c 900 if (!qname.isPartOf(zonename) && qname != zonename)
edda67a2 901 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
34df6ecc 902
1d6b70f9 903 new_records.clear();
f63168e6
CH
904 new_comments.clear();
905 // new_ptrs is merged
906 gatherRecords(rrset, new_records, new_ptrs);
907 gatherComments(rrset, new_comments, true);
908
ff05fd12 909 for(DNSResourceRecord& rr : new_records) {
f63168e6
CH
910 rr.domain_id = di.id;
911
912 if (rr.qname != qname || rr.qtype != qtype)
edda67a2 913 throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.getName()+" "+rr.content+": Record wrongly bundled with RRset " + qname.toString() + "/" + qtype.getName());
f63168e6 914
e325f20c 915 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
a6448d95 916 soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9 917 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
d708640f 918 }
6cc98ddf
CH
919 }
920
ff05fd12 921 for(Comment& c : new_comments) {
f63168e6 922 c.domain_id = di.id;
d1587ceb
CH
923 }
924
1f68b185
CH
925 bool replace_records = rrset["records"].is_array();
926 bool replace_comments = rrset["comments"].is_array();
f63168e6 927
d708640f 928 if (!replace_records && !replace_comments) {
edda67a2 929 throw ApiException("No change for RRset " + qname.toString() + "/" + qtype.getName());
d708640f 930 }
b3905a3d 931
d708640f
CH
932 if (replace_records) {
933 if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
934 throw ApiException("Hosting backend does not support editing records.");
935 }
936 }
937 if (replace_comments) {
938 if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
939 throw ApiException("Hosting backend does not support editing comments.");
940 }
941 }
6cc98ddf 942 }
d708640f
CH
943 else
944 throw ApiException("Changetype not understood");
6cc98ddf 945 }
d29d5db7
CH
946
947 // edit SOA (if needed)
948 if (!soa_edit_api_kind.empty() && !soa_edit_done) {
949 SOAData sd;
950 if (!B.getSOA(zonename, sd))
290a083d 951 throw ApiException("No SOA found for domain '"+zonename.toString()+"'");
d29d5db7
CH
952
953 DNSResourceRecord rr;
954 rr.qname = zonename;
955 rr.content = serializeSOAData(sd);
956 rr.qtype = "SOA";
957 rr.domain_id = di.id;
958 rr.auth = 1;
959 rr.ttl = sd.ttl;
a6448d95 960 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
961 // fixup dots after serializeSOAData/increaseSOARecord
962 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
d29d5db7
CH
963
964 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
965 throw ApiException("Hosting backend does not support editing records.");
966 }
967 }
968
d708640f
CH
969 } catch(...) {
970 di.backend->abortTransaction();
971 throw;
972 }
973 di.backend->commitTransaction();
b3905a3d 974
d708640f 975 extern PacketCache PC;
be9d7339 976 PC.purgeExact(zonename);
d1587ceb 977
d708640f 978 // now the PTRs
ff05fd12 979 for(const DNSResourceRecord& rr : new_ptrs) {
d708640f
CH
980 DNSPacket fakePacket;
981 SOAData sd;
79ba7763 982 sd.db = (DNSBackend *)-1; // getAuth() cache bypass
d708640f 983 fakePacket.qtype = QType::PTR;
d1587ceb 984
81c486ad 985 if (!B.getAuth(&fakePacket, &sd, rr.qname))
561434a6 986 throw ApiException("Could not find domain for PTR '"+rr.qname.toString()+"' requested for '"+rr.content+"' (while saving)");
d1587ceb 987
d708640f
CH
988 sd.db->startTransaction(rr.qname);
989 if (!sd.db->replaceRRSet(sd.domain_id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
d0f4bb38 990 sd.db->abortTransaction();
561434a6 991 throw ApiException("PTR-Hosting backend for "+rr.qname.toString()+"/"+rr.qtype.getName()+" does not support editing records.");
d1587ceb 992 }
d708640f 993 sd.db->commitTransaction();
be9d7339 994 PC.purgeExact(rr.qname);
b3905a3d 995 }
b3905a3d
CH
996
997 // success
d708640f 998 fillZone(zonename, resp);
b3905a3d
CH
999}
1000
b1902fab
CH
1001static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
1002 if(req->method != "GET")
1003 throw HttpMethodNotAllowedException();
1004
583ea80d 1005 string q = req->getvars["q"];
720ed2bd
AT
1006 string sMax = req->getvars["max"];
1007 int maxEnts = 100;
1008 int ents = 0;
1009
b1902fab
CH
1010 if (q.empty())
1011 throw ApiException("Query q can't be blank");
720ed2bd 1012 if (sMax.empty() == false)
335da0ba 1013 maxEnts = std::stoi(sMax);
720ed2bd
AT
1014 if (maxEnts < 1)
1015 throw ApiException("Maximum entries must be larger than 0");
b1902fab 1016
720ed2bd 1017 SimpleMatch sm(q,true);
b1902fab 1018 UeberBackend B;
b1902fab 1019 vector<DomainInfo> domains;
720ed2bd
AT
1020 vector<DNSResourceRecord> result_rr;
1021 vector<Comment> result_c;
1d6b70f9
CH
1022 map<int,DomainInfo> zoneIdZone;
1023 map<int,DomainInfo>::iterator val;
00963dea 1024 Json::array doc;
b1902fab 1025
720ed2bd 1026 B.getAllDomains(&domains, true);
d2d194a9 1027
720ed2bd 1028 for(const DomainInfo di: domains)
1d6b70f9 1029 {
720ed2bd 1030 if (ents < maxEnts && sm.match(di.zone)) {
00963dea
CH
1031 doc.push_back(Json::object {
1032 { "object_type", "zone" },
1033 { "zone_id", apiZoneNameToId(di.zone) },
1034 { "name", di.zone.toString() }
1035 });
720ed2bd 1036 ents++;
b1902fab 1037 }
1d6b70f9 1038 zoneIdZone[di.id] = di; // populate cache
720ed2bd 1039 }
b1902fab 1040
720ed2bd
AT
1041 if (B.searchRecords(q, maxEnts, result_rr))
1042 {
1043 for(const DNSResourceRecord& rr: result_rr)
1044 {
00963dea
CH
1045 auto object = Json::object {
1046 { "object_type", "record" },
1047 { "name", rr.qname.toString() },
1048 { "type", rr.qtype.getName() },
1049 { "ttl", (double)rr.ttl },
1050 { "disabled", rr.disabled },
1051 { "content", makeApiRecordContent(rr.qtype, rr.content) }
1052 };
720ed2bd 1053 if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
00963dea
CH
1054 object["zone_id"] = apiZoneNameToId(val->second.zone);
1055 object["zone"] = val->second.zone.toString();
720ed2bd 1056 }
00963dea 1057 doc.push_back(object);
b1902fab 1058 }
720ed2bd 1059 }
b1902fab 1060
720ed2bd
AT
1061 if (B.searchComments(q, maxEnts, result_c))
1062 {
1063 for(const Comment &c: result_c)
1064 {
00963dea
CH
1065 auto object = Json::object {
1066 { "object_type", "comment" },
1067 { "name", c.qname },
1068 { "content", c.content }
1069 };
720ed2bd 1070 if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
00963dea
CH
1071 object["zone_id"] = apiZoneNameToId(val->second.zone);
1072 object["zone"] = val->second.zone.toString();
720ed2bd 1073 }
00963dea 1074 doc.push_back(object);
b1902fab
CH
1075 }
1076 }
4bd3d119 1077
b1902fab
CH
1078 resp->setBody(doc);
1079}
1080
c0f6a1da 1081void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
a426cb89
CH
1082 if(req->method != "PUT")
1083 throw HttpMethodNotAllowedException();
80d59cd1 1084
c0f6a1da
CH
1085 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
1086
a426cb89 1087 extern PacketCache PC;
c0f6a1da 1088 int count = PC.purgeExact(canon);
ddc84d12 1089
a426cb89 1090 map<string, string> object;
335da0ba 1091 object["count"] = std::to_string(count);
a426cb89
CH
1092 object["result"] = "Flushed cache.";
1093 resp->body = returnJsonObject(object);
ddc84d12
CH
1094}
1095
dea47634 1096void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
c67bf8c5 1097{
80d59cd1
CH
1098 resp->headers["Cache-Control"] = "max-age=86400";
1099 resp->headers["Content-Type"] = "text/css";
c67bf8c5 1100
1071abdd 1101 ostringstream ret;
1071abdd
CH
1102 ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
1103 ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
1104 ret<<"a { color: #0959c2; }"<<endl;
1105 ret<<"a:hover { color: #3B8EC8; }"<<endl;
1106 ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
1107 ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
1108 ret<<".row:after { clear: both; }"<<endl;
1109 ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
1110 ret<<".all { width: 100%; }"<<endl;
1111 ret<<".headl { width: 60%; }"<<endl;
1112 ret<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
1113 ret<<"background-image: url(data:image/png;base64,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);";
1114 ret<<" width: 154px; height: 20px; }"<<endl;
1115 ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
1116 ret<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
1117 ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
1118 ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
1119 ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
1120 ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
1121 ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
1122 ret<<"table.data tr:hover { background: white; }"<<endl;
1123 ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
1124 ret<<".resetring {float: right; }"<<endl;
1125 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;
1126 ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
1127 ret<<".resizering {float: right;}"<<endl;
80d59cd1 1128 resp->body = ret.str();
c146576d 1129 resp->status = 200;
1071abdd
CH
1130}
1131
dea47634 1132void AuthWebServer::webThread()
12c86877
BH
1133{
1134 try {
479e0976 1135 if(::arg().mustDo("api")) {
c0f6a1da 1136 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
46d06a12 1137 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
46d06a12
PL
1138 d_ws->registerApiHandler("/api/v1/servers/localhost/search-log", &apiServerSearchLog);
1139 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
1140 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
1141 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
1142 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
1143 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
1144 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
1145 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
1146 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
1147 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
1148 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
1149 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
c67bf8c5 1150 }
bbef8f04
CH
1151 d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
1152 d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
96d299db 1153 d_ws->go();
12c86877
BH
1154 }
1155 catch(...) {
dea47634 1156 L<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
12c86877
BH
1157 exit(1);
1158 }
1159}