config.cpp raw

   1  // Copyright (c) 2023-present The Bitcoin Core 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 <common/settings.h>
   8  #include <sync.h>
   9  #include <tinyformat.h>
  10  #include <univalue.h>
  11  #include <util/chaintype.h>
  12  #include <util/fs.h>
  13  #include <util/log.h>
  14  #include <util/string.h>
  15  
  16  #include <algorithm>
  17  #include <cassert>
  18  #include <cstdlib>
  19  #include <filesystem>
  20  #include <fstream>
  21  #include <iostream>
  22  #include <sstream>
  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.starts_with("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)
  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              m_settings.ro_config[key.section][key.name].push_back(*value);
 111          } else {
 112              if (ignore_invalid_keys) {
 113                  LogWarning("Ignoring unknown configuration value %s", option.first);
 114              } else {
 115                  error = strprintf("Invalid configuration value %s", option.first);
 116                  return false;
 117              }
 118          }
 119      }
 120      return true;
 121  }
 122  
 123  bool ArgsManager::ReadConfigString(const std::string& str_config)
 124  {
 125      std::istringstream streamConfig(str_config);
 126      {
 127          LOCK(cs_args);
 128          m_settings.ro_config.clear();
 129          m_config_sections.clear();
 130      }
 131      std::string error;
 132      return ReadConfigStream(streamConfig, "", error);
 133  }
 134  
 135  bool ArgsManager::ReadConfigFiles(std::string& error, bool ignore_invalid_keys)
 136  {
 137      {
 138          LOCK(cs_args);
 139          m_settings.ro_config.clear();
 140          m_config_sections.clear();
 141          const auto conf_val = GetPathArg_("-conf", BITCOIN_CONF_FILENAME);
 142          m_config_path = (conf_val.is_absolute() || conf_val.empty()) ? conf_val : fsbridge::AbsPathJoin(GetDataDir(/*net_specific=*/false), conf_val);
 143      }
 144  
 145      const auto conf_path{GetConfigFilePath()};
 146      std::ifstream stream;
 147      if (!conf_path.empty()) { // path is empty when -noconf is specified
 148          if (fs::is_directory(conf_path)) {
 149              error = strprintf("Config file \"%s\" is a directory.", fs::PathToString(conf_path));
 150              return false;
 151          }
 152          stream = std::ifstream{conf_path.std_path()};
 153          // If the file is explicitly specified, it must be readable
 154          if (IsArgSet("-conf") && !stream.good()) {
 155              error = strprintf("specified config file \"%s\" could not be opened.", fs::PathToString(conf_path));
 156              return false;
 157          }
 158      }
 159      // ok to not have a config file
 160      if (stream.good()) {
 161          if (!ReadConfigStream(stream, fs::PathToString(conf_path), error, ignore_invalid_keys)) {
 162              return false;
 163          }
 164          // `-includeconf` cannot be included in the command line arguments except
 165          // as `-noincludeconf` (which indicates that no included conf file should be used).
 166          bool use_conf_file{true};
 167          {
 168              LOCK(cs_args);
 169              if (auto* includes = common::FindKey(m_settings.command_line_options, "includeconf")) {
 170                  // ParseParameters() fails if a non-negated -includeconf is passed on the command-line
 171                  assert(common::SettingsSpan(*includes).last_negated());
 172                  use_conf_file = false;
 173              }
 174          }
 175          if (use_conf_file) {
 176              std::string chain_id = GetChainTypeString();
 177              std::vector<std::string> conf_file_names;
 178  
 179              auto add_includes = [&](const std::string& network, size_t skip = 0) {
 180                  size_t num_values = 0;
 181                  LOCK(cs_args);
 182                  if (auto* section = common::FindKey(m_settings.ro_config, network)) {
 183                      if (auto* values = common::FindKey(*section, "includeconf")) {
 184                          for (size_t i = std::max(skip, common::SettingsSpan(*values).negated()); i < values->size(); ++i) {
 185                              conf_file_names.push_back((*values)[i].get_str());
 186                          }
 187                          num_values = values->size();
 188                      }
 189                  }
 190                  return num_values;
 191              };
 192  
 193              // We haven't set m_network yet (that happens in SelectParams()), so manually check
 194              // for network.includeconf args.
 195              const size_t chain_includes = add_includes(chain_id);
 196              const size_t default_includes = add_includes({});
 197  
 198              for (const std::string& conf_file_name : conf_file_names) {
 199                  const auto include_conf_path{AbsPathForConfigVal(*this, fs::PathFromString(conf_file_name), /*net_specific=*/false)};
 200                  if (fs::is_directory(include_conf_path)) {
 201                      error = strprintf("Included config file \"%s\" is a directory.", fs::PathToString(include_conf_path));
 202                      return false;
 203                  }
 204                  std::ifstream conf_file_stream{include_conf_path.std_path()};
 205                  if (conf_file_stream.good()) {
 206                      if (!ReadConfigStream(conf_file_stream, conf_file_name, error, ignore_invalid_keys)) {
 207                          return false;
 208                      }
 209                      LogInfo("Included configuration file %s\n", conf_file_name);
 210                  } else {
 211                      error = "Failed to include configuration file " + conf_file_name;
 212                      return false;
 213                  }
 214              }
 215  
 216              // Warn about recursive -includeconf
 217              conf_file_names.clear();
 218              add_includes(chain_id, /* skip= */ chain_includes);
 219              add_includes({}, /* skip= */ default_includes);
 220              std::string chain_id_final = GetChainTypeString();
 221              if (chain_id_final != chain_id) {
 222                  // Also warn about recursive includeconf for the chain that was specified in one of the includeconfs
 223                  add_includes(chain_id_final);
 224              }
 225              for (const std::string& conf_file_name : conf_file_names) {
 226                  tfm::format(std::cerr, "warning: -includeconf cannot be used from included files; ignoring -includeconf=%s\n", conf_file_name);
 227              }
 228          }
 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      return true;
 238  }
 239  
 240  fs::path AbsPathForConfigVal(const ArgsManager& args, const fs::path& path, bool net_specific)
 241  {
 242      if (path.is_absolute() || path.empty()) {
 243          return path;
 244      }
 245      return fsbridge::AbsPathJoin(net_specific ? args.GetDataDirNet() : args.GetDataDirBase(), path);
 246  }
 247