feat(validation): implement configuration validation framework with rules for required directives and allowed values

This commit is contained in:
whaffman 2025-10-06 14:48:15 +02:00
parent ceab8d7aab
commit 95753eea34
25 changed files with 623 additions and 43 deletions

View File

@ -28,7 +28,7 @@ server {
autoindex off;
root ./static_site/;
index index2.html index.html;
allowed_methods GET POST DELETE;
allowed_methods GET POST DELETE UPDATE;
}
location /uploads {

View File

@ -34,6 +34,17 @@ const ADirective *AConfig::getDirective(const std::string &name) const
return nullptr;
}
std::vector<const ADirective *> AConfig::getDirectives() const
{
std::vector<const ADirective *> result;
result.reserve(directives_.size());
for (const auto &directive : directives_)
{
result.push_back(directive.get());
}
return result;
}
bool AConfig::hasDirective(const std::string &name) const
{
for (const auto &directive : directives_)

View File

@ -19,11 +19,14 @@ class AConfig
AConfig &operator=(AConfig &&other) noexcept = delete;
virtual ~AConfig() = default;
[[nodiscard]] virtual std::string getName() const = 0;
[[nodiscard]] virtual std::string getType() const = 0;
void addDirective(const std::string &line);
[[nodiscard]] std::string getErrorPage(int statusCode) const;
[[nodiscard]] bool hasDirective(const std::string &name) const;
[[nodiscard]] const ADirective *getDirective(const std::string &name) const;
[[nodiscard]] std::vector<const ADirective *> getDirectives() const;
template <typename T>
std::optional<T> get(const std::string &name) const
@ -37,7 +40,6 @@ class AConfig
}
protected:
[[nodiscard]] const ADirective *getDirective(const std::string &name) const;
virtual void parseBlock(const std::string &block) = 0;
void parseDirectives(const std::string &declarations);
std::vector<std::unique_ptr<ADirective>>

View File

@ -12,6 +12,16 @@ GlobalConfig::GlobalConfig(const std::string &block)
parseBlock(block);
}
std::string GlobalConfig::getName() const
{
return "global";
}
std::string GlobalConfig::getType() const
{
return "global";
}
void GlobalConfig::parseBlock(const std::string &block)
{
std::string directives;

View File

@ -19,6 +19,10 @@ class GlobalConfig : public AConfig
GlobalConfig &operator=(GlobalConfig &&other) noexcept = delete;
~GlobalConfig() override = default;
[[nodiscard]] std::string getName() const override;
[[nodiscard]] std::string getType() const override;
[[nodiscard]] std::vector<ServerConfig *> getServerConfigs() const;
private:

View File

@ -8,6 +8,17 @@ LocationConfig::LocationConfig(const std::string &block, const std::string &path
parseBlock(block);
}
std::string LocationConfig::getName() const
{
auto parentName = parent_ != nullptr ? parent_->getName() : "root";
return parentName + ", location: " + _path;
}
std::string LocationConfig::getType() const
{
return "location";
}
void LocationConfig::parseBlock(const std::string &block)
{
Log::trace(LOCATION);

View File

@ -17,6 +17,8 @@ class LocationConfig : public AConfig
~LocationConfig() override = default;
[[nodiscard]] std::string getName() const override;
[[nodiscard]] std::string getType() const override;
[[nodiscard]] const std::string &getPath() const { return _path; }
private:

View File

@ -14,6 +14,17 @@ ServerConfig::ServerConfig(const std::string &block, const AConfig *parent) : AC
parseBlock(block);
}
std::string ServerConfig::getName() const
{
return "server: " + get<std::string>("server_name").value_or("unnamed") + " (port " +
std::to_string(get<int>("listen").value_or(-1)) + ")";
}
std::string ServerConfig::getType() const
{
return "server";
}
void ServerConfig::parseBlock(const std::string &block)
{
Log::trace(LOCATION);
@ -68,3 +79,4 @@ std::vector<std::string> ServerConfig::getLocationPaths() const
}
return paths;
}

View File

@ -21,9 +21,13 @@ class ServerConfig : public AConfig
~ServerConfig() override = default;
[[nodiscard]] std::string getName() const override;
[[nodiscard]] std::string getType() const override;
[[nodiscard]] const LocationConfig *getLocation(const std::string &path) const;
[[nodiscard]] std::vector<std::string> getLocationPaths() const;
private:
std::map<std::string, std::unique_ptr<LocationConfig>> locations_;
AConfig *parent_ = nullptr;

View File

@ -0,0 +1,44 @@
#include <webserv/config/AConfig.hpp>
#include <webserv/config/config_validator/AValidationRule.hpp>
#include <webserv/config/config_validator/ValidationResult.hpp>
#include <webserv/config/directive/ADirective.hpp>
#include <webserv/log/Log.hpp>
AValidationRule::AValidationRule(std::string ruleName, std::string description, bool requiresValue)
: ruleName_(std::move(ruleName)), description_(std::move(description)), requiresValue_(requiresValue)
{
}
ValidationResult AValidationRule::validate(const AConfig *config, const std::string &directiveName) const
{
Log::trace(LOCATION);
if (config == nullptr || directiveName.empty())
{
return ValidationResult::error("Invalid config or directive name");
}
if (!config->hasDirective(directiveName))
{
if (requiresValue_)
{
return ValidationResult::error("Directive '" + directiveName + "' is missing");
}
return ValidationResult::success();
}
return validateValue(config, directiveName);
}
std::string AValidationRule::getRuleName() const
{
return ruleName_;
}
std::string AValidationRule::getDescription() const
{
return description_;
}
bool AValidationRule::isRequired() const
{
return requiresValue_;
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <string>
class ValidationResult;
class ADirective;
class AConfig;
class AValidationRule
{
public:
virtual ~AValidationRule() = default;
AValidationRule(const AValidationRule &other) = delete;
AValidationRule &operator=(const AValidationRule &other) = delete;
AValidationRule(AValidationRule &&other) noexcept = delete;
AValidationRule &operator=(AValidationRule &&other) noexcept = delete;
[[nodiscard]] ValidationResult validate(const AConfig *config, const std::string &directiveName) const;
[[nodiscard]] bool isRequired() const;
[[nodiscard]] virtual ValidationResult validateValue(const AConfig *config, const std::string &directiveName) const = 0;
[[nodiscard]] std::string getRuleName() const;
[[nodiscard]] std::string getDescription() const;
protected:
AValidationRule(std::string ruleName, std::string description, bool requiresValue = true);
private:
std::string ruleName_;
std::string description_;
bool requiresValue_;
};

View File

@ -0,0 +1,60 @@
#include <webserv/config/AConfig.hpp>
#include <webserv/config/config_validator/AllowedValuesRule.hpp>
#include <webserv/config/config_validator/ValidationResult.hpp>
#include <webserv/config/directive/ADirective.hpp>
#include <algorithm> // for find
#include <string> // for string, basic_string, operator+, char_traits
#include <vector> // for vector
AllowedValuesRule::AllowedValuesRule(const std::vector<std::string> &allowedValues, bool requiresValue)
: AValidationRule("AllowedValuesRule", "Ensures that the directive's value is within the allowed set",
requiresValue),
allowedValues_(allowedValues)
{
}
ValidationResult AllowedValuesRule::validateValue(const AConfig *config, const std::string &directiveName) const
{
const ADirective *directive = config->getDirective(directiveName);
if (directive == nullptr)
{
return ValidationResult::error("Directive '" + directiveName + "' is missing");
}
auto checkValueAllowed = [&](const std::string &val) -> bool {
return std::ranges::find(allowedValues_, val) != allowedValues_.end();
};
auto makeErrorMsg = [&](const std::string &val) -> std::string {
return "Value '" + val + "' of directive '" + directiveName + "' is not in the allowed set";
};
if (directive->getValue().holds<std::string>())
{
const std::string &value = directive->getValue().get<std::string>();
if (!checkValueAllowed(value))
{
return ValidationResult::error(makeErrorMsg(value));
}
}
else if (directive->getValue().holds<std::vector<std::string>>())
{
const std::vector<std::string> &values = directive->getValue().get<std::vector<std::string>>();
for (const std::string &val : values)
{
if (!checkValueAllowed(val))
{
return ValidationResult::error(makeErrorMsg(val));
}
}
}
else
{
return ValidationResult::error("Directive '" + directiveName + "' has an unsupported value type");
}
return ValidationResult::success();
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "webserv/config/config_validator/AValidationRule.hpp"
#include "webserv/config/config_validator/ValidationResult.hpp"
#include <string>
#include <vector>
class AConfig;
class AllowedValuesRule : public AValidationRule
{
public:
explicit AllowedValuesRule(const std::vector<std::string> &allowedValues, bool requiresValue = true);
private:
[[nodiscard]] ValidationResult validateValue(const AConfig *config, const std::string &directiveName) const override;
std::vector<std::string> allowedValues_;
};

View File

@ -0,0 +1,38 @@
#include "webserv/config/config_validator/ConfigValidator.hpp"
#include "webserv/config/GlobalConfig.hpp"
#include "webserv/config/config_validator/AllowedValuesRule.hpp"
#include "webserv/config/config_validator/PortValidationRule.hpp"
#include "webserv/log/Log.hpp"
#include <string>
ConfigValidator::ConfigValidator(const GlobalConfig *config)
: engine_(std::make_unique<ValidationEngine>(config))
{
Log::trace(LOCATION);
engine_->addServerRule("listen", std::make_unique<PortValidationRule>());
engine_->addLocationRule("allowed_methods", std::make_unique<AllowedValuesRule>(std::vector<std::string>{"GET", "POST", "DELETE"}));
engine_->validate();
}
std::vector<ValidationResult> ConfigValidator::getValidationResults() const
{
return engine_->getValidationResults();
}
std::vector<ValidationResult> ConfigValidator::getErrors() const
{
return engine_->getErrors();
}
std::vector<ValidationResult> ConfigValidator::getWarnings() const
{
return engine_->getWarnings();
}
bool ConfigValidator::hasErrors() const
{
return engine_->hasErrors();
}

View File

@ -0,0 +1,26 @@
#pragma once
#include "webserv/config/config_validator/ValidationEngine.hpp"
#include <memory>
class GlobalConfig;
class ConfigValidator
{
public:
ConfigValidator(const GlobalConfig *config);
ConfigValidator(const ConfigValidator &other) = delete;
ConfigValidator &operator=(const ConfigValidator &other) = delete;
ConfigValidator(ConfigValidator &&other) noexcept = delete;
ConfigValidator &operator=(ConfigValidator &&other) noexcept = delete;
~ConfigValidator() = default;
[[nodiscard]] std::vector<ValidationResult> getValidationResults() const;
[[nodiscard]] std::vector<ValidationResult> getErrors() const;
[[nodiscard]] std::vector<ValidationResult> getWarnings() const;
[[nodiscard]] bool hasErrors() const;
private:
std::unique_ptr<ValidationEngine> engine_;
};

View File

@ -1,21 +0,0 @@
#pragma once
#include <string>
class ValidationResult;
class IValidationRule
{
public:
virtual ~IValidationRule() = default;
IValidationRule(const IValidationRule &other) = delete;
IValidationRule &operator=(const IValidationRule &other) = delete;
IValidationRule(IValidationRule &&other) noexcept = delete;
IValidationRule &operator=(IValidationRule &&other) noexcept = delete;
[[nodiscard]] virtual ValidationResult validate(const std::string &Adirective) const = 0;
[[nodiscard]] virtual std::string getRuleName() const = 0;
[[nodiscard]] virtual std::string getDescription() const = 0;
};

View File

@ -0,0 +1,32 @@
#include "webserv/config/config_validator/PortValidationRule.hpp"
#include "webserv/config/AConfig.hpp"
#include "webserv/config/config_validator/ValidationResult.hpp"
#include "webserv/config/directive/ADirective.hpp"
#include "webserv/config/directive/DirectiveValue.hpp"
#include "webserv/log/Log.hpp"
#include <string> // for string, basic_string, operator+, char_traits
PortValidationRule::PortValidationRule(bool requiresValue)
: AValidationRule("PortValidationRule", "Validates that the port number is within the valid range (1-65535)", requiresValue)
{
}
ValidationResult PortValidationRule::validateValue(const AConfig *config, const std::string &directiveName) const
{
Log::trace(LOCATION);
const ADirective *directive = config->getDirective(directiveName);
if (!directive->getValue().holds<int>())
{
return ValidationResult::error("Directive '" + directive->getName() + "' does not hold an integer value");
}
int port = directive->getValue().get<int>();
if (port < 1 || port > 65535)
{
return ValidationResult::error("Port number " + std::to_string(port) + " is out of valid range (1-65535)");
}
return ValidationResult::success();
}

View File

@ -1,18 +1,14 @@
#pragma once
#include "webserv/config/config_validator/IValidationRule.hpp"
class PortValidationRule : public IValidationRule
#include "webserv/config/config_validator/AValidationRule.hpp"
#include <string>
class AConfig;
class PortValidationRule : public AValidationRule
{
public:
PortValidationRule() = default;
~PortValidationRule() override = default;
PortValidationRule(bool requiresValue = true);
PortValidationRule(const PortValidationRule &other) = delete;
PortValidationRule &operator=(const PortValidationRule &other) = delete;
PortValidationRule(PortValidationRule &&other) noexcept = delete;
PortValidationRule &operator=(PortValidationRule &&other) noexcept = delete;
[[nodiscard]] ValidationResult validate(const std::string &value) const override;
[[nodiscard]] std::string getRuleName() const override { return "PortValidationRule"; }
[[nodiscard]] std::string getDescription() const override { return "Validates that a port number is between 1 and 65535"; }
private:
[[nodiscard]] ValidationResult validateValue(const AConfig *config, const std::string &directiveName) const override;
};

View File

@ -0,0 +1,14 @@
#include "webserv/config/config_validator/ValidationResult.hpp"
#include "webserv/config/config_validator/RequiredDirectiveRule.hpp"
#include <webserv/config/config_validator/AValidationRule.hpp>
#include <webserv/config/AConfig.hpp>
RequiredDirectiveRule::RequiredDirectiveRule()
: AValidationRule("RequiredDirectiveRule", "Ensures that a required directive is present in the configuration", true)
{
}
ValidationResult RequiredDirectiveRule::validateValue(const AConfig *config, const std::string &directiveName) const
{
return ValidationResult::success();
}

View File

@ -0,0 +1,17 @@
#pragma once
#include "webserv/config/config_validator/AValidationRule.hpp"
#include <string>
class AConfig;
class RequiredDirectiveRule : public AValidationRule
{
public:
RequiredDirectiveRule();
private:
[[nodiscard]] ValidationResult validateValue(const AConfig *config,
const std::string &directiveName) const override;
};

View File

@ -0,0 +1,157 @@
#include "webserv/config/config_validator/ValidationEngine.hpp"
#include "webserv/config/AConfig.hpp"
#include "webserv/config/LocationConfig.hpp"
#include "webserv/config/config_validator/AValidationRule.hpp"
#include "webserv/config/config_validator/ValidationResult.hpp"
#include "webserv/config/directive/ADirective.hpp"
#include "webserv/log/Log.hpp"
void ValidationEngine::addGlobalRule(const std::string &directiveName, std::unique_ptr<AValidationRule> rule)
{
Log::trace(LOCATION);
addRule(globalRules_, directiveName, std::move(rule));
}
void ValidationEngine::addServerRule(const std::string &directiveName, std::unique_ptr<AValidationRule> rule)
{
Log::trace(LOCATION);
addRule(serverRules_, directiveName, std::move(rule));
}
void ValidationEngine::addLocationRule(const std::string &directiveName, std::unique_ptr<AValidationRule> rule)
{
Log::trace(LOCATION);
addRule(locationRules_, directiveName, std::move(rule));
}
void ValidationEngine::addRule(RuleMap &ruleMap, const std::string &directiveName,
std::unique_ptr<AValidationRule> rule)
{
Log::trace(LOCATION);
ruleMap[directiveName].emplace_back(std::move(rule));
}
std::vector<ValidationResult> ValidationEngine::getValidationResults() const
{
return results_;
}
std::vector<ValidationResult> ValidationEngine::getErrors() const
{
std::vector<ValidationResult> errors;
for (const auto &result : results_)
{
if (!result.isValidResult())
{
errors.push_back(result);
}
}
return errors;
}
std::vector<ValidationResult> ValidationEngine::getWarnings() const
{
std::vector<ValidationResult> warnings;
for (const auto &result : results_)
{
if (result.getType() == ValidationResult::Type::WARNING)
{
warnings.push_back(result);
}
}
return warnings;
}
bool ValidationEngine::hasErrors() const
{
for (const auto &result : results_)
{
if (!result.isValidResult())
{
return true;
}
}
return false;
}
void ValidationEngine::validateConfig(RuleMap const &rulesMap, const AConfig *config)
{
Log::trace(LOCATION);
for (const auto &[directiveName, rules] : rulesMap)
{
if (!config->hasDirective(directiveName))
{
// Check if any rule requires the directive
for (const auto &rule : rules)
{
if (rule->isRequired())
{
ValidationResult result = rule->validate(config, directiveName);
if (!result.isValidResult())
{
results_.push_back(result); // Only failures
}
}
}
continue; // Directive not present, skip to next
}
// Validate each rule for this directive
for (const auto &rule : rules)
{
ValidationResult result = rule->validate(config, directiveName); // ✅
if (!result.isValidResult())
{
results_.push_back(result); // Only failures
}
}
}
}
void ValidationEngine::validateLocationConfig(const std::string &path, const LocationConfig *config)
{
Log::trace(LOCATION);
validateConfig(locationRules_, config);
}
void ValidationEngine::validateServerConfig(const ServerConfig *config)
{
Log::trace(LOCATION);
validateConfig(serverRules_, config);
for (const auto &path : config->getLocationPaths())
{
const LocationConfig *locationConfig = config->getLocation(path);
if (locationConfig != nullptr)
{
validateLocationConfig(path, locationConfig);
}
}
}
void ValidationEngine::validateGlobalConfig(const GlobalConfig *config)
{
Log::trace(LOCATION);
validateConfig(globalRules_, config);
for (const auto *serverConfig : config->getServerConfigs())
{
validateServerConfig(serverConfig);
}
}
ValidationEngine::ValidationEngine(const GlobalConfig *globalConfig) : globalConfig_(globalConfig)
{
Log::trace(LOCATION);
}
void ValidationEngine::validate()
{
Log::trace(LOCATION);
if (globalConfig_ != nullptr)
{
validateGlobalConfig(globalConfig_);
}
else
{
Log::warning("No GlobalConfig set for ValidationEngine; skipping validation.");
}
}

View File

@ -0,0 +1,50 @@
#pragma once
#include "webserv/config/GlobalConfig.hpp"
#include "webserv/config/config_validator/AValidationRule.hpp"
#include "webserv/config/config_validator/ValidationResult.hpp"
#include "webserv/config/LocationConfig.hpp"
#include "webserv/config/ServerConfig.hpp"
#include "webserv/config/AConfig.hpp"
#include <map>
#include <memory>
#include <string>
#include <vector>
class ValidationEngine
{
using RuleMap = std::map<std::string, std::vector<std::unique_ptr<AValidationRule>>>;
public:
ValidationEngine(const GlobalConfig *globalConfig = nullptr);
ValidationEngine(const ValidationEngine &other) = delete;
ValidationEngine &operator=(const ValidationEngine &other) = delete;
ValidationEngine(ValidationEngine &&other) noexcept = delete;
ValidationEngine &operator=(ValidationEngine &&other) noexcept = delete;
~ValidationEngine() = default;
void addGlobalRule(const std::string &directiveName, std::unique_ptr<AValidationRule> rule);
void addServerRule(const std::string &directiveName, std::unique_ptr<AValidationRule> rule);
void addLocationRule(const std::string &directiveName, std::unique_ptr<AValidationRule> rule);
void validate();
[[nodiscard]] std::vector<ValidationResult> getValidationResults() const;
[[nodiscard]] std::vector<ValidationResult> getErrors() const;
[[nodiscard]] std::vector<ValidationResult> getWarnings() const;
[[nodiscard]] bool hasErrors() const;
private:
static void addRule(RuleMap &ruleMap, const std::string &directiveName, std::unique_ptr<AValidationRule> rule);
void validateConfig(RuleMap const &rules, const AConfig *config);
void validateGlobalConfig(const GlobalConfig *config);
void validateServerConfig(const ServerConfig *config);
void validateLocationConfig(const std::string &path, const LocationConfig *config);
RuleMap globalRules_;
RuleMap serverRules_;
RuleMap locationRules_;
const GlobalConfig *globalConfig_;
std::vector<ValidationResult> results_;
};

View File

@ -0,0 +1,36 @@
#include <webserv/config/config_validator/ValidationResult.hpp>
#include <webserv/log/Log.hpp>
ValidationResult::ValidationResult(Type type, std::string message) : type_(type), message_(std::move(message)) {}
ValidationResult ValidationResult::success()
{
return {ValidationResult::Type::SUCCESS};
}
ValidationResult ValidationResult::error(const std::string &message)
{
Log::error(message);
return {ValidationResult::Type::ERROR, message};
}
ValidationResult ValidationResult::warning(const std::string &message)
{
Log::warning(message);
return {ValidationResult::Type::WARNING, message};
}
bool ValidationResult::isValidResult() const
{
return type_ == Type::SUCCESS;
}
ValidationResult::Type ValidationResult::getType() const
{
return type_;
}
std::string ValidationResult::getMessage() const
{
return message_;
}

View File

@ -1,22 +1,35 @@
#pragma once
#include <string>
#include <cstdint>
class ValidationResult
{
public:
enum class Type : uint8_t
{
SUCCESS,
ERROR,
WARNING
};
~ValidationResult() = default;
ValidationResult(const ValidationResult &other) = delete;
ValidationResult &operator=(const ValidationResult &other) = delete;
ValidationResult(const ValidationResult &other) = default;
ValidationResult &operator=(const ValidationResult &other) = default;
ValidationResult(ValidationResult &&other) noexcept = default;
ValidationResult &operator=(ValidationResult &&other) noexcept = default;
static ValidationResult success();
static ValidationResult error(const std::string &message);
static ValidationResult warning(const std::string &message);
[[nodiscard]] bool isValidResult() const;
[[nodiscard]] ValidationResult::Type getType() const ;
[[nodiscard]] std::string getMessage() const;
private:
ValidationResult(bool isValid, std::string errorMessage = "");
bool isValid;
std::string errorMessage;
ValidationResult(Type type, std::string message = "");
Type type_;
std::string message_;
};

View File

@ -1,6 +1,7 @@
#include <webserv/config/ConfigManager.hpp> // for ConfigManager
#include <webserv/log/Log.hpp> // for Log, LOCATION
#include <webserv/server/Server.hpp> // for Server
#include <webserv/config/config_validator/ConfigValidator.hpp> // for ConfigValidator
#include <iostream> // for basic_ostream, operator<<, cerr, ios_base
#include <string> // for basic_string, char_traits, allocator, operator+, operator<=>
@ -17,8 +18,19 @@ int main(int argc, char **argv)
Log::setStdoutChannel(Log::Level::Info);
Log::info("\n======================\nStarting webserv...\n======================\n");
ConfigManager::getInstance().init(argv[1]); // NOLINT
ConfigManager &configManager = ConfigManager::getInstance();
configManager.init(argv[1]); // NOLINT
ConfigValidator validator{configManager.getGlobalConfig()};
if (validator.hasErrors())
{
Log::error("Configuration validation failed with the following errors:");
for (const auto &error : validator.getErrors())
{
Log::error(" - " + error.getMessage());
}
return 1;
}
Log::debug("ConfigManager initialized successfully.");
Server server(configManager);