refactor
This commit is contained in:
parent
53771396ca
commit
5cfb47ce92
@ -11,7 +11,6 @@
|
||||
#include "myirc/ref.hpp"
|
||||
#include "myirc/irc_coroutine.hpp"
|
||||
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/log/expressions.hpp>
|
||||
@ -22,10 +21,6 @@
|
||||
#include <memory>
|
||||
|
||||
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<SaslMechanism>
|
||||
{
|
||||
if (settings.sasl_mechanism == "PLAIN" &&
|
||||
not settings.sasl_authcid.empty()
|
||||
) {
|
||||
return std::make_unique<SaslPlain>(
|
||||
settings.sasl_authcid,
|
||||
settings.sasl_authzid,
|
||||
settings.sasl_password);
|
||||
|
||||
} else if (settings.sasl_mechanism == "EXTERNAL") {
|
||||
return std::make_unique<SaslExternal>(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<SaslEcdsa>(
|
||||
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<Webhooks> webhook
|
||||
@ -110,7 +78,7 @@ static auto start(
|
||||
webhook->clear_connection();
|
||||
auto timer = std::make_shared<boost::asio::steady_timer>(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();
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <myirc/openssl_utils.hpp>
|
||||
|
||||
#define TOML_ENABLE_FORMATTERS 0
|
||||
#include <toml++/toml.hpp>
|
||||
|
||||
@ -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<myirc::SaslMechanism>
|
||||
{
|
||||
if (settings.sasl_mechanism == "PLAIN" &&
|
||||
not settings.sasl_authcid.empty()
|
||||
) {
|
||||
return std::make_unique<myirc::SaslPlain>(
|
||||
settings.sasl_authcid,
|
||||
settings.sasl_authzid,
|
||||
settings.sasl_password);
|
||||
|
||||
} else if (settings.sasl_mechanism == "EXTERNAL") {
|
||||
return std::make_unique<myirc::SaslExternal>(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<myirc::SaslEcdsa>(
|
||||
settings.sasl_authcid,
|
||||
settings.sasl_authzid,
|
||||
std::move(sasl_key));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <myirc/sasl_mechanism.hpp>
|
||||
|
||||
#include <istream>
|
||||
#include <string>
|
||||
|
||||
@ -32,3 +34,5 @@ struct Settings
|
||||
|
||||
static auto from_stream(std::istream &in) -> Settings;
|
||||
};
|
||||
|
||||
auto configure_sasl(const Settings &settings) -> std::unique_ptr<myirc::SaslMechanism>;
|
||||
|
119
driver/web.cpp
119
driver/web.cpp
@ -15,13 +15,14 @@
|
||||
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 {
|
||||
|
||||
// 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<http::string_body> 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<Webhooks> 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<Webhooks> 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 <class Body, class Allocator>
|
||||
static auto handle_request(
|
||||
auto handle_request(
|
||||
std::shared_ptr<Webhooks> self,
|
||||
http::request<Body, http::basic_fields<Allocator>> &&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<Webhooks> self) -> boost::asio::awaitable<void>
|
||||
{
|
||||
beast::tcp_stream stream{std::move(socket)};
|
||||
beast::flat_buffer buffer;
|
||||
http::request<http::string_body> 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<Webhooks> self
|
||||
@ -214,6 +231,7 @@ auto accept_loop(
|
||||
}
|
||||
}
|
||||
|
||||
// Launch the listening sockets
|
||||
auto spawn_webhook(
|
||||
boost::asio::io_context &io,
|
||||
const std::shared_ptr<Webhooks> 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<myirc::Connection> 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> 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<std::string, void (*)(std::shared_ptr<Webhooks>, const myirc::Bot::Command &)> webhook_commands{
|
||||
{"add-credential", [](std::shared_ptr<Webhooks> 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<std::string, void (*)(std::shared_ptr<Webhooks>, const myirc::Bot::Comm
|
||||
}
|
||||
}},
|
||||
{"drop-credential", [](std::shared_ptr<Webhooks> 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<std::string, void (*)(std::shared_ptr<Webhooks>, 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<std::string, void (*)(std::shared_ptr<Webhooks>, 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;
|
||||
}
|
||||
|
||||
|
@ -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<myirc::Connection> connection_;
|
||||
|
||||
// Buffered events in case connection was inactive when event was received
|
||||
std::vector<Notice> events_;
|
||||
std::vector<std::pair<std::string, std::string>> 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<myirc::Connection> connection) -> void;
|
||||
auto clear_connection() -> void;
|
||||
auto save_settings() const -> void;
|
||||
|
Loading…
x
Reference in New Issue
Block a user