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