Compare commits

...

4 Commits

Author SHA1 Message Date
Tiago Garcia 52ea711718
Add uninstaller
Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
2024-07-19 15:54:12 +01:00
Tiago Garcia e22fa683e0
Update Config
Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
2024-07-19 15:48:14 +01:00
Tiago Garcia 3234dc6e39
Add installer
Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
2024-07-19 15:29:19 +01:00
Tiago Garcia 04683c8ebc
Fix logging
Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
2024-07-19 12:52:41 +01:00
14 changed files with 308 additions and 112 deletions

26
.gitignore vendored
View File

@ -1,15 +1,19 @@
# CMake build directories
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
*.cmake
*.cbp
*.layout
*.stackdump
CPackConfig.cmake
Makefile
CTestTestfile.cmake
install_manifest.txt
/build/*
!/build/config.json
!/build/gh-wh-handler.service.in
!/build/uninstall.cmake.in
/CMakeFiles/
/CMakeCache.txt
/cmake_install.cmake
/*.cmake
/*.cbp
/*.layout
/*.stackdump
/CPackConfig.cmake
/Makefile
/CTestTestfile.cmake
/install_manifest.txt
# Compiled binaries
/bin/

View File

@ -48,15 +48,19 @@ set_target_properties(${EXECUTABLE_NAME} PROPERTIES LINK_FLAGS
# Install the executable
set(SERVICE_EXECUTABLE "/services/gh-wh-handler/${EXECUTABLE_NAME}")
set(SERVICE_CONFIG "/services/gh-wh-handler/config.json")
set(SERVICE_LOGS "/services/gh-wh-handler/logs")
configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/gh-wh-handler.service.in"
"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/gh-wh-handler.service"
"${CMAKE_CURRENT_BINARY_DIR}/gh-wh-handler.service"
@ONLY)
install(CODE "file(MAKE_DIRECTORY /services/gh-wh-handler)")
install(CODE "file(MAKE_DIRECTORY /services/gh-wh-handler/logs)")
install(TARGETS ${EXECUTABLE_NAME} DESTINATION /services/gh-wh-handler)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/config.json"
DESTINATION /services/gh-wh-handler)
install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
/services/gh-wh-handler/${EXECUTABLE_NAME} /usr/bin/gh-wh-handler)")
install(FILES "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/gh-wh-handler.service"
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/gh-wh-handler.service"
DESTINATION /etc/systemd/system)
install(CODE "execute_process(COMMAND systemctl daemon-reload)")
install(CODE "execute_process(COMMAND systemctl enable gh-wh-handler)")
@ -64,8 +68,8 @@ install(CODE "execute_process(COMMAND systemctl start gh-wh-handler)")
if(NOT TARGET uninstall)
configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/uninstall.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/uninstall.cmake" IMMEDIATE @ONLY)
add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/uninstall.cmake)
configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/uninstall.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/uninstall.cmake" IMMEDIATE @ONLY)
add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/uninstall.cmake)
endif()

View File

@ -2,9 +2,23 @@
## Simple C++ WebAPI to work with GitHub Webhooks
Currently creating a local copy of remote files on every push
This application is a simple C++ WebAPI that listens for GitHub Webhooks and performs actions based on the received data and configuration.
## Usage
## Installation
### Use installation script (recommended)
Run the installation script to install the application:
```console
curl -fsSL https://cdn.tiagorg.pt/gh-wh-handler/install.sh | sudo sh
```
You can uninstall the application using the following command:
```console
curl -fsSL https://cdn.tiagorg.pt/gh-wh-handler/uninstall.sh | sudo sh
```
### Run prebuilt binary
@ -12,7 +26,7 @@ Head over to the [Releases Page](https://github.com/TiagoRG/gh-wh-handler/releas
Run the application using your configuration file:
```console
/path/to/gh-wh-handler.<arch> /path/to/config.json
/path/to/gh-wh-handler.<arch> /path/to/config.json /path/to/logs_dir
```
You can see the config file format below.
@ -24,7 +38,7 @@ You can see the config file format below.
- [CrowCpp](https://crowcpp.org/master/)
- [nlohmann::json](https://github.com/nlohmann/json)
#### Build the application:
#### Build and install the application:
1. Clone the repository:
@ -47,7 +61,12 @@ cmake ..
sudo make install
```
#### Run the application:
If you want to uninstall the application, you can run the following command:
```console
sudo make uninstall
```
## Usage
The application is running on a systemd service, which is both enabled and started after installation.
@ -72,45 +91,51 @@ As of now, the configuration menu is not yet implemented so you have to create t
### Config File
The configuration file can be found in `/services/gh-wh-handler/config.json` and has the following format:
The configuration file can be found in `/services/gh-wh-handler/config.json` and has the following base format:
```json
{
"port": 65001,
"update-files": {
"owner/repo-name": {
"branch": "main",
"files": {
"path/to/remote/file": "/path/to/local/file",
"...": "..."
},
"post-update": [
"post-update-command",
"post-update-script",
"..."
]
}
},
"run-scripts": {
"owner/repo-name": {
"branch": "main",
"actions": [
"command",
"script",
]
}
},
"tokens": {
"owner/repo-name": "token"
}
}
```
This configuration will then have more fields for each endpoint that you want to configure.
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.
Since only the `/update-files` endpoint is implemented, the configuration file may not contain the `run-scripts` field.
### `/update-files`
#### Webhook event: `push`
This endpoint allows the application to update specific files on the server when a push to a specific branch is made. This way, there's no need to manually update the files on the server or to pull the entire repository.
It also allows the application to run post-update scripts after the files are updated.
The configuration file must contain the `update-files` field, which is an object with the following format:
```json
"update-files": {
"owner/repo-name": {
"branch": "main",
"files": {
"path/to/remote/file": "/path/to/local/file",
"...": "..."
},
"post-update": [
"post-update-command",
"post-update-script",
"..."
]
}
}
```
## License

9
build/config.json Normal file
View File

@ -0,0 +1,9 @@
{
"port": 65001,
"update-files": {
},
"run-scripts": {
},
"tokens": {
}
}

View File

@ -3,7 +3,7 @@ Description=Runs github webhook handler
After=network.target
[Service]
ExecStart=@SERVICE_EXECUTABLE@ @SERVICE_CONFIG@
ExecStart=@SERVICE_EXECUTABLE@ @SERVICE_CONFIG@ @SERVICE_LOGS@
Restart=always
Type=simple

View File

@ -16,7 +16,7 @@ execute_process(COMMAND rm /usr/bin/gh-wh-handler)
message(STATUS "[70%] Removing service executable from service directory...")
file(REMOVE /services/gh-wh-handler/@EXECUTABLE_NAME@)
message(STATUS "[85%] Removing service directory...")
file(REMOVE /services/gh-wh-handler)
message(STATUS "[85%] Removing all log files...")
execute_process(COMMAND rm -fr /services/gh-wh-handler/logs)
message(STATUS "[100%] Uninstallation complete!")

View File

@ -5,7 +5,7 @@
class Config {
public:
static void create_config();
static void create_config(std::string config_file_path);
static nlohmann::json get_config(std::string config_file_path);
static void open_config_menu();

68
installer/install.sh Executable file
View File

@ -0,0 +1,68 @@
#!/bin/sh
# Check if the script is being run as root
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root."
exit 1
fi
# Save the current directory
CUR_DIR=$(pwd)
# Get system architecture
ARCH=$(uname -m)
# Change to the temporary directory
cd /tmp
# Download the latest version of the package for the system architecture
# Exit if the download fails
echo "Downloading gh-wh-handler..."
curl -fsSL https://cdn.tiagorg.pt/gh-wh-handler/gh-wh-handler.${ARCH}.latest.tar.gz -o gh-wh-handler.tar.gz || { echo "Download failed."; exit 1; }
# Extract the package
echo "Extracting gh-wh-handler..."
tar -xzf gh-wh-handler.tar.gz || { echo "Extraction failed."; exit 1; }
# Change to the extracted directory
cd gh-wh-handler
# Install the package
echo "Installing gh-wh-handler..."
# Create service directory
echo "Creating service directory..."
mkdir -p /services/gh-wh-handler
mkdir -p /services/gh-wh-handler/logs
# Copy the binary and configuration file to the service directory
echo "Copying files..."
cp "gh-wh-handler.${ARCH}" /services/gh-wh-handler/
cp "config.json" /services/gh-wh-handler/
# Create a symbolic link to the binary in /usr/bin
echo "Creating symbolic link..."
ln -sf /services/gh-wh-handler/gh-wh-handler.${ARCH} /usr/bin/gh-wh-handler
# Copy the service file to the systemd directory
echo "Copying service file..."
cp "gh-wh-handler.service" /etc/systemd/system/
# Reload systemd
echo "Reloading systemd..."
systemctl daemon-reload
# Enable and start the service
echo "Enabling and starting service..."
systemctl enable gh-wh-handler
systemctl start gh-wh-handler
# Clean up
echo "Cleaning up..."
cd /tmp
rm -rf gh-wh-handler
rm gh-wh-handler.tar.gz
# Change back to the original directory
cd $CUR_DIR
echo "Installation complete."

42
installer/uninstall.sh Normal file
View File

@ -0,0 +1,42 @@
#!/bin/sh
# Check if the script is being run as root
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root."
exit 1
fi
# Save the current directory
CUR_DIR=$(pwd)
# Get system architecture
ARCH=$(uname -m)
echo "Uninstalling gh-wh-handler..."
# Stop and disable the service
echo "Stopping and disabling service..."
systemctl stop gh-wh-handler
systemctl disable gh-wh-handler
# Remove the service file
echo "Removing service file..."
rm /etc/systemd/system/gh-wh-handler.service
# Reload systemd
echo "Reloading systemd..."
systemctl daemon-reload
# Remove the symbolic link
echo "Removing symbolic link..."
rm /usr/bin/gh-wh-handler
# Remove the logs directory and binary
echo "Removing files..."
rm -rf /services/gh-wh-handler/logs
rm -f /services/gh-wh-handler/gh-wh-handler.${ARCH}
# Change back to the original directory
cd $CUR_DIR
echo "Uninstallation complete."

View File

@ -5,28 +5,27 @@
#include "logger.hpp"
void Config::create_config() {
void Config::create_config(std::string config_file_path) {
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")) {
std::string path_to_config = config_file_path.substr(0, config_file_path.find_last_of('/'));
if (!std::filesystem::exists(path_to_config)) {
try {
std::filesystem::create_directories("/services/gh-wh-handler");
std::filesystem::create_directories(path_to_config);
} catch (std::exception &e) {
Logger::error("[Config] Error creating directory '/services/gh-wh-handler/': " + std::string(e.what()));
Logger::error("[Config] Error creating directory '" + path_to_config +"': " + std::string(e.what()));
return;
}
}
try {
std::ofstream config_file("/services/gh-wh-handler/config.json");
std::ofstream config_file(config_file_path);
config_file << config.dump(2);
config_file.close();
} catch (std::exception& e) {
Logger::error("[Config] Error creating config file: " + std::string(e.what()));
Logger::fatal("[Config] Error creating config file: " + std::string(e.what()));
}
}
@ -39,7 +38,21 @@ nlohmann::json Config::get_config(std::string config_file_path) {
config_file >> config;
config_file.close();
} catch (std::exception& e) {
Logger::error("Error loading config file: " + std::string(e.what()));
Logger::fatal("Error loading config file: " + std::string(e.what()));
}
if (config.is_null()) {
Logger::fatal("[Config] Config file is empty");
}
if (config.find("port") == config.end()) {
Logger::warn("[Config] Port not found in config file, using default port 65001");
config["port"] = 65001;
}
if (config.find("tokens") == config.end()) {
Logger::warn("[Config] Tokens not found in config file, using empty array. Private repositories will not be accessible.");
config["tokens"] = nlohmann::json::array();
}
Logger::success("[Config] Loaded config file: " + config_file_path);

View File

@ -131,12 +131,13 @@ crow::response update_files(const nlohmann::json& config_update_files, const nlo
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) {
Logger::error("[/update-files] Failed to create directories for " + local_path + ": " + std::string(e.what()));
continue;
}
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()));
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 + "'";

View File

@ -1,4 +1,5 @@
#include "logger.hpp"
#include <filesystem>
#include <iostream>
#include <fstream>
#include <ctime>
@ -33,8 +34,29 @@
std::ofstream Logger::log_file;
void Logger::init(std::string log_file_path) {
void Logger::init(std::string logs_dir) {
std::cout << "Initializing logger" << std::endl;
std::cout << "Logs directory: " << logs_dir << std::endl;
// check if logs_dir exists
if (!std::filesystem::exists(logs_dir)) {
try {
std::filesystem::create_directories(logs_dir);
} catch (std::exception &e) {
std::cerr << "Error creating logs directory: " << e.what() << std::endl;
std::exit(1);
}
}
// check if logs_dir ends with a slash
if (logs_dir.back() != '/') {
logs_dir += "/";
}
std::time_t now = std::time(nullptr);
std::tm *now_tm = std::localtime(&now);
char time_buffer[80];
std::strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d_%H-%M-%S", now_tm);
std::string log_file_path = logs_dir + "gh-wh-handler_" + time_buffer + ".log";
std::cout << "Log file: " << log_file_path << std::endl;
Logger::log_file.open(log_file_path, std::ios::app);
if (!Logger::log_file.is_open()) {
@ -68,7 +90,6 @@ void Logger::code(std::string message) {
}
void Logger::log(std::string message, std::string level) {
// Implement logger with terminal colors if terminal supports it
std::string formatted_message = "";
if (isatty(fileno(stdout))) {
if (level == "INFO ") {
@ -98,8 +119,8 @@ void Logger::log(std::string message, std::string level) {
if (level == "CODE") {
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
int term_width = w.ws_col;
formatted_message += "\n" + std::string(term_width - 1, '=') + "\n" + message + "\n" + std::string(term_width - 1, '=');
int term_width = w.ws_col > 250 ? 250 : w.ws_col - 1;
formatted_message += "\n" + std::string(term_width, '=') + "\n" + message + "\n" + std::string(term_width, '=');
} else {
formatted_message += "[" + level + "] " + message;
}
@ -110,15 +131,15 @@ void Logger::log(std::string message, std::string level) {
if (level == "CODE") {
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
int term_width = w.ws_col;
Logger::log_file << std::string(term_width - 1, '=') << std::endl << message << std::endl << std::string(term_width - 1, '=') << std::endl;
int term_width = w.ws_col > 250 ? 250 : w.ws_col - 1;
Logger::log_file << std::string(term_width, '=') << std::endl << message << std::endl << std::string(term_width, '=') << std::endl;
} else {
Logger::log_file << "[" << level << "] " << message << std::endl;
}
Logger::log_file.flush();
if (level == "FATAL") {
if (level == "FATAL ") {
std::exit(1);
}
}

View File

@ -15,8 +15,8 @@ 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 || argc > 3) {
std::cerr << "Usage: " << 0[argv] << " </path/to/config.json> [--config]" << std::endl;
if (argc < 3 || argc > 4) {
std::cerr << "Usage: " << 0[argv] << " </path/to/config.json> </path/to/logs_dir> [--config]" << std::endl;
return 1;
}
@ -26,10 +26,11 @@ int main(int argc, char **argv) {
std::strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d_%H-%M-%S", now_tm);
std::string config_file_path = 1[argv];
std::string log_file_path = config_file_path.substr(0, config_file_path.find_last_of("/")) + "/gh-wh-handler_" + time_buffer + ".log";
Logger::init(log_file_path);
std::string logs_dir = 2[argv];
if (argc == 3 && std::string(argv[2]) == "--config") {
Logger::init(logs_dir);
if (argc == 4 && std::string(argv[3]) == "--config") {
Config::open_config_menu();
return 0;
}
@ -37,7 +38,7 @@ int main(int argc, char **argv) {
// Check if config file exists
if (!std::filesystem::exists(config_file_path)) {
Logger::warn("Config file does not exist, creating...");
Config::create_config();
Config::create_config(config_file_path);
}
// Load configuration

View File

@ -23,41 +23,49 @@ Routes::Routes(nlohmann::json config) {
return crow::response(200, response.dump());
});
Logger::info("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) {
try {
return update_files(config_update_files, config_tokens, req);
} catch (const std::exception &e) {
Logger::error("Unknown error in update_files: " + std::string(e.what()));
nlohmann::json response = {
{"status", 500},
{"error", "Internal server error"}
};
return crow::response(500, response.dump());
}
});
if (!config_update_files.is_null()) {
Logger::info("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) {
try {
return update_files(config_update_files, config_tokens, req);
} catch (const std::exception &e) {
Logger::error("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("Registering route \"/run-scripts\"");
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) {
Logger::error("Unknown error in run_scripts: " + std::string(e.what()));
nlohmann::json response = {
{"status", 500},
{"error", "Internal server error"}
};
return crow::response(500, response.dump());
}
});
if (!config_run_scripts.is_null()) {
Logger::info("Registering route \"/run-scripts\"");
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) {
Logger::error("Unknown error in run_scripts: " + std::string(e.what()));
nlohmann::json response = {
{"status", 500},
{"error", "Internal server error"}
};
return crow::response(500, response.dump());
}
});
}
Logger::info("Starting server");
this->app.port(config["port"].get<int>()).multithreaded().run();
try {
this->app.port(config["port"].get<int>()).multithreaded().run();
} catch (const std::exception &e) {
Logger::fatal("Error starting server: " + std::string(e.what()));
}
Logger::info("Server stopped");
}