diff --git a/CMakeLists.txt b/CMakeLists.txt index 7972500..3c4f475 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ foreach(file IN LISTS TEMPLATE_FILES) endforeach() # Use Crow from system include (installed via yay -S crow + asio) -include_directories(/usr/include src src/htmx src/shadowrun src/database) +include_directories(/usr/include src src/htmx src/shadowrun src/database src/login) add_executable(${TARGET_NAME} src/main.cpp @@ -70,6 +70,10 @@ add_executable(${TARGET_NAME} src/shadowrun/ShadowrunDb.cpp src/shadowrun/ShadowrunDb.hpp + # login + src/login/login.cpp + src/login/login.hpp + ) target_compile_definitions(${TARGET_NAME} PRIVATE APPLICATION_NAME="${TARGET_NAME}") diff --git a/README.md b/README.md index b7bf241..35bacd5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Setup -pacman -S crow asio gdb gcc cmake make sqlite3 +pacman -S crow asio gdb gcc cmake make sqlite3 libsodium ## Build @@ -9,6 +9,12 @@ pacman -S crow asio gdb gcc cmake make sqlite3 build : Ctrl+Shift+P → "CMake: Configure" run : F5 or Run → Start Debugging +### CompileDB + +``` +cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +``` + ## Make Package 1. tar the source files to make it cleaner diff --git a/src/database/database.cpp b/src/database/database.cpp index 08b7476..05b2700 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -52,6 +52,18 @@ map Database::getStrMap(const string& sql){ return map; } +string Database::getStr(const string& sql){ + sqlite3_stmt* stmt = prepareStmt(sql); + string str; + if (stmt == nullptr) + return str; + + str = reinterpret_cast(sqlite3_column_text(stmt, 0)); + + sqlite3_finalize(stmt); + return str; +} + set Database::getStrSet(const string& sql){ sqlite3_stmt* stmt = prepareStmt(sql); set vec; diff --git a/src/database/database.hpp b/src/database/database.hpp index 462d091..a5fbca7 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -24,6 +24,8 @@ public: std::set getStrSet(const std::string& sql); + string getStr(const string& sql) + std::map getStrMap(const std::string& sql); private: diff --git a/src/login/login.cpp b/src/login/login.cpp new file mode 100644 index 0000000..0325e3a --- /dev/null +++ b/src/login/login.cpp @@ -0,0 +1,206 @@ +#include "login.hpp" +#include "string.h" +#include "sodium.h" +#include "unordered_map" +#include "database.hpp" + +namespace login +{ +struct Session { + std::string user_id; + std::string csrf_token; +}; + +std::unordered_map sessions; + +bool verifyHashWithPassword(std::string& const hash, std::string const& password) +{ + if (crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.size()) == 0) { + return true; + } else { + return false; + } +} + +std::string hashPassword(std::string& const password) +{ + // Allocate storage for the hash + char hash[crypto_pwhash_STRBYTES]; + + // Hash the password using Argon2id + if (crypto_pwhash_str( + hash, + password.c_str(), + password.size(), + crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE + ) != 0) { + std::cerr << "Out of memory while hashing password!" << std::endl; + return ""; + } + return hash; +} + +std::string generate_session_id(size_t bytes = 32) { + std::vector buf(bytes); + randombytes_buf(buf.data(), buf.size()); + + // Convert to hex + std::string hex; + hex.reserve(bytes * 2); + static const char *hexmap = "0123456789abcdef"; + for (unsigned char b : buf) { + hex.push_back(hexmap[b >> 4]); + hex.push_back(hexmap[b & 0xF]); + } + return hex; +} + +std::string get_session_id(const crow::request& req) { + auto cookie_header = req.get_header_value("Cookie"); + std::string prefix = "session_id="; + auto pos = cookie_header.find(prefix); + if (pos == std::string::npos) return ""; + return cookie_header.substr(pos + prefix.size(), 32); +} + +// Utility: generate random string +std::string random_string(size_t length) { + static const char charset[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + std::string result; + result.resize(length); + std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution<> dist(0, sizeof(charset)-2); + for (size_t i = 0; i < length; ++i) result[i] = charset[dist(rng)]; + return result; +} + +bool is_logged_in(const crow::request& req) { + std::string session_id = get_session_id(req); + if (session_id.empty()) return false; + auto it = sessions.find(session_id); + if (it == sessions.end()) return false; + return !it->second.user_id.empty(); +} + +// lambda to be used by endpoint that requiere login +auto login_required = [](auto handler){ + return [handler](const crow::request& req){ + if (!is_logged_in(req)) { + crow::response res; + if (req.get_header_value("HX-Request") == "true") + res = crow::response(401, "Login required"); + else { + res = crow::response(302); + res.add_header("Location", "/login"); + } + return res; + } + return handler(req); + }; +}; + +bool loginUser(const std::string& username, const std::string& password) +{ + auto sql = format("SELECT password_hash FROM users HERE username = '{}' LIMIT 1;", username); + auto db = Database(); + + if (!db.open()) + return false; + + auto opt_str = db.getStr(sql.c_str()); + if (opt_str.has_value()) { + return verifyHashWithPassword(opt_str.value(), password); + } else { + return false; + } +} +} + + +bool initDB() +{ + auto db = Database(); + + if (!db.open()){ + return false; + } + + // Create a tables + const char* create_sql_chars = "CREATE TABLE IF NOT EXISTS users (" + "id INTEGER PRIMARY KEY," + "username TEXT NOT NULL," + "password_hash TEXT NOT NULL," + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP);"; + + if (!db.exec(create_sql_chars)){ + CROW_LOG_ERROR << "Failed to create users table"; + return false; + } + + return true; +} + +bool initLogin(crow::SimpleApp& app) +{ + if (sodium_init() < 0) { + CROW_LOG_ERROR << "Failed to Init Sodium"; + return false; + } + if(!initDB()) + { + return false; + } + + CROW_ROUTE(app, "/login").methods("GET"_method) + ([](const crow::request& req){ + std::string csrf = random_string(32); + // store CSRF in a temporary session cookie + std::string session_id = random_string(32); + sessions[session_id] = Session{"", csrf}; + + crow::response res; + res.body = "
" + "" + "" + "" + "
"; + res.add_header("Set-Cookie", "session_id=" + session_id + "; HttpOnly; Secure; SameSite=Strict"); + return res; + }); + + CROW_ROUTE(app, "/login").methods("POST"_method) + ([](const crow::request& req){ + auto cookie_it = req.get_header_value("Cookie").find("session_id="); + if (cookie_it == std::string::npos) + return crow::response(401, "No session"); + + // extract session_id + std::string session_id = req.get_header_value("Cookie").substr(cookie_it + 11, 32); + auto it = sessions.find(session_id); + if (it == sessions.end()) return crow::response(401, "Invalid session"); + + auto session = it->second; + + // parse form + auto body = crow::query_string(req.body); + std::string csrf_token = body.get("csrf_token"); + std::string username = body.get("username"); + std::string password = body.get("password"); + + if (csrf_token != session.csrf_token) return crow::response(403, "CSRF failed"); + + bool ok = loginUser(); + + if (!ok) return crow::response(401, "Invalid credentials"); + + // regenerate session, mark as logged in + sessions[session_id].user_id = generate_session_id(); // user ID + + crow::response res; + res.add_header("HX-Redirect", "/dashboard"); // htmx redirect + return res; + }); +} +} \ No newline at end of file diff --git a/src/login/login.hpp b/src/login/login.hpp new file mode 100644 index 0000000..33db34d --- /dev/null +++ b/src/login/login.hpp @@ -0,0 +1,11 @@ +#ifndef __LOGIN_H__ +#define __LOGIN_H__ + +#include + +namespace login { + +bool initLogin(crow::SimpleApp& app); + +} +#endif // __LOGIN_H__ \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 658fc91..ff0affd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include "htmx_helper.h" #include "systemd.h" #include "utils.hpp" +#include "login.hpp" #include "ShadowrunApi.hpp" using namespace std; @@ -38,7 +39,7 @@ int main() { return crow::response(utils::loadFile("templates/" + file)); }); - // Static file redirector + // Static file redirector CROW_ROUTE(app, "/redirect") ([](const crow::request& req) { auto file_param = req.url_params.get("file"); @@ -64,7 +65,7 @@ int main() { auto body = get_body_name(req.body); if (!body.has_value()) return crow::response(400); - + const string& serviceName = body.value(); auto opt_settings = AppSettings::loadAppSettings(); @@ -107,6 +108,7 @@ int main() { } shadowrun::initApi(app); + login::initLogin(app); app.loglevel(crow::LogLevel::INFO); app.port(httpPort).multithreaded().run(); diff --git a/templates/index.html b/templates/index.html index eefaabc..069ce0d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -56,6 +56,13 @@ +
+ + + + +
+