diff --git a/src/core/Db.cpp b/src/core/Db.cpp index 620f8f1..5da5058 100644 --- a/src/core/Db.cpp +++ b/src/core/Db.cpp @@ -7,15 +7,30 @@ #include #include #include +#include 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_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_SCHEMA_VERSION = 2; // +static std::string readFileAsText(const std::string& path) { + std::ifstream ifs(path); + auto res = std::string((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); + 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 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; } @@ -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); }); } -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() { if (std::filesystem::exists(dbPath())) { - if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK) - throw std::runtime_error("failed to open sqlite3 db"); + if (std::filesystem::exists(dbDir() + "/schema")) { + 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(); - return; - } + cleanupDb(); + 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) 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 char* errmsg = nullptr; const char* CHALLENGE_TABLE = R"#( CREATE TABLE challenges ( nonce TEXT NOT NULL, - ip TEXT NOT NULL, + fingerprint TEXT NOT NULL, difficulty INTEGER NOT NULL, epoch INTEGER NOT NULL, CONSTRAINT PK PRIMARY KEY (nonce) @@ -58,7 +81,7 @@ CREATE TABLE challenges ( const char* TOKENS_TABLE = R"#( CREATE TABLE tokens ( token TEXT NOT NULL, - ip TEXT NOT NULL, + fingerprint TEXT NOT NULL, epoch INTEGER NOT NULL, CONSTRAINT PK PRIMARY KEY (token) );)#"; @@ -75,14 +98,14 @@ void CDatabase::addChallenge(const SDatabaseChallengeEntry& entry) { if (!isHashValid(entry.nonce)) return; - if (!isIpValid(entry.ip)) + if (!isHashValid(entry.fingerprint)) return; const std::string CMD = fmt::format(R"#( INSERT INTO challenges VALUES ( "{}", "{}", {}, {} );)#", - entry.nonce, entry.ip, entry.difficulty, entry.epoch); + entry.nonce, entry.fingerprint, entry.difficulty, entry.epoch); char* errmsg = nullptr; 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) 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) { @@ -143,14 +166,14 @@ void CDatabase::addToken(const SDatabaseTokenEntry& entry) { if (!isHashValid(entry.token)) return; - if (!isIpValid(entry.ip)) + if (!isHashValid(entry.fingerprint)) return; const std::string CMD = fmt::format(R"#( INSERT INTO tokens VALUES ( "{}", "{}", {} );)#", - entry.token, entry.ip, entry.epoch); + entry.token, entry.fingerprint, entry.epoch); char* errmsg = nullptr; 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) 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() { diff --git a/src/core/Db.hpp b/src/core/Db.hpp index 8c96bbd..e8a33ef 100644 --- a/src/core/Db.hpp +++ b/src/core/Db.hpp @@ -8,16 +8,16 @@ #include struct SDatabaseChallengeEntry { - std::string nonce = ""; - int difficulty = 0; - unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); - std::string ip = ""; + std::string nonce = ""; + int difficulty = 0; + unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); + std::string fingerprint = ""; }; struct SDatabaseTokenEntry { - std::string token = ""; - unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); - std::string ip = ""; + std::string token = ""; + unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); + std::string fingerprint = ""; }; class CDatabase { diff --git a/src/core/Handler.cpp b/src/core/Handler.cpp index 3cc7389..7f6f0b0 100644 --- a/src/core/Handler.cpp +++ b/src/core/Handler.cpp @@ -3,6 +3,7 @@ #include "../headers/cfHeader.hpp" #include "../headers/xforwardfor.hpp" #include "../headers/gitProtocolHeader.hpp" +#include "../headers/acceptLanguageHeader.hpp" #include "../debug/log.hpp" #include "../GlobalState.hpp" #include "../config/Config.hpp" @@ -96,6 +97,54 @@ void CServerHandler::finish() { m_client = nullptr; } +std::string CServerHandler::fingerprintForRequest(const Pistache::Http::Request& req) { + const auto HEADERS = req.headers(); + std::shared_ptr acceptEncodingHeader; + std::shared_ptr userAgentHeader; + std::shared_ptr cfHeader; + std::shared_ptr languageHeader; + + std::string input = "checkpoint-"; + + try { + cfHeader = Pistache::Http::Header::header_cast(HEADERS.get("cf-connecting-ip")); + } catch (std::exception& e) { + ; // silent ignore + } + + try { + acceptEncodingHeader = Pistache::Http::Header::header_cast(HEADERS.get("Accept-Encoding")); + } catch (std::exception& e) { + ; // silent ignore + } + + try { + languageHeader = Pistache::Http::Header::header_cast(HEADERS.get("Accept-Language")); + } catch (std::exception& e) { + ; // silent ignore + } + + try { + userAgentHeader = Pistache::Http::Header::header_cast(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) { const auto HEADERS = req.headers(); std::shared_ptr 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); if (TOKEN) { 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)"); proxyPass(req, response); 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); - } - } + 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); } @@ -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) { - const auto JSON = req.body(); + const auto JSON = req.body(); + const auto FINGERPRINT = fingerprintForRequest(req); std::shared_ptr cfHeader; try { @@ -249,7 +305,7 @@ void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pist return; } - if (CHALLENGE->ip != req.address().host()) { + if (CHALLENGE->fingerprint != FINGERPRINT) { resp.error = "bad challenge"; response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); return; @@ -273,7 +329,7 @@ void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pist 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.token = TOKEN; @@ -290,7 +346,7 @@ void CServerHandler::serveStop(const Pistache::Http::Request& req, Pistache::Htt const auto NONCE = generateNonce(); 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("challengeNonce", CTinylatesProp(NONCE)); diff --git a/src/core/Handler.hpp b/src/core/Handler.hpp index edd98de..a7c36b1 100644 --- a/src/core/Handler.hpp +++ b/src/core/Handler.hpp @@ -19,9 +19,10 @@ 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 proxyPass(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response); - void challengeSubmitted(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 challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response); + std::string fingerprintForRequest(const Pistache::Http::Request& req); struct SChallengeResponse { std::string challenge; diff --git a/src/headers/acceptLanguageHeader.hpp b/src/headers/acceptLanguageHeader.hpp new file mode 100644 index 0000000..cf32596 --- /dev/null +++ b/src/headers/acceptLanguageHeader.hpp @@ -0,0 +1,24 @@ +#include +#include + +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 = ""; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index a26e654..318d848 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,6 +12,7 @@ #include "headers/xforwardfor.hpp" #include "headers/cfHeader.hpp" #include "headers/gitProtocolHeader.hpp" +#include "headers/acceptLanguageHeader.hpp" #include "debug/log.hpp" @@ -74,6 +75,7 @@ int main(int argc, char** argv, char** envp) { Pistache::Http::Header::Registry::instance().registerHeader(); Pistache::Http::Header::Registry::instance().registerHeader(); Pistache::Http::Header::Registry::instance().registerHeader(); + Pistache::Http::Header::Registry::instance().registerHeader(); g_pDB = std::make_unique();