From 5cfb47ce92bace15ad0a285d333d55896dd6c9a2 Mon Sep 17 00:00:00 2001 From: Eric Mertens Date: Sun, 2 Feb 2025 16:20:52 -0800 Subject: [PATCH] refactor --- driver/main.cpp | 40 ++------------- driver/settings.cpp | 30 +++++++++++ driver/settings.hpp | 4 ++ driver/web.cpp | 119 +++++++++++++++++++++++++------------------- driver/web.hpp | 11 +--- 5 files changed, 108 insertions(+), 96 deletions(-) diff --git a/driver/main.cpp b/driver/main.cpp index 8817678..4a1f2ee 100644 --- a/driver/main.cpp +++ b/driver/main.cpp @@ -11,7 +11,6 @@ #include "myirc/ref.hpp" #include "myirc/irc_coroutine.hpp" - #include #include #include @@ -22,10 +21,6 @@ #include using namespace std::literals; -using myirc::SaslMechanism; -using myirc::SaslPlain; -using myirc::SaslExternal; -using myirc::SaslEcdsa; using myirc::Bot; using myirc::Client; using myirc::Connection; @@ -33,34 +28,7 @@ using myirc::Registration; using myirc::Challenge; using myirc::Ref; -auto configure_sasl(const Settings &settings) -> std::unique_ptr -{ - if (settings.sasl_mechanism == "PLAIN" && - not settings.sasl_authcid.empty() - ) { - return std::make_unique( - settings.sasl_authcid, - settings.sasl_authzid, - settings.sasl_password); - - } else if (settings.sasl_mechanism == "EXTERNAL") { - return std::make_unique(settings.sasl_authzid); - - } else if ( - settings.sasl_mechanism == "ECDSA" && - not settings.sasl_authcid.empty() && - not settings.sasl_key_file.empty() - ) { - if (auto sasl_key = myirc::key_from_file(settings.sasl_key_file, settings.sasl_key_password)) - return std::make_unique( - settings.sasl_authcid, - settings.sasl_authzid, - std::move(sasl_key)); - } - return nullptr; -} - -static auto start( +static auto start_irc( boost::asio::io_context &io, const Settings &settings, std::shared_ptr webhook @@ -110,7 +78,7 @@ static auto start( webhook->clear_connection(); auto timer = std::make_shared(io); timer->expires_after(5s); - timer->async_wait([&io, &settings, timer, webhook](auto) { start(io, settings, webhook); }); + timer->async_wait([&io, &settings, timer, webhook](auto) { start_irc(io, settings, webhook); }); } ); @@ -155,9 +123,7 @@ auto main(int argc, char *argv[]) -> int } const auto settings = get_settings(argv[1]); auto io = boost::asio::io_context{}; - auto webhooks = start_webhook(io, argv[2]); - - start(io, settings, webhooks); + start_irc(io, settings, webhooks); io.run(); } diff --git a/driver/settings.cpp b/driver/settings.cpp index c36888b..af1b560 100644 --- a/driver/settings.cpp +++ b/driver/settings.cpp @@ -1,5 +1,7 @@ #include "settings.hpp" +#include + #define TOML_ENABLE_FORMATTERS 0 #include @@ -29,3 +31,31 @@ auto Settings::from_stream(std::istream &in) -> Settings .use_tls = config["use_tls"].value_or(false), }; } + +auto configure_sasl(const Settings &settings) -> std::unique_ptr +{ + if (settings.sasl_mechanism == "PLAIN" && + not settings.sasl_authcid.empty() + ) { + return std::make_unique( + settings.sasl_authcid, + settings.sasl_authzid, + settings.sasl_password); + + } else if (settings.sasl_mechanism == "EXTERNAL") { + return std::make_unique(settings.sasl_authzid); + + } else if ( + settings.sasl_mechanism == "ECDSA" && + not settings.sasl_authcid.empty() && + not settings.sasl_key_file.empty() + ) { + if (auto sasl_key = myirc::key_from_file(settings.sasl_key_file, settings.sasl_key_password)) + return std::make_unique( + settings.sasl_authcid, + settings.sasl_authzid, + std::move(sasl_key)); + } + return nullptr; +} + diff --git a/driver/settings.hpp b/driver/settings.hpp index d94a5b1..48a5966 100644 --- a/driver/settings.hpp +++ b/driver/settings.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -32,3 +34,5 @@ struct Settings static auto from_stream(std::istream &in) -> Settings; }; + +auto configure_sasl(const Settings &settings) -> std::unique_ptr; diff --git a/driver/web.cpp b/driver/web.cpp index 8fcd1e9..6345e82 100644 --- a/driver/web.cpp +++ b/driver/web.cpp @@ -15,13 +15,14 @@ namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from -namespace websocket = beast::websocket; using tcp = net::ip::tcp; // from using namespace std::literals; namespace { +// Used as the completion handler for coroutines in this module to print +// failure reasons to the log. auto report_error(std::exception_ptr eptr) -> void { if (eptr) @@ -32,12 +33,17 @@ auto report_error(std::exception_ptr eptr) -> void } catch (const std::exception &e) { - BOOST_LOG_TRIVIAL(error) << "An error occurred: " << e.what(); + BOOST_LOG_TRIVIAL(error) << "HTTP coroutine failed: " << e.what(); } } } -static auto simple_response(http::status status, unsigned version, bool keep_alive) -> http::message_generator +// Construct a simple, empty reply using the given status code. +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); @@ -46,7 +52,8 @@ static auto simple_response(http::status status, unsigned version, bool keep_ali return res; } -static auto compute_signature(const std::string_view secret, const std::string_view body) -> std::string +// Compute the expected signature string for the POST body. +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]; @@ -63,23 +70,37 @@ static auto compute_signature(const std::string_view secret, const std::string_v return ss.str(); } -static auto process_event( +// This event is ready to actually announce +auto announce_event( + std::shared_ptr self, + const ProjectSettings &project, + const std::string full_name, + const std::string_view event_name, + const boost::json::value event +) -> void { + const auto message = std::string{event_name} + " on " + full_name; + self->send_notice(project.channel, std::move(message)); +} + +// Determine if this event should be announced +auto process_event( std::shared_ptr self, const std::string_view notify_user, - const std::string_view event, + const std::string_view event_name, const boost::json::value &json ) -> void { - auto &project = json.as_object(); + auto &event = json.as_object(); + // Determine the project name. Repositories use: user/project. Organization events use: organization std::string full_name; - if (project.contains("repository")) + if (event.contains("repository")) { - full_name = std::string{project.at("repository").as_object().at("full_name").as_string()}; + full_name = std::string{event.at("repository").as_object().at("full_name").as_string()}; } - else if (project.contains("organization")) + else if (event.contains("organization")) { - full_name = std::string{project.at("organization").as_object().at("login").as_string()}; + full_name = std::string{event.at("organization").as_object().at("login").as_string()}; } else { @@ -96,17 +117,18 @@ static auto process_event( return; } - if (not settings.enabled) + if (not settings.enabled || not settings.events.contains(std::string{event_name})) { + // quietly ignore events we don't care about return; } - const auto message = std::string{event} + " on " + full_name; - self->send_notice({settings.channel, message}); + announce_event(self, settings, std::move(full_name), event_name, std::move(event)); } +// Process the HTTP request validating its structure and signature. template -static auto handle_request( +auto handle_request( std::shared_ptr self, http::request> &&req ) -> http::message_generator @@ -162,42 +184,37 @@ static auto handle_request( return simple_response(http::status::ok, req.version(), req.keep_alive()); } +// Repeatedly read HTTP requests off a socket and reply to them auto read_loop(tcp::socket socket, std::shared_ptr self) -> boost::asio::awaitable { beast::tcp_stream stream{std::move(socket)}; beast::flat_buffer buffer; http::request req; + bool keep_alive = true; - for (;;) + while (keep_alive) { req.clear(); stream.expires_after(30s); - boost::system::error_code ec; co_await http::async_read(stream, buffer, req, net::redirect_error(net::use_awaitable, ec)); if (ec == http::error::end_of_stream) { - stream.socket().shutdown(tcp::socket::shutdown_send, ec); - co_return; + break; } else if (ec) { throw boost::system::system_error{ec}; } - const auto keep_alive = req.keep_alive(); + keep_alive = req.keep_alive(); auto msg = handle_request(self, std::move(req)); - co_await beast::async_write(stream, std::move(msg), net::use_awaitable); - - if (!keep_alive) - { - stream.socket().shutdown(tcp::socket::shutdown_send, ec); - co_return; - } } + stream.socket().shutdown(tcp::socket::shutdown_both); } +// Repeatedly accept new connections on a listening socket auto accept_loop( tcp::acceptor acceptor, std::shared_ptr self @@ -214,6 +231,7 @@ auto accept_loop( } } +// Launch the listening sockets auto spawn_webhook( boost::asio::io_context &io, const std::shared_ptr webhook @@ -383,24 +401,24 @@ auto WebhookSettings::to_toml() const -> toml::table } // Either emit the event now or save it until a connection is set -auto Webhooks::send_notice(Notice notice) -> void +auto Webhooks::send_notice(std::string_view target, std::string message) -> void { if (connection_) { - connection_->send_notice(notice.target, notice.message); + connection_->send_notice(target, message); } else { - events_.emplace_back(std::move(notice)); + events_.emplace_back(std::string(target), std::move(message)); } } auto Webhooks::set_connection(std::shared_ptr connection) -> void { connection_ = std::move(connection); - for (auto &&event : events_) + for (auto &&[target, message] : std::move(events_)) { - connection_->send_notice(event.target, event.message); + connection_->send_notice(target, message); } events_.clear(); } @@ -414,25 +432,30 @@ static auto reply_to(std::shared_ptr webhooks, const myirc::Bot::Comma { if (cmd.target.starts_with("#")) { - webhooks->send_notice({std::string{cmd.target}, std::move(message)}); + webhooks->send_notice(cmd.target, std::move(message)); } else { - webhooks->send_notice({std::string{cmd.nick()}, std::move(message)}); + webhooks->send_notice(cmd.nick(), std::move(message)); } } -static auto authorized_for_project(const ProjectSettings &project, const std::string_view nick) -> bool +// Operators are authorized for all projects otherwise nickserv account names can be added to individual projects. +static auto authorized_for_project( + const myirc::Bot::Command &cmd, + const ProjectSettings &project, + const std::string_view nick +) -> bool { - return project.authorized_accounts.find(std::string{nick}) != project.authorized_accounts.end(); + return !cmd.oper.empty() || project.authorized_accounts.contains(std::string{nick}); } std::map, const myirc::Bot::Command &)> webhook_commands{ {"add-credential", [](std::shared_ptr webhooks, const myirc::Bot::Command &cmd) { - //if (cmd.oper.empty()) - //{ - // return; - //} + if (cmd.oper.empty()) + { + return; + } std::istringstream iss{std::string{cmd.arguments}}; std::string name, key; if (iss >> name >> key) @@ -443,10 +466,10 @@ std::map, const myirc::Bot::Comm } }}, {"drop-credential", [](std::shared_ptr webhooks, const myirc::Bot::Command &cmd) { - //if (cmd.oper.empty()) - //{ - // return; - //} + if (cmd.oper.empty()) + { + return; + } std::istringstream iss{std::string{cmd.arguments}}; std::string name; if (iss >> name) @@ -464,15 +487,13 @@ std::map, const myirc::Bot::Comm auto cursor = webhooks->settings_.projects.find(name); if (cursor == webhooks->settings_.projects.end()) { - reply_to(webhooks, cmd, "Unknown project " + name); return; } auto &project = cursor->second; - if (not authorized_for_project(project, cmd.account)) + if (not authorized_for_project(cmd, project, cmd.account)) { - reply_to(webhooks, cmd, "Unauthorized to enable project " + name); return; } @@ -493,15 +514,13 @@ std::map, const myirc::Bot::Comm auto cursor = webhooks->settings_.projects.find(name); if (cursor == webhooks->settings_.projects.end()) { - webhooks->send_notice({std::string{cmd.nick()}, "Unknown project " + name}); return; } auto &project = cursor->second; - if (not authorized_for_project(project, cmd.account)) + if (not authorized_for_project(cmd, project, cmd.account)) { - reply_to(webhooks, cmd, "Unauthorized to disable project " + name); return; } diff --git a/driver/web.hpp b/driver/web.hpp index 2d29c02..020240f 100644 --- a/driver/web.hpp +++ b/driver/web.hpp @@ -48,18 +48,11 @@ struct WebhookSettings { }; class Webhooks { -public: - struct Notice { - std::string target; - std::string message; - }; -private: - // IRC connection to announce on; could be empty std::shared_ptr connection_; // Buffered events in case connection was inactive when event was received - std::vector events_; + std::vector> events_; const char * settings_file; @@ -73,7 +66,7 @@ public: } // Either emit the event now or save it until a connection is set - auto send_notice(Notice event) -> void; + auto send_notice(std::string_view, std::string) -> void; auto set_connection(std::shared_ptr connection) -> void; auto clear_connection() -> void; auto save_settings() const -> void;