xbot/driver/web.cpp

263 lines
7.2 KiB
C++
Raw Normal View History

2025-02-01 11:04:33 -08:00
#include "web.hpp"
2025-02-01 20:57:57 -08:00
#include <boost/beast.hpp>
#include <boost/log/trivial.hpp>
#include <chrono>
#include <fstream>
#include <vector>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
namespace websocket = beast::websocket;
using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp>
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 <class Body, class Allocator>
auto handle_request(
std::shared_ptr<GithubWebhook> self,
http::request<Body, http::basic_fields<Allocator>> &&req
) -> http::message_generator
{
self->add_event({"project", "message"});
http::response<http::string_body> 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<GithubWebhook> self) -> boost::asio::awaitable<void>
{
beast::tcp_stream stream{std::move(socket)};
beast::flat_buffer buffer;
http::request<http::string_body> 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<GithubWebhook> self
) -> boost::asio::awaitable<void>
{
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<GithubWebhook> webhook
) -> boost::asio::awaitable<void>
{
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<GithubWebhook>
{
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<GithubWebhook>(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)}
};
}