config.cpp raw

   1  // Copyright (c) 2023 The Limenka developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <common/args.h>
   6  
   7  #include <chainparamsbase.h>
   8  #include <common/settings.h>
   9  #include <logging.h>
  10  #include <sync.h>
  11  #include <tinyformat.h>
  12  #include <univalue.h>
  13  #include <util/chaintype.h>
  14  #include <util/fs.h>
  15  #include <util/string.h>
  16  
  17  #include <algorithm>
  18  #include <cassert>
  19  #include <cstdlib>
  20  #include <filesystem>
  21  #include <fstream>
  22  #include <iostream>
  23  #include <list>
  24  #include <map>
  25  #include <memory>
  26  #include <optional>
  27  #include <string>
  28  #include <string_view>
  29  #include <utility>
  30  #include <vector>
  31  
  32  using util::TrimString;
  33  using util::TrimStringView;
  34  
  35  static bool GetConfigOptions(std::istream& stream, const std::string& filepath, std::string& error, std::vector<std::pair<std::string, std::string>>& options, std::list<SectionInfo>& sections)
  36  {
  37      std::string str, prefix;
  38      std::string::size_type pos;
  39      int linenr = 1;
  40      while (std::getline(stream, str)) {
  41          bool used_hash = false;
  42          if ((pos = str.find('#')) != std::string::npos) {
  43              str = str.substr(0, pos);
  44              used_hash = true;
  45          }
  46          const static std::string pattern = " \t\r\n";
  47          str = TrimString(str, pattern);
  48          if (!str.empty()) {
  49              if (*str.begin() == '[' && *str.rbegin() == ']') {
  50                  const std::string section = str.substr(1, str.size() - 2);
  51                  sections.emplace_back(SectionInfo{section, filepath, linenr});
  52                  prefix = section + '.';
  53              } else if (*str.begin() == '-') {
  54                  error = strprintf("parse error on line %i: %s, options in configuration file must be specified without leading -", linenr, str);
  55                  return false;
  56              } else if ((pos = str.find('=')) != std::string::npos) {
  57                  std::string name = prefix + TrimString(std::string_view{str}.substr(0, pos), pattern);
  58                  std::string_view value = TrimStringView(std::string_view{str}.substr(pos + 1), pattern);
  59                  if (used_hash && name.find("rpcpassword") != std::string::npos) {
  60                      error = strprintf("parse error on line %i, using # in rpcpassword can be ambiguous and should be avoided", linenr);
  61                      return false;
  62                  }
  63                  options.emplace_back(name, value);
  64                  if ((pos = name.rfind('.')) != std::string::npos && prefix.length() <= pos) {
  65                      sections.emplace_back(SectionInfo{name.substr(0, pos), filepath, linenr});
  66                  }
  67              } else {
  68                  error = strprintf("parse error on line %i: %s", linenr, str);
  69                  if (str.size() >= 2 && str.substr(0, 2) == "no") {
  70                      error += strprintf(", if you intended to specify a negated option, use %s=1 instead", str);
  71                  }
  72                  return false;
  73              }
  74          }
  75          ++linenr;
  76      }
  77      return true;
  78  }
  79  
  80  bool IsConfSupported(KeyInfo& key, std::string& error) {
  81      if (key.name == "conf") {
  82          error = "conf cannot be set in the configuration file; use includeconf= if you want to include additional config files";
  83          return false;
  84      }
  85      if (key.name == "reindex") {
  86          // reindex can be set in a config file but it is strongly discouraged as this will cause the node to reindex on
  87          // every restart. Allow the config but throw a warning
  88          LogWarning("reindex=1 is set in the configuration file, which will significantly slow down startup. Consider removing or commenting out this option for better performance, unless there is currently a condition which makes rebuilding the indexes necessary");
  89          return true;
  90      }
  91      return true;
  92  }
  93  
  94  bool ArgsManager::ReadConfigStream(std::istream& stream, const std::string& filepath, std::string& error, bool ignore_invalid_keys, std::map<std::string, std::vector<common::SettingsValue>>* settings_target)
  95  {
  96      LOCK(cs_args);
  97      std::vector<std::pair<std::string, std::string>> options;
  98      if (!GetConfigOptions(stream, filepath, error, options, m_config_sections)) {
  99          return false;
 100      }
 101      for (const std::pair<std::string, std::string>& option : options) {
 102          KeyInfo key = InterpretKey(option.first);
 103          std::optional<unsigned int> flags = GetArgFlags('-' + key.name);
 104          if (!IsConfSupported(key, error)) return false;
 105          if (flags) {
 106              std::optional<common::SettingsValue> value = InterpretValue(key, &option.second, *flags, error);
 107              if (!value) {
 108                  return false;
 109              }
 110              if (settings_target) {
 111                  (*settings_target)[key.name].push_back(*value);
 112              } else
 113              m_settings.ro_config[key.section][key.name].push_back(*value);
 114          } else {
 115              if (ignore_invalid_keys) {
 116                  LogWarning("Ignoring unknown configuration value %s", option.first);
 117              } else {
 118                  error = strprintf("Invalid configuration value %s", option.first);
 119                  return false;
 120              }
 121          }
 122      }
 123      return true;
 124  }
 125  
 126  bool ArgsManager::ReadConfigFiles(std::string& error, bool ignore_invalid_keys)
 127  {
 128      {
 129          LOCK(cs_args);
 130          m_settings.ro_config.clear();
 131          m_settings.rw_config.clear();
 132          m_rwconf_had_prune_option = false;
 133          m_config_sections.clear();
 134          m_config_path = AbsPathForConfigVal(*this, GetPathArg("-conf", LIMENKA_CONF_FILENAME), /*net_specific=*/false);
 135      }
 136  
 137      const auto conf_path{GetConfigFilePath()};
 138      std::ifstream stream;
 139      if (!conf_path.empty()) { // path is empty when -noconf is specified
 140          if (fs::is_directory(conf_path)) {
 141              error = strprintf("Config file \"%s\" is a directory.", fs::PathToString(conf_path));
 142              return false;
 143          }
 144          stream = std::ifstream{conf_path};
 145          // If the file is explicitly specified, it must be readable
 146          if (IsArgSet("-conf") && !stream.good()) {
 147              error = strprintf("specified config file \"%s\" could not be opened.", fs::PathToString(conf_path));
 148              return false;
 149          }
 150      }
 151      // ok to not have a config file
 152      if (stream.good()) {
 153          if (!ReadConfigStream(stream, fs::PathToString(conf_path), error, ignore_invalid_keys)) {
 154              return false;
 155          }
 156          // `-includeconf` cannot be included in the command line arguments except
 157          // as `-noincludeconf` (which indicates that no included conf file should be used).
 158          bool use_conf_file{true};
 159          {
 160              LOCK(cs_args);
 161              if (auto* includes = common::FindKey(m_settings.command_line_options, "includeconf")) {
 162                  // ParseParameters() fails if a non-negated -includeconf is passed on the command-line
 163                  assert(common::SettingsSpan(*includes).last_negated());
 164                  use_conf_file = false;
 165              }
 166          }
 167          if (use_conf_file) {
 168              std::string chain_id = GetChainTypeString();
 169              std::vector<std::string> conf_file_names;
 170  
 171              auto add_includes = [&](const std::string& network, size_t skip = 0) {
 172                  size_t num_values = 0;
 173                  LOCK(cs_args);
 174                  if (auto* section = common::FindKey(m_settings.ro_config, network)) {
 175                      if (auto* values = common::FindKey(*section, "includeconf")) {
 176                          for (size_t i = std::max(skip, common::SettingsSpan(*values).negated()); i < values->size(); ++i) {
 177                              conf_file_names.push_back((*values)[i].get_str());
 178                          }
 179                          num_values = values->size();
 180                      }
 181                  }
 182                  return num_values;
 183              };
 184  
 185              // We haven't set m_network yet (that happens in SelectParams()), so manually check
 186              // for network.includeconf args.
 187              const size_t chain_includes = add_includes(chain_id);
 188              const size_t default_includes = add_includes({});
 189  
 190              for (const std::string& conf_file_name : conf_file_names) {
 191                  const auto include_conf_path{AbsPathForConfigVal(*this, fs::PathFromString(conf_file_name), /*net_specific=*/false)};
 192                  if (fs::is_directory(include_conf_path)) {
 193                      error = strprintf("Included config file \"%s\" is a directory.", fs::PathToString(include_conf_path));
 194                      return false;
 195                  }
 196                  std::ifstream conf_file_stream{include_conf_path};
 197                  if (conf_file_stream.good()) {
 198                      if (!ReadConfigStream(conf_file_stream, conf_file_name, error, ignore_invalid_keys)) {
 199                          return false;
 200                      }
 201                      LogPrintf("Included configuration file %s\n", conf_file_name);
 202                  } else {
 203                      error = "Failed to include configuration file " + conf_file_name;
 204                      return false;
 205                  }
 206              }
 207  
 208              // Warn about recursive -includeconf
 209              conf_file_names.clear();
 210              add_includes(chain_id, /* skip= */ chain_includes);
 211              add_includes({}, /* skip= */ default_includes);
 212              std::string chain_id_final = GetChainTypeString();
 213              if (chain_id_final != chain_id) {
 214                  // Also warn about recursive includeconf for the chain that was specified in one of the includeconfs
 215                  add_includes(chain_id_final);
 216              }
 217              for (const std::string& conf_file_name : conf_file_names) {
 218                  tfm::format(std::cerr, "warning: -includeconf cannot be used from included files; ignoring -includeconf=%s\n", conf_file_name);
 219              }
 220          }
 221      }
 222  
 223      // Check for chain settings (BaseParams() calls are only valid after this clause)
 224      try {
 225          SelectBaseParams(gArgs.GetChainType());
 226      } catch (const std::exception& e) {
 227          error = e.what();
 228          return false;
 229      }
 230  
 231      // If datadir is changed in .conf file:
 232      ClearPathCache();
 233      if (!CheckDataDirOption(*this)) {
 234          error = strprintf("specified data directory \"%s\" does not exist.", GetArg("-datadir", ""));
 235          return false;
 236      }
 237  
 238      LOCK(cs_args);
 239      m_rwconf_path = AbsPathForConfigVal(*this, GetPathArg("-confrw", LIMENKA_RW_CONF_FILENAME));
 240      const auto rwconf_path{GetRWConfigFilePath()};
 241      std::ifstream rwconf_stream(rwconf_path);
 242      if (rwconf_stream.good()) {
 243          if (!ReadConfigStream(rwconf_stream, fs::PathToString(rwconf_path), error, ignore_invalid_keys, &m_settings.rw_config)) {
 244              return false;
 245          }
 246          m_rwconf_had_prune_option = m_settings.rw_config.count("prune");
 247      }
 248  
 249      return true;
 250  }
 251  
 252  fs::path AbsPathForConfigVal(const ArgsManager& args, const fs::path& path, bool net_specific)
 253  {
 254      if (path.is_absolute() || path.empty()) {
 255          return path;
 256      }
 257      return fsbridge::AbsPathJoin(net_specific ? args.GetDataDirNet() : args.GetDataDirBase(), path);
 258  }
 259