core: rework request rules

fixes #21
This commit is contained in:
Vaxry
2025-04-21 20:13:24 +01:00
parent cd0fadf952
commit f199569ff8
7 changed files with 139 additions and 99 deletions

View File

@@ -26,17 +26,24 @@
// NOT recommended to set to anything below 4 or above 5. // NOT recommended to set to anything below 4 or above 5.
"default_challenge_difficulty": 4, "default_challenge_difficulty": 4,
// specific ip range configs. // specific rules. They are checked top to bottom, and the first one to match will determine the request's fate
"ip_configs": [ "rules": [
{ {
"action": "ALLOW", "action": "ALLOW",
"ip_ranges": [ "ip_ranges": [
"127.0.0.1/24", "127.0.0.1/24",
"::1/128" "::1/128"
], ]
// if this regex matches the resource requested, a different rule will be applied }, {
"exclude_regex": ".*/commit/.*", "action": "DENY",
"action_on_exclude": "DENY" "user_agent": ".*(bot).*"
}, {
"action": "DENY",
"resource": "(/secret/).*"
}, {
"action": "CHALLENGE",
"difficulty": 5, // quite damn hard!
"resource": "(/hard/).*"
} }
], ],

View File

@@ -7,23 +7,22 @@
#include "../debug/log.hpp" #include "../debug/log.hpp"
static CConfig::eConfigIPAction strToAction(const std::string& s) { static eConfigIPAction strToAction(const std::string& s) {
// TODO: allow any case I'm lazy it's 1am
if (s.empty()) if (s.empty())
return CConfig::IP_ACTION_NONE; return IP_ACTION_NONE;
std::string LC = s; std::string LC = s;
std::transform(LC.begin(), LC.end(), LC.begin(), ::tolower); std::transform(LC.begin(), LC.end(), LC.begin(), ::tolower);
if (LC == "allow") if (LC == "allow")
return CConfig::IP_ACTION_ALLOW; return IP_ACTION_ALLOW;
if (LC == "deny") if (LC == "deny")
return CConfig::IP_ACTION_DENY; return IP_ACTION_DENY;
if (LC == "challenge") if (LC == "challenge")
return CConfig::IP_ACTION_CHALLENGE; return IP_ACTION_CHALLENGE;
Debug::log(ERR, "Invalid action: {}, assuming NONE", s); Debug::log(ERR, "Invalid action: {}, assuming NONE", s);
return CConfig::IP_ACTION_NONE; return IP_ACTION_NONE;
} }
CConfig::CConfig() { CConfig::CConfig() {
@@ -36,24 +35,33 @@ CConfig::CConfig() {
m_config = json.value(); m_config = json.value();
// parse some datas // parse some datas
for (const auto& ic : m_config.ip_configs) { for (const auto& ic : m_config.rules) {
SIPRangeConfigParsed parsed; CConfigRule rule;
parsed.action = strToAction(ic.action); rule.action = strToAction(ic.action);
parsed.difficulty = ic.difficulty;
parsed.action_on_exclude = strToAction(ic.action_on_exclude);
if (!ic.exclude_regex.empty()) { if (ic.difficulty != -1)
parsed.exclude_regex = std::make_unique<re2::RE2>(ic.exclude_regex); rule.difficulty = ic.difficulty;
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<re2::RE2>(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<re2::RE2>(ic.resource);
if ((*rule.resource)->error_code() != RE2::NoError) {
Debug::log(CRIT, "Regex \"{}\" failed to parse", ic.resource);
Debug::die("Failed to parse regex"); Debug::die("Failed to parse regex");
} }
} }
for (const auto& ir : ic.ip_ranges) { 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));
} }
} }

View File

@@ -5,51 +5,36 @@
#include <re2/re2.h> #include <re2/re2.h>
#include "IPRange.hpp" #include "ConfigRule.hpp"
class CConfig { class CConfig {
public: public:
CConfig(); CConfig();
enum eConfigIPAction : uint8_t { struct SConfigRule {
IP_ACTION_NONE = 0, std::string action = "";
IP_ACTION_DENY, int difficulty = -1;
IP_ACTION_ALLOW, std::vector<std::string> ip_ranges = {};
IP_ACTION_CHALLENGE std::string user_agent = "";
}; std::string resource = "";
struct SIPRangeConfig {
std::string action = "";
std::vector<std::string> ip_ranges;
int difficulty = -1;
std::string exclude_regex = "";
std::string action_on_exclude = "";
};
struct SIPRangeConfigParsed {
eConfigIPAction action = IP_ACTION_DENY;
std::vector<CIPRange> ip_ranges;
int difficulty = -1;
std::unique_ptr<re2::RE2> exclude_regex;
eConfigIPAction action_on_exclude = IP_ACTION_NONE;
}; };
struct SConfig { struct SConfig {
int port = 3001; int port = 3001;
std::string forward_address = "127.0.0.1:3000"; std::string forward_address = "127.0.0.1:3000";
std::string data_dir = ""; std::string data_dir = "";
std::string html_dir = ""; std::string html_dir = "";
unsigned long int max_request_size = 10000000; // 10MB unsigned long int max_request_size = 10000000; // 10MB
bool git_host = false; bool git_host = false;
unsigned long int proxy_timeout_sec = 120; // 2 minutes unsigned long int proxy_timeout_sec = 120; // 2 minutes
bool trace_logging = false; bool trace_logging = false;
std::vector<SIPRangeConfig> ip_configs = {}; std::vector<SConfigRule> rules = {};
int default_challenge_difficulty = 4; int default_challenge_difficulty = 4;
bool async_proxy = true; bool async_proxy = true;
} m_config; } m_config;
struct { struct {
std::vector<SIPRangeConfigParsed> ip_configs; std::vector<CConfigRule> configs;
} m_parsedConfigDatas; } m_parsedConfigDatas;
}; };

28
src/config/ConfigRule.cpp Normal file
View File

@@ -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;
}

26
src/config/ConfigRule.hpp Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include <re2/re2.h>
#include <string>
#include <memory>
#include <optional>
#include "ConfigTypes.hpp"
#include "IPRange.hpp"
class CConfigRule {
public:
eConfigIPAction action = IP_ACTION_DENY;
std::optional<int> difficulty;
bool passes(const CIP& ip, const std::string& ua, const std::string& res) const;
private:
std::vector<CIPRange> ip_ranges;
std::optional<std::unique_ptr<re2::RE2>> user_agent;
std::optional<std::unique_ptr<re2::RE2>> resource;
friend class CConfig;
};

View File

@@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
enum eConfigIPAction : uint8_t {
IP_ACTION_NONE = 0,
IP_ACTION_DENY,
IP_ACTION_ALLOW,
IP_ACTION_CHALLENGE
};

View File

@@ -241,53 +241,29 @@ void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Htt
int challengeDifficulty = g_pConfig->m_config.default_challenge_difficulty; 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); const auto IP = CIP(REQUEST_IP);
for (const auto& ic : g_pConfig->m_parsedConfigDatas.ip_configs) { for (const auto& ic : g_pConfig->m_parsedConfigDatas.configs) {
bool matched = false; if (ic.passes(IP, userAgentHeader ? userAgentHeader->agent() : "", req.resource())) {
switch (ic.action) {
for (const auto& ipr : ic.ip_ranges) { case IP_ACTION_DENY:
if (!ipr.ipMatches(IP)) Debug::log(LOG, " | Action: DENY (rule)");
continue; response.send(Pistache::Http::Code::Forbidden, "Blocked by checkpoint");
return;
matched = true; case IP_ACTION_ALLOW:
break; Debug::log(LOG, " | Action: PASS (rule)");
}
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);
proxyPass(req, response); proxyPass(req, response);
return; return;
} else if (ic.action_on_exclude == CConfig::IP_ACTION_DENY) { case IP_ACTION_CHALLENGE:
Debug::log(LOG, " | Action: DENY (ip rule matched for {}, excluded resource, exclude action is DENY)", REQUEST_IP); Debug::log(LOG, " | Action: CHALLENGE (rule)");
response.send(Pistache::Http::Code::Forbidden, "Forbidden"); challengeDifficulty = ic.difficulty.value_or(g_pConfig->m_config.default_challenge_difficulty);
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);
break; break;
} default:
Debug::log(LOG, " | ip rule matched for {}, excluded resource, exclude action is NONE", REQUEST_IP); Debug::log(LOG, " | Invalid rule found (no action) skipping");
continue;
} }
if (ic.action == CConfig::IP_ACTION_ALLOW) { if (ic.action == IP_ACTION_CHALLENGE)
Debug::log(LOG, " | Action: PASS (ip rule matched for {})", REQUEST_IP); break;
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;
} }
} }
} }