/ src / common / config.cpp
config.cpp
  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 <logging.h>
  9  #include <sync.h>
 10  #include <tinyformat.h>
 11  #include <univalue.h>
 12  #include <util/chaintype.h>
 13  #include <util/fs.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  }