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 # CMake build directories
CMakeFiles/ /build/*
CMakeCache.txt !/build/config.json
cmake_install.cmake !/build/gh-wh-handler.service.in
*.cmake !/build/uninstall.cmake.in
*.cbp /CMakeFiles/
*.layout /CMakeCache.txt
*.stackdump /cmake_install.cmake
CPackConfig.cmake /*.cmake
Makefile /*.cbp
CTestTestfile.cmake /*.layout
install_manifest.txt /*.stackdump
/CPackConfig.cmake
/Makefile
/CTestTestfile.cmake
/install_manifest.txt
# Compiled binaries # Compiled binaries
/bin/ /bin/

View File

@ -48,15 +48,19 @@ set_target_properties(${EXECUTABLE_NAME} PROPERTIES LINK_FLAGS
# Install the executable # Install the executable
set(SERVICE_EXECUTABLE "/services/gh-wh-handler/${EXECUTABLE_NAME}") set(SERVICE_EXECUTABLE "/services/gh-wh-handler/${EXECUTABLE_NAME}")
set(SERVICE_CONFIG "/services/gh-wh-handler/config.json") set(SERVICE_CONFIG "/services/gh-wh-handler/config.json")
set(SERVICE_LOGS "/services/gh-wh-handler/logs")
configure_file( configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/gh-wh-handler.service.in" "${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) @ONLY)
install(CODE "file(MAKE_DIRECTORY /services/gh-wh-handler)") 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(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 install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
/services/gh-wh-handler/${EXECUTABLE_NAME} /usr/bin/gh-wh-handler)") /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) DESTINATION /etc/systemd/system)
install(CODE "execute_process(COMMAND systemctl daemon-reload)") install(CODE "execute_process(COMMAND systemctl daemon-reload)")
install(CODE "execute_process(COMMAND systemctl enable gh-wh-handler)") install(CODE "execute_process(COMMAND systemctl enable gh-wh-handler)")

View File

@ -2,9 +2,23 @@
## Simple C++ WebAPI to work with GitHub Webhooks ## 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 ### 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: Run the application using your configuration file:
```console ```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. 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/) - [CrowCpp](https://crowcpp.org/master/)
- [nlohmann::json](https://github.com/nlohmann/json) - [nlohmann::json](https://github.com/nlohmann/json)
#### Build the application: #### Build and install the application:
1. Clone the repository: 1. Clone the repository:
@ -47,7 +61,12 @@ cmake ..
sudo make install 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. The application is running on a systemd service, which is both enabled and started after installation.
@ -72,12 +91,37 @@ As of now, the configuration menu is not yet implemented so you have to create t
### Config File ### 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 ```json
{ {
"port": 65001, "port": 65001,
"update-files": { "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.
### `/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": { "owner/repo-name": {
"branch": "main", "branch": "main",
"files": { "files": {
@ -90,28 +134,9 @@ The configuration file can be found in `/services/gh-wh-handler/config.json` and
"..." "..."
] ]
} }
},
"run-scripts": {
"owner/repo-name": {
"branch": "main",
"actions": [
"command",
"script",
]
}
},
"tokens": {
"owner/repo-name": "token"
}
} }
``` ```
## 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.
## License ## License
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.

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 After=network.target
[Service] [Service]
ExecStart=@SERVICE_EXECUTABLE@ @SERVICE_CONFIG@ ExecStart=@SERVICE_EXECUTABLE@ @SERVICE_CONFIG@ @SERVICE_LOGS@
Restart=always Restart=always
Type=simple 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...") message(STATUS "[70%] Removing service executable from service directory...")
file(REMOVE /services/gh-wh-handler/@EXECUTABLE_NAME@) file(REMOVE /services/gh-wh-handler/@EXECUTABLE_NAME@)
message(STATUS "[85%] Removing service directory...") message(STATUS "[85%] Removing all log files...")
file(REMOVE /services/gh-wh-handler) execute_process(COMMAND rm -fr /services/gh-wh-handler/logs)
message(STATUS "[100%] Uninstallation complete!") message(STATUS "[100%] Uninstallation complete!")

View File

@ -5,7 +5,7 @@
class Config { class Config {
public: 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 nlohmann::json get_config(std::string config_file_path);
static void open_config_menu(); 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" #include "logger.hpp"
void Config::create_config() { void Config::create_config(std::string config_file_path) {
std::cout << "Creating config file" << std::endl; std::cout << "Creating config file" << std::endl;
nlohmann::json config = { nlohmann::json config = {
{"port", 65001}, {"port", 65001},
{"update-files", nlohmann::json::array()},
{"run-scripts", nlohmann::json::array()},
{"tokens", 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 { try {
std::filesystem::create_directories("/services/gh-wh-handler"); std::filesystem::create_directories(path_to_config);
} catch (std::exception &e) { } 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; return;
} }
} }
try { 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 << config.dump(2);
config_file.close(); config_file.close();
} catch (std::exception& e) { } 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 >> config;
config_file.close(); config_file.close();
} catch (std::exception& e) { } 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); Logger::success("[Config] Loaded config file: " + config_file_path);

View File

@ -131,6 +131,7 @@ crow::response update_files(const nlohmann::json& config_update_files, const nlo
std::string remote_path = file[0]; std::string remote_path = file[0];
std::string local_path = config_update_files[repo]["files"][remote_path]; std::string local_path = config_update_files[repo]["files"][remote_path];
if (local_path.find_last_of('/') != std::string::npos)
try { try {
std::filesystem::create_directories(local_path.substr(0, local_path.find_last_of('/'))); std::filesystem::create_directories(local_path.substr(0, local_path.find_last_of('/')));
} catch (const std::exception &e) { } catch (const std::exception &e) {

View File

@ -1,4 +1,5 @@
#include "logger.hpp" #include "logger.hpp"
#include <filesystem>
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <ctime> #include <ctime>
@ -33,8 +34,29 @@
std::ofstream Logger::log_file; 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 << "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; std::cout << "Log file: " << log_file_path << std::endl;
Logger::log_file.open(log_file_path, std::ios::app); Logger::log_file.open(log_file_path, std::ios::app);
if (!Logger::log_file.is_open()) { 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) { void Logger::log(std::string message, std::string level) {
// Implement logger with terminal colors if terminal supports it
std::string formatted_message = ""; std::string formatted_message = "";
if (isatty(fileno(stdout))) { if (isatty(fileno(stdout))) {
if (level == "INFO ") { if (level == "INFO ") {
@ -98,8 +119,8 @@ void Logger::log(std::string message, std::string level) {
if (level == "CODE") { if (level == "CODE") {
struct winsize w; struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
int term_width = w.ws_col; int term_width = w.ws_col > 250 ? 250 : w.ws_col - 1;
formatted_message += "\n" + std::string(term_width - 1, '=') + "\n" + message + "\n" + std::string(term_width - 1, '='); formatted_message += "\n" + std::string(term_width, '=') + "\n" + message + "\n" + std::string(term_width, '=');
} else { } else {
formatted_message += "[" + level + "] " + message; formatted_message += "[" + level + "] " + message;
} }
@ -110,15 +131,15 @@ void Logger::log(std::string message, std::string level) {
if (level == "CODE") { if (level == "CODE") {
struct winsize w; struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
int term_width = w.ws_col; int term_width = w.ws_col > 250 ? 250 : w.ws_col - 1;
Logger::log_file << std::string(term_width - 1, '=') << std::endl << message << std::endl << std::string(term_width - 1, '=') << std::endl; Logger::log_file << std::string(term_width, '=') << std::endl << message << std::endl << std::string(term_width, '=') << std::endl;
} else { } else {
Logger::log_file << "[" << level << "] " << message << std::endl; Logger::log_file << "[" << level << "] " << message << std::endl;
} }
Logger::log_file.flush(); Logger::log_file.flush();
if (level == "FATAL") { if (level == "FATAL ") {
std::exit(1); std::exit(1);
} }
} }

View File

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

View File

@ -23,6 +23,7 @@ Routes::Routes(nlohmann::json config) {
return crow::response(200, response.dump()); return crow::response(200, response.dump());
}); });
if (!config_update_files.is_null()) {
Logger::info("Registering route \"/update-files\""); Logger::info("Registering route \"/update-files\"");
CROW_ROUTE(this->app, "/update-files") CROW_ROUTE(this->app, "/update-files")
.methods("POST"_method) .methods("POST"_method)
@ -39,7 +40,9 @@ Routes::Routes(nlohmann::json config) {
return crow::response(500, response.dump()); return crow::response(500, response.dump());
} }
}); });
}
if (!config_run_scripts.is_null()) {
Logger::info("Registering route \"/run-scripts\""); Logger::info("Registering route \"/run-scripts\"");
CROW_ROUTE(this->app, "/run-scripts") CROW_ROUTE(this->app, "/run-scripts")
.methods("POST"_method) .methods("POST"_method)
@ -56,8 +59,13 @@ Routes::Routes(nlohmann::json config) {
return crow::response(500, response.dump()); return crow::response(500, response.dump());
} }
}); });
}
Logger::info("Starting server"); Logger::info("Starting server");
try {
this->app.port(config["port"].get<int>()).multithreaded().run(); 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"); Logger::info("Server stopped");
} }