From 8c9678708b5a7c1867377cf931f287ebe2b0ccb4 Mon Sep 17 00:00:00 2001 From: Eric Mertens Date: Sun, 2 Feb 2025 12:40:55 -0800 Subject: [PATCH] checkpoint initial event processing --- CMakeLists.txt | 2 +- driver/CMakeLists.txt | 2 +- driver/web.cpp | 151 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 134 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b40e7df..3aaf126 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ find_package(OpenSSL REQUIRED) pkg_check_modules(LIBHS libhs REQUIRED IMPORTED_TARGET) -set(BOOST_INCLUDE_LIBRARIES asio log signals2 endian beast) +set(BOOST_INCLUDE_LIBRARIES asio log signals2 endian beast json) set(BOOST_ENABLE_CMAKE ON) include(FetchContent) FetchContent_Declare( diff --git a/driver/CMakeLists.txt b/driver/CMakeLists.txt index ca7a45b..0c17000 100644 --- a/driver/CMakeLists.txt +++ b/driver/CMakeLists.txt @@ -7,7 +7,7 @@ add_executable(xbot target_link_libraries(xbot PRIVATE myirc OpenSSL::SSL - Boost::signals2 Boost::log Boost::asio Boost::beast + Boost::signals2 Boost::log Boost::asio Boost::beast Boost::json tomlplusplus_tomlplusplus PkgConfig::LIBHS mysocks5 mybase64) diff --git a/driver/web.cpp b/driver/web.cpp index 7aca4ad..40851fc 100644 --- a/driver/web.cpp +++ b/driver/web.cpp @@ -1,10 +1,15 @@ #include "web.hpp" #include +#include #include +#include + +#include #include #include +#include #include namespace beast = boost::beast; // from @@ -32,23 +37,129 @@ auto report_error(std::exception_ptr eptr) -> void } } +static auto simple_response(http::status status, unsigned version, bool keep_alive) -> http::message_generator +{ + http::response res{status, version}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.keep_alive(keep_alive); + res.content_length(0); + return res; +} + +static auto compute_signature(const std::string_view secret, const std::string_view body) -> std::string +{ + unsigned int digest_length = EVP_MAX_MD_SIZE; + unsigned char digest[EVP_MAX_MD_SIZE]; + + HMAC(EVP_sha256(), secret.data(), secret.size(), reinterpret_cast(body.data()), body.size(), digest, &digest_length); + + std::stringstream ss; + ss << "sha256="; + ss << std::hex << std::setfill('0'); + for (unsigned int i = 0; i < digest_length; i++) + { + ss << std::setw(2) << static_cast(digest[i]); + } + return ss.str(); +} + +static auto process_event( + std::shared_ptr self, + const std::string_view notify_user, + const std::string_view event, + const boost::json::value &json +) -> void +{ + auto &project = json.as_object(); + + std::string full_name; + if (project.contains("repository")) + { + full_name = std::string{project.at("repository").as_object().at("full_name").as_string()}; + } + else if (project.contains("organization")) + { + full_name = std::string{project.at("organization").as_object().at("login").as_string()}; + } + else + { + BOOST_LOG_TRIVIAL(warning) << "No repository or organization detected"; + return; + } + + const auto &settings = self->settings_.projects.at(full_name); + + // Ensure that this sender is authorized to send for this project + if (settings.credential_name != notify_user) + { + BOOST_LOG_TRIVIAL(warning) << "Credential mismatch for " << full_name << " wanted: " << settings.credential_name << " got: " << notify_user; + return; + } + + if (not settings.enabled) + { + return; + } + + const auto message = std::string{event} + " on " + full_name; + self->add_event({settings.channel, message}); +} + template -auto handle_request( +static auto handle_request( std::shared_ptr self, http::request> &&req ) -> http::message_generator { - self->add_event({"project", "message"}); + BOOST_LOG_TRIVIAL(info) << "HTTP request " << req.method_string() << " " << req.target(); - http::response res{http::int_to_status(200), req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.keep_alive(req.keep_alive()); + if (not req.target().starts_with("/notify/")) + { + BOOST_LOG_TRIVIAL(warning) << "HTTP Bad target: " << req.target(); + return simple_response(http::status::not_found, req.version(), req.keep_alive()); + } + std::string notify_user = req.target().substr(8); - std::string reply_text = "Hello, world!"; - res.content_length(reply_text.size()); - res.body() = std::move(reply_text); + if (req.method() != http::verb::post) + { + BOOST_LOG_TRIVIAL(warning) << "HTTP Bad method: " << req.method_string(); + return simple_response(http::status::method_not_allowed, req.version(), req.keep_alive()); + } - return res; + const auto event = req["x-github-event"]; + const auto signature = req["x-hub-signature-256"]; + if (event.empty() || signature.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "HTTP Missing headers"; + return simple_response(http::status::bad_request, req.version(), req.keep_alive()); + } + + const auto credential_cursor = self->settings_.credentials.find(notify_user); + if (credential_cursor == self->settings_.credentials.end()) + { + BOOST_LOG_TRIVIAL(warning) << "HTTP Unknown user: " << notify_user; + return simple_response(http::status::unauthorized, req.version(), req.keep_alive()); + } + const auto &secret = credential_cursor->second; + + const auto expected_signature = compute_signature(secret, req.body()); + if (signature != expected_signature) + { + BOOST_LOG_TRIVIAL(warning) << "HTTP Bad signature: " << signature << " expected: " << expected_signature; + return simple_response(http::status::unauthorized, req.version(), req.keep_alive()); + } + + try + { + process_event(self, notify_user, event, boost::json::parse(req.body())); + } + catch (const boost::system::system_error &e) + { + BOOST_LOG_TRIVIAL(error) << "HTTP Failed to process event: " << e.what(); + return simple_response(http::status::internal_server_error, req.version(), req.keep_alive()); + } + + return simple_response(http::status::ok, req.version(), req.keep_alive()); } auto read_loop(tcp::socket socket, std::shared_ptr self) -> boost::asio::awaitable @@ -71,11 +182,11 @@ auto read_loop(tcp::socket socket, std::shared_ptr self) -> boost } else if (ec) { - co_return; + throw boost::system::system_error{ec}; } + const auto keep_alive = req.keep_alive(); auto msg = handle_request(self, std::move(req)); - const auto keep_alive = msg.keep_alive(); co_await beast::async_write(stream, std::move(msg), net::use_awaitable); @@ -128,7 +239,7 @@ auto spawn_webhook( auto start_webhook( boost::asio::io_context &io, - const char * webhook_settings_filename + const char *webhook_settings_filename ) -> std::shared_ptr { std::ifstream webhook_settings_file{webhook_settings_filename}; @@ -184,13 +295,13 @@ auto ProjectSettings::to_toml() const -> toml::table { events_array.emplace_back(event); } - + toml::array authorized_accounts_array; for (const auto &account : authorized_accounts) { authorized_accounts_array.emplace_back(account); } -\ + return toml::table{ {"channel", channel}, {"credential_name", credential_name}, @@ -205,7 +316,7 @@ auto WebhookSettings::from_toml(const toml::table &v) -> WebhookSettings WebhookSettings result; result.host = v["host"].value_or(""s); result.service = v["service"].value_or("http"s); - + if (const auto credentials = v["credentials"].as_array()) { for (const auto &credential : *credentials) @@ -214,7 +325,8 @@ auto WebhookSettings::from_toml(const toml::table &v) -> WebhookSettings { result.credentials.emplace( (*credential_table)["name"].value_or(""s), - (*credential_table)["key"].value_or(""s)); + (*credential_table)["key"].value_or(""s) + ); } } } @@ -227,7 +339,8 @@ auto WebhookSettings::from_toml(const toml::table &v) -> WebhookSettings { result.projects.emplace( (*project_table)["name"].value_or(""s), - ProjectSettings::from_toml(*project_table)); + ProjectSettings::from_toml(*project_table) + ); } } } @@ -240,7 +353,7 @@ auto WebhookSettings::to_toml() const -> toml::table toml::array credential_tables; for (const auto &[name, key] : credentials) { - credential_tables.emplace_back(toml::table { + credential_tables.emplace_back(toml::table{ {"name", name}, {"key", key} }); @@ -260,4 +373,4 @@ auto WebhookSettings::to_toml() const -> toml::table {"credentials", std::move(credential_tables)}, {"projects", std::move(project_tables)} }; -} \ No newline at end of file +}