checkpoint initial event processing
This commit is contained in:
parent
4c119c6138
commit
8c9678708b
@ -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(
|
||||
|
@ -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)
|
||||
|
151
driver/web.cpp
151
driver/web.cpp
@ -1,10 +1,15 @@
|
||||
#include "web.hpp"
|
||||
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
|
||||
#include <openssl/hmac.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace beast = boost::beast; // from <boost/beast.hpp>
|
||||
@ -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<http::string_body> 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<const unsigned char *>(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<int>(digest[i]);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static auto process_event(
|
||||
std::shared_ptr<GithubWebhook> 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 <class Body, class Allocator>
|
||||
auto handle_request(
|
||||
static auto handle_request(
|
||||
std::shared_ptr<GithubWebhook> self,
|
||||
http::request<Body, http::basic_fields<Allocator>> &&req
|
||||
) -> http::message_generator
|
||||
{
|
||||
self->add_event({"project", "message"});
|
||||
BOOST_LOG_TRIVIAL(info) << "HTTP request " << req.method_string() << " " << req.target();
|
||||
|
||||
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());
|
||||
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<GithubWebhook> self) -> boost::asio::awaitable<void>
|
||||
@ -71,11 +182,11 @@ auto read_loop(tcp::socket socket, std::shared_ptr<GithubWebhook> 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<GithubWebhook>
|
||||
{
|
||||
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)}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user