]>
Commit | Line | Data |
---|---|---|
6ec5e728 | 1 | /* |
6edbf68a PL |
2 | * This file is part of PowerDNS or dnsdist. |
3 | * Copyright -- PowerDNS.COM B.V. and its contributors | |
4 | * | |
5 | * This program is free software; you can redistribute it and/or modify | |
6 | * it under the terms of version 2 of the GNU General Public License as | |
7 | * published by the Free Software Foundation. | |
8 | * | |
9 | * In addition, for the avoidance of any doubt, permission is granted to | |
10 | * link this program with OpenSSL and to (re)distribute the binaries | |
11 | * produced as the result of such linking. | |
12 | * | |
13 | * This program is distributed in the hope that it will be useful, | |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | * GNU General Public License for more details. | |
17 | * | |
18 | * You should have received a copy of the GNU General Public License | |
19 | * along with this program; if not, write to the Free Software | |
20 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
21 | */ | |
870a0fe4 AT |
22 | #ifdef HAVE_CONFIG_H |
23 | #include "config.h" | |
24 | #endif | |
fa8fd4d2 | 25 | |
6ec5e728 | 26 | #include <boost/tokenizer.hpp> |
a48e03da OM |
27 | #include <boost/format.hpp> |
28 | ||
6ec5e728 CH |
29 | #include "namespaces.hh" |
30 | #include "ws-api.hh" | |
31 | #include "json.hh" | |
6ec5e728 CH |
32 | #include "version.hh" |
33 | #include "arguments.hh" | |
118d3b31 | 34 | #include "dnsparser.hh" |
6ab8d5ea OM |
35 | #ifdef RECURSOR |
36 | #include "syncres.hh" | |
37 | #else | |
118d3b31 CH |
38 | #include "responsestats.hh" |
39 | #include "statbag.hh" | |
2977c6bb | 40 | #endif |
dd754091 FM |
41 | #include <cstdio> |
42 | #include <cstring> | |
43 | #include <cctype> | |
6ec5e728 | 44 | #include <sys/types.h> |
3c3c006b | 45 | #include <iomanip> |
6ec5e728 | 46 | |
f461cf07 | 47 | using json11::Json; |
fd99e845 | 48 | |
118d3b31 CH |
49 | #ifndef RECURSOR |
50 | extern StatBag S; | |
51 | #endif | |
52 | ||
6ec5e728 CH |
53 | #ifndef HAVE_STRCASESTR |
54 | ||
55 | /* | |
56 | * strcasestr() locates the first occurrence in the string s1 of the | |
57 | * sequence of characters (excluding the terminating null character) | |
58 | * in the string s2, ignoring case. strcasestr() returns a pointer | |
59 | * to the located string, or a null pointer if the string is not found. | |
60 | * If s2 is empty, the function returns s1. | |
61 | */ | |
62 | ||
2056b0b4 FM |
63 | static char* |
64 | strcasestr(const char* s1, const char* s2) | |
6ec5e728 | 65 | { |
2056b0b4 FM |
66 | int* cm = __trans_lower; |
67 | const uchar_t* us1 = (const uchar_t*)s1; | |
68 | const uchar_t* us2 = (const uchar_t*)s2; | |
69 | const uchar_t* tptr; | |
70 | int c; | |
71 | ||
72 | if (us2 == NULL || *us2 == '\0') | |
73 | return ((char*)us1); | |
74 | ||
75 | c = cm[*us2]; | |
76 | while (*us1 != '\0') { | |
77 | if (c == cm[*us1++]) { | |
78 | tptr = us1; | |
79 | while (cm[c = *++us2] == cm[*us1++] && c != '\0') | |
80 | continue; | |
81 | if (c == '\0') | |
82 | return ((char*)tptr - 1); | |
83 | us1 = tptr; | |
84 | us2 = (const uchar_t*)s2; | |
85 | c = cm[*us2]; | |
86 | } | |
87 | } | |
88 | ||
89 | return (NULL); | |
6ec5e728 CH |
90 | } |
91 | ||
92 | #endif // HAVE_STRCASESTR | |
93 | ||
2056b0b4 FM |
94 | static Json getServerDetail() |
95 | { | |
96 | return Json::object{ | |
97 | {"type", "Server"}, | |
98 | {"id", "localhost"}, | |
99 | {"url", "/api/v1/servers/localhost"}, | |
100 | {"daemon_type", productTypeApiType()}, | |
101 | {"version", getPDNSVersion()}, | |
102 | {"config_url", "/api/v1/servers/localhost/config{/config_setting}"}, | |
103 | {"zones_url", "/api/v1/servers/localhost/zones{/zone}"}, | |
782e9b24 | 104 | #ifndef RECURSOR |
2056b0b4 | 105 | {"autoprimaries_url", "/api/v1/servers/localhost/autoprimaries{/autoprimary}"} |
782e9b24 | 106 | #endif |
f461cf07 | 107 | }; |
6ec5e728 CH |
108 | } |
109 | ||
9e6d2033 CH |
110 | /* Return information about the supported API versions. |
111 | * The format of this MUST NEVER CHANGE at it's not versioned. | |
112 | */ | |
2056b0b4 FM |
113 | void apiDiscovery(HttpRequest* /* req */, HttpResponse* resp) |
114 | { | |
115 | Json version1 = Json::object{ | |
116 | {"version", 1}, | |
117 | {"url", "/api/v1"}}; | |
118 | Json doc = Json::array{std::move(version1)}; | |
9e6d2033 | 119 | |
917f686a | 120 | resp->setJsonBody(doc); |
9e6d2033 CH |
121 | } |
122 | ||
2056b0b4 FM |
123 | void apiDiscoveryV1(HttpRequest* /* req */, HttpResponse* resp) |
124 | { | |
125 | const Json& version1 = Json::object{ | |
126 | {"server_url", "/api/v1/servers{/server}"}, | |
127 | {"api_features", Json::array{}}}; | |
128 | const Json& doc = Json::array{version1}; | |
bd7759ce PD |
129 | |
130 | resp->setJsonBody(doc); | |
bd7759ce PD |
131 | } |
132 | ||
2056b0b4 FM |
133 | void apiServer(HttpRequest* /* req */, HttpResponse* resp) |
134 | { | |
135 | const Json& doc = Json::array{getServerDetail()}; | |
917f686a | 136 | resp->setJsonBody(doc); |
6ec5e728 CH |
137 | } |
138 | ||
2056b0b4 FM |
139 | void apiServerDetail(HttpRequest* /* req */, HttpResponse* resp) |
140 | { | |
917f686a | 141 | resp->setJsonBody(getServerDetail()); |
6ec5e728 CH |
142 | } |
143 | ||
2056b0b4 FM |
144 | void apiServerConfig(HttpRequest* /* req */, HttpResponse* resp) |
145 | { | |
70c1c68e | 146 | const vector<string>& items = ::arg().list(); |
6ec5e728 | 147 | string value; |
9857fdbc | 148 | Json::array doc; |
2056b0b4 | 149 | for (const string& item : items) { |
dd754091 | 150 | if (item.find("password") != string::npos || item.find("api-key") != string::npos) { |
6ec5e728 | 151 | value = "***"; |
dd754091 FM |
152 | } |
153 | else { | |
6ec5e728 | 154 | value = ::arg()[item]; |
dd754091 | 155 | } |
6ec5e728 | 156 | |
2056b0b4 FM |
157 | doc.push_back(Json::object{ |
158 | {"type", "ConfigSetting"}, | |
159 | {"name", item}, | |
160 | {"value", value}, | |
9857fdbc | 161 | }); |
6ec5e728 | 162 | } |
917f686a | 163 | resp->setJsonBody(doc); |
6ec5e728 CH |
164 | } |
165 | ||
2056b0b4 FM |
166 | void apiServerStatistics(HttpRequest* req, HttpResponse* resp) |
167 | { | |
5376a5d7 RG |
168 | Json::array doc; |
169 | string name = req->getvars["statistic"]; | |
170 | if (!name.empty()) { | |
70c1c68e | 171 | const auto& stat = productServerStatisticsFetch(name); |
5376a5d7 RG |
172 | if (!stat) { |
173 | throw ApiException("Unknown statistic name"); | |
174 | } | |
175 | ||
2056b0b4 FM |
176 | doc.push_back(Json::object{ |
177 | {"type", "StatisticItem"}, | |
178 | {"name", name}, | |
179 | {"value", std::to_string(*stat)}, | |
5376a5d7 RG |
180 | }); |
181 | ||
917f686a | 182 | resp->setJsonBody(doc); |
5376a5d7 RG |
183 | |
184 | return; | |
185 | } | |
186 | ||
118d3b31 CH |
187 | typedef map<string, string> stat_items_t; |
188 | stat_items_t general_stats; | |
189 | productServerStatisticsFetch(general_stats); | |
190 | ||
2056b0b4 FM |
191 | for (const auto& item : general_stats) { |
192 | doc.push_back(Json::object{ | |
193 | {"type", "StatisticItem"}, | |
194 | {"name", item.first}, | |
195 | {"value", item.second}, | |
9aeac747 | 196 | }); |
6ec5e728 CH |
197 | } |
198 | ||
6ab8d5ea OM |
199 | #ifdef RECURSOR |
200 | auto stats = g_Counters.sum(rec::ResponseStats::responseStats); | |
201 | auto resp_qtype_stats = stats.getQTypeResponseCounts(); | |
202 | auto resp_size_stats = stats.getSizeResponseCounts(); | |
203 | auto resp_rcode_stats = stats.getRCodeResponseCounts(); | |
204 | #else | |
5376a5d7 RG |
205 | auto resp_qtype_stats = g_rs.getQTypeResponseCounts(); |
206 | auto resp_size_stats = g_rs.getSizeResponseCounts(); | |
207 | auto resp_rcode_stats = g_rs.getRCodeResponseCounts(); | |
6ab8d5ea | 208 | #endif |
118d3b31 CH |
209 | { |
210 | Json::array values; | |
2056b0b4 | 211 | for (const auto& item : resp_qtype_stats) { |
dd754091 | 212 | if (item.second == 0) { |
118d3b31 | 213 | continue; |
dd754091 | 214 | } |
2056b0b4 FM |
215 | values.push_back(Json::object{ |
216 | {"name", DNSRecordContent::NumberToType(item.first)}, | |
217 | {"value", std::to_string(item.second)}, | |
118d3b31 CH |
218 | }); |
219 | } | |
220 | ||
2056b0b4 FM |
221 | doc.push_back(Json::object{ |
222 | {"type", "MapStatisticItem"}, | |
223 | {"name", "response-by-qtype"}, | |
224 | {"value", values}, | |
118d3b31 CH |
225 | }); |
226 | } | |
227 | ||
228 | { | |
229 | Json::array values; | |
2056b0b4 | 230 | for (const auto& item : resp_size_stats) { |
dd754091 | 231 | if (item.second == 0) { |
118d3b31 | 232 | continue; |
dd754091 | 233 | } |
118d3b31 | 234 | |
2056b0b4 FM |
235 | values.push_back(Json::object{ |
236 | {"name", std::to_string(item.first)}, | |
237 | {"value", std::to_string(item.second)}, | |
118d3b31 CH |
238 | }); |
239 | } | |
240 | ||
2056b0b4 FM |
241 | doc.push_back(Json::object{ |
242 | {"type", "MapStatisticItem"}, | |
243 | {"name", "response-sizes"}, | |
244 | {"value", values}, | |
118d3b31 CH |
245 | }); |
246 | } | |
247 | ||
87798d5e TB |
248 | { |
249 | Json::array values; | |
2056b0b4 | 250 | for (const auto& item : resp_rcode_stats) { |
dd754091 | 251 | if (item.second == 0) { |
87798d5e | 252 | continue; |
dd754091 FM |
253 | } |
254 | ||
2056b0b4 FM |
255 | values.push_back(Json::object{ |
256 | {"name", RCode::to_s(item.first)}, | |
257 | {"value", std::to_string(item.second)}, | |
87798d5e TB |
258 | }); |
259 | } | |
260 | ||
2056b0b4 FM |
261 | doc.push_back(Json::object{ |
262 | {"type", "MapStatisticItem"}, | |
263 | {"name", "response-by-rcode"}, | |
264 | {"value", values}, | |
87798d5e TB |
265 | }); |
266 | } | |
267 | ||
118d3b31 | 268 | #ifndef RECURSOR |
dd754091 | 269 | if ((req->getvars.count("includerings") == 0) || req->getvars["includerings"] != "false") { |
2056b0b4 | 270 | for (const auto& ringName : S.listRings()) { |
c65b5e12 PD |
271 | Json::array values; |
272 | const auto& ring = S.getRing(ringName); | |
2056b0b4 | 273 | for (const auto& item : ring) { |
dd754091 | 274 | if (item.second == 0) { |
c65b5e12 | 275 | continue; |
dd754091 | 276 | } |
c65b5e12 | 277 | |
2056b0b4 FM |
278 | values.push_back(Json::object{ |
279 | {"name", item.first}, | |
280 | {"value", std::to_string(item.second)}, | |
c65b5e12 PD |
281 | }); |
282 | } | |
283 | ||
2056b0b4 FM |
284 | doc.push_back(Json::object{ |
285 | {"type", "RingStatisticItem"}, | |
286 | {"name", ringName}, | |
287 | {"size", std::to_string(S.getRingSize(ringName))}, | |
288 | {"value", values}, | |
118d3b31 CH |
289 | }); |
290 | } | |
118d3b31 CH |
291 | } |
292 | #endif | |
293 | ||
917f686a | 294 | resp->setJsonBody(doc); |
6ec5e728 | 295 | } |
3c3c006b | 296 | |
2056b0b4 FM |
297 | DNSName apiNameToDNSName(const string& name) |
298 | { | |
c576d0c5 CH |
299 | if (!isCanonical(name)) { |
300 | throw ApiException("DNS Name '" + name + "' is not canonical"); | |
301 | } | |
302 | try { | |
303 | return DNSName(name); | |
2056b0b4 FM |
304 | } |
305 | catch (...) { | |
c576d0c5 CH |
306 | throw ApiException("Unable to parse DNS Name '" + name + "'"); |
307 | } | |
308 | } | |
309 | ||
dd754091 | 310 | DNSName apiZoneIdToName(const string& identifier) |
2056b0b4 | 311 | { |
3c3c006b | 312 | string zonename; |
dd754091 | 313 | ostringstream outputStringStream; |
3c3c006b | 314 | |
dd754091 | 315 | if (identifier.empty()) { |
3c3c006b | 316 | throw HttpBadRequestException(); |
dd754091 | 317 | } |
3c3c006b | 318 | |
dd754091 FM |
319 | std::size_t lastpos = 0; |
320 | std::size_t pos = 0; | |
321 | while ((pos = identifier.find('=', lastpos)) != string::npos) { | |
322 | outputStringStream << identifier.substr(lastpos, pos - lastpos); | |
323 | char currentChar{}; | |
1dbe38ba | 324 | // decode tens |
dd754091 FM |
325 | if (identifier[pos + 1] >= '0' && identifier[pos + 1] <= '9') { |
326 | currentChar = static_cast<char>(identifier[pos + 1] - '0'); | |
2056b0b4 | 327 | } |
dd754091 FM |
328 | else if (identifier[pos + 1] >= 'A' && identifier[pos + 1] <= 'F') { |
329 | currentChar = static_cast<char>(identifier[pos + 1] - 'A' + 10); | |
2056b0b4 FM |
330 | } |
331 | else { | |
3c3c006b CH |
332 | throw HttpBadRequestException(); |
333 | } | |
dd754091 | 334 | currentChar = static_cast<char>(currentChar * 16); |
1dbe38ba CH |
335 | |
336 | // decode unit place | |
dd754091 FM |
337 | if (identifier[pos + 2] >= '0' && identifier[pos + 2] <= '9') { |
338 | currentChar = static_cast<char>(currentChar + identifier[pos + 2] - '0'); | |
2056b0b4 | 339 | } |
dd754091 FM |
340 | else if (identifier[pos + 2] >= 'A' && identifier[pos + 2] <= 'F') { |
341 | currentChar = static_cast<char>(currentChar + identifier[pos + 2] - 'A' + 10); | |
2056b0b4 FM |
342 | } |
343 | else { | |
1dbe38ba CH |
344 | throw HttpBadRequestException(); |
345 | } | |
346 | ||
dd754091 | 347 | outputStringStream << currentChar; |
3c3c006b | 348 | |
2056b0b4 | 349 | lastpos = pos + 3; |
3c3c006b CH |
350 | } |
351 | if (lastpos < pos) { | |
dd754091 | 352 | outputStringStream << identifier.substr(lastpos, pos - lastpos); |
3c3c006b CH |
353 | } |
354 | ||
dd754091 | 355 | zonename = outputStringStream.str(); |
3c3c006b | 356 | |
1d6b70f9 CH |
357 | try { |
358 | return DNSName(zonename); | |
2056b0b4 FM |
359 | } |
360 | catch (...) { | |
1d6b70f9 CH |
361 | throw ApiException("Unable to parse DNS Name '" + zonename + "'"); |
362 | } | |
3c3c006b CH |
363 | } |
364 | ||
2056b0b4 FM |
365 | string apiZoneNameToId(const DNSName& dname) |
366 | { | |
367 | string name = dname.toString(); | |
dd754091 | 368 | ostringstream outputStringStream; |
3c3c006b | 369 | |
2056b0b4 FM |
370 | for (char iter : name) { |
371 | if ((iter >= 'A' && iter <= 'Z') || (iter >= 'a' && iter <= 'z') || (iter >= '0' && iter <= '9') || (iter == '.') || (iter == '-')) { | |
dd754091 | 372 | outputStringStream << iter; |
2056b0b4 FM |
373 | } |
374 | else { | |
dd754091 | 375 | outputStringStream << (boost::format("=%02X") % (int)iter); |
3c3c006b CH |
376 | } |
377 | } | |
378 | ||
dd754091 | 379 | string identifier = outputStringStream.str(); |
161a8b69 | 380 | |
3c3c006b | 381 | // add trailing dot |
dd754091 FM |
382 | if (identifier.empty() || identifier.substr(identifier.size() - 1) != ".") { |
383 | identifier += "."; | |
161a8b69 | 384 | } |
3c3c006b CH |
385 | |
386 | // special handling for the root zone, as a dot on it's own doesn't work | |
387 | // everywhere. | |
dd754091 FM |
388 | if (identifier == ".") { |
389 | identifier = (boost::format("=%02X") % (int)('.')).str(); | |
3c3c006b | 390 | } |
dd754091 | 391 | return identifier; |
3c3c006b | 392 | } |
1d6b70f9 | 393 | |
2056b0b4 FM |
394 | void apiCheckNameAllowedCharacters(const string& name) |
395 | { | |
dd754091 | 396 | if (name.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_/.-") != std::string::npos) { |
2056b0b4 | 397 | throw ApiException("Name '" + name + "' contains unsupported characters"); |
dd754091 | 398 | } |
1d6b70f9 | 399 | } |
cb9b5901 | 400 | |
2056b0b4 FM |
401 | void apiCheckQNameAllowedCharacters(const string& qname) |
402 | { | |
dd754091 | 403 | if (qname.compare(0, 2, "*.") == 0) { |
2056b0b4 | 404 | apiCheckNameAllowedCharacters(qname.substr(2)); |
dd754091 FM |
405 | } |
406 | else { | |
2056b0b4 | 407 | apiCheckNameAllowedCharacters(qname); |
dd754091 | 408 | } |
cb9b5901 | 409 | } |