]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/ws-auth.cc
gsql: Remove stripDot where not needed
[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
b6bd795c 496 DNSSECKeeper::keyset_t keyset=dk.getKeys(zonename, 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 509
b6bd795c
PL
510 string keyType;
511 switch(value.second.keyType){
512 case DNSSECKeeper::KSK: keyType="ksk"; break;
513 case DNSSECKeeper::ZSK: keyType="zsk"; break;
514 case DNSSECKeeper::CSK: keyType="csk"; break;
515 }
516
24afabad
CH
517 Json::object key {
518 { "type", "Cryptokey" },
519 { "id", (int)value.second.id },
520 { "active", value.second.active },
b6bd795c
PL
521 { "keytype", keyType },
522 { "flags", (uint16_t)value.first.d_flags },
24afabad
CH
523 { "dnskey", value.first.getDNSKEY().getZoneRepresentation() }
524 };
525
583ea80d 526 if (req->parameters.count("key_id")) {
335da0ba 527 DNSSECPrivateKey dpk=dk.getKeyById(zonename, std::stoi(req->parameters["key_id"]));
24afabad 528 key["content"] = dpk.getKey()->convertToISC();
38809e97 529 }
4b7f120a 530
b6bd795c 531 if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
24afabad
CH
532 Json::array dses;
533 for(const int keyid : { 1, 2, 3, 4 })
4b7f120a 534 try {
24afabad
CH
535 dses.push_back(makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation());
536 } catch (...) {}
537 key["ds"] = dses;
4b7f120a 538 }
24afabad 539 doc.push_back(key);
4b7f120a
MS
540 }
541
542 resp->setBody(doc);
543}
544
1f68b185 545static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, DNSName zonename) {
0f0e73fe
MS
546 DNSResourceRecord rr;
547 vector<string> zonedata;
1f68b185 548 stringtok(zonedata, zonestring, "\r\n");
0f0e73fe
MS
549
550 ZoneParserTNG zpt(zonedata, zonename);
551
552 bool seenSOA=false;
553
554 string comment = "Imported via the API";
555
556 try {
557 while(zpt.get(rr, &comment)) {
558 if(seenSOA && rr.qtype.getCode() == QType::SOA)
559 continue;
560 if(rr.qtype.getCode() == QType::SOA)
561 seenSOA=true;
562
0f0e73fe
MS
563 new_records.push_back(rr);
564 }
565 }
566 catch(std::exception& ae) {
567 throw ApiException("An error occured while parsing the zonedata: "+string(ae.what()));
568 }
569}
570
80d59cd1 571static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
e2dba705 572 UeberBackend B;
559115f6 573 DNSSECKeeper dk;
d07bf7ff 574 if (req->method == "POST" && !::arg().mustDo("api-readonly")) {
e2dba705 575 DomainInfo di;
1f68b185 576 auto document = req->json();
c576d0c5 577 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
1d6b70f9 578 apiCheckNameAllowedCharacters(zonename.toString());
4ebf78b1 579
1f68b185 580 string zonestring = document["zone"].string_value();
406497f5 581
1d6b70f9 582 bool exists = B.getDomainInfo(zonename, di);
e2dba705 583 if(exists)
1d6b70f9 584 throw ApiException("Domain '"+zonename.toString()+"' already exists");
e2dba705 585
bb9fd223 586 // validate 'kind' is set
4bdff352 587 DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
bb9fd223 588
1f68b185
CH
589 auto records = document["records"];
590 if (records.is_array() && zonestring != "")
0f0e73fe
MS
591 throw ApiException("You cannot give zonedata AND records");
592
1f68b185
CH
593 auto nameservers = document["nameservers"];
594 if (!nameservers.is_array() && zonekind != DomainInfo::Slave)
f63168e6 595 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
e2dba705 596
f63168e6 597 string soa_edit_api_kind;
1f68b185
CH
598 if (document["soa_edit_api"].is_string()) {
599 soa_edit_api_kind = document["soa_edit_api"].string_value();
a6448d95
CH
600 }
601 else {
602 soa_edit_api_kind = "DEFAULT";
603 }
1f68b185 604 string soa_edit_kind = document["soa_edit"].string_value();
e90b4e38 605
f63168e6
CH
606 // if records/comments are given, load and check them
607 bool have_soa = false;
608 vector<DNSResourceRecord> new_records;
609 vector<Comment> new_comments;
610 vector<DNSResourceRecord> new_ptrs;
0f0e73fe 611
1f68b185 612 if (records.is_array()) {
0f0e73fe
MS
613 gatherRecords(document, new_records, new_ptrs);
614 } else if (zonestring != "") {
1f68b185 615 gatherRecordsFromZone(zonestring, new_records, zonename);
0f0e73fe
MS
616 }
617
f63168e6 618 gatherComments(document, new_comments, false);
e2dba705 619
1f68b185 620 for(auto& rr : new_records) {
1d6b70f9 621 if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
561434a6 622 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": Name is out of zone");
cb9b5901 623 apiCheckQNameAllowedCharacters(rr.qname.toString());
f63168e6 624
1d6b70f9 625 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
f63168e6 626 have_soa = true;
a6448d95 627 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
628 // fixup dots after serializeSOAData/increaseSOARecord
629 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
f63168e6
CH
630 }
631 }
f7bfeb30
CH
632
633 // synthesize RRs as needed
634 DNSResourceRecord autorr;
1d6b70f9 635 autorr.qname = zonename;
f7bfeb30
CH
636 autorr.auth = 1;
637 autorr.ttl = ::arg().asNum("default-ttl");
e2dba705 638
4de11a54 639 if (!have_soa && zonekind != DomainInfo::Slave) {
f63168e6 640 // synthesize a SOA record so the zone "really" exists
1d6b70f9
CH
641 string soa = (boost::format("%s %s %lu")
642 % ::arg()["default-soa-name"]
643 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename).toString() : ::arg()["default-soa-mail"])
1f68b185 644 % document["serial"].int_value()
1d6b70f9 645 ).str();
f63168e6 646 SOAData sd;
1d6b70f9 647 fillSOAData(soa, sd); // fills out default values for us
f7bfeb30 648 autorr.qtype = "SOA";
1d6b70f9 649 autorr.content = serializeSOAData(sd);
f7bfeb30 650 increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
651 // fixup dots after serializeSOAData/increaseSOARecord
652 autorr.content = makeBackendRecordContent(autorr.qtype, autorr.content);
f7bfeb30 653 new_records.push_back(autorr);
f63168e6
CH
654 }
655
656 // create NS records if nameservers are given
1f68b185
CH
657 for (auto value : nameservers.array_items()) {
658 string nameserver = value.string_value();
659 if (nameserver.empty())
660 throw ApiException("Nameservers must be non-empty strings");
661 if (!isCanonical(nameserver))
662 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
663 try {
664 // ensure the name parses
665 autorr.content = DNSName(nameserver).toStringNoDot();
666 } catch (...) {
667 throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
4bdff352 668 }
1f68b185
CH
669 autorr.qtype = "NS";
670 new_records.push_back(autorr);
e2dba705
CH
671 }
672
f63168e6 673 // no going back after this
1d6b70f9
CH
674 if(!B.createDomain(zonename))
675 throw ApiException("Creating domain '"+zonename.toString()+"' failed");
f63168e6 676
1d6b70f9
CH
677 if(!B.getDomainInfo(zonename, di))
678 throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");
f63168e6 679
1d6b70f9 680 di.backend->startTransaction(zonename, di.id);
f63168e6 681
abb873ee 682 for(auto rr : new_records) {
f63168e6 683 rr.domain_id = di.id;
e2dba705
CH
684 di.backend->feedRecord(rr);
685 }
1d6b70f9 686 for(Comment& c : new_comments) {
f63168e6
CH
687 c.domain_id = di.id;
688 di.backend->feedComment(c);
689 }
e2dba705 690
1d6b70f9 691 updateDomainSettingsFromDocument(di, zonename, document);
e2dba705 692
f63168e6
CH
693 di.backend->commitTransaction();
694
1d6b70f9 695 fillZone(zonename, resp);
64a36f0d 696 resp->status = 201;
e2dba705
CH
697 return;
698 }
699
c67bf8c5
CH
700 if(req->method != "GET")
701 throw HttpMethodNotAllowedException();
702
c67bf8c5 703 vector<DomainInfo> domains;
cea26350 704 B.getAllDomains(&domains, true); // incl. disabled
c67bf8c5 705
62a9a74c
CH
706 Json::array doc;
707 for(const DomainInfo& di : domains) {
708 doc.push_back(getZoneInfo(di));
c67bf8c5 709 }
669822d0 710 resp->setBody(doc);
c67bf8c5
CH
711}
712
05776d2f 713static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
290a083d 714 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
05776d2f 715
d07bf7ff 716 if(req->method == "PUT" && !::arg().mustDo("api-readonly")) {
7c0ba3d2
CH
717 // update domain settings
718 UeberBackend B;
719 DomainInfo di;
720 if(!B.getDomainInfo(zonename, di))
290a083d 721 throw ApiException("Could not find domain '"+zonename.toString()+"'");
7c0ba3d2 722
1f68b185 723 updateDomainSettingsFromDocument(di, zonename, req->json());
7c0ba3d2 724
669822d0 725 fillZone(zonename, resp);
7c0ba3d2
CH
726 return;
727 }
d07bf7ff 728 else if(req->method == "DELETE" && !::arg().mustDo("api-readonly")) {
a462a01d
CH
729 // delete domain
730 UeberBackend B;
731 DomainInfo di;
732 if(!B.getDomainInfo(zonename, di))
290a083d 733 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a462a01d
CH
734
735 if(!di.backend->deleteDomain(zonename))
290a083d 736 throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");
a462a01d
CH
737
738 // empty body on success
739 resp->body = "";
37663c3b 740 resp->status = 204; // No Content: declare that the zone is gone now
a462a01d 741 return;
d07bf7ff 742 } else if (req->method == "PATCH" && !::arg().mustDo("api-readonly")) {
d708640f 743 patchZone(req, resp);
6cc98ddf
CH
744 return;
745 } else if (req->method == "GET") {
746 fillZone(zonename, resp);
747 return;
a462a01d 748 }
7c0ba3d2 749
6cc98ddf 750 throw HttpMethodNotAllowedException();
05776d2f
CH
751}
752
a83004d3 753static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
290a083d 754 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a83004d3
CH
755
756 if(req->method != "GET")
757 throw HttpMethodNotAllowedException();
758
759 ostringstream ss;
760
761 UeberBackend B;
762 DomainInfo di;
763 if(!B.getDomainInfo(zonename, di))
290a083d 764 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a83004d3
CH
765
766 DNSResourceRecord rr;
767 SOAData sd;
768 di.backend->list(zonename, di.id);
769 while(di.backend->get(rr)) {
770 if (!rr.qtype.getCode())
771 continue; // skip empty non-terminals
772
a83004d3 773 ss <<
675fa24c 774 rr.qname.toString() << "\t" <<
a83004d3
CH
775 rr.ttl << "\t" <<
776 rr.qtype.getName() << "\t" <<
1d6b70f9 777 makeApiRecordContent(rr.qtype, rr.content) <<
a83004d3
CH
778 endl;
779 }
780
781 if (req->accept_json) {
41873e7c 782 resp->setBody(Json::object { { "zone", ss.str() } });
a83004d3
CH
783 } else {
784 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
785 resp->body = ss.str();
786 }
787}
788
a426cb89 789static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
290a083d 790 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89
CH
791
792 if(req->method != "PUT")
793 throw HttpMethodNotAllowedException();
794
795 UeberBackend B;
796 DomainInfo di;
797 if(!B.getDomainInfo(zonename, di))
290a083d 798 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a426cb89
CH
799
800 if(di.masters.empty())
290a083d 801 throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");
a426cb89
CH
802
803 random_shuffle(di.masters.begin(), di.masters.end());
804 Communicator.addSuckRequest(zonename, di.masters.front());
692829aa 805 resp->setSuccessResult("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front());
a426cb89
CH
806}
807
808static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
290a083d 809 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
a426cb89
CH
810
811 if(req->method != "PUT")
812 throw HttpMethodNotAllowedException();
813
814 UeberBackend B;
815 DomainInfo di;
816 if(!B.getDomainInfo(zonename, di))
290a083d 817 throw ApiException("Could not find domain '"+zonename.toString()+"'");
a426cb89
CH
818
819 if(!Communicator.notifyDomain(zonename))
820 throw ApiException("Failed to add to the queue - see server log");
821
692829aa 822 resp->setSuccessResult("Notification queued");
a426cb89
CH
823}
824
d1587ceb
CH
825static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
826 if (rr.qtype.getCode() == QType::A) {
827 uint32_t ip;
828 if (!IpToU32(rr.content, &ip)) {
829 throw ApiException("PTR: Invalid IP address given");
830 }
1d6b70f9 831 ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
d1587ceb
CH
832 % ((ip >> 24) & 0xff)
833 % ((ip >> 16) & 0xff)
834 % ((ip >> 8) & 0xff)
835 % ((ip ) & 0xff)
1d6b70f9 836 ).str());
d1587ceb
CH
837 } else if (rr.qtype.getCode() == QType::AAAA) {
838 ComboAddress ca(rr.content);
5fb3aa58 839 char buf[3];
d1587ceb 840 ostringstream ss;
5fb3aa58
CH
841 for (int octet = 0; octet < 16; ++octet) {
842 if (snprintf(buf, sizeof(buf), "%02x", ca.sin6.sin6_addr.s6_addr[octet]) != (sizeof(buf)-1)) {
843 // this should be impossible: no byte should give more than two digits in hex format
844 throw PDNSException("Formatting IPv6 address failed");
845 }
846 ss << buf[0] << '.' << buf[1] << '.';
d1587ceb 847 }
5fb3aa58
CH
848 string tmp = ss.str();
849 tmp.resize(tmp.size()-1); // remove last dot
850 // reverse and append arpa domain
1d6b70f9 851 ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
d1587ceb 852 } else {
675fa24c 853 throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
d1587ceb
CH
854 }
855
856 ptr->qtype = "PTR";
857 ptr->ttl = rr.ttl;
858 ptr->disabled = rr.disabled;
675fa24c 859 ptr->content = rr.qname.toString();
d1587ceb
CH
860}
861
d708640f 862static void patchZone(HttpRequest* req, HttpResponse* resp) {
b3905a3d
CH
863 UeberBackend B;
864 DomainInfo di;
290a083d 865 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
d708640f 866 if (!B.getDomainInfo(zonename, di))
290a083d 867 throw ApiException("Could not find domain '"+zonename.toString()+"'");
b3905a3d 868
f63168e6
CH
869 vector<DNSResourceRecord> new_records;
870 vector<Comment> new_comments;
d708640f
CH
871 vector<DNSResourceRecord> new_ptrs;
872
1f68b185 873 Json document = req->json();
b3905a3d 874
1f68b185
CH
875 auto rrsets = document["rrsets"];
876 if (!rrsets.is_array())
d708640f 877 throw ApiException("No rrsets given in update request");
b3905a3d 878
d708640f 879 di.backend->startTransaction(zonename);
6cc98ddf 880
d708640f 881 try {
d29d5db7 882 string soa_edit_api_kind;
a6448d95 883 string soa_edit_kind;
d29d5db7 884 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
a6448d95 885 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
d29d5db7
CH
886 bool soa_edit_done = false;
887
1f68b185 888 for (auto rrset : rrsets.array_items()) {
edda67a2 889 string changetype;
d708640f 890 QType qtype;
c576d0c5 891 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
cb9b5901 892 apiCheckQNameAllowedCharacters(qname.toString());
d708640f
CH
893 qtype = stringFromJson(rrset, "type");
894 changetype = toUpper(stringFromJson(rrset, "changetype"));
895
d708640f
CH
896 if (changetype == "DELETE") {
897 // delete all matching qname/qtype RRs (and, implictly comments).
898 if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
899 throw ApiException("Hosting backend does not support editing records.");
6cc98ddf 900 }
d708640f
CH
901 }
902 else if (changetype == "REPLACE") {
1d6b70f9 903 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
e325f20c 904 if (!qname.isPartOf(zonename) && qname != zonename)
edda67a2 905 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
34df6ecc 906
1d6b70f9 907 new_records.clear();
f63168e6
CH
908 new_comments.clear();
909 // new_ptrs is merged
910 gatherRecords(rrset, new_records, new_ptrs);
911 gatherComments(rrset, new_comments, true);
912
ff05fd12 913 for(DNSResourceRecord& rr : new_records) {
f63168e6
CH
914 rr.domain_id = di.id;
915
916 if (rr.qname != qname || rr.qtype != qtype)
edda67a2 917 throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.getName()+" "+rr.content+": Record wrongly bundled with RRset " + qname.toString() + "/" + qtype.getName());
f63168e6 918
e325f20c 919 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
a6448d95 920 soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9 921 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
d708640f 922 }
6cc98ddf
CH
923 }
924
ff05fd12 925 for(Comment& c : new_comments) {
f63168e6 926 c.domain_id = di.id;
d1587ceb
CH
927 }
928
1f68b185
CH
929 bool replace_records = rrset["records"].is_array();
930 bool replace_comments = rrset["comments"].is_array();
f63168e6 931
d708640f 932 if (!replace_records && !replace_comments) {
edda67a2 933 throw ApiException("No change for RRset " + qname.toString() + "/" + qtype.getName());
d708640f 934 }
b3905a3d 935
d708640f
CH
936 if (replace_records) {
937 if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
938 throw ApiException("Hosting backend does not support editing records.");
939 }
940 }
941 if (replace_comments) {
942 if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
943 throw ApiException("Hosting backend does not support editing comments.");
944 }
945 }
6cc98ddf 946 }
d708640f
CH
947 else
948 throw ApiException("Changetype not understood");
6cc98ddf 949 }
d29d5db7
CH
950
951 // edit SOA (if needed)
952 if (!soa_edit_api_kind.empty() && !soa_edit_done) {
953 SOAData sd;
954 if (!B.getSOA(zonename, sd))
290a083d 955 throw ApiException("No SOA found for domain '"+zonename.toString()+"'");
d29d5db7
CH
956
957 DNSResourceRecord rr;
958 rr.qname = zonename;
959 rr.content = serializeSOAData(sd);
960 rr.qtype = "SOA";
961 rr.domain_id = di.id;
962 rr.auth = 1;
963 rr.ttl = sd.ttl;
a6448d95 964 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1d6b70f9
CH
965 // fixup dots after serializeSOAData/increaseSOARecord
966 rr.content = makeBackendRecordContent(rr.qtype, rr.content);
d29d5db7
CH
967
968 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
969 throw ApiException("Hosting backend does not support editing records.");
970 }
971 }
972
d708640f
CH
973 } catch(...) {
974 di.backend->abortTransaction();
975 throw;
976 }
977 di.backend->commitTransaction();
b3905a3d 978
d708640f 979 extern PacketCache PC;
be9d7339 980 PC.purgeExact(zonename);
d1587ceb 981
d708640f 982 // now the PTRs
ff05fd12 983 for(const DNSResourceRecord& rr : new_ptrs) {
d708640f
CH
984 DNSPacket fakePacket;
985 SOAData sd;
79ba7763 986 sd.db = (DNSBackend *)-1; // getAuth() cache bypass
d708640f 987 fakePacket.qtype = QType::PTR;
d1587ceb 988
81c486ad 989 if (!B.getAuth(&fakePacket, &sd, rr.qname))
561434a6 990 throw ApiException("Could not find domain for PTR '"+rr.qname.toString()+"' requested for '"+rr.content+"' (while saving)");
d1587ceb 991
d708640f
CH
992 sd.db->startTransaction(rr.qname);
993 if (!sd.db->replaceRRSet(sd.domain_id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
d0f4bb38 994 sd.db->abortTransaction();
561434a6 995 throw ApiException("PTR-Hosting backend for "+rr.qname.toString()+"/"+rr.qtype.getName()+" does not support editing records.");
d1587ceb 996 }
d708640f 997 sd.db->commitTransaction();
be9d7339 998 PC.purgeExact(rr.qname);
b3905a3d 999 }
b3905a3d
CH
1000
1001 // success
d708640f 1002 fillZone(zonename, resp);
b3905a3d
CH
1003}
1004
b1902fab
CH
1005static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
1006 if(req->method != "GET")
1007 throw HttpMethodNotAllowedException();
1008
583ea80d 1009 string q = req->getvars["q"];
720ed2bd
AT
1010 string sMax = req->getvars["max"];
1011 int maxEnts = 100;
1012 int ents = 0;
1013
b1902fab
CH
1014 if (q.empty())
1015 throw ApiException("Query q can't be blank");
720ed2bd 1016 if (sMax.empty() == false)
335da0ba 1017 maxEnts = std::stoi(sMax);
720ed2bd
AT
1018 if (maxEnts < 1)
1019 throw ApiException("Maximum entries must be larger than 0");
b1902fab 1020
720ed2bd 1021 SimpleMatch sm(q,true);
b1902fab 1022 UeberBackend B;
b1902fab 1023 vector<DomainInfo> domains;
720ed2bd
AT
1024 vector<DNSResourceRecord> result_rr;
1025 vector<Comment> result_c;
1d6b70f9
CH
1026 map<int,DomainInfo> zoneIdZone;
1027 map<int,DomainInfo>::iterator val;
00963dea 1028 Json::array doc;
b1902fab 1029
720ed2bd 1030 B.getAllDomains(&domains, true);
d2d194a9 1031
720ed2bd 1032 for(const DomainInfo di: domains)
1d6b70f9 1033 {
720ed2bd 1034 if (ents < maxEnts && sm.match(di.zone)) {
00963dea
CH
1035 doc.push_back(Json::object {
1036 { "object_type", "zone" },
1037 { "zone_id", apiZoneNameToId(di.zone) },
1038 { "name", di.zone.toString() }
1039 });
720ed2bd 1040 ents++;
b1902fab 1041 }
1d6b70f9 1042 zoneIdZone[di.id] = di; // populate cache
720ed2bd 1043 }
b1902fab 1044
720ed2bd
AT
1045 if (B.searchRecords(q, maxEnts, result_rr))
1046 {
1047 for(const DNSResourceRecord& rr: result_rr)
1048 {
00963dea
CH
1049 auto object = Json::object {
1050 { "object_type", "record" },
1051 { "name", rr.qname.toString() },
1052 { "type", rr.qtype.getName() },
1053 { "ttl", (double)rr.ttl },
1054 { "disabled", rr.disabled },
1055 { "content", makeApiRecordContent(rr.qtype, rr.content) }
1056 };
720ed2bd 1057 if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
00963dea
CH
1058 object["zone_id"] = apiZoneNameToId(val->second.zone);
1059 object["zone"] = val->second.zone.toString();
720ed2bd 1060 }
00963dea 1061 doc.push_back(object);
b1902fab 1062 }
720ed2bd 1063 }
b1902fab 1064
720ed2bd
AT
1065 if (B.searchComments(q, maxEnts, result_c))
1066 {
1067 for(const Comment &c: result_c)
1068 {
00963dea
CH
1069 auto object = Json::object {
1070 { "object_type", "comment" },
1071 { "name", c.qname },
1072 { "content", c.content }
1073 };
720ed2bd 1074 if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
00963dea
CH
1075 object["zone_id"] = apiZoneNameToId(val->second.zone);
1076 object["zone"] = val->second.zone.toString();
720ed2bd 1077 }
00963dea 1078 doc.push_back(object);
b1902fab
CH
1079 }
1080 }
4bd3d119 1081
b1902fab
CH
1082 resp->setBody(doc);
1083}
1084
c0f6a1da 1085void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
a426cb89
CH
1086 if(req->method != "PUT")
1087 throw HttpMethodNotAllowedException();
80d59cd1 1088
c0f6a1da
CH
1089 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
1090
a426cb89 1091 extern PacketCache PC;
c0f6a1da 1092 int count = PC.purgeExact(canon);
f682752a
CH
1093 resp->setBody(Json::object {
1094 { "count", count },
1095 { "result", "Flushed cache." }
1096 });
ddc84d12
CH
1097}
1098
dea47634 1099void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
c67bf8c5 1100{
80d59cd1
CH
1101 resp->headers["Cache-Control"] = "max-age=86400";
1102 resp->headers["Content-Type"] = "text/css";
c67bf8c5 1103
1071abdd 1104 ostringstream ret;
1071abdd
CH
1105 ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
1106 ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
1107 ret<<"a { color: #0959c2; }"<<endl;
1108 ret<<"a:hover { color: #3B8EC8; }"<<endl;
1109 ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
1110 ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
1111 ret<<".row:after { clear: both; }"<<endl;
1112 ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
1113 ret<<".all { width: 100%; }"<<endl;
1114 ret<<".headl { width: 60%; }"<<endl;
1115 ret<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
1116 ret<<"background-image: url(data:image/png;base64,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);";
1117 ret<<" width: 154px; height: 20px; }"<<endl;
1118 ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
1119 ret<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
1120 ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
1121 ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
1122 ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
1123 ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
1124 ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
1125 ret<<"table.data tr:hover { background: white; }"<<endl;
1126 ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
1127 ret<<".resetring {float: right; }"<<endl;
1128 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;
1129 ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
1130 ret<<".resizering {float: right;}"<<endl;
80d59cd1 1131 resp->body = ret.str();
c146576d 1132 resp->status = 200;
1071abdd
CH
1133}
1134
dea47634 1135void AuthWebServer::webThread()
12c86877
BH
1136{
1137 try {
479e0976 1138 if(::arg().mustDo("api")) {
c0f6a1da 1139 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
46d06a12 1140 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
46d06a12
PL
1141 d_ws->registerApiHandler("/api/v1/servers/localhost/search-log", &apiServerSearchLog);
1142 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
1143 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
1144 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
1145 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
1146 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
1147 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
1148 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
1149 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
1150 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
1151 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
1152 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
c67bf8c5 1153 }
bbef8f04
CH
1154 d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
1155 d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
96d299db 1156 d_ws->go();
12c86877
BH
1157 }
1158 catch(...) {
dea47634 1159 L<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
12c86877
BH
1160 exit(1);
1161 }
1162}