]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/arguments.cc
Meson: Separate test files from common files
[thirdparty/pdns.git] / pdns / arguments.cc
1 /*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 #include "arguments.hh"
26 #include <boost/algorithm/string.hpp>
27 #include <boost/algorithm/string/compare.hpp>
28 #include <boost/algorithm/string/predicate.hpp>
29
30 #include "namespaces.hh"
31 #include "logger.hh"
32 #include <sys/types.h>
33 #include <dirent.h>
34 #include <sys/stat.h>
35 #include <unistd.h>
36 #include <climits>
37
38 ArgvMap::param_t::const_iterator ArgvMap::begin()
39 {
40 return d_params.begin();
41 }
42
43 ArgvMap::param_t::const_iterator ArgvMap::end()
44 {
45 return d_params.end();
46 }
47
48 string& ArgvMap::set(const string& var)
49 {
50 return d_params[var];
51 }
52
53 void ArgvMap::setDefault(const string& var, const string& value)
54 {
55 if (defaultmap.count(var) == 0) {
56 defaultmap.insert(pair<string, string>(var, value));
57 }
58 }
59
60 void ArgvMap::setDefaults()
61 {
62 for (const auto& param : d_params) {
63 if (defaultmap.count(param.first) == 0) {
64 defaultmap.insert(param);
65 }
66 }
67 }
68
69 bool ArgvMap::mustDo(const string& var)
70 {
71 return ((*this)[var] != "no") && ((*this)[var] != "off");
72 }
73
74 vector<string> ArgvMap::list()
75 {
76 vector<string> ret;
77 ret.reserve(d_params.size());
78 for (const auto& param : d_params) {
79 ret.push_back(param.first);
80 }
81 return ret;
82 }
83
84 string& ArgvMap::set(const string& var, const string& help)
85 {
86 helpmap[var] = help;
87 d_typeMap[var] = "Parameter";
88 return set(var);
89 }
90
91 void ArgvMap::setCmd(const string& var, const string& help)
92 {
93 helpmap[var] = help;
94 d_typeMap[var] = "Command";
95 set(var) = "no";
96 }
97
98 string& ArgvMap::setSwitch(const string& var, const string& help)
99 {
100 helpmap[var] = help;
101 d_typeMap[var] = "Switch";
102 return set(var);
103 }
104
105 bool ArgvMap::contains(const string& var, const string& val)
106 {
107 const auto& param = d_params.find(var);
108 if (param == d_params.end() || param->second.empty()) {
109 return false;
110 }
111 vector<string> parts;
112
113 stringtok(parts, param->second, ", \t");
114 return std::any_of(parts.begin(), parts.end(), [&](const std::string& str) { return str == val; });
115 }
116
117 string ArgvMap::helpstring(string prefix)
118 {
119 if (prefix == "no") {
120 prefix = "";
121 }
122
123 string help;
124
125 for (const auto& helpitem : helpmap) {
126 if (!prefix.empty() && helpitem.first.find(prefix) != 0) { // only print items with prefix
127 continue;
128 }
129
130 help += " --";
131 help += helpitem.first;
132
133 string type = d_typeMap[helpitem.first];
134
135 if (type == "Parameter") {
136 help += "=...";
137 }
138 else if (type == "Switch") {
139 help += " | --" + helpitem.first + "=yes";
140 help += " | --" + helpitem.first + "=no";
141 }
142
143 help += "\n\t";
144 help += helpitem.second;
145 help += "\n";
146 }
147 return help;
148 }
149
150 string ArgvMap::formatOne(bool running, bool full, const string& var, const string& help, const string& theDefault, const string& current)
151 {
152 string out;
153
154 if (!running || full) {
155 out += "#################################\n";
156 out += "# ";
157 out += var;
158 out += "\t";
159 out += help;
160 out += "\n#\n";
161 }
162 else {
163 if (theDefault == current) {
164 return "";
165 }
166 }
167
168 if (!running || theDefault == current) {
169 out += "# ";
170 }
171
172 if (running) {
173 out += var + "=" + current + "\n";
174 if (full) {
175 out += "\n";
176 }
177 }
178 else {
179 out += var + "=" + theDefault + "\n\n";
180 }
181
182 return out;
183 }
184
185 // If running and full, only changed settings are returned.
186 string ArgvMap::configstring(bool running, bool full)
187 {
188 string help;
189
190 if (running) {
191 help = "# Autogenerated configuration file based on running instance (" + nowTime() + ")\n\n";
192 }
193 else {
194 help = "# Autogenerated configuration file template\n\n";
195 }
196
197 // Affects parsing, should come first.
198 help += formatOne(running, full, "ignore-unknown-settings", helpmap["ignore-unknown-settings"], defaultmap["ignore-unknown-settings"], d_params["ignore-unknown-settings"]);
199
200 for (const auto& helpitem : helpmap) {
201 if (d_typeMap[helpitem.first] == "Command") {
202 continue;
203 }
204 if (helpitem.first == "ignore-unknown-settings") {
205 continue;
206 }
207
208 if (defaultmap.count(helpitem.first) == 0) {
209 throw ArgException(string("Default for setting '") + helpitem.first + "' not set");
210 }
211
212 help += formatOne(running, full, helpitem.first, helpitem.second, defaultmap[helpitem.first], d_params[helpitem.first]);
213 }
214
215 if (running) {
216 for (const auto& unknown : d_unknownParams) {
217 help += formatOne(running, full, unknown.first, "unknown setting", "", unknown.second);
218 }
219 }
220
221 return help;
222 }
223
224 const string& ArgvMap::operator[](const string& arg)
225 {
226 if (!parmIsset(arg)) {
227 throw ArgException(string("Undefined but needed argument: '") + arg + "'");
228 }
229
230 return d_params[arg];
231 }
232
233 mode_t ArgvMap::asMode(const string& arg)
234 {
235 if (!parmIsset(arg)) {
236 throw ArgException(string("Undefined but needed argument: '") + arg + "'");
237 }
238
239 const auto* const cptr_orig = d_params[arg].c_str();
240 char* cptr_ret = nullptr;
241
242 auto mode = static_cast<mode_t>(strtol(cptr_orig, &cptr_ret, 8));
243 if (mode == 0 && cptr_ret == cptr_orig) {
244 throw ArgException("'" + arg + string("' contains invalid octal mode"));
245 }
246 return mode;
247 }
248
249 gid_t ArgvMap::asGid(const string& arg)
250 {
251 if (!parmIsset(arg)) {
252 throw ArgException(string("Undefined but needed argument: '") + arg + "'");
253 }
254
255 const auto* cptr_orig = d_params[arg].c_str();
256 char* cptr_ret = nullptr;
257 auto gid = static_cast<gid_t>(strtol(cptr_orig, &cptr_ret, 0));
258 if (gid == 0 && cptr_ret == cptr_orig) {
259 // try to resolve
260
261 struct group* group = getgrnam(d_params[arg].c_str()); // NOLINT: called before going multi-threaded
262 if (group == nullptr) {
263 throw ArgException("'" + arg + string("' contains invalid group"));
264 }
265 gid = group->gr_gid;
266 }
267 return gid;
268 }
269
270 uid_t ArgvMap::asUid(const string& arg)
271 {
272 if (!parmIsset(arg)) {
273 throw ArgException(string("Undefined but needed argument: '") + arg + "'");
274 }
275
276 const auto* cptr_orig = d_params[arg].c_str();
277 char* cptr_ret = nullptr;
278
279 auto uid = static_cast<uid_t>(strtol(cptr_orig, &cptr_ret, 0));
280 if (uid == 0 && cptr_ret == cptr_orig) {
281 // try to resolve
282 struct passwd* pwent = getpwnam(d_params[arg].c_str()); // NOLINT: called before going multi-threaded
283 if (pwent == nullptr) {
284 throw ArgException("'" + arg + string("' contains invalid group"));
285 }
286 uid = pwent->pw_uid;
287 }
288 return uid;
289 }
290
291 int ArgvMap::asNum(const string& arg, int def)
292 {
293 if (!parmIsset(arg)) {
294 throw ArgException(string("Undefined but needed argument: '") + arg + "'");
295 }
296
297 // use default for empty values
298 if (d_params[arg].empty()) {
299 return def;
300 }
301
302 const auto* cptr_orig = d_params[arg].c_str();
303 char* cptr_ret = nullptr;
304 auto retval = static_cast<int>(strtol(cptr_orig, &cptr_ret, 0));
305 if (retval == 0 && cptr_ret == cptr_orig) {
306 throw ArgException("'" + arg + "' value '" + string(cptr_orig) + string("' is not a valid number"));
307 }
308
309 return retval;
310 }
311
312 bool ArgvMap::isEmpty(const string& arg)
313 {
314 if (!parmIsset(arg)) {
315 return true;
316 }
317 return d_params[arg].empty();
318 }
319
320 double ArgvMap::asDouble(const string& arg)
321 {
322 if (!parmIsset(arg)) {
323 throw ArgException(string("Undefined but needed argument: '") + arg + "'");
324 }
325
326 if (d_params[arg].empty()) {
327 return 0.0;
328 }
329
330 const auto* cptr_orig = d_params[arg].c_str();
331 char* cptr_ret = nullptr;
332 auto retval = strtod(cptr_orig, &cptr_ret);
333
334 if (retval == 0 && cptr_ret == cptr_orig) {
335 throw ArgException("'" + arg + string("' is not valid double"));
336 }
337
338 return retval;
339 }
340
341 ArgvMap::ArgvMap()
342 {
343 set("ignore-unknown-settings", "Configuration settings to ignore if they are unknown") = "";
344 }
345
346 bool ArgvMap::parmIsset(const string& var)
347 {
348 return d_params.find(var) != d_params.end();
349 }
350
351 // ATM Shared between Recursor and Auth, is that a good idea?
352 static const map<string, string> deprecateList = {
353 {"stats-api-blacklist", "stats-api-disabled-list"},
354 {"stats-carbon-blacklist", "stats-carbon-disabled-list"},
355 {"stats-rec-control-blacklist", "stats-rec-control-disabled-list"},
356 {"stats-snmp-blacklist", "stats-snmp-disabled-list"},
357 {"edns-subnet-whitelist", "edns-subnet-allow-list"},
358 {"new-domain-whitelist", "new-domain-ignore-list"},
359 {"snmp-master-socket", "snmp-daemon-socket"},
360 {"xpf-allow-from", "Proxy Protocol"},
361 {"xpf-rr-code", "Proxy Protocol"},
362 {"domain-metadata-cache-ttl", "zone-metadata-cache-ttl"},
363 };
364
365 // NOLINTNEXTLINE(readability-convert-member-functions-to-static): accesses d_log (compiled out in auth, hence clang-tidy message)
366 void ArgvMap::warnIfDeprecated(const string& var) const
367 {
368 const auto msg = deprecateList.find(var);
369 if (msg != deprecateList.end()) {
370 SLOG(g_log << Logger::Warning << "'" << var << "' is deprecated and will be removed in a future release, use '" << msg->second << "' instead" << endl,
371 d_log->info(Logr::Warning, "Option is deprecated and will be removed in a future release", "deprecatedName", Logging::Loggable(var), "alternative", Logging::Loggable(msg->second)));
372 }
373 }
374
375 string ArgvMap::isDeprecated(const string& var)
376 {
377 const auto msg = deprecateList.find(var);
378 return msg != deprecateList.end() ? msg->second : "";
379 }
380
381 void ArgvMap::parseOne(const string& arg, const string& parseOnly, bool lax)
382 {
383 string var;
384 string val;
385 string::size_type pos = 0;
386 bool incremental = false;
387
388 if (arg.find("--") == 0 && (pos = arg.find("+=")) != string::npos) // this is a --port+=25 case
389 {
390 var = arg.substr(2, pos - 2);
391 val = arg.substr(pos + 2);
392 incremental = true;
393 }
394 else if (arg.find("--") == 0 && (pos = arg.find('=')) != string::npos) // this is a --port=25 case
395 {
396 var = arg.substr(2, pos - 2);
397 val = arg.substr(pos + 1);
398 }
399 else if (arg.find("--") == 0 && (arg.find('=') == string::npos)) // this is a --daemon case
400 {
401 var = arg.substr(2);
402 val = "";
403 }
404 else if (arg[0] == '-' && arg.length() > 1) {
405 var = arg.substr(1);
406 val = "";
407 }
408 else { // command
409 d_cmds.push_back(arg);
410 }
411
412 boost::trim(var);
413
414 if (!var.empty() && (parseOnly.empty() || var == parseOnly)) {
415 if (!lax) {
416 warnIfDeprecated(var);
417 }
418 pos = val.find_first_not_of(" \t"); // strip leading whitespace
419 if (pos != 0 && pos != string::npos) {
420 val = val.substr(pos);
421 }
422 if (parmIsset(var)) {
423 if (incremental) {
424 if (d_params[var].empty()) {
425 if (d_cleared.count(var) == 0) {
426 throw ArgException("Incremental setting '" + var + "' without a parent");
427 }
428 d_params[var] = val;
429 }
430 else {
431 d_params[var] += ", " + val;
432 }
433 }
434 else {
435 d_params[var] = val;
436 d_cleared.insert(var);
437 }
438 }
439 else {
440 // unknown setting encountered. see if its on the ignore list before throwing.
441 vector<string> parts;
442 stringtok(parts, d_params["ignore-unknown-settings"], " ,\t\n\r");
443 if (find(parts.begin(), parts.end(), var) != parts.end()) {
444 d_unknownParams[var] = std::move(val);
445 SLOG(g_log << Logger::Warning << "Ignoring unknown setting '" << var << "' as requested" << endl,
446 d_log->info(Logr::Warning, "Ignoring unknown setting as requested", "name", Logging::Loggable(var)));
447 return;
448 }
449
450 if (!lax) {
451 throw ArgException("Trying to set unknown setting '" + var + "'");
452 }
453 }
454 }
455 }
456
457 const vector<string>& ArgvMap::getCommands()
458 {
459 return d_cmds;
460 }
461
462 void ArgvMap::parse(int& argc, char** argv, bool lax)
463 {
464 d_cmds.clear();
465 d_cleared.clear();
466 for (int i = 1; i < argc; i++) {
467 parseOne(argv[i], "", lax); // NOLINT: Posix argument parsing
468 }
469 }
470
471 void ArgvMap::preParse(int& argc, char** argv, const string& arg)
472 {
473 for (int i = 1; i < argc; i++) {
474 string varval = argv[i]; // NOLINT: Posix argument parsing
475 if (varval.find("--" + arg) == 0) {
476 parseOne(argv[i]); // NOLINT: Posix argument parsing
477 }
478 }
479 }
480
481 bool ArgvMap::parseFile(const string& fname, const string& arg, bool lax)
482 {
483 string line;
484 string pline;
485
486 std::ifstream configFileStream(fname);
487 if (!configFileStream) {
488 return false;
489 }
490
491 while (getline(configFileStream, pline)) {
492 boost::trim_right(pline);
493
494 if (!pline.empty() && pline[pline.size() - 1] == '\\') {
495 line += pline.substr(0, pline.length() - 1);
496 continue;
497 }
498
499 line += pline;
500
501 // strip everything after a #
502 string::size_type pos = line.find('#');
503 if (pos != string::npos) {
504 // make sure it's either first char or has whitespace before
505 // fixes issue #354
506 if (pos == 0 || (std::isspace(line[pos - 1]) != 0)) {
507 line = line.substr(0, pos);
508 }
509 }
510
511 // strip trailing spaces
512 boost::trim_right(line);
513
514 // strip leading spaces
515 pos = line.find_first_not_of(" \t\r\n");
516 if (pos != string::npos) {
517 line = line.substr(pos);
518 }
519
520 // gpgsql-basic-query=sdfsdfs dfsdfsdf sdfsdfsfd
521
522 parseOne(string("--") + line, arg, lax);
523 line = "";
524 }
525
526 return true;
527 }
528
529 bool ArgvMap::preParseFile(const string& fname, const string& arg, const string& theDefault)
530 {
531 d_params[arg] = theDefault;
532
533 return parseFile(fname, arg, false);
534 }
535
536 bool ArgvMap::file(const string& fname, bool lax)
537 {
538 return file(fname, lax, false);
539 }
540
541 bool ArgvMap::file(const string& fname, bool lax, bool included)
542 {
543 if (!parmIsset("include-dir")) { // inject include-dir
544 set("include-dir", "Directory to include configuration files from");
545 }
546
547 if (!parseFile(fname, "", lax)) {
548 SLOG(g_log << Logger::Warning << "Unable to open " << fname << std::endl,
549 d_log->error(Logr::Warning, "Unable to open file", "name", Logging::Loggable(fname)));
550 return false;
551 }
552
553 // handle include here (avoid re-include)
554 if (!included && !d_params["include-dir"].empty()) {
555 std::vector<std::string> extraConfigs;
556 gatherIncludes(d_params["include-dir"], ".conf", extraConfigs);
557 for (const std::string& filename : extraConfigs) {
558 if (!file(filename.c_str(), lax, true)) {
559 SLOG(g_log << Logger::Error << filename << " could not be parsed" << std::endl,
560 d_log->info(Logr::Error, "Unable to parse config file", "name", Logging::Loggable(filename)));
561 throw ArgException(filename + " could not be parsed");
562 }
563 }
564 }
565
566 return true;
567 }
568
569 // NOLINTNEXTLINE(readability-convert-member-functions-to-static): accesses d_log (compiled out in auth, hence clang-tidy message)
570 void ArgvMap::gatherIncludes(const std::string& directory, const std::string& suffix, std::vector<std::string>& extraConfigs)
571 {
572 if (directory.empty()) {
573 return; // nothing to do
574 }
575
576 std::vector<std::string> vec;
577 auto directoryError = pdns::visit_directory(directory, [this, &directory, &suffix, &vec]([[maybe_unused]] ino_t inodeNumber, const std::string_view& name) {
578 (void)this;
579 if (boost::starts_with(name, ".")) {
580 return true; // skip any dots
581 }
582 if (boost::ends_with(name, suffix)) {
583 // build name
584 string fullName = directory + "/" + std::string(name);
585 // ensure it's readable file
586 struct stat statInfo
587 {
588 };
589 if (stat(fullName.c_str(), &statInfo) != 0 || !S_ISREG(statInfo.st_mode)) {
590 string msg = fullName + " is not a regular file";
591 SLOG(g_log << Logger::Error << msg << std::endl,
592 d_log->info(Logr::Error, "Unable to open non-regular file", "name", Logging::Loggable(fullName)));
593 throw ArgException(msg);
594 }
595 vec.emplace_back(fullName);
596 }
597 return true;
598 });
599
600 if (directoryError) {
601 int err = errno;
602 string msg = directory + " is not accessible: " + stringerror(err);
603 SLOG(g_log << Logger::Error << msg << std::endl,
604 d_log->error(Logr::Error, err, "Directory is not accessible", "name", Logging::Loggable(directory)));
605 throw ArgException(msg);
606 }
607
608 std::sort(vec.begin(), vec.end(), CIStringComparePOSIX());
609 extraConfigs.insert(extraConfigs.end(), vec.begin(), vec.end());
610 }