From f199569ff8b9d5b46b8a228bfc54dbce235d1179 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Mon, 21 Apr 2025 20:13:24 +0100 Subject: [PATCH] core: rework request rules fixes #21 --- example/config.jsonc | 19 +++++++++---- src/config/Config.cpp | 44 +++++++++++++++++------------ src/config/Config.hpp | 53 +++++++++++++--------------------- src/config/ConfigRule.cpp | 28 ++++++++++++++++++ src/config/ConfigRule.hpp | 26 +++++++++++++++++ src/config/ConfigTypes.hpp | 10 +++++++ src/core/Handler.cpp | 58 +++++++++++--------------------------- 7 files changed, 139 insertions(+), 99 deletions(-) create mode 100644 src/config/ConfigRule.cpp create mode 100644 src/config/ConfigRule.hpp create mode 100644 src/config/ConfigTypes.hpp diff --git a/example/config.jsonc b/example/config.jsonc index 63b9a9d..f0e7faf 100644 --- a/example/config.jsonc +++ b/example/config.jsonc @@ -26,17 +26,24 @@ // NOT recommended to set to anything below 4 or above 5. "default_challenge_difficulty": 4, - // specific ip range configs. - "ip_configs": [ + // specific rules. They are checked top to bottom, and the first one to match will determine the request's fate + "rules": [ { "action": "ALLOW", "ip_ranges": [ "127.0.0.1/24", "::1/128" - ], - // if this regex matches the resource requested, a different rule will be applied - "exclude_regex": ".*/commit/.*", - "action_on_exclude": "DENY" + ] + }, { + "action": "DENY", + "user_agent": ".*(bot).*" + }, { + "action": "DENY", + "resource": "(/secret/).*" + }, { + "action": "CHALLENGE", + "difficulty": 5, // quite damn hard! + "resource": "(/hard/).*" } ], diff --git a/src/config/Config.cpp b/src/config/Config.cpp index 6cd3ef6..67c89c0 100644 --- a/src/config/Config.cpp +++ b/src/config/Config.cpp @@ -7,23 +7,22 @@ #include "../debug/log.hpp" -static CConfig::eConfigIPAction strToAction(const std::string& s) { - // TODO: allow any case I'm lazy it's 1am +static eConfigIPAction strToAction(const std::string& s) { if (s.empty()) - return CConfig::IP_ACTION_NONE; + return IP_ACTION_NONE; std::string LC = s; std::transform(LC.begin(), LC.end(), LC.begin(), ::tolower); if (LC == "allow") - return CConfig::IP_ACTION_ALLOW; + return IP_ACTION_ALLOW; if (LC == "deny") - return CConfig::IP_ACTION_DENY; + return IP_ACTION_DENY; if (LC == "challenge") - return CConfig::IP_ACTION_CHALLENGE; + return IP_ACTION_CHALLENGE; Debug::log(ERR, "Invalid action: {}, assuming NONE", s); - return CConfig::IP_ACTION_NONE; + return IP_ACTION_NONE; } CConfig::CConfig() { @@ -36,24 +35,33 @@ CConfig::CConfig() { m_config = json.value(); // parse some datas - for (const auto& ic : m_config.ip_configs) { - SIPRangeConfigParsed parsed; - parsed.action = strToAction(ic.action); - parsed.difficulty = ic.difficulty; - parsed.action_on_exclude = strToAction(ic.action_on_exclude); + for (const auto& ic : m_config.rules) { + CConfigRule rule; + rule.action = strToAction(ic.action); + + if (ic.difficulty != -1) + rule.difficulty = ic.difficulty; - if (!ic.exclude_regex.empty()) { - parsed.exclude_regex = std::make_unique(ic.exclude_regex); - if (parsed.exclude_regex->error_code() != RE2::NoError) { - Debug::log(CRIT, "Regex \"{}\" failed to parse", ic.exclude_regex); + if (!ic.user_agent.empty()) { + rule.user_agent = std::make_unique(ic.user_agent); + if ((*rule.user_agent)->error_code() != RE2::NoError) { + Debug::log(CRIT, "Regex \"{}\" failed to parse", ic.user_agent); + Debug::die("Failed to parse regex"); + } + } + + if (!ic.resource.empty()) { + rule.resource = std::make_unique(ic.resource); + if ((*rule.resource)->error_code() != RE2::NoError) { + Debug::log(CRIT, "Regex \"{}\" failed to parse", ic.resource); Debug::die("Failed to parse regex"); } } for (const auto& ir : ic.ip_ranges) { - parsed.ip_ranges.emplace_back(CIPRange(ir)); + rule.ip_ranges.emplace_back(CIPRange(ir)); } - m_parsedConfigDatas.ip_configs.emplace_back(std::move(parsed)); + m_parsedConfigDatas.configs.emplace_back(std::move(rule)); } } \ No newline at end of file diff --git a/src/config/Config.hpp b/src/config/Config.hpp index a830d28..ed66bee 100644 --- a/src/config/Config.hpp +++ b/src/config/Config.hpp @@ -5,51 +5,36 @@ #include -#include "IPRange.hpp" +#include "ConfigRule.hpp" class CConfig { public: CConfig(); - enum eConfigIPAction : uint8_t { - IP_ACTION_NONE = 0, - IP_ACTION_DENY, - IP_ACTION_ALLOW, - IP_ACTION_CHALLENGE - }; - - struct SIPRangeConfig { - std::string action = ""; - std::vector ip_ranges; - int difficulty = -1; - std::string exclude_regex = ""; - std::string action_on_exclude = ""; - }; - - struct SIPRangeConfigParsed { - eConfigIPAction action = IP_ACTION_DENY; - std::vector ip_ranges; - int difficulty = -1; - std::unique_ptr exclude_regex; - eConfigIPAction action_on_exclude = IP_ACTION_NONE; + struct SConfigRule { + std::string action = ""; + int difficulty = -1; + std::vector ip_ranges = {}; + std::string user_agent = ""; + std::string resource = ""; }; struct SConfig { - int port = 3001; - std::string forward_address = "127.0.0.1:3000"; - std::string data_dir = ""; - std::string html_dir = ""; - unsigned long int max_request_size = 10000000; // 10MB - bool git_host = false; - unsigned long int proxy_timeout_sec = 120; // 2 minutes - bool trace_logging = false; - std::vector ip_configs = {}; - int default_challenge_difficulty = 4; - bool async_proxy = true; + int port = 3001; + std::string forward_address = "127.0.0.1:3000"; + std::string data_dir = ""; + std::string html_dir = ""; + unsigned long int max_request_size = 10000000; // 10MB + bool git_host = false; + unsigned long int proxy_timeout_sec = 120; // 2 minutes + bool trace_logging = false; + std::vector rules = {}; + int default_challenge_difficulty = 4; + bool async_proxy = true; } m_config; struct { - std::vector ip_configs; + std::vector configs; } m_parsedConfigDatas; }; diff --git a/src/config/ConfigRule.cpp b/src/config/ConfigRule.cpp new file mode 100644 index 0000000..a2108b8 --- /dev/null +++ b/src/config/ConfigRule.cpp @@ -0,0 +1,28 @@ +#include "ConfigRule.hpp" + +bool CConfigRule::passes(const CIP& ip, const std::string& ua, const std::string& res) const { + if (!ip_ranges.empty()) { + bool passed = false; + for (const auto& r : ip_ranges) { + if (r.ipMatches(ip)) { + passed = true; + break; + } + } + + if (!passed) + return false; + } + + if (user_agent.has_value()) { + if (!RE2::FullMatch(ua, **user_agent)) + return false; + } + + if (resource.has_value()) { + if (!RE2::FullMatch(res, **resource)) + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/config/ConfigRule.hpp b/src/config/ConfigRule.hpp new file mode 100644 index 0000000..4e6943a --- /dev/null +++ b/src/config/ConfigRule.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include +#include +#include + +#include "ConfigTypes.hpp" +#include "IPRange.hpp" + +class CConfigRule { + public: + eConfigIPAction action = IP_ACTION_DENY; + + std::optional difficulty; + + bool passes(const CIP& ip, const std::string& ua, const std::string& res) const; + + private: + std::vector ip_ranges; + std::optional> user_agent; + std::optional> resource; + + friend class CConfig; +}; \ No newline at end of file diff --git a/src/config/ConfigTypes.hpp b/src/config/ConfigTypes.hpp new file mode 100644 index 0000000..e3761d6 --- /dev/null +++ b/src/config/ConfigTypes.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +enum eConfigIPAction : uint8_t { + IP_ACTION_NONE = 0, + IP_ACTION_DENY, + IP_ACTION_ALLOW, + IP_ACTION_CHALLENGE +}; \ No newline at end of file diff --git a/src/core/Handler.cpp b/src/core/Handler.cpp index 05e46aa..50cf42f 100644 --- a/src/core/Handler.cpp +++ b/src/core/Handler.cpp @@ -241,53 +241,29 @@ void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Htt int challengeDifficulty = g_pConfig->m_config.default_challenge_difficulty; - if (!g_pConfig->m_parsedConfigDatas.ip_configs.empty()) { + if (!g_pConfig->m_parsedConfigDatas.configs.empty()) { const auto IP = CIP(REQUEST_IP); - for (const auto& ic : g_pConfig->m_parsedConfigDatas.ip_configs) { - bool matched = false; - - for (const auto& ipr : ic.ip_ranges) { - if (!ipr.ipMatches(IP)) - continue; - - matched = true; - break; - } - - if (matched) { - if (ic.difficulty != -1) - challengeDifficulty = ic.difficulty; - - // if we have an exclude regex and it matches the resource, skip this rule - if (ic.exclude_regex && RE2::FullMatch(req.resource(), *ic.exclude_regex)) { - if (ic.action_on_exclude == CConfig::IP_ACTION_ALLOW) { - Debug::log(LOG, " | Action: PASS (ip rule matched for {}, excluded resource, exclude action is PASS)", REQUEST_IP); + for (const auto& ic : g_pConfig->m_parsedConfigDatas.configs) { + if (ic.passes(IP, userAgentHeader ? userAgentHeader->agent() : "", req.resource())) { + switch (ic.action) { + case IP_ACTION_DENY: + Debug::log(LOG, " | Action: DENY (rule)"); + response.send(Pistache::Http::Code::Forbidden, "Blocked by checkpoint"); + return; + case IP_ACTION_ALLOW: + Debug::log(LOG, " | Action: PASS (rule)"); proxyPass(req, response); return; - } else if (ic.action_on_exclude == CConfig::IP_ACTION_DENY) { - Debug::log(LOG, " | Action: DENY (ip rule matched for {}, excluded resource, exclude action is DENY)", REQUEST_IP); - response.send(Pistache::Http::Code::Forbidden, "Forbidden"); - return; - } else if (ic.action_on_exclude == CConfig::IP_ACTION_CHALLENGE) { - Debug::log(LOG, " | ip rule matched for {}, excluded resource, exclude action is CHALLENGE", REQUEST_IP); + case IP_ACTION_CHALLENGE: + Debug::log(LOG, " | Action: CHALLENGE (rule)"); + challengeDifficulty = ic.difficulty.value_or(g_pConfig->m_config.default_challenge_difficulty); break; - } - Debug::log(LOG, " | ip rule matched for {}, excluded resource, exclude action is NONE", REQUEST_IP); - continue; + default: + Debug::log(LOG, " | Invalid rule found (no action) skipping"); } - if (ic.action == CConfig::IP_ACTION_ALLOW) { - Debug::log(LOG, " | Action: PASS (ip rule matched for {})", REQUEST_IP); - proxyPass(req, response); - return; - } else if (ic.action == CConfig::IP_ACTION_DENY) { - Debug::log(LOG, " | Action: DENY (ip rule matched for {})", REQUEST_IP); - response.send(Pistache::Http::Code::Forbidden, "Forbidden"); - return; - } - - // if it's challenge then it's default so just set the difficulty if applicable and proceed - break; + if (ic.action == IP_ACTION_CHALLENGE) + break; } } }