diff --git a/src/zm_config.cpp b/src/zm_config.cpp index 680aa2cfd..11f71a4e4 100644 --- a/src/zm_config.cpp +++ b/src/zm_config.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,8 @@ // config. void zmLoadStaticConfig() { + // Start over, so that a reload forgets a line that has been removed. + staticConfig.file_options.clear(); // Process name, value pairs from the main config file first process_configfile(ZM_CONFIG); @@ -190,6 +193,8 @@ void process_configfile(char const *configFile) { white_len += strspn(val_ptr, "\'\""); val_ptr += white_len; + staticConfig.file_options[name_ptr] = val_ptr; + if ( strcasecmp(name_ptr, "ZM_DB_HOST") == 0 ) staticConfig.DB_HOST = std::string(val_ptr); else if ( strcasecmp(name_ptr, "ZM_DB_NAME") == 0 ) @@ -249,9 +254,7 @@ void process_configfile(char const *configFile) { staticConfig.STREAM_SOCKET_STALL_SECS = parse_unsigned_setting(name_ptr, val_ptr, staticConfig.STREAM_SOCKET_STALL_SECS); else { - // We ignore this now as there may be more parameters than the - // c/c++ binaries are bothered about - // Warning( "Invalid parameter '%s' in %s", name_ptr, ZM_CONFIG ); + // Any other option is in file_options, which Config::Load() applies. } } // end foreach line of the config } @@ -394,6 +397,7 @@ void Config::Load() { " OR `DefaultValue` IS NULL"); if (!result) { Warning("Failed to load config from database, using compiled-in defaults"); + ApplyFileOptions(); return; } @@ -410,6 +414,35 @@ void Config::Load() { Debug(1, "Config loaded: %d items overriding compiled-in defaults (%zu registered)", loaded, bindings_.size()); + ApplyFileOptions(); +} + +std::string ConfigFileValue(const std::string &value, const char *type) { + if (strcmp(type, "boolean") != 0) return value; + // The Config table stores 1/0; a person editing a file may write a word. + std::string word; + for (char c : value) word += static_cast(tolower(static_cast(c))); + if (word == "yes" or word == "true" or word == "on") return "1"; + if (word == "no" or word == "false" or word == "off") return "0"; + return value; +} + +void Config::ApplyFileOptions() { + for (const auto &option : staticConfig.file_options) { + auto bind_it = bindings_.find(option.first); + if (bind_it == bindings_.end()) continue; // a static setting, or not one zmc uses + + const char *type = "string"; + switch (bind_it->second.type) { + case MemberBinding::BOOL: type = "boolean"; break; + case MemberBinding::INT: type = "integer"; break; + case MemberBinding::DOUBLE: type = "decimal"; break; + case MemberBinding::STRING: break; + } + // Name only: the value may be a secret such as ZM_AUTH_HASH_SECRET. + Debug(1, "%s is set in a config file, overriding the database", option.first.c_str()); + ApplyItem(option.first.c_str(), ConfigFileValue(option.second, type).c_str(), type); + } } Config config; diff --git a/src/zm_config.h b/src/zm_config.h index a30f6e450..34955ee10 100644 --- a/src/zm_config.h +++ b/src/zm_config.h @@ -23,6 +23,7 @@ #include "config.h" #include "zm_config_data.h" #include "zm_config_defines.h" +#include #include #include @@ -73,6 +74,10 @@ struct StaticConfig { unsigned int STREAM_SOCKET_QUEUE_BYTES = 8 * 1024 * 1024; unsigned int STREAM_SOCKET_QUEUE_MSGS = 256; unsigned int STREAM_SOCKET_STALL_SECS = 10; + // Every name = value in zm.conf and conf.d, later files winning. The owner of + // the box sets these and the ZoneMinder admin the Config table, so + // Config::Load() applies them over the database values. + std::map file_options; std::string capture_file_format; std::string analyse_file_format; std::string general_file_format; @@ -81,6 +86,10 @@ struct StaticConfig { extern StaticConfig staticConfig; +// A value from zm.conf or conf.d as the Config table would hold it for an +// option of `type`: a boolean may be written yes/no, true/false or on/off. +std::string ConfigFileValue(const std::string &value, const char *type); + class ConfigItem { private: std::string name_; @@ -127,6 +136,8 @@ class Config { ~Config() = default; void Load(); + // Applies StaticConfig::file_options to the options they name. + void ApplyFileOptions(); }; extern Config config; diff --git a/tests/zm_config.cpp b/tests/zm_config.cpp index 13e35a036..4421e2712 100644 --- a/tests/zm_config.cpp +++ b/tests/zm_config.cpp @@ -130,3 +130,61 @@ TEST_CASE("Config members hold compiled-in defaults before any database load") { REQUIRE(fresh.font_file_location != nullptr); REQUIRE(std::string(fresh.font_file_location) != ""); } + +// zm.conf and conf.d belong to the owner of the box, the Config table to the +// ZoneMinder admin, and the owner must be able to limit the admin. PHP and Perl +// already let a file value win; the daemons ignored every file option outside +// a fixed list and read it from the database, so an admin could turn off, say, +// ZM_OPT_USE_AUTH for zms while the web UI kept it on. +TEST_CASE("Config: an option set in a config file wins over the database") { + std::string first = WriteTempConf("ZM_OPT_USE_AUTH=0\nZM_AUTH_HASH_TTL=5\n"); + std::string second = WriteTempConf("ZM_OPT_USE_AUTH=1\nZM_NOT_AN_OPTION=x\n"); + staticConfig.file_options.clear(); + process_configfile(first.c_str()); + process_configfile(second.c_str()); + unlink(first.c_str()); + unlink(second.c_str()); + + // conf.d files are read in order and a later one wins. + REQUIRE(staticConfig.file_options["ZM_OPT_USE_AUTH"] == "1"); + + Config cfg; + cfg.opt_use_auth = false; // as loaded from the Config table + cfg.auth_hash_ttl = 2; + cfg.ApplyFileOptions(); + REQUIRE(cfg.opt_use_auth == true); + REQUIRE(cfg.auth_hash_ttl == 5); + staticConfig.file_options.clear(); +} + +TEST_CASE("Config: a boolean in a config file may be written as a word") { + // The Config table stores 1/0, but a person editing a file writes yes or true. + const char *on[] = {"1", "yes", "Yes", "true", "on"}; + const char *off[] = {"0", "no", "false", "off", ""}; + for (const char *value : on) { + CAPTURE(value); + staticConfig.file_options = {{"ZM_OPT_USE_AUTH", value}}; + Config cfg; + cfg.ApplyFileOptions(); + REQUIRE(cfg.opt_use_auth == true); + } + for (const char *value : off) { + CAPTURE(value); + staticConfig.file_options = {{"ZM_OPT_USE_AUTH", value}}; + Config cfg; + cfg.opt_use_auth = true; + cfg.ApplyFileOptions(); + REQUIRE(cfg.opt_use_auth == false); + } + staticConfig.file_options.clear(); +} + +TEST_CASE("Config: static settings in a config file still apply") { + std::string path = WriteTempConf("ZM_SERVER_NAME=box1\n"); + staticConfig.file_options.clear(); + staticConfig.SERVER_NAME.clear(); + process_configfile(path.c_str()); + unlink(path.c_str()); + REQUIRE(staticConfig.SERVER_NAME == "box1"); + staticConfig.file_options.clear(); +}