core: use fingerprints instead of ips for db

This commit is contained in:
Vaxry
2025-04-13 16:08:51 +01:00
parent 9b3fd6efac
commit 5a0d332466
6 changed files with 144 additions and 38 deletions

View File

@@ -7,15 +7,30 @@
#include <filesystem> #include <filesystem>
#include <string_view> #include <string_view>
#include <algorithm> #include <algorithm>
#include <fstream>
constexpr const char* DB_FILE = "data.db"; constexpr const char* DB_FILE = "data.db";
constexpr const uint64_t DB_TIME_BEFORE_CLEANUP_MS = 1000 * 60 * 10; // 10 mins constexpr const uint64_t DB_TIME_BEFORE_CLEANUP_MS = 1000 * 60 * 10; // 10 mins
constexpr const uint64_t DB_TOKEN_LIFE_LENGTH_S = 60 * 60; // 1hr constexpr const uint64_t DB_TOKEN_LIFE_LENGTH_S = 60 * 60; // 1hr
constexpr const uint64_t DB_CHALLENGE_LIFE_LENGTH_S = 60 * 10; // 10 mins constexpr const uint64_t DB_CHALLENGE_LIFE_LENGTH_S = 60 * 10; // 10 mins
constexpr const uint64_t DB_SCHEMA_VERSION = 2;
// //
static std::string readFileAsText(const std::string& path) {
std::ifstream ifs(path);
auto res = std::string((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
if (res.back() == '\n')
res.pop_back();
return res;
}
static std::string dbDir() {
static const std::string dir = std::filesystem::canonical(g_pGlobalState->cwd + "/" + g_pConfig->m_config.data_dir).string();
return dir;
}
static std::string dbPath() { static std::string dbPath() {
static const std::string path = std::filesystem::canonical(g_pGlobalState->cwd + "/" + g_pConfig->m_config.data_dir).string() + "/" + DB_FILE; static const std::string path = dbDir() + "/" + DB_FILE;
return path; return path;
} }
@@ -23,31 +38,39 @@ static bool isHashValid(const std::string_view sv) {
return std::all_of(sv.begin(), sv.end(), [](const char& c) { return (c >= 'a' && c <= 'f') || std::isdigit(c); }); return std::all_of(sv.begin(), sv.end(), [](const char& c) { return (c >= 'a' && c <= 'f') || std::isdigit(c); });
} }
static bool isIpValid(const std::string_view sv) {
return std::all_of(sv.begin(), sv.end(), [](const char& c) { return c == '.' || c == ':' || std::isdigit(c); });
}
CDatabase::CDatabase() { CDatabase::CDatabase() {
if (std::filesystem::exists(dbPath())) { if (std::filesystem::exists(dbPath())) {
if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK) if (std::filesystem::exists(dbDir() + "/schema")) {
throw std::runtime_error("failed to open sqlite3 db"); int schema = std::stoi(readFileAsText(dbDir() + "/schema"));
if (schema == DB_SCHEMA_VERSION) {
if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK)
throw std::runtime_error("failed to open sqlite3 db");
cleanupDb(); cleanupDb();
return; return;
} } else
Debug::log(LOG, "Database outdated, recreating db");
} else
Debug::log(LOG, "Database schema not present, recreating db");
} else
Debug::log(LOG, "Database not present, creating one");
Debug::log(LOG, "Database not present, creating one"); std::filesystem::remove(dbPath());
if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK) if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK)
throw std::runtime_error("failed to open sqlite3 db"); throw std::runtime_error("failed to open sqlite3 db");
std::ofstream of(dbDir() + "/schema", std::ios::trunc);
of << DB_SCHEMA_VERSION;
of.close();
// create db layout // create db layout
char* errmsg = nullptr; char* errmsg = nullptr;
const char* CHALLENGE_TABLE = R"#( const char* CHALLENGE_TABLE = R"#(
CREATE TABLE challenges ( CREATE TABLE challenges (
nonce TEXT NOT NULL, nonce TEXT NOT NULL,
ip TEXT NOT NULL, fingerprint TEXT NOT NULL,
difficulty INTEGER NOT NULL, difficulty INTEGER NOT NULL,
epoch INTEGER NOT NULL, epoch INTEGER NOT NULL,
CONSTRAINT PK PRIMARY KEY (nonce) CONSTRAINT PK PRIMARY KEY (nonce)
@@ -58,7 +81,7 @@ CREATE TABLE challenges (
const char* TOKENS_TABLE = R"#( const char* TOKENS_TABLE = R"#(
CREATE TABLE tokens ( CREATE TABLE tokens (
token TEXT NOT NULL, token TEXT NOT NULL,
ip TEXT NOT NULL, fingerprint TEXT NOT NULL,
epoch INTEGER NOT NULL, epoch INTEGER NOT NULL,
CONSTRAINT PK PRIMARY KEY (token) CONSTRAINT PK PRIMARY KEY (token)
);)#"; );)#";
@@ -75,14 +98,14 @@ void CDatabase::addChallenge(const SDatabaseChallengeEntry& entry) {
if (!isHashValid(entry.nonce)) if (!isHashValid(entry.nonce))
return; return;
if (!isIpValid(entry.ip)) if (!isHashValid(entry.fingerprint))
return; return;
const std::string CMD = fmt::format(R"#( const std::string CMD = fmt::format(R"#(
INSERT INTO challenges VALUES ( INSERT INTO challenges VALUES (
"{}", "{}", {}, {} "{}", "{}", {}, {}
);)#", );)#",
entry.nonce, entry.ip, entry.difficulty, entry.epoch); entry.nonce, entry.fingerprint, entry.difficulty, entry.epoch);
char* errmsg = nullptr; char* errmsg = nullptr;
sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg);
@@ -120,7 +143,7 @@ SELECT * FROM challenges WHERE nonce = "{}";
if (errmsg || result.result.size() < 4) if (errmsg || result.result.size() < 4)
return std::nullopt; return std::nullopt;
return SDatabaseChallengeEntry{.nonce = nonce, .difficulty = std::stoi(result.result.at(2)), .epoch = std::stoull(result.result.at(3)), .ip = result.result.at(1)}; return SDatabaseChallengeEntry{.nonce = nonce, .difficulty = std::stoi(result.result.at(2)), .epoch = std::stoull(result.result.at(3)), .fingerprint = result.result.at(1)};
} }
void CDatabase::dropChallenge(const std::string& nonce) { void CDatabase::dropChallenge(const std::string& nonce) {
@@ -143,14 +166,14 @@ void CDatabase::addToken(const SDatabaseTokenEntry& entry) {
if (!isHashValid(entry.token)) if (!isHashValid(entry.token))
return; return;
if (!isIpValid(entry.ip)) if (!isHashValid(entry.fingerprint))
return; return;
const std::string CMD = fmt::format(R"#( const std::string CMD = fmt::format(R"#(
INSERT INTO tokens VALUES ( INSERT INTO tokens VALUES (
"{}", "{}", {} "{}", "{}", {}
);)#", );)#",
entry.token, entry.ip, entry.epoch); entry.token, entry.fingerprint, entry.epoch);
char* errmsg = nullptr; char* errmsg = nullptr;
sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg);
@@ -207,7 +230,7 @@ SELECT * FROM tokens WHERE token = "{}";
if (errmsg || result.result.size() < 3) if (errmsg || result.result.size() < 3)
return std::nullopt; return std::nullopt;
return SDatabaseTokenEntry{.token = token, .epoch = std::stoull(result.result.at(2)), .ip = result.result.at(1)}; return SDatabaseTokenEntry{.token = token, .epoch = std::stoull(result.result.at(2)), .fingerprint = result.result.at(1)};
} }
bool CDatabase::shouldCleanupDb() { bool CDatabase::shouldCleanupDb() {

View File

@@ -8,16 +8,16 @@
#include <vector> #include <vector>
struct SDatabaseChallengeEntry { struct SDatabaseChallengeEntry {
std::string nonce = ""; std::string nonce = "";
int difficulty = 0; int difficulty = 0;
unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1);
std::string ip = ""; std::string fingerprint = "";
}; };
struct SDatabaseTokenEntry { struct SDatabaseTokenEntry {
std::string token = ""; std::string token = "";
unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1);
std::string ip = ""; std::string fingerprint = "";
}; };
class CDatabase { class CDatabase {

View File

@@ -3,6 +3,7 @@
#include "../headers/cfHeader.hpp" #include "../headers/cfHeader.hpp"
#include "../headers/xforwardfor.hpp" #include "../headers/xforwardfor.hpp"
#include "../headers/gitProtocolHeader.hpp" #include "../headers/gitProtocolHeader.hpp"
#include "../headers/acceptLanguageHeader.hpp"
#include "../debug/log.hpp" #include "../debug/log.hpp"
#include "../GlobalState.hpp" #include "../GlobalState.hpp"
#include "../config/Config.hpp" #include "../config/Config.hpp"
@@ -96,6 +97,54 @@ void CServerHandler::finish() {
m_client = nullptr; m_client = nullptr;
} }
std::string CServerHandler::fingerprintForRequest(const Pistache::Http::Request& req) {
const auto HEADERS = req.headers();
std::shared_ptr<const Pistache::Http::Header::AcceptEncoding> acceptEncodingHeader;
std::shared_ptr<const Pistache::Http::Header::UserAgent> userAgentHeader;
std::shared_ptr<const CFConnectingIPHeader> cfHeader;
std::shared_ptr<const AcceptLanguageHeader> languageHeader;
std::string input = "checkpoint-";
try {
cfHeader = Pistache::Http::Header::header_cast<CFConnectingIPHeader>(HEADERS.get("cf-connecting-ip"));
} catch (std::exception& e) {
; // silent ignore
}
try {
acceptEncodingHeader = Pistache::Http::Header::header_cast<Pistache::Http::Header::AcceptEncoding>(HEADERS.get("Accept-Encoding"));
} catch (std::exception& e) {
; // silent ignore
}
try {
languageHeader = Pistache::Http::Header::header_cast<AcceptLanguageHeader>(HEADERS.get("Accept-Language"));
} catch (std::exception& e) {
; // silent ignore
}
try {
userAgentHeader = Pistache::Http::Header::header_cast<Pistache::Http::Header::UserAgent>(HEADERS.get("User-Agent"));
} catch (std::exception& e) {
; // silent ignore
}
if (cfHeader)
input += cfHeader->ip();
// TODO: those seem to change. Find better things to hash.
// if (acceptEncodingHeader)
// input += HEADERS.getRaw("Accept-Encoding").value();
// if (languageHeader)
// input += languageHeader->language();
if (userAgentHeader)
input += userAgentHeader->agent();
input += req.address().host();
return sha256(input);
}
void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter response) { void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter response) {
const auto HEADERS = req.headers(); const auto HEADERS = req.headers();
std::shared_ptr<const Pistache::Http::Header::Host> hostHeader; std::shared_ptr<const Pistache::Http::Header::Host> hostHeader;
@@ -203,16 +252,22 @@ void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Htt
const auto TOKEN = g_pDB->getToken(req.cookies().get("CheckpointToken").value); const auto TOKEN = g_pDB->getToken(req.cookies().get("CheckpointToken").value);
if (TOKEN) { if (TOKEN) {
const auto AGE = std::chrono::milliseconds(std::time(nullptr)).count() - TOKEN->epoch; const auto AGE = std::chrono::milliseconds(std::time(nullptr)).count() - TOKEN->epoch;
if (AGE <= TOKEN_MAX_AGE_MS && TOKEN->ip == (cfHeader ? cfHeader->ip() : req.address().host())) { if (AGE <= TOKEN_MAX_AGE_MS && TOKEN->fingerprint == fingerprintForRequest(req)) {
Debug::log(LOG, " | Action: PASS (token)"); Debug::log(LOG, " | Action: PASS (token)");
proxyPass(req, response); proxyPass(req, response);
return; return;
} else // token has been used from a different IP or is expired. Nuke it. } else { // token has been used from a different IP or is expired. Nuke it.
g_pDB->dropToken(TOKEN->token); g_pDB->dropToken(TOKEN->token);
} if (AGE > TOKEN_MAX_AGE_MS)
} Debug::log(LOG, " | Action: CHALLENGE (token expired)");
else
Debug::log(LOG, " | Action: CHALLENGE (token fingerprint mismatch)");
}
} else
Debug::log(LOG, " | Action: CHALLENGE (token not found in db)");
} else
Debug::log(LOG, " | Action: CHALLENGE (no token)");
Debug::log(LOG, " | Action: CHALLENGE");
serveStop(req, response); serveStop(req, response);
} }
@@ -221,7 +276,8 @@ void CServerHandler::onTimeout(const Pistache::Http::Request& request, Pistache:
} }
void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) { void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) {
const auto JSON = req.body(); const auto JSON = req.body();
const auto FINGERPRINT = fingerprintForRequest(req);
std::shared_ptr<const CFConnectingIPHeader> cfHeader; std::shared_ptr<const CFConnectingIPHeader> cfHeader;
try { try {
@@ -249,7 +305,7 @@ void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pist
return; return;
} }
if (CHALLENGE->ip != req.address().host()) { if (CHALLENGE->fingerprint != FINGERPRINT) {
resp.error = "bad challenge"; resp.error = "bad challenge";
response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value());
return; return;
@@ -273,7 +329,7 @@ void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pist
const auto TOKEN = generateToken(); const auto TOKEN = generateToken();
g_pDB->addToken(SDatabaseTokenEntry{.token = TOKEN, .ip = (cfHeader ? cfHeader->ip() : req.address().host())}); g_pDB->addToken(SDatabaseTokenEntry{.token = TOKEN, .fingerprint = FINGERPRINT});
resp.success = true; resp.success = true;
resp.token = TOKEN; resp.token = TOKEN;
@@ -290,7 +346,7 @@ void CServerHandler::serveStop(const Pistache::Http::Request& req, Pistache::Htt
const auto NONCE = generateNonce(); const auto NONCE = generateNonce();
const auto DIFFICULTY = 4; const auto DIFFICULTY = 4;
g_pDB->addChallenge(SDatabaseChallengeEntry{.nonce = NONCE, .difficulty = DIFFICULTY, .ip = req.address().host()}); g_pDB->addChallenge(SDatabaseChallengeEntry{.nonce = NONCE, .difficulty = DIFFICULTY, .fingerprint = fingerprintForRequest(req)});
page.add("challengeDifficulty", CTinylatesProp(std::to_string(DIFFICULTY))); page.add("challengeDifficulty", CTinylatesProp(std::to_string(DIFFICULTY)));
page.add("challengeNonce", CTinylatesProp(NONCE)); page.add("challengeNonce", CTinylatesProp(NONCE));

View File

@@ -19,9 +19,10 @@ class CServerHandler : public Pistache::Http::Handler {
void onTimeout(const Pistache::Http::Request& request, Pistache::Http::ResponseWriter response); void onTimeout(const Pistache::Http::Request& request, Pistache::Http::ResponseWriter response);
private: private:
void serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response); void serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
void proxyPass(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response); void proxyPass(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
void challengeSubmitted(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);
struct SChallengeResponse { struct SChallengeResponse {
std::string challenge; std::string challenge;

View File

@@ -0,0 +1,24 @@
#include <pistache/http_headers.h>
#include <pistache/net.h>
class AcceptLanguageHeader : public Pistache::Http::Header::Header {
public:
NAME("Accept-Language");
AcceptLanguageHeader() = default;
void parse(const std::string& str) override {
m_language = str;
}
void write(std::ostream& os) const override {
os << m_language;
}
std::string language() const {
return m_language;
}
private:
std::string m_language = "";
};

View File

@@ -12,6 +12,7 @@
#include "headers/xforwardfor.hpp" #include "headers/xforwardfor.hpp"
#include "headers/cfHeader.hpp" #include "headers/cfHeader.hpp"
#include "headers/gitProtocolHeader.hpp" #include "headers/gitProtocolHeader.hpp"
#include "headers/acceptLanguageHeader.hpp"
#include "debug/log.hpp" #include "debug/log.hpp"
@@ -74,6 +75,7 @@ int main(int argc, char** argv, char** envp) {
Pistache::Http::Header::Registry::instance().registerHeader<CFConnectingIPHeader>(); Pistache::Http::Header::Registry::instance().registerHeader<CFConnectingIPHeader>();
Pistache::Http::Header::Registry::instance().registerHeader<XForwardedForHeader>(); Pistache::Http::Header::Registry::instance().registerHeader<XForwardedForHeader>();
Pistache::Http::Header::Registry::instance().registerHeader<GitProtocolHeader>(); Pistache::Http::Header::Registry::instance().registerHeader<GitProtocolHeader>();
Pistache::Http::Header::Registry::instance().registerHeader<AcceptLanguageHeader>();
g_pDB = std::make_unique<CDatabase>(); g_pDB = std::make_unique<CDatabase>();