/ src / common / config.cpp
config.cpp
  1  // Copyright (c) 2023 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 <list>
 23  #include <map>
 24  #include <memory>
 25  #include <optional>
 26  #include <string>
 27  #include <string_view>
 28  #include <utility>
 29  #include <vector>
 30  
 31  using util::TrimString;
 32  using util::TrimStringView;
 33  
 34  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)
 35  {
 36      std::string str, prefix;
 37      std::string::size_type pos;
 38      int linenr = 1;
 39      while (std::getline(stream, str)) {
 40          bool used_hash = false;
 41          if ((pos = str.find('#')) != std::string::npos) {
 42              str = str.substr(0, pos);
 43              used_hash = true;
 44          }
 45          const static std::string pattern = " \t\r\n";
 46          str = TrimString(str, pattern);
 47          if (!str.empty()) {
 48              if (*str.begin() == '[' && *str.rbegin() == ']') {
 49                  const std::string section = str.substr(1, str.size() - 2);
 50                  sections.emplace_back(SectionInfo{section, filepath, linenr});
 51                  prefix = section + '.';
 52              } else if (*str.begin() == '-') {
 53                  error = strprintf("parse error on line %i: %s, options in configuration file must be specified without leading -", linenr, str);
 54                  return false;
 55              } else if ((pos = str.find('=')) != std::string::npos) {
 56                  std::string name = prefix + TrimString(std::string_view{str}.substr(0, pos), pattern);
 57                  std::string_view value = TrimStringView(std::string_view{str}.substr(pos + 1), pattern);
 58                  if (used_hash && name.find("rpcpassword") != std::string::npos) {
 59                      error = strprintf("parse error on line %i, using # in rpcpassword can be ambiguous and should be avoided", linenr);
 60                      return false;
 61                  }
 62                  options.emplace_back(name, value);
 63                  if ((pos = name.rfind('.')) != std::string::npos && prefix.length() <= pos) {
 64                      sections.emplace_back(SectionInfo{name.substr(0, pos), filepath, linenr});
 65                  }
 66              } else {
 67                  error = strprintf("parse error on line %i: %s", linenr, str);
 68                  if (str.size() >= 2 && str.starts_with("no")) {
 69                      error += strprintf(", if you intended to specify a negated option, use %s=1 instead", str);
 70                  }
 71                  return false;
 72              }
 73          }
 74          ++linenr;
 75      }
 76      return true;
 77  }
 78  
 79  bool IsConfSupported(KeyInfo& key, std::string& error) {
 80      if (key.name == "conf") {
 81          error = "conf cannot be set in the configuration file; use includeconf= if you want to include additional config files";
 82          return false;
 83      }
 84      if (key.name == "reindex") {
 85          // reindex can be set in a config file but it is strongly discouraged as this will cause the node to reindex on
 86          // every restart. Allow the config but throw a warning
 87          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");
 88          return true;
 89      }
 90      return true;
 91  }
 92  
 93  bool ArgsManager::ReadConfigStream(std::istream& stream, const std::string& filepath, std::string& error, bool ignore_invalid_keys)
 94  {
 95      LOCK(cs_args);
 96      std::vector<std::pair<std::string, std::string>> options;
 97      if (!GetConfigOptions(stream, filepath, error, options, m_config_sections)) {
 98          return false;
 99      }
100      for (const std::pair<std::string, std::string>& option : options) {
101          KeyInfo key = InterpretKey(option.first);
102          std::optional<unsigned int> flags = GetArgFlags('-' + key.name);
103          if (!IsConfSupported(key, error)) return false;
104          if (flags) {
105              std::optional<common::SettingsValue> value = InterpretValue(key, &option.second, *flags, error);
106              if (!value) {
107                  return false;
108              }
109              m_settings.ro_config[key.section][key.name].push_back(*value);
110          } else {
111              if (ignore_invalid_keys) {
112                  LogWarning("Ignoring unknown configuration value %s", option.first);
113              } else {
114                  error = strprintf("Invalid configuration value %s", option.first);
115                  return false;
116              }
117          }
118      }
119      return true;
120  }
121  
122  bool ArgsManager::ReadConfigFiles(std::string& error, bool ignore_invalid_keys)
123  {
124      {
125          LOCK(cs_args);
126          m_settings.ro_config.clear();
127          m_config_sections.clear();
128          m_config_path = AbsPathForConfigVal(*this, GetPathArg("-conf", BITCOIN_CONF_FILENAME), /*net_specific=*/false);
129      }
130  
131      const auto conf_path{GetConfigFilePath()};
132      std::ifstream stream;
133      if (!conf_path.empty()) { // path is empty when -noconf is specified
134          if (fs::is_directory(conf_path)) {
135              error = strprintf("Config file \"%s\" is a directory.", fs::PathToString(conf_path));
136              return false;
137          }
138          stream = std::ifstream{conf_path.std_path()};
139          // If the file is explicitly specified, it must be readable
140          if (IsArgSet("-conf") && !stream.good()) {
141              error = strprintf("specified config file \"%s\" could not be opened.", fs::PathToString(conf_path));
142              return false;
143          }
144      }
145      // ok to not have a config file
146      if (stream.good()) {
147          if (!ReadConfigStream(stream, fs::PathToString(conf_path), error, ignore_invalid_keys)) {
148              return false;
149          }
150          // `-includeconf` cannot be included in the command line arguments except
151          // as `-noincludeconf` (which indicates that no included conf file should be used).
152          bool use_conf_file{true};
153          {
154              LOCK(cs_args);
155              if (auto* includes = common::FindKey(m_settings.command_line_options, "includeconf")) {
156                  // ParseParameters() fails if a non-negated -includeconf is passed on the command-line
157                  assert(common::SettingsSpan(*includes).last_negated());
158                  use_conf_file = false;
159              }
160          }
161          if (use_conf_file) {
162              std::string chain_id = GetChainTypeString();
163              std::vector<std::string> conf_file_names;
164  
165              auto add_includes = [&](const std::string& network, size_t skip = 0) {
166                  size_t num_values = 0;
167                  LOCK(cs_args);
168                  if (auto* section = common::FindKey(m_settings.ro_config, network)) {
169                      if (auto* values = common::FindKey(*section, "includeconf")) {
170                          for (size_t i = std::max(skip, common::SettingsSpan(*values).negated()); i < values->size(); ++i) {
171                              conf_file_names.push_back((*values)[i].get_str());
172                          }
173                          num_values = values->size();
174                      }
175                  }
176                  return num_values;
177              };
178  
179              // We haven't set m_network yet (that happens in SelectParams()), so manually check
180              // for network.includeconf args.
181              const size_t chain_includes = add_includes(chain_id);
182              const size_t default_includes = add_includes({});
183  
184              for (const std::string& conf_file_name : conf_file_names) {
185                  const auto include_conf_path{AbsPathForConfigVal(*this, fs::PathFromString(conf_file_name), /*net_specific=*/false)};
186                  if (fs::is_directory(include_conf_path)) {
187                      error = strprintf("Included config file \"%s\" is a directory.", fs::PathToString(include_conf_path));
188                      return false;
189                  }
190                  std::ifstream conf_file_stream{include_conf_path.std_path()};
191                  if (conf_file_stream.good()) {
192                      if (!ReadConfigStream(conf_file_stream, conf_file_name, error, ignore_invalid_keys)) {
193                          return false;
194                      }
195                      LogPrintf("Included configuration file %s\n", conf_file_name);
196                  } else {
197                      error = "Failed to include configuration file " + conf_file_name;
198                      return false;
199                  }
200              }
201  
202              // Warn about recursive -includeconf
203              conf_file_names.clear();
204              add_includes(chain_id, /* skip= */ chain_includes);
205              add_includes({}, /* skip= */ default_includes);
206              std::string chain_id_final = GetChainTypeString();
207              if (chain_id_final != chain_id) {
208                  // Also warn about recursive includeconf for the chain that was specified in one of the includeconfs
209                  add_includes(chain_id_final);
210              }
211              for (const std::string& conf_file_name : conf_file_names) {
212                  tfm::format(std::cerr, "warning: -includeconf cannot be used from included files; ignoring -includeconf=%s\n", conf_file_name);
213              }
214          }
215      }
216  
217      // If datadir is changed in .conf file:
218      ClearPathCache();
219      if (!CheckDataDirOption(*this)) {
220          error = strprintf("specified data directory \"%s\" does not exist.", GetArg("-datadir", ""));
221          return false;
222      }
223      return true;
224  }
225  
226  fs::path AbsPathForConfigVal(const ArgsManager& args, const fs::path& path, bool net_specific)
227  {
228      if (path.is_absolute() || path.empty()) {
229          return path;
230      }
231      return fsbridge::AbsPathJoin(net_specific ? args.GetDataDirNet() : args.GetDataDirBase(), path);
232  }