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.
+
+
+
+
+
+
+
+
\ 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.
+
\ 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