#include "web.hpp" #include #include #include #include #include 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 { auto report_error(std::exception_ptr eptr) -> void { if (eptr) { try { std::rethrow_exception(eptr); } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "An error occurred: " << e.what(); } } } template auto handle_request( std::shared_ptr self, http::request> &&req ) -> http::message_generator { self->add_event({"project", "message"}); 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()); std::string reply_text = "Hello, world!"; res.content_length(reply_text.size()); res.body() = std::move(reply_text); return res; } 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; for (;;) { 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; } else if (ec) { co_return; } 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); if (!keep_alive) { stream.socket().shutdown(tcp::socket::shutdown_send, ec); co_return; } } } auto accept_loop( tcp::acceptor acceptor, std::shared_ptr self ) -> boost::asio::awaitable { for (;;) { auto socket = co_await acceptor.async_accept(net::use_awaitable); boost::asio::co_spawn( acceptor.get_executor(), read_loop(std::move(socket), self), report_error ); } } auto spawn_webhook( boost::asio::io_context &io, const std::shared_ptr webhook ) -> boost::asio::awaitable { tcp::resolver resolver{io}; auto results = co_await resolver.async_resolve(webhook->settings_.host, webhook->settings_.service, tcp::resolver::passive, boost::asio::use_awaitable); for (auto &&result : results) { const auto endpoint = result.endpoint(); BOOST_LOG_TRIVIAL(info) << "HTTP: Listening on " << endpoint; tcp::acceptor acceptor{io}; acceptor.open(endpoint.protocol()); acceptor.set_option(net::socket_base::reuse_address(true)); acceptor.bind(endpoint); acceptor.listen(net::socket_base::max_listen_connections); boost::asio::co_spawn(io, accept_loop(std::move(acceptor), webhook), report_error); } } } // namespace auto start_webhook( boost::asio::io_context &io, const char * webhook_settings_filename ) -> std::shared_ptr { std::ifstream webhook_settings_file{webhook_settings_filename}; if (!webhook_settings_file) { BOOST_LOG_TRIVIAL(error) << "Unable to open webhook settings file"; std::exit(1); } auto webhook_settings = toml::parse(webhook_settings_file); WebhookSettings settings = WebhookSettings::from_toml(webhook_settings); BOOST_LOG_TRIVIAL(info) << "Webhook settings: " << settings.to_toml(); auto webhook = std::make_shared(std::move(settings)); boost::asio::co_spawn(io, spawn_webhook(io, webhook), report_error); return webhook; } auto GithubWebhook::write_event(WebhookEvent event) -> void { connection_->send_notice("glguy", event.channel + ": " + event.message); } auto ProjectSettings::from_toml(const toml::table &v) -> ProjectSettings { ProjectSettings result; result.channel = v["channel"].value_or(""s); result.credential_name = v["credential_name"].value_or(""s); result.enabled = v["enabled"].value_or(false); if (const auto events = v["events"].as_array()) { for (const auto &event : *events) { result.events.insert(event.value_or(""s)); } } if (const auto accounts = v["authorized_accounts"].as_array()) { for (const auto &account : *accounts) { result.authorized_accounts.insert(account.value_or(""s)); } } return result; } auto ProjectSettings::to_toml() const -> toml::table { toml::array events_array; for (const auto &event : events) { 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}, {"enabled", enabled}, {"events", std::move(events_array)}, {"authorized_accounts", std::move(authorized_accounts_array)} }; } 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) { if (auto credential_table = credential.as_table()) { result.credentials.emplace( (*credential_table)["name"].value_or(""s), (*credential_table)["key"].value_or(""s)); } } } if (const auto projects = v["projects"].as_array()) { for (const auto &project : *projects) { if (auto project_table = project.as_table()) { result.projects.emplace( (*project_table)["name"].value_or(""s), ProjectSettings::from_toml(*project_table)); } } } return result; } auto WebhookSettings::to_toml() const -> toml::table { toml::array credential_tables; for (const auto &[name, key] : credentials) { credential_tables.emplace_back(toml::table { {"name", name}, {"key", key} }); } toml::array project_tables; for (const auto &[name, project] : projects) { auto tab = project.to_toml(); tab.emplace("name", name); project_tables.emplace_back(std::move(tab)); } return toml::table{ {"host", host}, {"service", service}, {"credentials", std::move(credential_tables)}, {"projects", std::move(project_tables)} }; }