core: add IP-range based rules for permissions and fifficulty

This commit is contained in:
Vaxry
2025-04-15 01:34:57 +01:00
parent 1bdc60b8d2
commit 321d1eb326
9 changed files with 602 additions and 20 deletions

View File

@@ -5,6 +5,18 @@
#include "../helpers/FsUtils.hpp"
#include "../GlobalState.hpp"
static CConfig::eConfigIPAction strToAction(const std::string& s) {
// TODO: allow any case I'm lazy it's 1am
if (s == "ALLOW" || s == "allow" || s == "Allow")
return CConfig::IP_ACTION_ALLOW;
if (s == "Deny" || s == "deny" || s == "Deny")
return CConfig::IP_ACTION_DENY;
if (s == "CHALLENGE" || s == "challenge" || s == "Challenge")
return CConfig::IP_ACTION_CHALLENGE;
throw std::runtime_error("Invalid ip config action");
}
CConfig::CConfig() {
auto json = glz::read_jsonc<SConfig>(
NFsUtils::readFileAsString(NFsUtils::isAbsolute(g_pGlobalState->configPath) ? g_pGlobalState->configPath : g_pGlobalState->cwd + "/" + g_pGlobalState->configPath).value());
@@ -13,4 +25,17 @@ CConfig::CConfig() {
throw std::runtime_error("No config / bad config format");
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;
for (const auto& ir : ic.ip_ranges) {
parsed.ip_ranges.emplace_back(CIPRange(ir));
}
m_parsedConfigDatas.ip_configs.emplace_back(std::move(parsed));
}
}

View File

@@ -3,20 +3,46 @@
#include <string>
#include <memory>
#include "IPRange.hpp"
class CConfig {
public:
CConfig();
enum eConfigIPAction : uint8_t {
IP_ACTION_DENY = 0,
IP_ACTION_ALLOW,
IP_ACTION_CHALLENGE
};
struct SIPRangeConfig {
std::string action = "";
std::vector<std::string> ip_ranges;
int difficulty = -1;
};
struct SIPRangeConfigParsed {
eConfigIPAction action = IP_ACTION_DENY;
std::vector<CIPRange> ip_ranges;
int difficulty = -1;
};
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;
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<SIPRangeConfig> ip_configs;
int default_challenge_difficulty = 4;
} m_config;
struct {
std::vector<SIPRangeConfigParsed> ip_configs;
} m_parsedConfigDatas;
};
inline std::unique_ptr<CConfig> g_pConfig;

129
src/config/IPRange.cpp Normal file
View File

@@ -0,0 +1,129 @@
#include "IPRange.hpp"
#include <algorithm>
#include <stdexcept>
CIP::CIP(const std::string& ip) {
if (std::count(ip.begin(), ip.end(), '.') == 3)
parseV4(ip);
else if (std::count(ip.begin(), ip.end(), ':') >= 2)
parseV6(ip);
else
throw std::runtime_error("IP not valid");
}
void CIP::parseV4(const std::string& ip) {
m_v6 = false;
std::string_view curr;
size_t lastPos = 0;
auto advance = [&]() {
size_t prev = lastPos ? lastPos + 1 : lastPos;
lastPos = ip.find('.', prev);
if (lastPos == std::string::npos)
curr = std::string_view{ip}.substr(prev);
else
curr = std::string_view{ip}.substr(prev, lastPos - prev);
};
for (size_t i = 0; i < 4; ++i) {
advance();
m_blocks.push_back(std::stoul(std::string{curr}));
if (m_blocks.back() > 0xFF)
throw std::runtime_error("Invalid IPv4 byte");
}
}
void CIP::parseV6(const std::string& ip) {
const auto COLONS = std::count(ip.begin(), ip.end(), ':');
m_v6 = true;
std::string_view curr;
size_t lastPos = 0;
bool first = true;
auto advance = [&]() {
size_t prev = !first ? lastPos + 1 : lastPos;
lastPos = ip.find(':', prev);
if (lastPos == std::string::npos)
curr = std::string_view{ip}.substr(prev);
else
curr = std::string_view{ip}.substr(prev, lastPos - prev);
first = false;
};
for (size_t i = 0; i < 8; ++i) {
advance();
if (curr.empty()) {
for (size_t j = 0; j < 8 - COLONS; ++j) {
i++;
m_blocks.push_back(0);
}
if (ip.starts_with("::") || ip.ends_with("::")) {
m_blocks.push_back(0);
advance();
} else
i--;
continue;
} else
m_blocks.push_back(std::stoul(std::string{curr}, nullptr, 16));
if (m_blocks.back() > 0xFFFF)
throw std::runtime_error("Invalid IPv6 byte");
}
}
CIPRange::CIPRange(const std::string& range) {
if (!range.contains('/'))
throw std::runtime_error("Range has no subnet");
m_subnet = std::stoul(range.substr(range.find('/') + 1));
m_ip = CIP(range.substr(0, range.find('/')));
}
bool CIPRange::ipMatches(const CIP& ip) const {
if (m_ip.m_v6 != ip.m_v6)
return false;
if (m_ip.m_v6)
return ipMatchesV6(ip);
return ipMatchesV4(ip);
}
bool CIPRange::ipMatchesV4(const CIP& ip) const {
uint32_t rangeMask = 0xFFFFFFFF << (32 - m_subnet);
uint32_t rangeIP =
(((uint32_t)m_ip.m_blocks.at(0)) << 24) | (((uint32_t)m_ip.m_blocks.at(1)) << 16) | (((uint32_t)m_ip.m_blocks.at(2)) << 8) | (((uint32_t)m_ip.m_blocks.at(3)) << 0);
uint32_t incomingIP =
(((uint32_t)ip.m_blocks.at(0)) << 24) | (((uint32_t)ip.m_blocks.at(1)) << 16) | (((uint32_t)ip.m_blocks.at(2)) << 8) | (((uint32_t)ip.m_blocks.at(3)) << 0);
return (rangeMask & rangeIP) == (rangeMask & incomingIP);
}
bool CIPRange::ipMatchesV6(const CIP& ip) const {
uint64_t rangeMaskLeft = 0xFFFFFFFFFFFFFFFF << (m_subnet > 64 ? 0 : 64 - m_subnet);
uint64_t rangeIPLeft =
(((uint64_t)m_ip.m_blocks.at(0)) << 48) | (((uint64_t)m_ip.m_blocks.at(1)) << 32) | (((uint64_t)m_ip.m_blocks.at(2)) << 16) | (((uint64_t)m_ip.m_blocks.at(3)) << 0);
uint64_t incomingIPLeft =
(((uint64_t)ip.m_blocks.at(0)) << 48) | (((uint64_t)ip.m_blocks.at(1)) << 32) | (((uint64_t)ip.m_blocks.at(2)) << 16) | (((uint64_t)ip.m_blocks.at(3)) << 0);
if ((rangeMaskLeft & rangeIPLeft) != (rangeMaskLeft & incomingIPLeft))
return false;
if (m_subnet <= 64)
return true;
uint64_t rangeMaskRight = 0xFFFFFFFFFFFFFFFF << (/* m_subnet > 64 */ 128 - m_subnet);
uint64_t rangeIPRight =
(((uint64_t)m_ip.m_blocks.at(4)) << 48) | (((uint64_t)m_ip.m_blocks.at(5)) << 32) | (((uint64_t)m_ip.m_blocks.at(6)) << 16) | (((uint64_t)m_ip.m_blocks.at(7)) << 0);
uint64_t incomingIPRight =
(((uint64_t)ip.m_blocks.at(4)) << 48) | (((uint64_t)ip.m_blocks.at(5)) << 32) | (((uint64_t)ip.m_blocks.at(6)) << 16) | (((uint64_t)ip.m_blocks.at(7)) << 0);
return (rangeMaskRight & rangeIPRight) == (rangeMaskRight & incomingIPRight);
}

33
src/config/IPRange.hpp Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
class CIP {
public:
CIP() = default;
CIP(const std::string& ip);
bool m_v6 = false;
std::vector<uint16_t> m_blocks;
private:
void parseV4(const std::string& ip);
void parseV6(const std::string& ip);
};
// Accepts both ipv4 and ipv6
class CIPRange {
public:
CIPRange(const std::string& range);
bool ipMatches(const CIP& ip) const;
private:
CIP m_ip;
size_t m_subnet = 0;
bool ipMatchesV6(const CIP& ip) const;
bool ipMatchesV4(const CIP& ip) const;
};

View File

@@ -213,6 +213,40 @@ 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()) {
const auto IP = CIP(req.address().host());
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.action == CConfig::IP_ACTION_ALLOW) {
Debug::log(LOG, " | Action: PASS (ip rule matched for {})", req.address().host());
proxyPass(req, response);
return;
} else if (ic.action == CConfig::IP_ACTION_DENY) {
Debug::log(LOG, " | Action: DENY (ip rule matched for {})", req.address().host());
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
if (ic.difficulty != -1)
challengeDifficulty = ic.difficulty;
break;
}
}
}
if (req.cookies().has(TOKEN_COOKIE_NAME)) {
// check the token
const auto TOKEN = CToken(req.cookies().get(TOKEN_COOKIE_NAME).value);
@@ -234,7 +268,7 @@ void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Htt
} else
Debug::log(LOG, " | Action: CHALLENGE (no token)");
serveStop(req, response);
serveStop(req, response, challengeDifficulty);
}
void CServerHandler::onTimeout(const Pistache::Http::Request& request, Pistache::Http::ResponseWriter response) {
@@ -274,18 +308,16 @@ void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pist
response.send(Pistache::Http::Code::Ok, "Ok");
}
void CServerHandler::serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) {
void CServerHandler::serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response, int difficulty) {
static const auto PAGE_INDEX = NFsUtils::readFileAsString(NFsUtils::htmlPath("/index.min.html")).value();
static const auto PAGE_ROOT = PAGE_INDEX.substr(0, PAGE_INDEX.find_last_of("/") + 1);
CTinylates page(PAGE_INDEX);
page.setTemplateRoot(PAGE_ROOT);
const auto NONCE = generateNonce();
const auto DIFFICULTY = 4;
const auto NONCE = generateNonce();
const auto CHALLENGE = CChallenge(fingerprintForRequest(req), NONCE, difficulty);
const auto CHALLENGE = CChallenge(fingerprintForRequest(req), NONCE, DIFFICULTY);
page.add("challengeDifficulty", CTinylatesProp(std::to_string(DIFFICULTY)));
page.add("challengeDifficulty", CTinylatesProp(std::to_string(difficulty)));
page.add("challengeNonce", CTinylatesProp(NONCE));
page.add("challengeSignature", CTinylatesProp(CHALLENGE.signature()));
page.add("challengeFingerprint", CTinylatesProp(CHALLENGE.fingerprint()));

View File

@@ -16,7 +16,7 @@ class CServerHandler : public Pistache::Http::Handler {
void onTimeout(const Pistache::Http::Request& request, Pistache::Http::ResponseWriter response);
private:
void serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
void serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response, int difficulty);
void proxyPass(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
void challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
std::string fingerprintForRequest(const Pistache::Http::Request& req);