diff --git a/.gitignore b/.gitignore index 30a202f..795d4b1 100644 --- a/.gitignore +++ b/.gitignore @@ -53,5 +53,3 @@ /test /tests -# Configuration files -config.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 014ea08..cd25836 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,21 +3,8 @@ cmake_minimum_required(VERSION 3.20) # Project name and version project(gh-wh-handler VERSION 0.1.0) -# Determine the architecture -if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") - set(ARCH "x86_64") -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") - set(ARCH "aarch64") -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "armv7l") - set(ARCH "armv7l") -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "i686") - set(ARCH "i686") -else() - set(ARCH "unknown") -endif() - # Set the executable name -set(EXECUTABLE_NAME "gh-wh-handler.${ARCH}") +set(EXECUTABLE_NAME "gh-wh-handler") # Set the C++ standard set(CMAKE_CXX_STANDARD 23) @@ -26,11 +13,16 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) # Set the output directory set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) +# Add include directories +set(INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include") +include_directories(${INCLUDE_DIR}) + # Add source files file(GLOB_RECURSE SOURCES "src/*.cpp") # Add the executable add_executable(${EXECUTABLE_NAME} ${SOURCES}) +target_include_directories(${EXECUTABLE_NAME} PUBLIC ${INCLUDE_DIR}) # Add compilation flags target_compile_options(${EXECUTABLE_NAME} PRIVATE -Wall -Werror) @@ -40,3 +32,16 @@ set_target_properties(${EXECUTABLE_NAME} PROPERTIES LINK_FLAGS "-static -static-libgcc -static-libstdc++" ) +# Install the service +set(CMAKE_INSTALL_PREFIX "/usr/") +install(TARGETS ${EXECUTABLE_NAME} DESTINATION bin) +set(SERVICE_EXECUTABLE "/services/gh-wh-handler/gh-wh-handler.aarch64") +set(SERVICE_CONFIG_FILE "/services/gh-wh-handler/config.json") +configure_file(${CMAKE_SOURCE_DIR}/gh-wh-handler.service.in ${CMAKE_BINARY_DIR}/gh-wh-handler.service @ONLY) +install(FILES ${CMAKE_BINARY_DIR}/gh-wh-handler.service DESTINATION /etc/systemd/system) +install(CODE " + execute_process(COMMAND systemctl daemon-reload) + execute_process(COMMAND systemctl enable gh-wh-handler.service) + execute_process(COMMAND systemctl start gh-wh-handler.service) +") + diff --git a/gh-wh-handler.service.in b/gh-wh-handler.service.in new file mode 100644 index 0000000..6e2e360 --- /dev/null +++ b/gh-wh-handler.service.in @@ -0,0 +1,11 @@ +[Unit] +Description=Runs github webhook handler +After=network.target + +[Service] +ExecStart=@SERVICE_EXECUTABLE@ @SERVICE_CONFIG_FILE@ +Restart=always +Type=simple + +[Install] +WantedBy=default.target diff --git a/include/config.hpp b/include/config.hpp new file mode 100644 index 0000000..ecf3e1b --- /dev/null +++ b/include/config.hpp @@ -0,0 +1,25 @@ +#include + +class Config { + public: + static void create_config(); + static nlohmann::json get_config(); + static void open_config_menu(); + + private: + static void set_port(int port); + + static void add_update_files_repo(std::string repo, std::string branch); + static void add_update_files_file(std::string repo, std::string remote_path, std::string local_path); + static void add_update_files_post_update(std::string repo, std::string command); + static void add_run_scripts_repo(std::string repo, std::string branch); + static void add_run_scripts_script(std::string repo, std::string script_path); + static void add_token(std::string repo, std::string token); + + static void remove_update_files_repo(std::string repo); + static void remove_update_files_file(std::string repo, std::string remote_path); + static void remove_update_files_post_update(std::string repo, std::string command); + static void remove_run_scripts_repo(std::string repo); + static void remove_run_scripts_script(std::string repo, std::string script_path); + static void remove_token(std::string repo, std::string token); +}; diff --git a/include/endpoints/run-scripts.hpp b/include/endpoints/run-scripts.hpp new file mode 100644 index 0000000..ef3e53e --- /dev/null +++ b/include/endpoints/run-scripts.hpp @@ -0,0 +1,5 @@ +#include +#include +#include + +crow::response run_scripts(const nlohmann::json &, const nlohmann::json &,const crow::request &); diff --git a/include/endpoints/update-files.hpp b/include/endpoints/update-files.hpp new file mode 100644 index 0000000..e9677d0 --- /dev/null +++ b/include/endpoints/update-files.hpp @@ -0,0 +1,5 @@ +#include +#include +#include + +crow::response update_files(const nlohmann::json &, const nlohmann::json &, const crow::request &); diff --git a/include/routes.hpp b/include/routes.hpp new file mode 100644 index 0000000..8d4f4a8 --- /dev/null +++ b/include/routes.hpp @@ -0,0 +1,11 @@ +#include +#include + +class Routes { + public: + Routes(nlohmann::json); + ~Routes(); + + private: + crow::SimpleApp app; +}; diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..dcb110f --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,46 @@ +#include "config.hpp" +#include +#include +#include + +void Config::create_config() { + std::cout << "Creating config file" << std::endl; + nlohmann::json config = { + {"port", 65001}, + {"update-files", nlohmann::json::array()}, + {"run-scripts", nlohmann::json::array()}, + {"tokens", nlohmann::json::array()}, + }; + if (!std::filesystem::exists("/services/gh-wh-handler")) { + try { + std::filesystem::create_directories("/services/gh-wh-handler"); + } catch (std::exception& e) { + std::cerr << "Error creating directory '/services/gh-wh-handler/': " << e.what() << std::endl; + return; + } + } + try { + std::ofstream config_file("/services/gh-wh-handler/config.json"); + config_file << config.dump(2); + config_file.close(); + } catch (std::exception& e) { + std::cerr << "Error creating config file: " << e.what() << std::endl; + } +} + +nlohmann::json Config::get_config() { + nlohmann::json config; + try { + std::ifstream config_file("/services/gh-wh-handler/config.json"); + config_file >> config; + config_file.close(); + } catch (std::exception& e) { + std::cerr << "Error loading config file: " << e.what() << std::endl; + } + + return config; +} + +void Config::open_config_menu() { + std::cout << "Not implemented yet" << std::endl; +} diff --git a/src/endpoints/run-scripts.cpp b/src/endpoints/run-scripts.cpp new file mode 100644 index 0000000..3cad475 --- /dev/null +++ b/src/endpoints/run-scripts.cpp @@ -0,0 +1,6 @@ +#include "endpoints/run-scripts.hpp" + +crow::response run_scripts(const nlohmann::json &run_scripts, const nlohmann::json &tokens, const crow::request &req) { + // TODO: Implement run_scripts +} + diff --git a/src/endpoints/update-files.cpp b/src/endpoints/update-files.cpp new file mode 100644 index 0000000..6377da0 --- /dev/null +++ b/src/endpoints/update-files.cpp @@ -0,0 +1,142 @@ +#include "endpoints/update-files.hpp" +#include + +crow::response update_files(const nlohmann::json& config_update_files, const nlohmann::json& config_tokens, const crow::request& req) { + nlohmann::json payload; + try { + payload = nlohmann::json::parse(req.body); + } catch (nlohmann::json::parse_error& e) { + std::cerr << "Error parsing payload: " << e.what() << std::endl; + nlohmann::json response = { + {"status", 400}, + {"error", "Error parsing payload"} + }; + return crow::response(400, response.dump()); + } + + std::string ref; + std::string repo; + bool is_private; + std::string token; + + try { + ref = payload["ref"]; + if (size_t last_slash = ref.find_last_of('/'); last_slash != std::string::npos && last_slash + 1 < ref.length()) + ref = ref.substr(last_slash + 1); + repo = payload["repository"]["full_name"]; + is_private = payload["repository"]["private"]; + if (is_private) { + if (config_tokens.find(repo) == config_tokens.end()) { + printf("No token configured for private repo %s\n", repo.c_str()); + nlohmann::json response = { + {"status", 403}, + {"error", "No token configured for private repo"} + }; + return crow::response(403, response.dump()); + } + token = config_tokens[repo]; + } + } catch (nlohmann::json::out_of_range& e) { + std::cerr << "Error parsing payload: " << e.what() << std::endl; + nlohmann::json response = { + {"status", 400}, + {"error", "Invalid JSON payload"} + }; + return crow::response(400, response.dump()); + } + + printf("Received push to %s:%s (private: %s)\n", repo.c_str(), ref.c_str(), is_private ? "true" : "false"); + + if (config_update_files.find(repo) == config_update_files.end()) { + printf("No update-files webhook configuration for repo %s\n", repo.c_str()); + nlohmann::json response = { + {"status", 404}, + {"error", "No update-files webhook configuration for repo"} + }; + return crow::response(404, response.dump()); + } + + nlohmann::json config; + bool found = false; + for (auto c_repo = config_update_files.begin(); c_repo != config_update_files.end(); ++c_repo) { + if (const std::string &c_repo_name = c_repo.key(); c_repo_name == repo) continue; + if (c_repo.value()["branch"] != ref) continue; + config = c_repo.value(); + found = true; + } + if (!found) { + printf("No update-files webhook configuration for repo %s:%s\n", repo.c_str(), ref.c_str()); + nlohmann::json response = { + {"status", 404}, + {"error", "No update-files webhook configuration for branch" + ref} + }; + return crow::response(404, response.dump()); + } + + if (config["files"].empty()) { + printf("No files configured for repo %s:%s\n", repo.c_str(), ref.c_str()); + nlohmann::json response = { + {"status", 404}, + {"error", "No files configured for branch" + ref} + }; + return crow::response(404, response.dump()); + } + + std::vector> modified_files; + for (auto &commit : payload["commits"]) { + for (auto &file : commit["added"]) { + std::string file_path = file; + if (config_update_files[repo]["files"].find(file_path) == config_update_files[repo]["files"].end()) continue; + std::vector file_info = {file_path, "added"}; + modified_files.push_back(file_info); + } + for (auto &file : commit["modified"]) { + std::string file_path = file; + if (config_update_files[repo]["files"].find(file_path) == config_update_files[repo]["files"].end()) continue; + std::vector file_info = {file_path, "modified"}; + modified_files.push_back(file_info); + } + } + + nlohmann::json response = { + {"status", 200}, + {"message", "OK"}, + {"updated-files", nlohmann::json::array()} + }; + for (auto &file : modified_files) { + std::string remote_path = file[0]; + + std::string local_path = config_update_files[repo]["files"][remote_path]; + try { + std::filesystem::create_directories(local_path.substr(0, local_path.find_last_of('/'))); + } catch (const std::exception &e) { + std::cerr << "Failed to create directories for " << local_path << ": " << e.what() << std::endl; + continue; + } + + std::string command = "curl -s https://raw.githubusercontent.com/" + repo + "/" + ref + "/" + remote_path + " -o " + local_path; + if (is_private) command += " -H 'Authorization: token " + token + "'"; + std::system(command.c_str()); + printf("%s %s\n", file[1] == "added" ? "Created" : "Updated", local_path.c_str()); + response["file_count"] = response["file_count"].get() + 1; + response["updated"].push_back(remote_path); + } + + for (auto &c_action : config_update_files[repo]["post-update"]) { + std::string action = c_action.get(); + printf("Running post-update action: %s\n", action.c_str()); + int return_code = std::system(action.c_str()); + std::ofstream log_file("/var/log/gh-wh-handler.log", std::ios_base::app); + time_t now = time(0); + if (return_code == 0) { + printf("Post-update action %s ran successfully\n", action.c_str()); + log_file << ctime(&now) << "Post-update action " << action << " ran successfully\n"; + } else { + printf("Post-update action %s failed with return code %d\n", action.c_str(), return_code); + log_file << ctime(&now) << "Post-update action " << action << " failed with return code " << return_code << "\n"; + } + } + + return crow::response(200, response.dump()); +} + diff --git a/src/main.cpp b/src/main.cpp index 9312996..9b7ebe1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,11 @@ #include -#include #include #include -#include #include +#include "routes.hpp" +#include "config.hpp" + void signal_handler(const int signum) { // std::cout << "Interrupt signal (" << signum << ") received.\n"; std::cout << "Exiting..." << std::endl; @@ -13,203 +14,30 @@ void signal_handler(const int signum) { int main(int argc, char **argv) { // Check for config file argument, exit if it's not there - if (argc > 2) { + if (argc < 2 || argc > 2) { std::cerr << "Usage: " << 0[argv] << " " << std::endl; return 1; } - std::string config_file_path = argc == 2 ? 1[argv] : "/etc/gh-wh-handler/config.json"; - - // Open config file, exit if it fails - std::ifstream config_file(config_file_path); - if (!config_file.is_open()) { - std::cerr << "Failed to open config.json" << std::endl; - return 1; + if (std::string(argv[1]) == "config") { + return 0; } - // Parse config file, exit if it fails - nlohmann::json config; - try { - config_file >> config; - } catch (const std::exception &e) { - std::cerr << "Failed to parse config.json: " << e.what() << std::endl; - return 1; + std::string config_file_path = 1[argv]; + + // Check if config file exists + if (!std::filesystem::exists(config_file_path)) { + std::cerr << "Config file does not exist, creating..." << std::endl; + Config::create_config(); } - // Set up variables from config file - int port = config["port"]; - nlohmann::json &repos = config["repos"]; - nlohmann::json &tokens = config["tokens"]; - nlohmann::json &actions = config["actions"]; - - // Close config file - config_file.close(); - - // Set up web server - crow::SimpleApp app; - - // Set up route for updating files - CROW_ROUTE(app, "/update-files") - .methods("POST"_method) - ([&repos, &tokens, &actions](const crow::request &req) { - nlohmann::json payload; - try { - // Parse JSON payload from the request body, exit if it fails - payload = nlohmann::json::parse(req.body); - } catch (const std::exception &e) { - std::cerr << "Error processing webhook: " << e.what() << std::endl; - nlohmann::json response = { - {"status", 400}, - {"error", "Invalid JSON payload"} - }; - return crow::response(400, response.dump()); - } - - try { - std::string ref; - std::string repo; - bool is_private; - std::string token; - // Parse the payload - try { - ref = payload["ref"]; - if (size_t last_slash = ref.find_last_of('/'); last_slash != std::string::npos && last_slash + 1 < ref.length()) - ref = ref.substr(last_slash + 1); - repo = payload["repository"]["full_name"]; - is_private = payload["repository"]["private"]; - token = std::string(); - if (is_private) { - if (tokens.find(repo) == tokens.end()) { - printf("No token configured for private repo %s\n", repo.c_str()); - nlohmann::json response = { - {"status", 403}, - {"error", "No token configured for private repo"} - }; - return crow::response(403, response.dump()); - } - token = tokens[repo]; - } - } catch (const std::exception &e) { - std::cerr << "Error parsing payload: " << e.what() << std::endl; - nlohmann::json response = { - {"status", 400}, - {"error", "Invalid JSON payload"} - }; - return crow::response(400, response.dump()); - } - - printf("Received push to %s:%s (private: %s)\n", repo.c_str(), ref.c_str(), is_private ? "true" : "false"); - - // Check if the repo is configured - if (repos.find(repo) == repos.end()) { - printf("No webhook configured for %s\n", repo.c_str()); - nlohmann::json response = { - {"status", 404}, - {"error", "No webhook configured for repo"} - }; - return crow::response(404, response.dump()); - } - - // Check if the branch is configured - nlohmann::json repo_data; - bool is_valid_branch = false; - for (auto c_repo = repos.begin(); c_repo != repos.end(); ++c_repo) { - if (const std::string &c_repo_name = c_repo.key(); c_repo_name != repo) continue; - if (c_repo.value()["branch"] != ref) continue; - is_valid_branch = true; - repo_data = c_repo.value(); - } - - if (!is_valid_branch) { - printf("No webhook configured for %s:%s\n", repo.c_str(), ref.c_str()); - nlohmann::json response = { - {"status", 404}, - {"error", "No webhook configured for branch" + ref} - }; - return crow::response(404, response.dump()); - } - - // Check if any files are configured - if (repo_data["files"].empty()) { - printf("No files configured for %s:%s\n", repo.c_str(), ref.c_str()); - nlohmann::json response = { - {"status", 404}, - {"error", "No files configured for branch" + ref} - }; - return crow::response(404, response.dump()); - } - - // Get list with modified files - std::vector> modified_files; - for (auto &commit : payload["commits"]) { - for (auto &file : commit["modified"]) { - std::string file_path = file; - if (repos[repo]["files"].find(file_path) == repos[repo]["files"].end()) continue; - std::vector file_data = {file_path, "modified"}; - modified_files.push_back(file_data); - } - for (auto &file : commit["added"]) { - std::string file_path = file; - if (repos[repo]["files"].find(file_path) == repos[repo]["files"].end()) continue; - std::vector file_data = {file_path, "added"}; - modified_files.push_back(file_data); - } - } - - // Download files - nlohmann::json response = { - {"status", 200}, - {"file_count", 0}, - {"updated", nlohmann::json::array()} - }; - for (std::vector &file_data : modified_files) { - std::string file_path = file_data[0]; - - std::string path = repos[repo]["files"][file_path]; - try { - std::filesystem::create_directories(path.substr(0, path.find_last_of('/'))); - } catch (const std::exception &e) { - std::cerr << "Failed to create directories for " << path << ": " << e.what() << std::endl; - continue; - } - - std::string command = "curl -s https://raw.githubusercontent.com/" + repo + "/" + ref + "/" + file_path + " -o " + path; - if (is_private) command += " -H 'Authorization: token " + token + "'"; - std::system(command.c_str()); - printf("%s %s\n", file_data[1] == "added" ? "Created" : "Updated", path.c_str()); - response["file_count"] = response["file_count"].get() + 1; - response["updated"].push_back(file_path); - } - - // Run actions - for (auto c_action_list = actions.begin(); c_action_list != actions.end(); ++c_action_list) { - if (const std::string &c_action_repo = c_action_list.key(); c_action_repo != repo) continue; - for (auto &action : c_action_list.value()) { - std::cout << "Executing action: " << action << std::endl; - std::string command = action; - int return_code = std::system(command.c_str()); - std::ofstream log_file("/var/log/gh_wh_handler.log", std::ios_base::app); - time_t now = time(0); - if (return_code == 0) { - printf("Successfully executed action: %s\n", command.c_str()); - log_file << now << " > Successfully executed action: " << command << std::endl; - } else { - printf("Failed to execute action: %s\n", command.c_str()); - log_file << now << " > Failed to execute action: " << command << std::endl; - } - } - } - - return crow::response(200, response.dump()); - } catch (const std::exception &e) { - std::cerr << "Error processing webhook: " << e.what() << std::endl; - return crow::response(500); - } - }); + // Load configuration + nlohmann::json config = Config::get_config(); std::signal(SIGINT, signal_handler); - app.port(port).multithreaded().run(); + // Start server + Routes routes(config); return 0; } diff --git a/src/routes.cpp b/src/routes.cpp new file mode 100644 index 0000000..e08aad4 --- /dev/null +++ b/src/routes.cpp @@ -0,0 +1,44 @@ +#include "routes.hpp" +#include "endpoints/update-files.hpp" +#include "endpoints/run-scripts.hpp" +#include + +Routes::Routes(nlohmann::json config) { + const nlohmann::json config_update_files = config["update-files"]; + const nlohmann::json config_run_scripts = config["run-scripts"]; + const nlohmann::json config_tokens = config["tokens"]; + + CROW_ROUTE(this->app, "/update-files") + .methods("POST"_method) + .name("Update Files") + ([&config_update_files, &config_tokens](const crow::request &req) { + try { + return update_files(config_update_files, config_tokens, req); + } catch (const std::exception &e) { + std::cerr << "Unknown error in update_files: " << e.what() << std::endl; + nlohmann::json response = { + {"status", 500}, + {"error", "Internal server error"} + }; + return crow::response(500, response.dump()); + } + }); + + CROW_ROUTE(this->app, "/run-scripts") + .methods("POST"_method) + .name("Run Scripts") + ([&config_run_scripts, &config_tokens](const crow::request &req) { + try { + return run_scripts(config_run_scripts, config_tokens, req); + } catch (const std::exception &e) { + std::cerr << "Unknown error in run_scripts: " << e.what() << std::endl; + nlohmann::json response = { + {"status", 500}, + {"error", "Internal server error"} + }; + return crow::response(500, response.dump()); + } + }); + + this->app.port(config["port"].get()).multithreaded().run(); +}