code: Initial Commit
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -30,3 +30,10 @@
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
.cache/
|
||||
.vscode
|
||||
build/
|
||||
data/
|
||||
|
||||
config.json
|
||||
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal file
@@ -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
|
||||
39
CMakeLists.txt
Normal file
39
CMakeLists.txt
Normal file
@@ -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
|
||||
)
|
||||
7
config.jsonc
Normal file
7
config.jsonc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"html_dir": "./html",
|
||||
"data_dir": "./data",
|
||||
"port": 3001,
|
||||
"forward_address": "127.0.0.1:3000",
|
||||
"max_request_size": 10000000
|
||||
}
|
||||
16
example/config.jsonc
Normal file
16
example/config.jsonc
Normal file
@@ -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
|
||||
}
|
||||
331
html/index.html
Normal file
331
html/index.html
Normal file
@@ -0,0 +1,331 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>STOP! - Checkpoint</title>
|
||||
</head>
|
||||
|
||||
<body style="background-color: #0e0e0e; overflow: hidden;">
|
||||
|
||||
<style>
|
||||
.middle-box {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 30rem;
|
||||
height: calc(100% - 2rem);
|
||||
left: calc(50% - 15rem);
|
||||
top: 1rem;
|
||||
background-color: #111111;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #280d0e;
|
||||
}
|
||||
|
||||
.big-icon {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 8rem;
|
||||
color: white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtext {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 3rem;
|
||||
color: #d9d9d9;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-hr {
|
||||
margin: auto;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0;
|
||||
background-color: #444444;
|
||||
width: 50%;
|
||||
box-shadow: none;
|
||||
text-align: center;
|
||||
height: 1px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.text-hr-small {
|
||||
margin: auto;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0;
|
||||
background-color: #444444;
|
||||
width: 25%;
|
||||
box-shadow: none;
|
||||
text-align: center;
|
||||
height: 1px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.text-description {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
color: #d9d9d9;
|
||||
width: 85%;
|
||||
margin-left: 7.5%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-question {
|
||||
margin: 0;
|
||||
margin-top: 4rem;
|
||||
padding: 0;
|
||||
font-size: 1.4rem;
|
||||
color: #d9d9d9;
|
||||
width: 85%;
|
||||
margin-left: 7.5%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
bottom: 0.4rem;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.bottom-hash-text {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
color: #d9d9d9;
|
||||
width: 85%;
|
||||
margin-left: 7.5%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bottom-credit-text {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.5rem;
|
||||
color: #4b4b4b;
|
||||
width: 98%;
|
||||
text-align: right;
|
||||
transition: ease-in-out 0.1s;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.bottom-credit-text:hover,
|
||||
.bottom-credit-text:link:hover,
|
||||
.bottom-credit-text:visited:hover {
|
||||
color: #764343;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bottom-credit-text:link,
|
||||
.bottom-credit-text:visited {
|
||||
color: #4b4b4b;
|
||||
}
|
||||
|
||||
.bottom-progress {
|
||||
position: relative;
|
||||
height: 3px;
|
||||
background-color: #222222;
|
||||
width: 80%;
|
||||
margin-left: 10%;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.bottom-progress-light {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0%;
|
||||
height: 3px;
|
||||
background-color: #692225;
|
||||
transition: ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
@media (pointer:none), (pointer:coarse) {
|
||||
.big-icon {
|
||||
margin-top: 10rem;
|
||||
}
|
||||
|
||||
.middle-box {
|
||||
width: 90%;
|
||||
left: 5%;
|
||||
}
|
||||
|
||||
.subtext {
|
||||
font-size: 6rem;
|
||||
}
|
||||
|
||||
.text-description {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.text-hr {
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.bottom-hash-text {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.bottom-credit-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="middle-box">
|
||||
<p class="big-icon">
|
||||
🛑 <!-- 🛑 -->
|
||||
</p>
|
||||
<p class="subtext" id="subtext">
|
||||
STOP!
|
||||
</p>
|
||||
<hr class="text-hr" />
|
||||
<p class="text-description">
|
||||
Verifying that you are not a bot. This might take a short moment.
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
You do not need to do anything.
|
||||
</p>
|
||||
|
||||
<p class="text-question">
|
||||
Why am I seeing this?
|
||||
</p>
|
||||
|
||||
<hr class="text-hr-small" />
|
||||
|
||||
<p class="text-description">
|
||||
This website protects itself from AI bots and scrapers
|
||||
by asking you to complete a cryptographic challenge before allowing you entry.
|
||||
</p>
|
||||
|
||||
<div class="bottom-bar">
|
||||
<div class="bottom-progress">
|
||||
<div class="bottom-progress-light" name="progress-light"></div>
|
||||
</div>
|
||||
|
||||
<p class="bottom-hash-text" id="results">
|
||||
Difficulty {{ tl:text challengeDifficulty }}, elapsed 0ms, 0h, 0h/s
|
||||
</p>
|
||||
|
||||
<a class="bottom-credit-text">
|
||||
<i>Powered by checkpoint</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
setTimeout(
|
||||
async function () {
|
||||
var it = 0;
|
||||
var start = Date.now();
|
||||
const challengeNonce = "{{ tl:text challengeNonce }}";
|
||||
const difficulty = parseInt("{{ tl:text challengeDifficulty }}");
|
||||
|
||||
function valid(sha) {
|
||||
const MIN_ZEROES = difficulty;
|
||||
for (let i = 0; i < MIN_ZEROES; i += 1) {
|
||||
if (sha[i] != '0')
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function itShort(it) {
|
||||
if (it > 1000000)
|
||||
return parseInt(it / 100000) / 10 + "M";
|
||||
|
||||
return parseInt(it / 100) / 10 + "k";
|
||||
}
|
||||
|
||||
function timeShort(ms) {
|
||||
if (ms > 1000)
|
||||
return parseInt(ms / 100) / 10 + "s";
|
||||
return ms + "ms";
|
||||
}
|
||||
|
||||
function getCompletionPercent(it) {
|
||||
const exp = Math.pow(2, 4 * difficulty);
|
||||
return Math.floor((1 - 1 / (Math.pow(it / exp * 3, 2) + 1)) * 100);
|
||||
}
|
||||
|
||||
function success(it) {
|
||||
document.getElementById("results").innerHTML = "Success! Completed challenge after " + itShort(it) + " iterations, in " + timeShort(Math.floor(Date.now() - start)) + ".";
|
||||
document.getElementsByName("progress-light")[0].style.width = "100%";
|
||||
|
||||
const data = JSON.stringify({
|
||||
challenge: challengeNonce,
|
||||
solution: it,
|
||||
});
|
||||
|
||||
fetch("/checkpoint/challenge",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: data
|
||||
}
|
||||
).then((res) => {
|
||||
if (res.status == 200) {
|
||||
console.log("Got token.");
|
||||
const json = res.json().then((parsed) => {
|
||||
document.cookie = "CheckpointToken=" + parsed.token + "; path=/";
|
||||
window.location.reload();
|
||||
});
|
||||
} else
|
||||
console.log("Server error");
|
||||
});
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
while (true) {
|
||||
const data = encoder.encode(challengeNonce + it);
|
||||
const buffer = await window.crypto.subtle.digest("SHA-256", data);
|
||||
const hashArray = Array.from(new Uint8Array(buffer));
|
||||
const sha = hashArray
|
||||
.map((item) => item.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
if (valid(sha)) {
|
||||
success(it);
|
||||
console.log("Success: it " + it + ": " + sha);
|
||||
break;
|
||||
}
|
||||
|
||||
it++;
|
||||
|
||||
if (it % 11377 == 0) {
|
||||
let ms = Math.floor(Date.now() - start);
|
||||
let hpers = it / (ms / 1000);
|
||||
document.getElementById("results").innerHTML = "Difficulty: " + difficulty + ", elapsed " + timeShort(ms) + ", " + itShort(it) + "h, " + itShort(hpers) + "h/s";
|
||||
document.getElementsByName("progress-light")[0].style.width = getCompletionPercent(it) + "%";
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
|
||||
var currentTitle = 1;
|
||||
|
||||
setInterval(
|
||||
() => {
|
||||
const titles = ["STOP", "HALT", "STÓJ", "ARRÊT", "СТІЙ"];
|
||||
|
||||
document.getElementById("subtext").innerHTML = titles[currentTitle] + "!";
|
||||
|
||||
currentTitle++;
|
||||
if (currentTitle >= titles.length)
|
||||
currentTitle = 0;
|
||||
}, 2000
|
||||
)
|
||||
</script>
|
||||
|
||||
</body>
|
||||
2
html/index.min.html
Normal file
2
html/index.min.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html><head><title>STOP! - Checkpoint</title></head><body style="background-color:#0e0e0e;overflow:hidden"><style>.middle-box{display:block;position:absolute;width:30rem;height:calc(100% - 2rem);left:calc(50% - 15rem);top:1rem;background-color:#111;border-radius:.5rem;border:1px solid #280d0e}.big-icon{margin:0;padding:0;font-size:8rem;color:#fff;width:100%;text-align:center}.subtext{margin:0;padding:0;font-size:3rem;color:#d9d9d9;width:100%;text-align:center}.text-hr{margin:auto;margin-top:1rem;margin-bottom:1rem;padding:0;background-color:#444;width:50%;box-shadow:none;text-align:center;height:1px;border:none}.text-hr-small{margin:auto;margin-top:1rem;margin-bottom:1rem;padding:0;background-color:#444;width:25%;box-shadow:none;text-align:center;height:1px;border:none}.text-description{margin:0;padding:0;font-size:1rem;color:#d9d9d9;width:85%;margin-left:7.5%;text-align:center}.text-question{margin:0;margin-top:4rem;padding:0;font-size:1.4rem;color:#d9d9d9;width:85%;margin-left:7.5%;text-align:center}.bottom-bar{position:absolute;width:100%;height:auto;bottom:.4rem;left:0}.bottom-hash-text{margin:0;padding:0;font-size:1rem;color:#d9d9d9;width:85%;margin-left:7.5%;text-align:center}.bottom-credit-text{display:block;margin:0;padding:0;font-size:.5rem;color:#4b4b4b;width:98%;text-align:right;transition:ease-in-out .1s;margin-top:.2rem}.bottom-credit-text:hover,.bottom-credit-text:link:hover,.bottom-credit-text:visited:hover{color:#764343;cursor:pointer}.bottom-credit-text:link,.bottom-credit-text:visited{color:#4b4b4b}.bottom-progress{position:relative;height:3px;background-color:#222;width:80%;margin-left:10%;margin-bottom:.8rem}.bottom-progress-light{position:absolute;left:0;top:0;width:0%;height:3px;background-color:#692225;transition:ease-in-out .1s}@media (pointer:none),(pointer:coarse){.big-icon{margin-top:10rem}.middle-box{width:90%;left:5%}.subtext{font-size:6rem}.text-description{font-size:3rem}.text-hr{height:3px}.bottom-hash-text{font-size:2.5rem}.bottom-credit-text{font-size:1rem}}</style><div class="middle-box"><p class="big-icon">🛑</p><p class="subtext" id="subtext">STOP!</p><hr class="text-hr"><p class="text-description">Verifying that you are not a bot. This might take a short moment.<br><br>You do not need to do anything.</p><p class="text-question">Why am I seeing this?</p><hr class="text-hr-small"><p class="text-description">This website protects itself from AI bots and scrapers by asking you to complete a cryptographic challenge before allowing you entry.</p><div class="bottom-bar"><div class="bottom-progress"><div class="bottom-progress-light" name="progress-light"></div></div><p class="bottom-hash-text" id="results">Difficulty {{ tl:text challengeDifficulty }}, elapsed 0ms, 0h, 0h/s</p><a class="bottom-credit-text"><i>Powered by checkpoint</i></a></div></div>
|
||||
<script type="text/javascript">setTimeout(async function(){var e=0,t=Date.now();let n="{{ tl:text challengeNonce }}",o=parseInt("{{ tl:text challengeDifficulty }}");function $(e){let t=o;for(let n=0;n<t;n+=1)if("0"!=e[n])return!1;return!0}function l(e){return e>1e6?parseInt(e/1e5)/10+"M":parseInt(e/100)/10+"k"}function r(e){return e>1e3?parseInt(e/100)/10+"s":e+"ms"}function i(e){return Math.floor((1-1/(Math.pow(e/Math.pow(2,4*o)*3,2)+1))*100)}function s(e){document.getElementById("results").innerHTML="Success! Completed challenge after "+l(e)+" iterations, in "+r(Math.floor(Date.now()-t))+".",document.getElementsByName("progress-light")[0].style.width="100%";let o=JSON.stringify({challenge:n,solution:e});fetch("/checkpoint/challenge",{headers:{"Content-Type":"application/json"},method:"POST",body:o}).then(e=>{200==e.status?(console.log("Got token."),e.json().then(e=>{document.cookie="CheckpointToken="+e.token+"; path=/",window.location.reload()})):console.log("Server error")})}let c=new TextEncoder;for(;;){let _=c.encode(n+e),a=await window.crypto.subtle.digest("SHA-256",_),u=Array.from(new Uint8Array(a)),f=u.map(e=>e.toString(16).padStart(2,"0")).join("");if($(f)){s(e),console.log("Success: it "+e+": "+f);break}if(++e%11377==0){let g=Math.floor(Date.now()-t),h=e/(g/1e3);document.getElementById("results").innerHTML="Difficulty: "+o+", elapsed "+r(g)+", "+l(e)+"h, "+l(h)+"h/s",document.getElementsByName("progress-light")[0].style.width=i(e)+"%"}}},100);var currentTitle=1;setInterval(()=>{let e=["STOP","HALT","STÓJ","ARRÊT","СТІЙ"];document.getElementById("subtext").innerHTML=e[currentTitle]+"!",++currentTitle>=e.length&&(currentTitle=0)},2e3);</script></body>
|
||||
11
src/GlobalState.hpp
Normal file
11
src/GlobalState.hpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
struct SGlobalState {
|
||||
std::string cwd;
|
||||
std::string configPath;
|
||||
};
|
||||
|
||||
inline std::unique_ptr<SGlobalState> g_pGlobalState = std::make_unique<SGlobalState>();
|
||||
22
src/config/Config.cpp
Normal file
22
src/config/Config.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "Config.hpp"
|
||||
|
||||
#include <glaze/glaze.hpp>
|
||||
|
||||
#include "../GlobalState.hpp"
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
CConfig::CConfig() {
|
||||
auto json = glz::read_jsonc<SConfig>(readFileAsText(g_pGlobalState->cwd + "/" + g_pGlobalState->configPath));
|
||||
|
||||
if (!json.has_value())
|
||||
throw std::runtime_error("No config / bad config format");
|
||||
|
||||
m_config = json.value();
|
||||
}
|
||||
19
src/config/Config.hpp
Normal file
19
src/config/Config.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
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<CConfig> g_pConfig;
|
||||
248
src/core/Db.cpp
Normal file
248
src/core/Db.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#include "Db.hpp"
|
||||
|
||||
#include "../GlobalState.hpp"
|
||||
#include "../debug/log.hpp"
|
||||
#include "../config/Config.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
#include <algorithm>
|
||||
|
||||
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<SDatabaseChallengeEntry> 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<CDatabase::SQueryResult*>(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<SDatabaseTokenEntry> 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<CDatabase::SQueryResult*>(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::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
const auto LAST = std::chrono::duration_cast<std::chrono::milliseconds>(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);
|
||||
}
|
||||
50
src/core/Db.hpp
Normal file
50
src/core/Db.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
|
||||
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<SDatabaseChallengeEntry> getChallenge(const std::string& nonce);
|
||||
void dropChallenge(const std::string& nonce);
|
||||
|
||||
void addToken(const SDatabaseTokenEntry& entry);
|
||||
std::optional<SDatabaseTokenEntry> getToken(const std::string& token);
|
||||
void dropToken(const std::string& token);
|
||||
|
||||
private:
|
||||
struct SQueryResult {
|
||||
bool failed = false;
|
||||
std::string error = "";
|
||||
std::vector<std::string> result;
|
||||
std::vector<std::string> 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<CDatabase> g_pDB;
|
||||
330
src/core/Handler.cpp
Normal file
330
src/core/Handler.cpp
Normal file
@@ -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 <fstream>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
|
||||
#include <pistache/client.h>
|
||||
#include <tinylates/tinylates.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <glaze/glaze.hpp>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
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<char>(ifs)), (std::istreambuf_iterator<char>()));
|
||||
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<const Pistache::Http::Header::Host> hostHeader;
|
||||
std::shared_ptr<const Pistache::Http::Header::ContentType> contentTypeHeader;
|
||||
std::shared_ptr<const CFConnectingIPHeader> cfHeader;
|
||||
std::shared_ptr<const XForwardedForHeader> xForwardedForHeader;
|
||||
std::shared_ptr<const AuthorizationHeader> authHeader;
|
||||
|
||||
try {
|
||||
hostHeader = Pistache::Http::Header::header_cast<Pistache::Http::Header::Host>(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<CFConnectingIPHeader>(HEADERS.get("cf-connecting-ip"));
|
||||
} catch (std::exception& e) {
|
||||
; // silent ignore
|
||||
}
|
||||
|
||||
try {
|
||||
xForwardedForHeader = Pistache::Http::Header::header_cast<XForwardedForHeader>(HEADERS.get("X-Forwarded-For"));
|
||||
} catch (std::exception& e) {
|
||||
; // silent ignore
|
||||
}
|
||||
|
||||
try {
|
||||
authHeader = Pistache::Http::Header::header_cast<AuthorizationHeader>(HEADERS.get("Authorization"));
|
||||
} catch (std::exception& e) {
|
||||
; // silent ignore
|
||||
}
|
||||
|
||||
try {
|
||||
contentTypeHeader = Pistache::Http::Header::header_cast<Pistache::Http::Header::ContentType>(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<const CFConnectingIPHeader> cfHeader;
|
||||
try {
|
||||
cfHeader = Pistache::Http::Header::header_cast<CFConnectingIPHeader>(req.headers().get("cf-connecting-ip"));
|
||||
} catch (std::exception& e) {
|
||||
; // silent ignore
|
||||
}
|
||||
|
||||
auto json = glz::read_json<SChallengeResponse>(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<Pistache::Http::Response> 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<Pistache::Http::Response> 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<Pistache::Http::Response> 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<Pistache::Http::Response> 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<Pistache::Http::Response> 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();
|
||||
}
|
||||
28
src/core/Handler.hpp
Normal file
28
src/core/Handler.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <pistache/http.h>
|
||||
|
||||
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 = "";
|
||||
};
|
||||
};
|
||||
36
src/debug/log.hpp
Normal file
36
src/debug/log.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <fmt/format.h>
|
||||
#include <iostream>
|
||||
|
||||
enum LogLevel {
|
||||
NONE = -1,
|
||||
LOG = 0,
|
||||
WARN,
|
||||
ERR,
|
||||
CRIT,
|
||||
INFO,
|
||||
TRACE
|
||||
};
|
||||
|
||||
namespace Debug {
|
||||
template <typename... Args>
|
||||
void log(LogLevel level, fmt::format_string<Args...> 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";
|
||||
}
|
||||
};
|
||||
24
src/headers/authorization.hpp
Normal file
24
src/headers/authorization.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <pistache/http_headers.h>
|
||||
#include <pistache/net.h>
|
||||
|
||||
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 = "";
|
||||
};
|
||||
24
src/headers/cfHeader.hpp
Normal file
24
src/headers/cfHeader.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <pistache/http_headers.h>
|
||||
#include <pistache/net.h>
|
||||
|
||||
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 = "";
|
||||
};
|
||||
24
src/headers/xforwardfor.hpp
Normal file
24
src/headers/xforwardfor.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <pistache/http_headers.h>
|
||||
#include <pistache/net.h>
|
||||
|
||||
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 = "";
|
||||
};
|
||||
115
src/main.cpp
Normal file
115
src/main.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <pistache/common.h>
|
||||
#include <pistache/cookie.h>
|
||||
#include <pistache/endpoint.h>
|
||||
#include <pistache/http.h>
|
||||
#include <pistache/http_headers.h>
|
||||
#include <pistache/net.h>
|
||||
#include <pistache/peer.h>
|
||||
|
||||
#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 <signal.h>
|
||||
|
||||
int main(int argc, char** argv, char** envp) {
|
||||
|
||||
if (argc < 2) {
|
||||
Debug::log(CRIT, "Missing param for websites storage");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<std::string> ARGS{};
|
||||
ARGS.resize(argc);
|
||||
for (int i = 0; i < argc; ++i) {
|
||||
ARGS[i] = std::string{argv[i]};
|
||||
}
|
||||
|
||||
std::vector<std::string> 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<CConfig>();
|
||||
|
||||
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<CFConnectingIPHeader>();
|
||||
Pistache::Http::Header::Registry::instance().registerHeader<XForwardedForHeader>();
|
||||
|
||||
g_pDB = std::make_unique<CDatabase>();
|
||||
|
||||
auto endpoint = std::make_unique<Pistache::Http::Endpoint>(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<CServerHandler>();
|
||||
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;
|
||||
}
|
||||
1
subprojects/fmt
Submodule
1
subprojects/fmt
Submodule
Submodule subprojects/fmt added at 64db979e38
1
subprojects/glaze
Submodule
1
subprojects/glaze
Submodule
Submodule subprojects/glaze added at 8fd8d001df
1
subprojects/pistache
Submodule
1
subprojects/pistache
Submodule
Submodule subprojects/pistache added at 31ef83778e
1
subprojects/tinylates
Submodule
1
subprojects/tinylates
Submodule
Submodule subprojects/tinylates added at d91590f4eb
Reference in New Issue
Block a user