diff --git a/.gitignore b/.gitignore index 259148f..0bd5df8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,10 @@ *.exe *.out *.app + +.cache/ +.vscode +build/ +data/ + +config.json \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3c54048 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "subprojects/tinylates"] + path = subprojects/tinylates + url = https://github.com/vaxerski/tinylates +[submodule "subprojects/pistache"] + path = subprojects/pistache + url = https://github.com/pistacheio/pistache +[submodule "subprojects/fmt"] + path = subprojects/fmt + url = https://github.com/fmtlib/fmt +[submodule "subprojects/glaze"] + path = subprojects/glaze + url = https://github.com/stephenberry/glaze diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7391148 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.19) + +project( + checkpoint + DESCRIPTION "Tiny reverse proxy that attempts to block AI scrapers" +) + +set(PISTACHE_BUILD_TESTS OFF) +set(PISTACHE_BUILD_FUZZ OFF) + +add_subdirectory(subprojects/pistache) +add_subdirectory(subprojects/fmt) +add_subdirectory(subprojects/tinylates) +add_subdirectory(subprojects/glaze) + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") + +set(CMAKE_CXX_STANDARD 23) + +add_executable(checkpoint ${SRCFILES}) + +add_compile_options(-Wno-deprecated-declarations -Wno-deprecated) + +find_package(PkgConfig REQUIRED) + +pkg_check_modules(deps IMPORTED_TARGET openssl sqlite3) + +target_include_directories(checkpoint +PRIVATE + "./subprojects/pistache/include" + "./subprojects/pistache/subprojects/cpp-httplib" + "./subprojects/pistache/subprojects/hinnant-date/include" +) +target_link_libraries(checkpoint + PkgConfig::deps + pistache + fmt + tinylates +) diff --git a/config.jsonc b/config.jsonc new file mode 100644 index 0000000..70a8f1a --- /dev/null +++ b/config.jsonc @@ -0,0 +1,7 @@ +{ + "html_dir": "./html", + "data_dir": "./data", + "port": 3001, + "forward_address": "127.0.0.1:3000", + "max_request_size": 10000000 +} \ No newline at end of file diff --git a/example/config.jsonc b/example/config.jsonc new file mode 100644 index 0000000..e8d17d5 --- /dev/null +++ b/example/config.jsonc @@ -0,0 +1,16 @@ +{ + // where the html files are located + "html_dir": "./html", + + // where the proxy should store its db (directory) + "data_dir": "./data", + + // what port the proxy should listen on + "port": 3001, + + // what address should the proxy pass after successful verification + "forward_address": "127.0.0.1:3000", + + // max request size of 10MB + "max_request_size": 10000000 +} \ No newline at end of file diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..315ce1c --- /dev/null +++ b/html/index.html @@ -0,0 +1,331 @@ + + + + STOP! - Checkpoint + + + + + + +
+

+ 🛑 +

+

+ STOP! +

+
+

+ Verifying that you are not a bot. This might take a short moment. + +
+
+ + You do not need to do anything. +

+ +

+ Why am I seeing this? +

+ +
+ +

+ This website protects itself from AI bots and scrapers + by asking you to complete a cryptographic challenge before allowing you entry. +

+ +
+
+
+
+ +

+ Difficulty {{ tl:text challengeDifficulty }}, elapsed 0ms, 0h, 0h/s +

+ + + Powered by checkpoint + +
+
+ + + + \ No newline at end of file diff --git a/html/index.min.html b/html/index.min.html new file mode 100644 index 0000000..e229b78 --- /dev/null +++ b/html/index.min.html @@ -0,0 +1,2 @@ +STOP! - Checkpoint

🛑

STOP!


Verifying that you are not a bot. This might take a short moment.

You do not need to do anything.

Why am I seeing this?


This website protects itself from AI bots and scrapers by asking you to complete a cryptographic challenge before allowing you entry.

Difficulty {{ tl:text challengeDifficulty }}, elapsed 0ms, 0h, 0h/s

Powered by checkpoint
+ \ No newline at end of file diff --git a/src/GlobalState.hpp b/src/GlobalState.hpp new file mode 100644 index 0000000..5a4cd1e --- /dev/null +++ b/src/GlobalState.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +struct SGlobalState { + std::string cwd; + std::string configPath; +}; + +inline std::unique_ptr g_pGlobalState = std::make_unique(); diff --git a/src/config/Config.cpp b/src/config/Config.cpp new file mode 100644 index 0000000..b843501 --- /dev/null +++ b/src/config/Config.cpp @@ -0,0 +1,22 @@ +#include "Config.hpp" + +#include + +#include "../GlobalState.hpp" + +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; +} + +CConfig::CConfig() { + auto json = glz::read_jsonc(readFileAsText(g_pGlobalState->cwd + "/" + g_pGlobalState->configPath)); + + if (!json.has_value()) + throw std::runtime_error("No config / bad config format"); + + m_config = json.value(); +} \ No newline at end of file diff --git a/src/config/Config.hpp b/src/config/Config.hpp new file mode 100644 index 0000000..d6eb8ef --- /dev/null +++ b/src/config/Config.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +class CConfig { + public: + CConfig(); + + 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 + } m_config; +}; + +inline std::unique_ptr g_pConfig; \ No newline at end of file diff --git a/src/core/Db.cpp b/src/core/Db.cpp new file mode 100644 index 0000000..145c6d0 --- /dev/null +++ b/src/core/Db.cpp @@ -0,0 +1,248 @@ +#include "Db.hpp" + +#include "../GlobalState.hpp" +#include "../debug/log.hpp" +#include "../config/Config.hpp" + +#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 + +// +static std::string dbPath() { + static const std::string path = std::filesystem::canonical(g_pGlobalState->cwd + "/" + g_pConfig->m_config.data_dir).string() + "/" + DB_FILE; + return path; +} + +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"); + + cleanupDb(); + return; + } + + Debug::log(LOG, "Database not present, creating one"); + + if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK) + throw std::runtime_error("failed to open sqlite3 db"); + + // create db layout + char* errmsg = nullptr; + + const char* CHALLENGE_TABLE = R"#( +CREATE TABLE challenges ( + nonce TEXT NOT NULL, + ip TEXT NOT NULL, + difficulty INTEGER NOT NULL, + epoch INTEGER NOT NULL, + CONSTRAINT PK PRIMARY KEY (nonce) +);)#"; + + sqlite3_exec(m_db, CHALLENGE_TABLE, [](void* data, int len, char** a, char** b) -> int { return 0; }, nullptr, &errmsg); + + const char* TOKENS_TABLE = R"#( +CREATE TABLE tokens ( + token TEXT NOT NULL, + ip TEXT NOT NULL, + epoch INTEGER NOT NULL, + CONSTRAINT PK PRIMARY KEY (token) +);)#"; + + sqlite3_exec(m_db, TOKENS_TABLE, [](void* data, int len, char** a, char** b) -> int { return 0; }, nullptr, &errmsg); +} + +CDatabase::~CDatabase() { + if (m_db) + sqlite3_close(m_db); +} + +void CDatabase::addChallenge(const SDatabaseChallengeEntry& entry) { + if (!isHashValid(entry.nonce)) + return; + + if (!isIpValid(entry.ip)) + return; + + const std::string CMD = fmt::format(R"#( +INSERT INTO challenges VALUES ( +"{}", "{}", {}, {} +);)#", + entry.nonce, entry.ip, entry.difficulty, entry.epoch); + + char* errmsg = nullptr; + sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); + + if (errmsg) + Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); +} + +std::optional CDatabase::getChallenge(const std::string& nonce) { + if (!isHashValid(nonce)) + return std::nullopt; + + const std::string CMD = fmt::format(R"#( +SELECT * FROM challenges WHERE nonce = "{}"; +)#", + nonce); + + char* errmsg = nullptr; + CDatabase::SQueryResult result; + + sqlite3_exec( + m_db, CMD.c_str(), + [](void* result, int len, char** a, char** b) -> int { + auto res = reinterpret_cast(result); + + for (size_t i = 0; i < len; ++i) { + res->result.push_back(a[i]); + res->result2.push_back(b[i]); + } + + return 0; + }, + &result, &errmsg); + + 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)}; +} + +void CDatabase::dropChallenge(const std::string& nonce) { + if (!isHashValid(nonce)) + return; + + const std::string CMD = fmt::format(R"#( +DELETE FROM challenges WHERE token = "{}" +)#", + nonce); + + char* errmsg = nullptr; + sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); + + if (errmsg) + Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); +} + +void CDatabase::addToken(const SDatabaseTokenEntry& entry) { + if (!isHashValid(entry.token)) + return; + + if (!isIpValid(entry.ip)) + return; + + const std::string CMD = fmt::format(R"#( +INSERT INTO tokens VALUES ( +"{}", "{}", {} +);)#", + entry.token, entry.ip, entry.epoch); + + char* errmsg = nullptr; + sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); + + if (errmsg) + Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); +} + +void CDatabase::dropToken(const std::string& token) { + if (!isHashValid(token)) + return; + + const std::string CMD = fmt::format(R"#( +DELETE FROM tokens WHERE token = "{}" +)#", + token); + + char* errmsg = nullptr; + sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); + + if (errmsg) + Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); +} + +std::optional CDatabase::getToken(const std::string& token) { + if (!isHashValid(token)) + return std::nullopt; + + if (shouldCleanupDb()) + cleanupDb(); + + const std::string CMD = fmt::format(R"#( +SELECT * FROM tokens WHERE token = "{}"; +)#", + token); + + char* errmsg = nullptr; + CDatabase::SQueryResult result; + + sqlite3_exec( + m_db, CMD.c_str(), + [](void* result, int len, char** a, char** b) -> int { + auto res = reinterpret_cast(result); + + for (size_t i = 0; i < len; ++i) { + res->result.push_back(a[i]); + res->result2.push_back(b[i]); + } + + return 0; + }, + &result, &errmsg); + + 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)}; +} + +bool CDatabase::shouldCleanupDb() { + const auto TIME = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + const auto LAST = std::chrono::duration_cast(m_lastDbCleanup.time_since_epoch()).count(); + + if (TIME - LAST > DB_TIME_BEFORE_CLEANUP_MS) + return true; + + return false; +} + +void CDatabase::cleanupDb() { + m_lastDbCleanup = std::chrono::steady_clock::now(); + + const auto TIME = std::chrono::milliseconds(std::time(nullptr)).count(); + + std::string CMD = fmt::format(R"#( +DELETE FROM tokens WHERE epoch < {}; +)#", + TIME - DB_TOKEN_LIFE_LENGTH_S); + + char* errmsg = nullptr; + sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); + + if (errmsg) + Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); + + CMD = fmt::format(R"#( +DELETE FROM challenges WHERE epoch < {}; +)#", + TIME - DB_CHALLENGE_LIFE_LENGTH_S); + + sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg); + + if (errmsg) + Debug::log(ERR, "sqlite3 error: tried to persist:\n{}\nGot: {}", CMD, errmsg); +} diff --git a/src/core/Db.hpp b/src/core/Db.hpp new file mode 100644 index 0000000..47badce --- /dev/null +++ b/src/core/Db.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#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 = ""; +}; + +struct SDatabaseTokenEntry { + std::string token = ""; + unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); + std::string ip = ""; +}; + +class CDatabase { + public: + CDatabase(); + ~CDatabase(); + + void addChallenge(const SDatabaseChallengeEntry& entry); + std::optional getChallenge(const std::string& nonce); + void dropChallenge(const std::string& nonce); + + void addToken(const SDatabaseTokenEntry& entry); + std::optional getToken(const std::string& token); + void dropToken(const std::string& token); + + private: + struct SQueryResult { + bool failed = false; + std::string error = ""; + std::vector result; + std::vector result2; + }; + + sqlite3* m_db = nullptr; + std::chrono::steady_clock::time_point m_lastDbCleanup = std::chrono::steady_clock::now(); + + void cleanupDb(); + bool shouldCleanupDb(); +}; + +inline std::unique_ptr g_pDB; \ No newline at end of file diff --git a/src/core/Handler.cpp b/src/core/Handler.cpp new file mode 100644 index 0000000..797a67e --- /dev/null +++ b/src/core/Handler.cpp @@ -0,0 +1,330 @@ +#include "Handler.hpp" +#include "../headers/authorization.hpp" +#include "../headers/cfHeader.hpp" +#include "../headers/xforwardfor.hpp" +#include "../debug/log.hpp" +#include "../GlobalState.hpp" +#include "../config/Config.hpp" +#include "Db.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +constexpr const uint64_t TOKEN_MAX_AGE_MS = 1000 * 60 * 60; // 1hr + +// +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 generateNonce() { + static std::random_device dev; + std::mt19937 engine(dev()); + std::uniform_int_distribution<> distribution(0, INT32_MAX); + + std::stringstream ss; + for (size_t i = 0; i < 32; ++i) { + ss << fmt::format("{:08x}", distribution(engine)); + } + + return ss.str(); +} + +static std::string generateToken() { + static std::random_device dev; + std::mt19937 engine(dev()); + std::uniform_int_distribution<> distribution(0, INT32_MAX); + + std::stringstream ss; + for (size_t i = 0; i < 16; ++i) { + ss << fmt::format("{:08x}", distribution(engine)); + } + + return ss.str(); +} + +static std::string sha256(const std::string& string) { + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + if (!ctx) + return ""; + + if (!EVP_DigestInit(ctx, EVP_sha256())) { + EVP_MD_CTX_free(ctx); + return ""; + } + + if (!EVP_DigestUpdate(ctx, string.c_str(), string.size())) { + EVP_MD_CTX_free(ctx); + return ""; + } + + uint8_t buf[32]; + + if (!EVP_DigestFinal(ctx, buf, nullptr)) { + EVP_MD_CTX_free(ctx); + return ""; + } + + std::stringstream ss; + for (size_t i = 0; i < 32; ++i) { + ss << fmt::format("{:02x}", buf[i]); + } + + return ss.str(); +} + +void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter response) { + const auto HEADERS = req.headers(); + std::shared_ptr hostHeader; + std::shared_ptr contentTypeHeader; + std::shared_ptr cfHeader; + std::shared_ptr xForwardedForHeader; + std::shared_ptr authHeader; + + try { + hostHeader = Pistache::Http::Header::header_cast(HEADERS.get("Host")); + } catch (std::exception& e) { + Debug::log(ERR, "Request has no Host header?"); + response.send(Pistache::Http::Code::Bad_Request, "Bad Request"); + return; + } + + try { + cfHeader = Pistache::Http::Header::header_cast(HEADERS.get("cf-connecting-ip")); + } catch (std::exception& e) { + ; // silent ignore + } + + try { + xForwardedForHeader = Pistache::Http::Header::header_cast(HEADERS.get("X-Forwarded-For")); + } catch (std::exception& e) { + ; // silent ignore + } + + try { + authHeader = Pistache::Http::Header::header_cast(HEADERS.get("Authorization")); + } catch (std::exception& e) { + ; // silent ignore + } + + try { + contentTypeHeader = Pistache::Http::Header::header_cast(HEADERS.get("Content-Type")); + } catch (std::exception& e) { + ; // silent ignore + } + + Debug::log(LOG, "Got request for: {}:{}{}", hostHeader->host(), hostHeader->port().toString(), req.resource()); + Debug::log(LOG, "Request author: IP {}", req.address().host()); + if (cfHeader) + Debug::log(LOG, "CloudFlare reports IP: {}", cfHeader->ip()); + else + Debug::log(WARN, "Connection does not come through CloudFlare"); + + if (req.resource() == "/checkpoint/challenge") { + if (req.method() == Pistache::Http::Method::Post) + challengeSubmitted(req, response); + else + response.send(Pistache::Http::Code::Bad_Request, "Bad Request"); + return; + } + + if (req.cookies().has("CheckpointToken")) { + // check the token + 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())) { + proxyPass(req, response); + return; + } else // token has been used from a different IP or is expired. Nuke it. + g_pDB->dropToken(TOKEN->token); + } + } + + serveStop(req, response); +} + +void CServerHandler::onTimeout(const Pistache::Http::Request& request, Pistache::Http::ResponseWriter response) { + response.send(Pistache::Http::Code::Request_Timeout, "Timeout").then([=](ssize_t) {}, PrintException()); +} + +void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) { + const auto JSON = req.body(); + + std::shared_ptr cfHeader; + try { + cfHeader = Pistache::Http::Header::header_cast(req.headers().get("cf-connecting-ip")); + } catch (std::exception& e) { + ; // silent ignore + } + + auto json = glz::read_json(JSON); + STokenResponse resp; + + if (!json) { + resp.error = "bad input"; + response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); + return; + } + + auto val = json.value(); + + const auto CHALLENGE = g_pDB->getChallenge(val.challenge); + + if (!CHALLENGE.has_value()) { + resp.error = "bad challenge"; + response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); + return; + } + + if (CHALLENGE->ip != req.address().host()) { + resp.error = "bad challenge"; + response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); + return; + } + + // drop challenge already. + g_pDB->dropChallenge(val.challenge); + + // verify challenge + const auto SHA = sha256(val.challenge + std::to_string(val.solution)); + + for (int i = 0; i < CHALLENGE->difficulty; ++i) { + if (SHA.at(i) != '0') { + resp.error = "bad solution"; + response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value()); + return; + } + } + + // correct solution, return a token + + const auto TOKEN = generateToken(); + + g_pDB->addToken(SDatabaseTokenEntry{.token = TOKEN, .ip = (cfHeader ? cfHeader->ip() : req.address().host())}); + + resp.success = true; + resp.token = TOKEN; + + response.send(Pistache::Http::Code::Ok, glz::write_json(resp).value()); +} + +void CServerHandler::serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) { + static const auto PATH = std::filesystem::canonical(g_pGlobalState->cwd + "/" + g_pConfig->m_config.html_dir).string(); + /* static */ const auto PAGE_INDEX = readFileAsText(PATH + "/index.min.html"); + CTinylates page(PAGE_INDEX); + page.setTemplateRoot(PATH); + + const auto NONCE = generateNonce(); + const auto DIFFICULTY = 4; + + g_pDB->addChallenge(SDatabaseChallengeEntry{.nonce = NONCE, .difficulty = DIFFICULTY, .ip = req.address().host()}); + + page.add("challengeDifficulty", CTinylatesProp(std::to_string(DIFFICULTY))); + page.add("challengeNonce", CTinylatesProp(NONCE)); + response.send(Pistache::Http::Code::Ok, page.render().value_or("error")); +} + +void CServerHandler::proxyPass(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) { + Pistache::Http::Experimental::Client client; + client.init(Pistache::Http::Experimental::Client::options().threads(1).maxConnectionsPerHost(8)); + const std::string FORWARD_ADDR = g_pConfig->m_config.forward_address; + + switch (req.method()) { + // there are some crazy semantics going on here, idk how to make this cleaner with less c+p + + case Pistache::Http::Method::Get: { + Debug::log(LOG, "Get: Forwarding to {}", FORWARD_ADDR + req.resource()); + auto builder = client.get(FORWARD_ADDR + req.resource()).body(req.body()); + for (auto it = req.cookies().begin(); it != req.cookies().end(); ++it) { + builder.cookie(*it); + } + builder.timeout(std::chrono::milliseconds(10000)); + + auto resp = builder.send(); + resp.then([&](Pistache::Http::Response resp) { response.send(Pistache::Http::Code::Ok, resp.body()); }, + [&](std::exception_ptr e) { response.send(Pistache::Http::Code::Internal_Server_Error, "Internal Proxy Error"); }); + Pistache::Async::Barrier b(resp); + b.wait_for(std::chrono::seconds(10)); + break; + } + case Pistache::Http::Method::Post: { + Debug::log(LOG, "Post: Forwarding to {}", FORWARD_ADDR + req.resource()); + auto builder = client.post(FORWARD_ADDR + req.resource()).body(req.body()); + for (auto it = req.cookies().begin(); it != req.cookies().end(); ++it) { + builder.cookie(*it); + } + builder.timeout(std::chrono::milliseconds(10000)); + + auto resp = builder.send(); + resp.then([&](Pistache::Http::Response resp) { response.send(Pistache::Http::Code::Ok, resp.body()); }, + [&](std::exception_ptr e) { response.send(Pistache::Http::Code::Internal_Server_Error, "Internal Proxy Error"); }); + Pistache::Async::Barrier b(resp); + b.wait_for(std::chrono::seconds(10)); + break; + } + case Pistache::Http::Method::Put: { + Debug::log(LOG, "Put: Forwarding to {}", FORWARD_ADDR + req.resource()); + auto builder = client.put(FORWARD_ADDR + req.resource()).body(req.body()); + for (auto it = req.cookies().begin(); it != req.cookies().end(); ++it) { + builder.cookie(*it); + } + builder.timeout(std::chrono::milliseconds(10000)); + + auto resp = builder.send(); + resp.then([&](Pistache::Http::Response resp) { response.send(Pistache::Http::Code::Ok, resp.body()); }, + [&](std::exception_ptr e) { response.send(Pistache::Http::Code::Internal_Server_Error, "Internal Proxy Error"); }); + Pistache::Async::Barrier b(resp); + b.wait_for(std::chrono::seconds(10)); + break; + } + case Pistache::Http::Method::Delete: { + Debug::log(LOG, "Delete: Forwarding to {}", FORWARD_ADDR + req.resource()); + auto builder = client.del(FORWARD_ADDR + req.resource()).body(req.body()); + for (auto it = req.cookies().begin(); it != req.cookies().end(); ++it) { + builder.cookie(*it); + } + builder.timeout(std::chrono::milliseconds(10000)); + + auto resp = builder.send(); + resp.then([&](Pistache::Http::Response resp) { response.send(Pistache::Http::Code::Ok, resp.body()); }, + [&](std::exception_ptr e) { response.send(Pistache::Http::Code::Internal_Server_Error, "Internal Proxy Error"); }); + Pistache::Async::Barrier b(resp); + b.wait_for(std::chrono::seconds(10)); + break; + } + case Pistache::Http::Method::Patch: { + Debug::log(LOG, "Patch: Forwarding to {}", FORWARD_ADDR + req.resource()); + auto builder = client.patch(FORWARD_ADDR + req.resource()).body(req.body()); + for (auto it = req.cookies().begin(); it != req.cookies().end(); ++it) { + builder.cookie(*it); + } + builder.timeout(std::chrono::milliseconds(10000)); + + auto resp = builder.send(); + resp.then([&](Pistache::Http::Response resp) { response.send(Pistache::Http::Code::Ok, resp.body()); }, + [&](std::exception_ptr e) { response.send(Pistache::Http::Code::Internal_Server_Error, "Internal Proxy Error"); }); + Pistache::Async::Barrier b(resp); + b.wait_for(std::chrono::seconds(10)); + break; + } + + default: { + response.send(Pistache::Http::Code::Internal_Server_Error, "Invalid request type for proxy"); + } + } + + client.shutdown(); +} \ No newline at end of file diff --git a/src/core/Handler.hpp b/src/core/Handler.hpp new file mode 100644 index 0000000..6c438f3 --- /dev/null +++ b/src/core/Handler.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +class CServerHandler : public Pistache::Http::Handler { + + HTTP_PROTOTYPE(CServerHandler) + + void onRequest(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter response); + + 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); + + struct SChallengeResponse { + std::string challenge; + unsigned long int solution = 0; + }; + + struct STokenResponse { + bool success = false; + std::string token = ""; + std::string error = ""; + }; +}; \ No newline at end of file diff --git a/src/debug/log.hpp b/src/debug/log.hpp new file mode 100644 index 0000000..ef3ed70 --- /dev/null +++ b/src/debug/log.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include + +enum LogLevel { + NONE = -1, + LOG = 0, + WARN, + ERR, + CRIT, + INFO, + TRACE +}; + +namespace Debug { + template + void log(LogLevel level, fmt::format_string fmt, Args&&... args) { + + std::string logMsg = ""; + + switch (level) { + case LOG: logMsg += "[LOG] "; break; + case WARN: logMsg += "[WARN] "; break; + case ERR: logMsg += "[ERR] "; break; + case CRIT: logMsg += "[CRITICAL] "; break; + case INFO: logMsg += "[INFO] "; break; + case TRACE: logMsg += "[TRACE] "; break; + default: break; + } + + logMsg += fmt::vformat(fmt.get(), fmt::make_format_args(args...)); + + std::cout << logMsg << "\n"; + } +}; diff --git a/src/headers/authorization.hpp b/src/headers/authorization.hpp new file mode 100644 index 0000000..2414bbf --- /dev/null +++ b/src/headers/authorization.hpp @@ -0,0 +1,24 @@ +#include +#include + +class AuthorizationHeader : public Pistache::Http::Header::Header { + public: + NAME("Authorization"); + + AuthorizationHeader() = default; + + void parse(const std::string& str) override { + m_token = str; + } + + void write(std::ostream& os) const override { + os << m_token; + } + + std::string token() const { + return m_token; + } + + private: + std::string m_token = ""; +}; \ No newline at end of file diff --git a/src/headers/cfHeader.hpp b/src/headers/cfHeader.hpp new file mode 100644 index 0000000..0715616 --- /dev/null +++ b/src/headers/cfHeader.hpp @@ -0,0 +1,24 @@ +#include +#include + +class CFConnectingIPHeader : public Pistache::Http::Header::Header { + public: + NAME("cf-connecting-ip"); + + CFConnectingIPHeader() = default; + + void parse(const std::string& str) override { + m_ip = str; + } + + void write(std::ostream& os) const override { + os << m_ip; + } + + std::string ip() const { + return m_ip; + } + + private: + std::string m_ip = ""; +}; \ No newline at end of file diff --git a/src/headers/xforwardfor.hpp b/src/headers/xforwardfor.hpp new file mode 100644 index 0000000..3cc7e43 --- /dev/null +++ b/src/headers/xforwardfor.hpp @@ -0,0 +1,24 @@ +#include +#include + +class XForwardedForHeader : public Pistache::Http::Header::Header { + public: + NAME("X-Forwarded-For"); + + XForwardedForHeader() = default; + + void parse(const std::string& str) override { + m_for = str; + } + + void write(std::ostream& os) const override { + os << m_for; + } + + std::string from() const { + return m_for; + } + + private: + std::string m_for = ""; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..32bfc09 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "headers/authorization.hpp" +#include "headers/xforwardfor.hpp" +#include "headers/cfHeader.hpp" + +#include "debug/log.hpp" + +#include "core/Handler.hpp" +#include "core/Db.hpp" + +#include "config/Config.hpp" + +#include "GlobalState.hpp" + +#include + +int main(int argc, char** argv, char** envp) { + + if (argc < 2) { + Debug::log(CRIT, "Missing param for websites storage"); + return 1; + } + + std::vector ARGS{}; + ARGS.resize(argc); + for (int i = 0; i < argc; ++i) { + ARGS[i] = std::string{argv[i]}; + } + + std::vector command; + + g_pGlobalState->cwd = std::filesystem::current_path(); + + for (int i = 1; i < argc; ++i) { + if (ARGS[i].starts_with("-")) { + if (ARGS[i] == "--help" || ARGS[i] == "-h") { + std::cout << "-h [html_root] -p [port]\n"; + return 0; + } else if ((ARGS[i] == "--config" || ARGS[i] == "-c") && i + 1 < argc) { + g_pGlobalState->configPath = ARGS[i + 1]; + i++; + } else { + std::cerr << "Unrecognized / invalid use of option " << ARGS[i] << "\nContinuing...\n"; + continue; + } + } else + command.push_back(ARGS[i]); + } + + g_pConfig = std::make_unique(); + + if (g_pConfig->m_config.html_dir.empty() || g_pConfig->m_config.data_dir.empty()) + return 1; + + sigset_t signals; + if (sigemptyset(&signals) != 0 || sigaddset(&signals, SIGTERM) != 0 || sigaddset(&signals, SIGINT) != 0 || sigaddset(&signals, SIGQUIT) != 0 || + sigaddset(&signals, SIGPIPE) != 0 || sigaddset(&signals, SIGALRM) != 0 || sigprocmask(SIG_BLOCK, &signals, nullptr) != 0) + return 1; + + int threads = 1; + Pistache::Address address = {Pistache::Ipv4::any(), (uint16_t)g_pConfig->m_config.port}; + Debug::log(LOG, "Starting the server on {}:{}\n", address.host(), address.port().toString()); + + Pistache::Http::Header::Registry::instance().registerHeader(); + Pistache::Http::Header::Registry::instance().registerHeader(); + + g_pDB = std::make_unique(); + + auto endpoint = std::make_unique(address); + auto opts = Pistache::Http::Endpoint::options().threads(threads).flags(Pistache::Tcp::Options::ReuseAddr | Pistache::Tcp::Options::ReusePort); + opts.maxRequestSize(g_pConfig->m_config.max_request_size); // 150MB TODO: configurable + endpoint->init(opts); + auto handler = Pistache::Http::make_handler(); + endpoint->setHandler(handler); + + endpoint->serveThreaded(); + + bool terminate = false; + while (!terminate) { + int number = 0; + int status = sigwait(&signals, &number); + if (status != 0) { + Debug::log(CRIT, "sigwait threw {} :(", status); + break; + } + + Debug::log(LOG, "Caught signal {}", number); + + switch (number) { + case SIGINT: terminate = true; break; + case SIGTERM: terminate = true; break; + case SIGQUIT: terminate = true; break; + case SIGPIPE: break; + case SIGALRM: break; + } + } + + sigprocmask(SIG_UNBLOCK, &signals, nullptr); + + Debug::log(LOG, "Shutting down, bye!"); + + endpoint->shutdown(); + endpoint = nullptr; + + return 0; +} \ No newline at end of file diff --git a/subprojects/fmt b/subprojects/fmt new file mode 160000 index 0000000..64db979 --- /dev/null +++ b/subprojects/fmt @@ -0,0 +1 @@ +Subproject commit 64db979e38ec644b1798e41610b28c8d2c8a2739 diff --git a/subprojects/glaze b/subprojects/glaze new file mode 160000 index 0000000..8fd8d00 --- /dev/null +++ b/subprojects/glaze @@ -0,0 +1 @@ +Subproject commit 8fd8d001dfcfbf04f4ccfaa6367e4155eb0ed8ac diff --git a/subprojects/pistache b/subprojects/pistache new file mode 160000 index 0000000..31ef837 --- /dev/null +++ b/subprojects/pistache @@ -0,0 +1 @@ +Subproject commit 31ef83778e075939d13f48ba7d2de805cec5d246 diff --git a/subprojects/tinylates b/subprojects/tinylates new file mode 160000 index 0000000..d91590f --- /dev/null +++ b/subprojects/tinylates @@ -0,0 +1 @@ +Subproject commit d91590f4ebee81ad8017cb6f18ed6b14d822c621