Ctrl K
ComunidadGuías…

src/zm_config.cpp

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//
// ZoneMinder Configuration Implementation, $Date$, $Revision$
// Copyright (C) 2001-2008 Philip Coombes
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//

#include "zm_config.h"

#include "zm_db.h"
#include "zm_logger.h"
#include "zm_utils.h"
#include <cerrno>
#include <climits>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <cctype>
#include <fstream>
#include <glob.h>
#include <string>
#include <vector>

// Note that Error and Debug calls won't actually go anywhere unless you
// set the relevant ENV vars because the logger gets it's setting from the
// 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);

  // Search for user created config files. If one or more are found then
  // update the Config hash with those values
  DIR *configSubFolder = opendir(ZM_CONFIG_SUBDIR);
  if (configSubFolder) { // subfolder exists and is readable
    std::string glob_pattern = stringtf("%s/*.conf", ZM_CONFIG_SUBDIR);

    glob_t pglob;
    int glob_status = glob(glob_pattern.c_str(), 0, nullptr, &pglob);
    if (glob_status != 0) {
      if (glob_status < 0) {
        Error("Can't glob '%s': %s", glob_pattern.c_str(), strerror(errno));
      } else {
        Debug(1, "Can't glob '%s': %d", glob_pattern.c_str(), glob_status);
      }
    } else {
      for (unsigned int i = 0; i < pglob.gl_pathc; i++) {
        process_configfile(pglob.gl_pathv[i]);
      }
    }
    globfree(&pglob);
    closedir(configSubFolder);
  }
}

void zmLoadDBConfig() {
  if (!zmDbConnected) {
    Fatal("Not connected to the database. Can't continue.");
  }
  config.Load();

  // Populate the server config entries
  if (!staticConfig.SERVER_ID) {
    if (!staticConfig.SERVER_NAME.empty()) {

      Debug(1, "Fetching ZM_SERVER_ID For Name = %s", staticConfig.SERVER_NAME.c_str());
      std::string sql = stringtf("SELECT `Id` FROM `Servers` WHERE `Name`='%s'",
                                 zmDbEscapeString(staticConfig.SERVER_NAME).c_str());
      zmDbRow dbrow;
      if (dbrow.fetch(sql)) {
        staticConfig.SERVER_ID = atoi(dbrow[0]);
      } else {
        Fatal("Can't get ServerId for Server %s", staticConfig.SERVER_NAME.c_str());
      }

    } // end if has SERVER_NAME
  } else if (staticConfig.SERVER_NAME.empty()) {
    Debug(1, "Fetching ZM_SERVER_NAME For Id = %d", staticConfig.SERVER_ID);
    std::string sql = stringtf("SELECT `Name` FROM `Servers` WHERE `Id`='%d'", staticConfig.SERVER_ID);

    zmDbRow dbrow;
    if (dbrow.fetch(sql)) {
      staticConfig.SERVER_NAME = std::string(dbrow[0]);
    } else {
      Fatal("Can't get ServerName for Server ID %d", staticConfig.SERVER_ID);
    }
  }

  if (staticConfig.SERVER_ID) {
    Debug(3, "Multi-server configuration detected. Server is %d.", staticConfig.SERVER_ID);
  } else {
    Debug(3, "Single server configuration assumed because no Server ID or Name was specified.");
  }

  staticConfig.capture_file_format = stringtf("%%s/%%0%dd-capture.jpg", config.event_image_digits);
  staticConfig.analyse_file_format = stringtf("%%s/%%0%dd-analyse.jpg", config.event_image_digits);
  staticConfig.general_file_format = stringtf("%%s/%%0%dd-%%s", config.event_image_digits);
  staticConfig.video_file_format = "%s/%s";
}

// Parses a non-negative integer zm.conf value. Anything else (negative,
// non-numeric, trailing junk, out of range) keeps the compiled-in default
// rather than wrapping into a huge unsigned value.
static unsigned int parse_unsigned_setting(const char *name, const char *value, unsigned int fallback) {
  errno = 0;
  char *end = nullptr;
  unsigned long parsed = strtoul(value, &end, 10);
  bool negative = value[0] == '-';
  if (negative or end == value or *end != '\0' or errno == ERANGE or parsed > UINT_MAX) {
    Warning("Ignoring invalid value '%s' for %s, keeping %u", value, name, fallback);
    return fallback;
  }
  return static_cast<unsigned int>(parsed);
}

void process_configfile(char const *configFile) {
  std::ifstream cfg(configFile);
  if ( !cfg.is_open() ) {
    Error("Can't open %s: %s", configFile, strerror(errno));
    return;
  }
  std::string raw;
  while ( std::getline(cfg, raw) ) {
    // Tolerate Windows line endings.
    if ( !raw.empty() && raw.back() == '\r' ) raw.pop_back();

    // Backslash before the newline means the value continues on the next
    // physical line. Trailing whitespace before the '\' is allowed.
    // Continuation lines have their leading whitespace stripped so callers
    // can indent for readability without it leaking into the value.
    while ( true ) {
      size_t last_non_ws = raw.find_last_not_of(" \t");
      if ( last_non_ws == std::string::npos || raw[last_non_ws] != '\\' ) break;
      raw.erase(last_non_ws);
      std::string next;
      if ( !std::getline(cfg, next) ) break;
      if ( !next.empty() && next.back() == '\r' ) next.pop_back();
      size_t lead = next.find_first_not_of(" \t");
      if ( lead != std::string::npos ) raw.append(next, lead, std::string::npos);
    }

    std::vector<char> line(raw.begin(), raw.end());
    line.push_back('\0');
    char *line_ptr = line.data();

    // Remove leading white space
    int white_len = strspn(line_ptr, " \t");
    line_ptr += white_len;

    // Check for comment or empty line
    if ( *line_ptr == '\0' || *line_ptr == '#' )
      continue;

    // Remove trailing white space and trailing quotes
    char *temp_ptr = line_ptr+strlen(line_ptr)-1;
    while ( *temp_ptr == ' ' || *temp_ptr == '\t' || *temp_ptr == '\'' || *temp_ptr == '\"') {
      *temp_ptr-- = '\0';
    }

    // Now look for the '=' in the middle of the line
    temp_ptr = strchr(line_ptr, '=');
    if ( !temp_ptr ) {
      Warning("Invalid data in %s: '%s'", configFile, line.data());
      continue;
    }

    // Assign the name and value parts
    char *name_ptr = line_ptr;
    char *val_ptr = temp_ptr+1;

    // Trim trailing space from the name part
    do {
      *temp_ptr = '\0';
      temp_ptr--;
    } while ( temp_ptr >= name_ptr && (*temp_ptr == ' ' || *temp_ptr == '\t') );

    // Remove leading white space and leading quotes from the value part
    white_len = strspn(val_ptr, " \t");
    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 )
      staticConfig.DB_NAME = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DB_USER") == 0 )
      staticConfig.DB_USER = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DB_PASS") == 0 )
      staticConfig.DB_PASS = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DB_SSL_CA_CERT") == 0 )
      staticConfig.DB_SSL_CA_CERT = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DB_SSL_CLIENT_KEY") == 0 )
      staticConfig.DB_SSL_CLIENT_KEY = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DB_SSL_CLIENT_CERT") == 0 )
      staticConfig.DB_SSL_CLIENT_CERT = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DB_SSL_VERIFY_SERVER_CERT") == 0 )
      staticConfig.DB_SSL_VERIFY_SERVER_CERT = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_PATH_WEB") == 0 )
      staticConfig.PATH_WEB = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_SERVER_HOST") == 0 )
      staticConfig.SERVER_NAME = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_SERVER_NAME") == 0 )
      staticConfig.SERVER_NAME = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_SERVER_ID") == 0 )
      staticConfig.SERVER_ID = atoi(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DIR_EVENTS") == 0 )
      staticConfig.DIR_EVENTS = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DIR_SOUNDS") == 0 )
      staticConfig.DIR_SOUNDS = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_DIR_EXPORTS") == 0 )
      staticConfig.DIR_EXPORTS = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_PATH_ZMS") == 0 )
      staticConfig.PATH_ZMS = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_PATH_MAP") == 0 )
      staticConfig.PATH_MAP = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_PATH_SOCKS") == 0 )
      staticConfig.PATH_SOCKS = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_PATH_LOGS") == 0 )
      staticConfig.PATH_LOGS = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_PATH_SWAP") == 0 )
      staticConfig.PATH_SWAP = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_PATH_ARP") == 0 )
      staticConfig.PATH_ARP = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_STREAM_SOCKET_GROUP") == 0 )
      staticConfig.STREAM_SOCKET_GROUP = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_STREAM_SOCKET_ALLOWED_UIDS") == 0 )
      staticConfig.STREAM_SOCKET_ALLOWED_UIDS = std::string(val_ptr);
    else if ( strcasecmp(name_ptr, "ZM_STREAM_SOCKET_MAX_CLIENTS") == 0 )
      staticConfig.STREAM_SOCKET_MAX_CLIENTS =
        parse_unsigned_setting(name_ptr, val_ptr, staticConfig.STREAM_SOCKET_MAX_CLIENTS);
    else if ( strcasecmp(name_ptr, "ZM_STREAM_SOCKET_QUEUE_BYTES") == 0 )
      staticConfig.STREAM_SOCKET_QUEUE_BYTES =
        parse_unsigned_setting(name_ptr, val_ptr, staticConfig.STREAM_SOCKET_QUEUE_BYTES);
    else if ( strcasecmp(name_ptr, "ZM_STREAM_SOCKET_QUEUE_MSGS") == 0 )
      staticConfig.STREAM_SOCKET_QUEUE_MSGS =
        parse_unsigned_setting(name_ptr, val_ptr, staticConfig.STREAM_SOCKET_QUEUE_MSGS);
    else if ( strcasecmp(name_ptr, "ZM_STREAM_SOCKET_STALL_SECS") == 0 )
      staticConfig.STREAM_SOCKET_STALL_SECS =
        parse_unsigned_setting(name_ptr, val_ptr, staticConfig.STREAM_SOCKET_STALL_SECS);
    else {
      // Any other option is in file_options, which Config::Load() applies.
    }
  } // end foreach line of the config
}

StaticConfig staticConfig;

ConfigItem::ConfigItem() : cfg_type_(CFG_UNKNOWN), accessed_(false) {
  cfg_value_.integer_value = 0;
}

ConfigItem::ConfigItem(const char *p_name, const char *p_value, const char *const p_type)
    : name_(p_name), value_(p_value), type_(p_type), cfg_type_(CFG_UNKNOWN), accessed_(false) {
  cfg_value_.integer_value = 0;
}

void ConfigItem::ConvertValue() const {
  if ( type_ == "boolean" ) {
    cfg_type_ = CFG_BOOLEAN;
    cfg_value_.boolean_value = (bool)strtol(value_.c_str(), nullptr, 0);
  } else if ( type_ == "integer" ) {
    cfg_type_ = CFG_INTEGER;
    cfg_value_.integer_value = strtol(value_.c_str(), nullptr, 10);
  } else if ( type_ == "hexadecimal" ) {
    cfg_type_ = CFG_INTEGER;
    cfg_value_.integer_value = strtol(value_.c_str(), nullptr, 16);
  } else if ( type_ == "decimal" ) {
    cfg_type_ = CFG_DECIMAL;
    cfg_value_.decimal_value = strtod(value_.c_str(), nullptr);
  } else {
    cfg_type_ = CFG_STRING;
  }
  accessed_ = true;
}

bool ConfigItem::BooleanValue() const {
  if ( !accessed_ )
    ConvertValue();

  if ( cfg_type_ != CFG_BOOLEAN ) {
    Warning("Attempt to fetch boolean value for %s, actual type is %s. Try running 'zmupdate.pl -f' to reload config.",
            name_.c_str(), type_.c_str());
    return !value_.empty() && value_ != "0";
  }

  return cfg_value_.boolean_value;
}

int ConfigItem::IntegerValue() const {
  if ( !accessed_ )
    ConvertValue();

  if ( cfg_type_ != CFG_INTEGER ) {
    Warning("Attempt to fetch integer value for %s, actual type is %s. Try running 'zmupdate.pl -f' to reload config.",
            name_.c_str(), type_.c_str());
    return static_cast<int>(strtol(value_.c_str(), nullptr, 0));
  }

  return cfg_value_.integer_value;
}

double ConfigItem::DecimalValue() const {
  if ( !accessed_ )
    ConvertValue();

  if ( cfg_type_ != CFG_DECIMAL ) {
    Warning("Attempt to fetch decimal value for %s, actual type is %s. Try running 'zmupdate.pl -f' to reload config.",
            name_.c_str(), type_.c_str());
    return strtod(value_.c_str(), nullptr);
  }

  return cfg_value_.decimal_value;
}

const char *ConfigItem::StringValue() const {
  if ( !accessed_ )
    ConvertValue();

  if ( cfg_type_ != CFG_STRING ) {
    Warning("Attempt to fetch string value for %s, actual type is %s. Try running 'zmupdate.pl -f' to reload config.",
            name_.c_str(), type_.c_str());
  }

  return value_.c_str();
}

Config::Config() {
  // Set all members to compiled-in defaults
  ZM_CFG_DEFAULTS_INIT

  // Register name-to-member bindings for DB loading
  ZM_CFG_MAP_INIT
}

void Config::RegisterBinding(const char *name, MemberBinding::Type type, void *ptr) {
  bindings_[name] = {type, ptr};
}

void Config::ApplyItem(const char *name, const char *value, const char *type) {
  auto bind_it = bindings_.find(name);
  if (bind_it == bindings_.end()) {
    return;
  }

  // Store ConfigItem to own the string memory for const char* members
  auto [item_it, inserted] = items_.emplace(
      std::piecewise_construct,
      std::forward_as_tuple(name),
      std::forward_as_tuple(name, value, type));
  if (!inserted) {
    // Replace existing item
    item_it->second = ConfigItem(name, value, type);
  }

  const ConfigItem &item = item_it->second;
  const MemberBinding &binding = bind_it->second;

  switch (binding.type) {
    case MemberBinding::BOOL:
      *static_cast<bool*>(binding.ptr) = item.BooleanValue();
      break;
    case MemberBinding::INT:
      *static_cast<int*>(binding.ptr) = item.IntegerValue();
      break;
    case MemberBinding::DOUBLE:
      *static_cast<double*>(binding.ptr) = item.DecimalValue();
      break;
    case MemberBinding::STRING:
      *static_cast<const char**>(binding.ptr) = item.StringValue();
      break;
  }
}

void Config::Load() {
  // Only load rows where the user has changed the value from the default.
  // Compiled-in defaults (from ZM_CFG_DEFAULTS_INIT) cover everything else.
  // DefaultValue is NULL on very old schemas, so also load those to be safe.
  MYSQL_RES *result = zmDbFetch(
      "SELECT `Name`, `Value`, `Type` FROM `Config`"
      " WHERE `Value` != `DefaultValue`"
      " OR `DefaultValue` IS NULL");
  if (!result) {
    Warning("Failed to load config from database, using compiled-in defaults");
    ApplyFileOptions();
    return;
  }

  int loaded = 0;
  while (MYSQL_ROW dbrow = mysql_fetch_row(result)) {
    if (dbrow[0] && dbrow[1] && dbrow[2]) {
      if (bindings_.count(dbrow[0])) {
        ApplyItem(dbrow[0], dbrow[1], dbrow[2]);
        loaded++;
      }
    }
  }
  mysql_free_result(result);

  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<char>(tolower(static_cast<unsigned char>(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;