From d5a2353ded62b3ad8c35fb3bef1e2abee2ce2a18 Mon Sep 17 00:00:00 2001 From: Tiago Garcia Date: Fri, 19 Jul 2024 21:46:06 +0100 Subject: [PATCH] Fix vulnerabilities Signed-off-by: Tiago Garcia --- README.md | 54 ++++++------- include/logger.hpp | 1 + src/endpoints/run-actions.cpp | 93 ++++++++++------------ src/endpoints/update-files.cpp | 141 ++++++++++++++++++++++----------- src/logger.cpp | 9 +++ src/routes.cpp | 50 ++++++------ 6 files changed, 194 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 855c347..1c5fe0f 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,27 @@ Note: Tokens are only required for private repositories. ## Endpoints -Currently, the only endpoint for the application is `/update-files`, which is used to update the local files on every push as well as run post-update scripts. +### `/run-actions` + +#### Webhook event: `push` + +This endpoint allows the application to run specific actions when a push to a specific branch is made. This way, there's no need to manually run the actions on the server. + +The configuration file must contain the `run-actions` field, which is an object with the following format: + +```json +"run-actions": { + "owner/repo-name": { + "branch": "main", + "actions": [ + { + "name": "action-name", + "command": "command-to-run" + } + ] + } +} +``` ### `/update-files` @@ -129,43 +149,15 @@ The configuration file must contain the `update-files` field, which is an object "...": "..." }, "post-update": [ - "post-update-command", - "post-update-script", - "..." - ] - } -} -``` - -### `/run-actions` - -#### Webhook event: `push` - -This endpoint allows the application to run specific actions when a push to a specific branch is made. This way, there's no need to manually run the actions on the server. - -The configuration file must contain the `run-actions` field, which is an object with the following format: - -```json -"run-actions": { - "owner/repo-name": { - "branch": "main", - "actions": [ { - "name": "action-name", - "command": "command-to-run", - "args": [ - "arg1", - "arg2", - "..." - ] + "name": "post-update-action-name", + "command": "command-to-run" } ] } } ``` -Note: if you don't want to use the `args` field, just leave an empty array such as `"args": []`. - ## Nginx Configuration If you want to use Nginx as a reverse proxy for the application, you can use the following example configuration: diff --git a/include/logger.hpp b/include/logger.hpp index bce0520..089768e 100644 --- a/include/logger.hpp +++ b/include/logger.hpp @@ -12,6 +12,7 @@ class Logger { static void fatal(std::string message); static void success(std::string message); static void code(std::string message); + static void debug(std::string message); private: static void log(std::string message, std::string level); static std::ofstream log_file; diff --git a/src/endpoints/run-actions.cpp b/src/endpoints/run-actions.cpp index f64d5b5..9b65475 100644 --- a/src/endpoints/run-actions.cpp +++ b/src/endpoints/run-actions.cpp @@ -2,7 +2,7 @@ #include "logger.hpp" -crow::response run_actions(const nlohmann::json &run_actions, const crow::request &req) { +crow::response run_actions(const nlohmann::json &config_run_actions, const crow::request &req) { nlohmann::json payload; try { payload = nlohmann::json::parse(req.body); @@ -15,61 +15,62 @@ crow::response run_actions(const nlohmann::json &run_actions, const crow::reques return crow::response(400, response.dump()); } - std::string ref; - std::string repo; - - 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"]; - } catch (nlohmann::json::out_of_range& e) { - Logger::error("[/run-actions] Invalid JSON payload: " + std::string(e.what())); + if (payload.find("ref") == payload.end() || payload.find("repository") == payload.end() || payload["repository"].find("full_name") == payload["repository"].end()) { + Logger::error("[/run-actions] Invalid JSON payload"); nlohmann::json response = { {"status", 400}, {"error", "Invalid JSON payload"} }; return crow::response(400, response.dump()); } + std::string 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); + + std::string repo = payload["repository"]["full_name"]; Logger::info("[/run-actions] Received push to " + repo + ":" + ref); - if (run_actions.find(repo) == run_actions.end()) { - Logger::warn("[/run-actions] No run-actions webhook configuration for repo " + repo); + if (config_run_actions.find(repo) == config_run_actions.end()) { + Logger::warn("[/run-actions] No run-actions configuration for repo " + repo); nlohmann::json response = { {"status", 404}, - {"error", "No run-actions webhook configuration for repo"} + {"error", "No run-actions configuration for repo"} }; return crow::response(404, response.dump()); } - Logger::info("[/run-actions] Found run-actions webhook configuration for repo " + repo); + Logger::info("[/run-actions] Found run-actions configuration for repo " + repo); nlohmann::json config; - std::string config_branch; - nlohmann::json config_actions; - try { - config = run_actions[repo]; - config_branch = config["branch"]; - config_actions = config["actions"]; - } catch (nlohmann::json::out_of_range& e) { - Logger::error("[/run-actions] Invalid run-actions configuration for repo " + repo + ": " + std::string(e.what())); - nlohmann::json response = { - {"status", 500}, - {"error", "Invalid run-actions configuration"} - }; - return crow::response(500, response.dump()); + bool found = false; + for (auto c_repo = config_run_actions.begin(); c_repo != config_run_actions.end(); ++c_repo) { + if (c_repo.key() != repo) continue; + if (c_repo.value().find("branch") == c_repo.value().end() || c_repo.value().find("actions") == c_repo.value().end()) { + Logger::error("[/run-actions] Invalid run-actions configuration found for repo " + c_repo.key()); + nlohmann::json response = { + {"status", 500}, + {"error", "Invalid update-files configuration"} + }; + return crow::response(500, response.dump()); + } + if (c_repo.value()["branch"].get() != ref) continue; + Logger::info("[/run-actions] Found run-actions configuration for branch " + ref); + config = c_repo.value(); + found = true; } - - if (ref != config_branch) { - Logger::info("[/run-actions] Ignoring push to " + repo + " on branch " + ref); + if (!found) { + Logger::info("[/run-actions] Ignoring push to non-configured branch " + ref + " on repo " + repo); nlohmann::json response = { {"status", 200}, - {"message", "Ignoring push to " + repo + " on branch " + ref} + {"message", "Ignoring push to non-configured branch" + ref} }; return crow::response(200, response.dump()); } + std::string config_branch = config["branch"]; + nlohmann::json config_actions = config["actions"]; + if (config_actions.empty()) { Logger::info("[/run-actions] No actions configured for " + repo + " on branch " + ref); nlohmann::json response = { @@ -88,30 +89,22 @@ crow::response run_actions(const nlohmann::json &run_actions, const crow::reques {"failed-actions", nlohmann::json::array()} }; for (const auto &action : config_actions) { - std::string action_name; - std::string action_command; - nlohmann::json action_args; - try { - action_name = action["name"]; - action_command = action["command"]; - action_args = action["args"]; - } catch (nlohmann::json::out_of_range& e) { - Logger::error("[/run-actions] Invalid action configuration for repo " + repo + ": " + std::string(e.what())); - nlohmann::json response = { + if (action.find("name") == action.end() || action.find("command") == action.end()) { + Logger::error("[/run-actions] Invalid action configuration for repo " + repo); + nlohmann::json e_response = { {"status", 500}, - {"error", "Invalid action configuration"} + {"error", "Invalid action configuration"}, + {"successful-actions", response["successful-actions"]}, + {"failed-actions", response["failed-actions"]} }; - return crow::response(500, response.dump()); + return crow::response(500, e_response.dump()); } + std::string action_name = action["name"]; + std::string action_command = action["command"]; Logger::info("[/run-actions] Running action '" + action_name + "'"); - std::string action_command_with_args = action_command; - for (const auto &arg : action_args) { - action_command_with_args += " " + arg.get(); - } - - int ret = std::system(action_command_with_args.c_str()); + int ret = std::system(action_command.c_str()); if (ret == 0) { Logger::info("[/run-actions] Action " + action_name + " completed successfully"); response["successful-actions"].push_back(action_name); diff --git a/src/endpoints/update-files.cpp b/src/endpoints/update-files.cpp index 8a171c3..e9dac6b 100644 --- a/src/endpoints/update-files.cpp +++ b/src/endpoints/update-files.cpp @@ -1,4 +1,5 @@ #include "endpoints/update-files.hpp" +#include #include #include "logger.hpp" @@ -16,30 +17,10 @@ crow::response update_files(const nlohmann::json& config_update_files, const nlo 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()) { - Logger::warn("[/update-files] No token configured for private repo " + repo); - 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) { - Logger::error("[/update-files] Invalid JSON payload: " + std::string(e.what())); + if (payload.find("ref") == payload.end() || payload.find("repository") == payload.end() || + payload["repository"].find("full_name") == payload["repository"].end() || payload["repository"].find("private") == payload["repository"].end() || + payload.find("commits") == payload.end() || !payload["commits"].is_array() || payload["commits"].empty()){ + Logger::error("[/update-files] Invalid JSON payload"); nlohmann::json response = { {"status", 400}, {"error", "Invalid JSON payload"} @@ -47,6 +28,24 @@ crow::response update_files(const nlohmann::json& config_update_files, const nlo return crow::response(400, response.dump()); } + std::string 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); + std::string repo = payload["repository"]["full_name"]; + bool is_private = payload["repository"]["private"]; + std::string token; + if (is_private) { + if (config_tokens.find(repo) == config_tokens.end()) { + Logger::warn("[/update-files] No token configured for private repo " + repo); + nlohmann::json response = { + {"status", 403}, + {"error", "No token configured for private repo"} + }; + return crow::response(403, response.dump()); + } + token = config_tokens[repo]; + } + Logger::info("[/update-files] Received push to " + repo + ":" + ref + " (private: " + (is_private ? "true" : "false") + ")"); if (config_update_files.find(repo) == config_update_files.end()) { @@ -63,16 +62,24 @@ crow::response update_files(const nlohmann::json& config_update_files, const nlo 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.key() != repo) continue; + if (c_repo.value().find("branch") == c_repo.value().end() || c_repo.value().find("files") == c_repo.value().end()) { + Logger::error("[/update-files] Invalid update-files configuration found for repo " + c_repo.key()); + nlohmann::json response = { + {"status", 500}, + {"error", "Invalid update-files configuration"} + }; + return crow::response(500, response.dump()); + } if (c_repo.value()["branch"] != ref) continue; config = c_repo.value(); found = true; } if (!found) { - Logger::warn("[/update-files] No update-files webhook configuration for branch " + ref); + Logger::warn("[/update-files] Ignoring push to non-configured branch" + ref + " on repo " + repo); nlohmann::json response = { - {"status", 404}, - {"error", "No update-files webhook configuration for branch" + ref} + {"status", 200}, + {"message", "Ignoring push to non-configured branch" + ref} }; return crow::response(404, response.dump()); } @@ -82,23 +89,31 @@ crow::response update_files(const nlohmann::json& config_update_files, const nlo if (config["files"].empty()) { Logger::warn("[/update-files] No files configured for repo " + repo + ":" + ref); nlohmann::json response = { - {"status", 404}, - {"error", "No files configured for repo" + repo + ":" + ref} + {"status", 200}, + {"message", "No files configured for repo " + repo + ":" + ref} }; - return crow::response(404, response.dump()); + return crow::response(200, response.dump()); } std::vector> modified_files; for (auto &commit : payload["commits"]) { + if (commit.find("added") == commit.end() || commit.find("modified") == commit.end()) { + Logger::error("[/update-files] Invalid JSON payload"); + nlohmann::json response = { + {"status", 400}, + {"error", "Invalid JSON payload"} + }; + return crow::response(400, response.dump()); + } 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; + if (config["files"].find(file_path) == config["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; + if (config["files"].find(file_path) == config["files"].end()) continue; std::vector file_info = {file_path, "modified"}; modified_files.push_back(file_info); } @@ -120,8 +135,11 @@ crow::response update_files(const nlohmann::json& config_update_files, const nlo nlohmann::json response = { {"status", 200}, {"message", "OK"}, - {"file_count", 0}, - {"updated-files", nlohmann::json::array()}, + {"updated-file-count", 0}, + {"updated-files", { + {"successful", nlohmann::json::array()}, + {"failed", nlohmann::json::array()} + }}, {"post-update", { {"successful", nlohmann::json::array()}, {"failed", nlohmann::json::array()} @@ -130,36 +148,63 @@ crow::response update_files(const nlohmann::json& config_update_files, const nlo for (auto &file : modified_files) { std::string remote_path = file[0]; - std::string local_path = config_update_files[repo]["files"][remote_path]; + std::string local_path = config["files"][remote_path]; if (local_path.find_last_of('/') != std::string::npos) try { std::filesystem::create_directories(local_path.substr(0, local_path.find_last_of('/'))); } catch (const std::exception &e) { Logger::error("[/update-files] Failed to create directories for " + local_path + ": " + std::string(e.what())); + response["updated-files"]["failed"].push_back(local_path); 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()); + int ret = std::system(command.c_str()); + if (ret != 0) { + Logger::error("[/update-files] Failed to update " + local_path); + response["updated-files"]["failed"].push_back(local_path); + continue; + } Logger::info("[/update-files] " + std::string(file[1] == "added" ? "Created" : "Updated") + " " + local_path); - response["file_count"] = response["file_count"].get() + 1; - response["updated-files"].push_back(remote_path); + response["updated-file-count"] = response["updated-file-count"].get() + 1; + response["updated-files"]["successful"].push_back(local_path); } Logger::success("[/update-files] Finished updating " + std::to_string(modified_files.size()) + " files for repo " + repo + ":" + ref); + + if (config.find("post-update") == config.end()) { + Logger::info("[/update-files] No post-update actions configured for repo " + repo + ":" + ref); + return crow::response(200, response.dump()); + } + + nlohmann::json post_update_actions = config["post-update"]; + Logger::info("[/update-files] Running post-update actions for repo " + repo + ":" + ref); - for (auto &c_action : config_update_files[repo]["post-update"]) { - std::string action = c_action.get(); - Logger::info("[/update-files] Running post-update action: " + action); - int return_code = std::system(action.c_str()); - if (return_code == 0) { - Logger::success("[/update-files] Post-update action " + action + " ran successfully"); - response["post-update"]["successful"].push_back(action); + for (const auto &action : post_update_actions) { + if (action.find("name") == action.end() || action.find("command") == action.end()) { + Logger::error("[/update-files] Invalid post-update configuration for repo " + repo); + nlohmann::json e_response = { + {"status", 500}, + {"error", "Invalid action configuration"}, + {"updated-files", response["updated-files"]}, + {"post-update", response["post-update"]} + }; + return crow::response(500, e_response.dump()); + } + std::string action_name = action["name"]; + std::string action_command = action["command"]; + + Logger::info("[/update-files] Running post-update action '" + action_name + "'"); + + int ret = std::system(action_command.c_str()); + if (ret == 0) { + Logger::info("[/update-files] Action " + action_name + " completed successfully"); + response["post-update"]["successful"].push_back(action_name); } else { - Logger::error("[/update-files] Post-update action " + action + " failed with return code " + std::to_string(return_code)); - response["post-update"]["failed"].push_back(action); + Logger::error("[/update-files] Action " + action_name + " failed with exit code " + std::to_string(ret)); + response["post-update"]["failed"].push_back(action_name); } } diff --git a/src/logger.cpp b/src/logger.cpp index a94347f..c4d263a 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -89,6 +89,10 @@ void Logger::code(std::string message) { Logger::log(message, "CODE"); } +void Logger::debug(std::string message) { + Logger::log(message, "DEBUG "); +} + void Logger::log(std::string message, std::string level) { std::string formatted_message = ""; if (isatty(fileno(stdout))) { @@ -107,6 +111,11 @@ void Logger::log(std::string message, std::string level) { } else if (level == "CODE") { formatted_message += COLORS_FG_WHITE; formatted_message += COLORS_DIM; + } else if (level == "DEBUG ") { + formatted_message += COLORS_FG_BLUE; + formatted_message += COLORS_UNDERLINED; + formatted_message += COLORS_DIM; + formatted_message += COLORS_BOLD; } } diff --git a/src/routes.cpp b/src/routes.cpp index 0817e21..3d5c472 100644 --- a/src/routes.cpp +++ b/src/routes.cpp @@ -23,31 +23,6 @@ Routes::Routes(nlohmann::json config) { return crow::response(200, response.dump()); }); - Logger::info("[Routes] Registering route \"/update-files\""); - CROW_ROUTE(this->app, "/update-files") - .methods("POST"_method) - .name("Update Files") - ([&config_update_files, &config_tokens](const crow::request &req) { - if (config_update_files.is_null()) { - Logger::warn("[Routes] No update-files configuration found"); - nlohmann::json response = { - {"status", 404}, - {"error", "No update-files configuration found"} - }; - return crow::response(404, response.dump()); - } - try { - return update_files(config_update_files, config_tokens, req); - } catch (const std::exception &e) { - Logger::error("[Routes] Unknown error in update_files: " + std::string(e.what())); - nlohmann::json response = { - {"status", 500}, - {"error", "Internal server error"} - }; - return crow::response(500, response.dump()); - } - }); - Logger::info("[Routes] Registering route \"/run-actions\""); CROW_ROUTE(this->app, "/run-actions") .methods("POST"_method) @@ -73,6 +48,31 @@ Routes::Routes(nlohmann::json config) { } }); + Logger::info("[Routes] Registering route \"/update-files\""); + CROW_ROUTE(this->app, "/update-files") + .methods("POST"_method) + .name("Update Files") + ([&config_update_files, &config_tokens](const crow::request &req) { + if (config_update_files.is_null()) { + Logger::warn("[Routes] No update-files configuration found"); + nlohmann::json response = { + {"status", 404}, + {"error", "No update-files configuration found"} + }; + return crow::response(404, response.dump()); + } + try { + return update_files(config_update_files, config_tokens, req); + } catch (const std::exception &e) { + Logger::error("[Routes] Unknown error in update_files: " + std::string(e.what())); + nlohmann::json response = { + {"status", 500}, + {"error", "Internal server error"} + }; + return crow::response(500, response.dump()); + } + }); + Logger::info("[Routes] Registering catch-all route (404)"); CROW_CATCHALL_ROUTE(this->app) ([](const crow::request &req) {