Compare commits

...

43 Commits

Author SHA1 Message Date
7793e8b02c simpler initialization 2025-03-09 13:52:46 -07:00
04a092b9a3 Clean up the reply codes 2025-02-08 11:08:55 -08:00
4fc8d4d49c join channels that we're announcing to 2025-02-07 17:34:03 -08:00
44ef4c0689 client should clean up 2025-02-06 20:52:42 -08:00
847a149e12 consolidate connection 2025-02-05 09:47:52 -08:00
2b4bb1f071 unify with snowcone 2025-02-05 09:24:47 -08:00
5f2439e5af consolidate command interface 2025-02-03 09:35:50 -08:00
5aec8397bb initial formatter framework 2025-02-02 19:46:11 -08:00
39a4d84a54 add commands to add/drop access 2025-02-02 19:20:15 -08:00
5cfb47ce92 refactor 2025-02-02 16:20:52 -08:00
53771396ca rename 2025-02-02 15:02:08 -08:00
178d7dfcfe implement a few commands 2025-02-02 14:56:34 -08:00
8c9678708b checkpoint initial event processing 2025-02-02 12:40:55 -08:00
4c119c6138 initial webhook infrastructure 2025-02-01 20:57:57 -08:00
1a6ec835ed make a myirc namespace 2025-02-01 11:04:33 -08:00
8324a496b6 initial rate limit support 2025-01-31 16:14:13 -08:00
68429bc1e4 pull sasl config out of registration 2025-01-31 09:36:08 -08:00
7728bc6aee use shared-ptr more consistently 2025-01-31 08:38:14 -08:00
eb01b304e3 implement ecdsa 2025-01-30 16:39:23 -08:00
15c48ab1dc fdrop snotes 2025-01-30 13:36:18 -08:00
206b4c9d89 operspy who 2025-01-30 12:55:40 -08:00
0e708e72f8 clean up ref use 2025-01-30 11:56:03 -08:00
c3650ba38d fix move semantics of Ref 2025-01-30 11:47:26 -08:00
281937e2c5 split up driver and library 2025-01-30 09:28:28 -08:00
5218ea0892 snotes 2025-01-30 08:27:57 -08:00
0e88f3bd7a more snotes 2025-01-29 20:43:03 -08:00
8d544e31de openssl utils 2025-01-29 18:41:28 -08:00
Eric Mertens
5f32505b93 add KILL 2025-01-29 16:32:08 -08:00
Eric Mertens
a9efb96837 more send commands 2025-01-29 15:17:19 -08:00
Eric Mertens
bdf7202e7d missing commands 2025-01-29 11:13:20 -08:00
Eric Mertens
ef223f9cc1 Treat ERR_NOMOTD as registration completion 2025-01-29 10:54:48 -08:00
Eric Mertens
5801a5404a all Ref to support uprefable types 2025-01-29 09:54:17 -08:00
f5b49ebf66 spambot snote 2025-01-28 22:54:55 -08:00
b0f254eb13 two more snotes 2025-01-28 21:43:44 -08:00
9f49baa6ad set challenge slot 2025-01-28 20:42:42 -08:00
1aa56453cc fixup challenge 2025-01-28 20:01:51 -08:00
Eric Mertens
72b2756f34 add openssl_errors.hpp 2025-01-28 19:05:00 -08:00
Eric Mertens
40bd9186da add ref.hpp 2025-01-28 19:04:36 -08:00
Eric Mertens
de19233dd7 challenge.cpp 2025-01-28 19:02:30 -08:00
Eric Mertens
21090f05ab error handling in challenge 2025-01-28 17:15:13 -08:00
763bcffe23 add all the reply argument counts 2025-01-27 20:02:31 -08:00
e7aba11d05 support client certificates 2025-01-27 18:55:19 -08:00
41b1148005 add on_chat layer 2025-01-27 17:46:07 -08:00
48 changed files with 2995 additions and 1249 deletions

View File

@@ -10,7 +10,7 @@ find_package(OpenSSL REQUIRED)
pkg_check_modules(LIBHS libhs REQUIRED IMPORTED_TARGET) pkg_check_modules(LIBHS libhs REQUIRED IMPORTED_TARGET)
set(BOOST_INCLUDE_LIBRARIES asio log signals2 endian) set(BOOST_INCLUDE_LIBRARIES asio log signals2 endian beast json)
set(BOOST_ENABLE_CMAKE ON) set(BOOST_ENABLE_CMAKE ON)
include(FetchContent) include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
@@ -26,37 +26,7 @@ FetchContent_Declare(
FetchContent_MakeAvailable(tomlplusplus) FetchContent_MakeAvailable(tomlplusplus)
FetchContent_MakeAvailable(Boost) FetchContent_MakeAvailable(Boost)
add_custom_command(
OUTPUT irc_commands.inc
COMMAND
gperf
-C -Z IrcCommandHash -K text -L C++ -t
--output-file irc_commands.inc
${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
VERBATIM)
add_subdirectory(mybase64) add_subdirectory(mybase64)
add_subdirectory(mysocks5) add_subdirectory(mysocks5)
add_subdirectory(myirc)
add_executable(xbot add_subdirectory(driver)
main.cpp
irc_commands.inc
bot.cpp
connection.cpp
irc_coroutine.cpp
ircmsg.cpp
registration.cpp
sasl_mechanism.cpp
client.cpp
settings.cpp
snote.cpp
)
target_include_directories(xbot PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(xbot PRIVATE
OpenSSL::SSL
Boost::signals2 Boost::log Boost::asio
tomlplusplus_tomlplusplus
PkgConfig::LIBHS
mysocks5 mybase64)

36
bot.hpp
View File

@@ -1,36 +0,0 @@
#pragma once
#include "client.hpp"
#include <boost/signals2.hpp>
#include <memory>
struct Command
{
std::string_view source;
std::string_view target;
std::string_view oper;
std::string_view account;
std::string_view command;
std::string_view arguments;
};
struct Bot : std::enable_shared_from_this<Bot>
{
std::shared_ptr<Client> self_;
char command_prefix_;
boost::signals2::signal<void(const Command &)> sig_command;
Bot(std::shared_ptr<Client> self)
: self_{std::move(self)}
, command_prefix_{'!'}
{}
auto on_ircmsg(IrcCommand, const IrcMsg &) -> void;
auto process_command(std::string_view message, const IrcMsg &msg) -> void;
static auto start(std::shared_ptr<Client>) -> std::shared_ptr<Bot>;
auto shutdown() -> void;
};

View File

@@ -1,89 +0,0 @@
#pragma once
#include "connection.hpp"
#include "sasl_mechanism.hpp"
#include <string>
#include <unordered_set>
struct Connection;
struct IrcMsg;
enum class Casemap
{
Rfc1459,
Rfc1459_Strict,
Ascii,
};
/**
* @brief Thread to track this connection's identity, and IRC state.
*
*/
class Client
{
Connection &connection_;
std::string nickname_;
std::string mode_;
std::unordered_set<std::string> channels_;
// RPL_ISUPPORT state
std::unordered_map<std::string, std::string> isupport_;
std::unique_ptr<SaslMechanism> sasl_mechanism_;
Casemap casemap_;
std::string channel_prefix_;
std::unordered_map<std::string, std::string> caps_available_;
std::unordered_set<std::string> caps_;
auto on_welcome(const IrcMsg &irc) -> void;
auto on_isupport(const IrcMsg &irc) -> void;
auto on_nick(const IrcMsg &irc) -> void;
auto on_umodeis(const IrcMsg &irc) -> void;
auto on_join(const IrcMsg &irc) -> void;
auto on_kick(const IrcMsg &irc) -> void;
auto on_part(const IrcMsg &irc) -> void;
auto on_mode(const IrcMsg &irc) -> void;
auto on_cap(const IrcMsg &irc) -> void;
auto on_authenticate(std::string_view) -> void;
auto on_registered() -> void;
public:
boost::signals2::signal<void()> sig_registered;
boost::signals2::signal<void(const std::unordered_map<std::string, std::string> &)> sig_cap_ls;
Client(Connection &connection)
: connection_{connection}
, casemap_{Casemap::Rfc1459}
, channel_prefix_{"#&"}
{
}
auto get_connection() -> Connection & { return connection_; }
static auto start(Connection &) -> std::shared_ptr<Client>;
auto start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void;
auto get_connection() const -> std::shared_ptr<Connection>
{
return connection_.shared_from_this();
}
auto get_my_nickname() const -> const std::string &;
auto get_my_mode() const -> const std::string &;
auto get_my_channels() const -> const std::unordered_set<std::string> &;
auto list_caps() -> void;
auto is_my_nick(std::string_view nick) const -> bool;
auto is_my_mask(std::string_view mask) const -> bool;
auto is_channel(std::string_view name) const -> bool;
auto casemap(std::string_view) const -> std::string;
auto casemap_compare(std::string_view, std::string_view) const -> int;
auto shutdown() -> void;
};

View File

@@ -1,122 +0,0 @@
#pragma once
#include "irc_command.hpp"
#include "ircmsg.hpp"
#include "snote.hpp"
#include "stream.hpp"
#include <boost/asio.hpp>
#include <boost/signals2.hpp>
#include <list>
#include <memory>
#include <string>
template <typename T, int(*UpRef)(T*), void(*Free)(T*)>
class Ref {
struct Deleter { auto operator()(auto ptr) { Free(ptr); }};
std::unique_ptr<T, Deleter> obj;
public:
Ref() = default;
Ref(T* t) : obj{t} { if (t) UpRef(t); }
auto get() const -> T* { return obj.get(); }
};
struct ConnectSettings
{
bool tls;
std::string host;
std::uint16_t port;
Ref<X509, X509_up_ref, X509_free> client_cert;
Ref<EVP_PKEY, EVP_PKEY_up_ref, EVP_PKEY_free> client_key;
std::string verify;
std::string sni;
std::string socks_host;
std::uint16_t socks_port;
std::string socks_user;
std::string socks_pass;
};
class Connection : public std::enable_shared_from_this<Connection>
{
private:
Stream stream_;
boost::asio::steady_timer watchdog_timer_;
std::list<std::string> write_strings_;
bool write_posted_;
// Set true when watchdog triggers.
// Set false when message received.
bool stalled_;
// AUTHENTICATE support
std::string authenticate_buffer_;
auto write_buffers() -> void;
auto dispatch_line(char *line) -> void;
static constexpr std::chrono::seconds watchdog_duration = std::chrono::seconds{30};
auto watchdog() -> void;
auto watchdog_activity() -> void;
auto connect(ConnectSettings settings) -> boost::asio::awaitable<void>;
auto on_authenticate(std::string_view) -> void;
/// Build and send well-formed IRC message from individual parameters
auto write_irc(std::string) -> void;
auto write_irc(std::string, std::string_view) -> void;
template <typename... Args>
auto write_irc(std::string front, std::string_view next, Args... rest) -> void;
public:
boost::signals2::signal<void()> sig_connect;
boost::signals2::signal<void()> sig_disconnect;
boost::signals2::signal<void(IrcCommand, const IrcMsg &)> sig_ircmsg;
boost::signals2::signal<void(SnoteMatch &)> sig_snote;
boost::signals2::signal<void(std::string_view)> sig_authenticate;
Connection(boost::asio::io_context &io);
/// Write bytes into the socket.
auto write_line(std::string message) -> void;
auto get_executor() -> boost::asio::any_io_executor
{
return stream_.get_executor();
}
auto start(ConnectSettings) -> void;
auto close() -> void;
auto send_ping(std::string_view) -> void;
auto send_pong(std::string_view) -> void;
auto send_pass(std::string_view) -> void;
auto send_user(std::string_view, std::string_view) -> void;
auto send_nick(std::string_view) -> void;
auto send_join(std::string_view) -> void;
auto send_cap_ls() -> void;
auto send_cap_end() -> void;
auto send_cap_req(std::string_view) -> void;
auto send_privmsg(std::string_view, std::string_view) -> void;
auto send_notice(std::string_view, std::string_view) -> void;
auto send_authenticate(std::string_view message) -> void;
auto send_authenticate_encoded(std::string_view message) -> void;
auto send_authenticate_abort() -> void;
};
template <typename... Args>
auto Connection::write_irc(std::string front, std::string_view next, Args... rest) -> void
{
using namespace std::literals;
if (next.empty() || next.front() == ':' || next.find_first_of("\r\n \0"sv) != next.npos)
{
throw std::runtime_error{"bad irc argument"};
}
front += " ";
front += next;
write_irc(std::move(front), rest...);
}

13
driver/CMakeLists.txt Normal file
View File

@@ -0,0 +1,13 @@
add_executable(xbot
main.cpp
settings.cpp
web.cpp
)
target_link_libraries(xbot PRIVATE
myirc
OpenSSL::SSL
Boost::signals2 Boost::log Boost::asio Boost::beast Boost::json
tomlplusplus_tomlplusplus
PkgConfig::LIBHS
mysocks5 mybase64)

133
driver/main.cpp Normal file
View File

@@ -0,0 +1,133 @@
#include "settings.hpp"
#include "web.hpp"
#include "myirc/bot.hpp"
#include "myirc/challenge.hpp"
#include "myirc/client.hpp"
#include "myirc/connection.hpp"
#include "myirc/openssl_utils.hpp"
#include "myirc/registration.hpp"
#include "myirc/sasl_mechanism.hpp"
#include "myirc/ref.hpp"
#include "myirc/irc_coroutine.hpp"
#include <boost/asio.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/expressions.hpp>
#include <openssl/pem.h>
#include <fstream>
#include <memory>
using namespace std::literals;
using myirc::Bot;
using myirc::Client;
using myirc::Connection;
using myirc::Registration;
using myirc::Challenge;
using myirc::Ref;
static auto start_irc(
boost::asio::io_context &io,
const Settings &settings,
std::shared_ptr<Webhooks> webhook
) -> void
{
Ref<X509> tls_cert;
if (settings.use_tls && not settings.tls_cert_file.empty())
{
tls_cert = myirc::cert_from_file(settings.tls_cert_file);
}
Ref<EVP_PKEY> tls_key;
if (settings.use_tls && not settings.tls_key_file.empty())
{
tls_key = myirc::key_from_file(settings.tls_key_file, settings.tls_key_password);
}
const auto connection = std::make_shared<Connection>(io);
const auto client = Client::start(connection);
const auto bot = Bot::start(client);
Registration::start({
.nickname = settings.nickname,
.realname = settings.realname,
.username = settings.username,
.password = settings.password,
.sasl_mechanism = configure_sasl(settings),
}, client);
// Configure CHALLENGE on registration if applicable
if (not settings.challenge_username.empty() && not settings.challenge_key_file.empty()) {
if (auto key = myirc::key_from_file(settings.challenge_key_file, settings.challenge_key_password)) {
client->sig_registered.connect([&settings, client, key = std::move(key)]() {
Challenge::start(client, settings.challenge_username, key);
});
}
}
client->sig_registered.connect([client, webhook]() {
webhook->set_client(client);
});
// On disconnect reconnect in 5 seconds
// connection is captured in the disconnect handler so it can keep itself alive
connection->sig_disconnect.connect(
[&io, &settings, connection, webhook](auto) {
webhook->clear_client();
auto timer = std::make_shared<boost::asio::steady_timer>(io);
timer->expires_after(5s);
timer->async_wait([&io, &settings, timer, webhook](auto) { start_irc(io, settings, webhook); });
}
);
// Dispatch commands to the webhook logic
bot->sig_command.connect([webhook, connection](const Bot::Command &cmd) {
auto cursor = webhook_commands.find(std::string{cmd.command});
if (cursor != webhook_commands.end()) {
try {
cursor->second(webhook, cmd);
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(error) << "Command handler failed: " << e.what();
}
}
});
connection->start({
.tls = settings.use_tls,
.host = settings.host,
.port = settings.service,
.verify = settings.tls_hostname,
.client_cert = std::move(tls_cert),
.client_key = std::move(tls_key),
});
}
static auto get_settings(const char * const filename) -> Settings
{
if (auto config_stream = std::ifstream{filename})
{
return Settings::from_stream(config_stream);
}
else
{
BOOST_LOG_TRIVIAL(error) << "Unable to open configuration";
std::exit(1);
}
}
auto main(int argc, char *argv[]) -> int
{
//boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::warning);
if (argc != 3) {
BOOST_LOG_TRIVIAL(error) << "Bad arguments";
return 1;
}
const auto settings = get_settings(argv[1]);
auto io = boost::asio::io_context{};
auto webhooks = start_webhook(io, argv[2]);
start_irc(io, settings, webhooks);
io.run();
}

61
driver/settings.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "settings.hpp"
#include <myirc/openssl_utils.hpp>
#define TOML_ENABLE_FORMATTERS 0
#include <toml++/toml.hpp>
auto Settings::from_stream(std::istream &in) -> Settings
{
const auto config = toml::parse(in);
return Settings{
.host = config["host"].value_or(std::string{}),
.service = config["port"].value_or(std::uint16_t{6667}),
.password = config["password"].value_or(std::string{}),
.username = config["username"].value_or(std::string{}),
.realname = config["realname"].value_or(std::string{}),
.nickname = config["nickname"].value_or(std::string{}),
.sasl_mechanism = config["sasl_mechanism"].value_or(std::string{}),
.sasl_authcid = config["sasl_authcid"].value_or(std::string{}),
.sasl_authzid = config["sasl_authzid"].value_or(std::string{}),
.sasl_password = config["sasl_password"].value_or(std::string{}),
.sasl_key_file = config["sasl_key_file"].value_or(std::string{}),
.sasl_key_password = config["sasl_key_password"].value_or(std::string{}),
.tls_hostname = config["tls_hostname"].value_or(std::string{}),
.tls_cert_file = config["tls_cert_file"].value_or(std::string{}),
.tls_key_file = config["tls_key_file"].value_or(std::string{}),
.tls_key_password = config["tls_key_password"].value_or(std::string{}),
.challenge_username = config["challenge_username"].value_or(std::string{}),
.challenge_key_file = config["challenge_key_file"].value_or(std::string{}),
.challenge_key_password = config["challenge_key_password"].value_or(std::string{}),
.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;
}

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <myirc/sasl_mechanism.hpp>
#include <istream> #include <istream>
#include <string> #include <string>
@@ -16,9 +18,21 @@ struct Settings
std::string sasl_authcid; std::string sasl_authcid;
std::string sasl_authzid; std::string sasl_authzid;
std::string sasl_password; std::string sasl_password;
std::string sasl_key_file;
std::string sasl_key_password;
std::string tls_hostname; std::string tls_hostname;
std::string tls_cert_file;
std::string tls_key_file;
std::string tls_key_password;
std::string challenge_username;
std::string challenge_key_file;
std::string challenge_key_password;
bool use_tls; bool use_tls;
static auto from_stream(std::istream &in) -> Settings; static auto from_stream(std::istream &in) -> Settings;
}; };
auto configure_sasl(const Settings &settings) -> std::unique_ptr<myirc::SaslMechanism>;

624
driver/web.cpp Normal file
View File

@@ -0,0 +1,624 @@
#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>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp>
using namespace std::literals;
namespace {
std::map<std::string, void(*)(std::shared_ptr<Webhooks>, const ProjectSettings &, std::string_view, const boost::json::object &)> formatters {
{"push", [](std::shared_ptr<Webhooks> webhooks, const ProjectSettings &project, std::string_view full_name, const boost::json::object &body) {
webhooks->send_notice(project.channel, "push");
}},
};
// 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)
{
try
{
std::rethrow_exception(eptr);
}
catch (const std::exception &e)
{
BOOST_LOG_TRIVIAL(error) << "HTTP coroutine failed: " << e.what();
}
}
}
// 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);
res.keep_alive(keep_alive);
res.content_length(0);
return res;
}
// 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];
HMAC(EVP_sha256(), secret.data(), static_cast<int>(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();
}
// 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 event_name,
const boost::json::value &json
) -> void
{
auto &event = json.as_object();
// Determine the project name. Repositories use: user/project. Organization events use: organization
std::string full_name;
if (event.contains("repository"))
{
full_name = std::string{event.at("repository").as_object().at("full_name").as_string()};
}
else if (event.contains("organization"))
{
full_name = std::string{event.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 || not settings.events.contains(event_name))
{
// quietly ignore events we don't care about
return;
}
auto formatter_cursor = formatters.find(event_name);
if (formatter_cursor != formatters.end()) {
formatter_cursor->second(self, settings, full_name, event);
}
}
// Process the HTTP request validating its structure and signature.
template <class Body, class Allocator>
auto handle_request(
std::shared_ptr<Webhooks> self,
http::request<Body, http::basic_fields<Allocator>> &&req
) -> http::message_generator
{
BOOST_LOG_TRIVIAL(info) << "HTTP request " << req.method_string() << " " << req.target();
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);
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());
}
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());
}
// 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;
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)
{
break;
}
else if (ec)
{
throw boost::system::system_error{ec};
}
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);
}
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
) -> 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
);
}
}
// Launch the listening sockets
auto spawn_webhook(
boost::asio::io_context &io,
const std::shared_ptr<Webhooks> 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<Webhooks>
{
auto webhook = std::make_shared<Webhooks>(webhook_settings_filename);
webhook->load_settings();
boost::asio::co_spawn(io, spawn_webhook(io, webhook), report_error);
return webhook;
}
auto Webhooks::load_settings() -> void
{
std::ifstream webhook_settings_file{settings_filename_};
if (!webhook_settings_file)
{
BOOST_LOG_TRIVIAL(error) << "Unable to open webhook settings file";
}
auto webhook_settings = toml::parse(webhook_settings_file);
settings_ = WebhookSettings::from_toml(webhook_settings);
}
auto Webhooks::save_settings() const -> void
{
std::ofstream webhook_settings_file{settings_filename_};
if (!webhook_settings_file)
{
BOOST_LOG_TRIVIAL(error) << "Unable to open webhook settings file";
return;
}
webhook_settings_file << settings_.to_toml() << "\n";
}
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)}
};
}
// Either emit the event now or save it until a connection is set
auto Webhooks::send_notice(std::string_view target, std::string message) -> void
{
if (client_)
{
client_->send_notice(target, message);
}
}
auto Webhooks::refresh_channels() const -> void
{
if (not client_) return;
std::stringstream ss;
std::set<std::string_view> added;
bool first = true;
for (auto &&[_, project] : settings_.projects) {
auto &channel = project.channel;
if (!channel.empty() &&
!client_->is_on_channel(channel) &&
added.insert(channel).second)
{
if (first) {
first = false;
} else {
ss << ",";
}
ss << channel;
}
}
if (not first) {
client_->send_join(ss.str());
}
}
auto Webhooks::set_client(std::shared_ptr<myirc::Client> client) -> void
{
client_ = std::move(client);
refresh_channels();
}
auto Webhooks::clear_client() -> void
{
client_.reset();
}
static auto reply_to(std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd, std::string message) -> void
{
if (cmd.target.starts_with("#"))
{
webhooks->send_notice(cmd.target, std::move(message));
}
else
{
webhooks->send_notice(cmd.nick(), std::move(message));
}
}
// 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 !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{
{"announce", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
std::istringstream iss{std::string{cmd.arguments}};
std::string name, mode;
if (iss >> name >> mode)
{
auto &project = webhooks->settings_.projects.at(name);
if (not authorized_for_project(cmd, project, cmd.account))
{
return;
}
if (mode == "on") {
project.enabled = true;
reply_to(webhooks, cmd, "Enabled project " + name);
} else if (mode == "off") {
project.enabled = false;
reply_to(webhooks, cmd, "Disabled project " + name);
} else {
return;
}
webhooks->save_settings();
}
}},
{"event", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
std::istringstream iss{std::string{cmd.arguments}};
std::string name, mode;
if (iss >> name >> mode)
{
auto &project = webhooks->settings_.projects.at(name);
if (not authorized_for_project(cmd, project, cmd.account))
{
return;
}
if (mode == "list") {
std::stringstream ss;
ss << "Events for " << name << ":";
for (auto &&event : project.events) {
ss << " " << event;
}
reply_to(webhooks, cmd, ss.str());
return;
}
unsigned n_added = 0, n_removed = 0, n_skipped = 0, n_unknown = 0;
if (mode == "add") {
while (iss >> name) {
if (formatters.contains(name)) {
const auto [_, added] = project.events.insert(name);
if (added) { n_added++; } else { n_skipped++; }
} else {
n_unknown++;
}
}
} else if (mode == "del") {
while (iss >> name) {
if (formatters.contains(name)) {
const auto removed = project.events.erase(name);
if (removed) { n_removed++; } else { n_skipped++; }
} else {
n_unknown++;
}
}
}
webhooks->save_settings();
std::stringstream ss;
ss << "Events updated:";
if (n_added) { ss << " added " << n_added; }
if (n_removed) { ss << " removed " << n_removed; }
if (n_skipped) { ss << " skipped " << n_skipped; }
if (n_unknown) { ss << " unknown " << n_unknown; }
reply_to(webhooks, cmd, ss.str());
}
}},
{"auth", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
if (cmd.oper.empty())
{
return;
}
std::istringstream iss{std::string{cmd.arguments}};
std::string name, mode;
if (iss >> name >> mode)
{
auto &project = webhooks->settings_.projects.at(name);
if (mode == "list") {
std::stringstream ss;
ss << "Authorized accounts:";
for (auto &&event : project.authorized_accounts) {
ss << " " << event;
}
reply_to(webhooks, cmd, ss.str());
return;
}
unsigned n_added = 0, n_removed = 0, n_skipped = 0;
if (mode == "add") {
while (iss >> name) {
const auto [_, added] = project.authorized_accounts.insert(name);
if (added) { n_added++; } else { n_skipped++; }
}
} else if (mode == "del") {
while (iss >> name) {
const auto removed = project.authorized_accounts.erase(name);
if (removed) { n_removed++; } else { n_skipped++; }
}
}
webhooks->save_settings();
std::stringstream ss;
ss << "Authorized accounts updated:";
if (n_added) { ss << " added " << n_added; }
if (n_removed) { ss << " removed " << n_removed; }
if (n_skipped) { ss << " skipped " << n_skipped; }
reply_to(webhooks, cmd, ss.str());
}
}},
{"setchannel", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
if (cmd.oper.empty())
{
return;
}
std::istringstream iss{std::string{cmd.arguments}};
std::string name, channel;
if (iss >> name >> channel)
{
auto &project = webhooks->settings_.projects.at(name);
project.channel = channel;
webhooks->save_settings();
reply_to(webhooks, cmd, "Channel assigned");
webhooks->refresh_channels();
}
}},
{"rehash", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
if (cmd.oper.empty())
{
return;
}
webhooks->load_settings();
reply_to(webhooks, cmd, "Rehashed");
}},
};

76
driver/web.hpp Normal file
View File

@@ -0,0 +1,76 @@
#pragma once
#include <myirc/bot.hpp>
#include <myirc/client.hpp>
#include <toml++/toml.hpp>
#include <boost/asio.hpp>
#include <boost/signals2.hpp>
#include <memory>
#include <map>
#include <set>
struct ProjectSettings {
// *** Administrative settings ***
// IRC channel to announce to
std::string channel;
// name extracted from notify/$user
std::string credential_name;
// Authorized accounts can edit the event list
std::set<std::string> authorized_accounts;
// *** User settings ***
// Events to announce
std::set<std::string> events;
// Whether to announce events
bool enabled;
auto to_toml() const -> toml::table;
static auto from_toml(const toml::table &v) -> ProjectSettings;
};
struct WebhookSettings {
std::string host;
std::string service;
std::map<std::string, std::string> credentials;
std::map<std::string, ProjectSettings> projects;
auto to_toml() const -> toml::table;
static auto from_toml(const toml::table &v) -> WebhookSettings;
};
class Webhooks {
// IRC connection to announce on; could be empty
std::shared_ptr<myirc::Client> client_;
const char * settings_filename_;
public:
WebhookSettings settings_;
Webhooks(const char * settings_filename)
: settings_filename_{settings_filename}
{
}
// Either emit the event now or save it until a connection is set
auto send_notice(std::string_view, std::string) -> void;
auto set_client(std::shared_ptr<myirc::Client> client) -> void;
auto clear_client() -> void;
auto save_settings() const -> void;
auto load_settings() -> void;
auto refresh_channels() const -> void;
};
auto start_webhook(boost::asio::io_context &io, const char *) -> std::shared_ptr<Webhooks>;
extern std::map<std::string, void(*)(std::shared_ptr<Webhooks>, const myirc::Bot::Command &)> webhook_commands;

View File

@@ -1,278 +0,0 @@
struct RecognizedCommand {
char const* text;
IrcCommand command;
std::size_t min_args;
std::size_t max_args;
};
%%
001, IrcCommand::RPL_WELCOME, 2, 2
002, IrcCommand::RPL_YOURHOST, 2, 2
003, IrcCommand::RPL_CREATED, 2, 2
004, IrcCommand::RPL_MYINFO, 5, 6
005, IrcCommand::RPL_ISUPPORT, 2, 15
008, IrcCommand::RPL_SNOMASK, 3, 3
010, IrcCommand::RPL_REDIR, 4, 4
015, IrcCommand::RPL_MAP, 2, 2
017, IrcCommand::RPL_MAPEND, 2, 2
043, IrcCommand::RPL_SAVENICK, 3, 3
200, IrcCommand::RPL_TRACELINK, 5, 5
201, IrcCommand::RPL_TRACECONNECTING, 4, 4
202, IrcCommand::RPL_TRACEHANDSHAKE, 4, 4
203, IrcCommand::RPL_TRACEUNKNOWN, 6, 6
204, IrcCommand::RPL_TRACEOPERATOR
205, IrcCommand::RPL_TRACEUSER
206, IrcCommand::RPL_TRACESERVER
208, IrcCommand::RPL_TRACENEWTYPE
209, IrcCommand::RPL_TRACECLASS
211, IrcCommand::RPL_STATSLINKINFO
212, IrcCommand::RPL_STATSCOMMANDS
213, IrcCommand::RPL_STATSCLINE
214, IrcCommand::RPL_STATSNLINE
215, IrcCommand::RPL_STATSILINE, 8, 8
216, IrcCommand::RPL_STATSKLINE
217, IrcCommand::RPL_STATSQLINE
218, IrcCommand::RPL_STATSYLINE
219, IrcCommand::RPL_ENDOFSTATS
220, IrcCommand::RPL_STATSPLINE
221, IrcCommand::RPL_UMODEIS, 2, 2
224, IrcCommand::RPL_STATSFLINE
225, IrcCommand::RPL_STATSDLINE
234, IrcCommand::RPL_SERVLIST
235, IrcCommand::RPL_SERVLISTEND
241, IrcCommand::RPL_STATSLLINE
242, IrcCommand::RPL_STATSUPTIME
243, IrcCommand::RPL_STATSOLINE
244, IrcCommand::RPL_STATSHLINE
245, IrcCommand::RPL_STATSSLINE
247, IrcCommand::RPL_STATSXLINE
248, IrcCommand::RPL_STATSULINE
249, IrcCommand::RPL_STATSDEBUG
250, IrcCommand::RPL_STATSCONN, 2, 2
251, IrcCommand::RPL_LUSERCLIENT, 2, 2
252, IrcCommand::RPL_LUSEROP
253, IrcCommand::RPL_LUSERUNKNOWN
254, IrcCommand::RPL_LUSERCHANNELS
255, IrcCommand::RPL_LUSERME, 2, 2
256, IrcCommand::RPL_ADMINME, 3, 3
257, IrcCommand::RPL_ADMINLOC1, 2, 2
258, IrcCommand::RPL_ADMINLOC2, 2, 2
259, IrcCommand::RPL_ADMINEMAIL, 2, 2
261, IrcCommand::RPL_TRACELOG
262, IrcCommand::RPL_ENDOFTRACE, 3, 3
263, IrcCommand::RPL_LOAD2HI
265, IrcCommand::RPL_LOCALUSERS, 4, 4
266, IrcCommand::RPL_GLOBALUSERS, 4, 4
270, IrcCommand::RPL_PRIVS, 3, 3
276, IrcCommand::RPL_WHOISCERTFP, 3, 3
281, IrcCommand::RPL_ACCEPTLIST, 1, 15
282, IrcCommand::RPL_ENDOFACCEPT, 2, 2
300, IrcCommand::RPL_NONE
301, IrcCommand::RPL_AWAY
302, IrcCommand::RPL_USERHOST
303, IrcCommand::RPL_ISON, 2, 2
304, IrcCommand::RPL_TEXT
305, IrcCommand::RPL_UNAWAY
306, IrcCommand::RPL_NOWAWAY
310, IrcCommand::RPL_WHOISHELPOP
311, IrcCommand::RPL_WHOISUSER, 6, 6
312, IrcCommand::RPL_WHOISSERVER, 4, 4
313, IrcCommand::RPL_WHOISOPERATOR, 3, 3
314, IrcCommand::RPL_WHOWASUSER, 6, 6
369, IrcCommand::RPL_ENDOFWHOWAS, 3, 3
316, IrcCommand::RPL_WHOISCHANOP
317, IrcCommand::RPL_WHOISIDLE, 5, 5
318, IrcCommand::RPL_ENDOFWHOIS, 3, 3
319, IrcCommand::RPL_WHOISCHANNELS, 3, 3
320, IrcCommand::RPL_WHOISSPECIAL, 3, 3
321, IrcCommand::RPL_LISTSTART, 3, 3
322, IrcCommand::RPL_LIST, 4, 4
323, IrcCommand::RPL_LISTEND, 2, 2
324, IrcCommand::RPL_CHANNELMODEIS, 3, 3
325, IrcCommand::RPL_CHANNELMLOCK
328, IrcCommand::RPL_CHANNELURL
329, IrcCommand::RPL_CREATIONTIME, 3, 3
330, IrcCommand::RPL_WHOISLOGGEDIN, 4, 4
331, IrcCommand::RPL_NOTOPIC, 3, 3
332, IrcCommand::RPL_TOPIC, 3, 3
333, IrcCommand::RPL_TOPICWHOTIME, 4, 4
338, IrcCommand::RPL_WHOISACTUALLY
341, IrcCommand::RPL_INVITING
342, IrcCommand::RPL_SUMMONING
346, IrcCommand::RPL_INVITELIST
347, IrcCommand::RPL_ENDOFINVITELIST
348, IrcCommand::RPL_EXCEPTLIST
349, IrcCommand::RPL_ENDOFEXCEPTLIST, 3, 3
351, IrcCommand::RPL_VERSION, 4, 4
352, IrcCommand::RPL_WHOREPLY
354, IrcCommand::RPL_WHOSPCRPL
315, IrcCommand::RPL_ENDOFWHO
353, IrcCommand::RPL_NAMREPLY, 4, 4
360, IrcCommand::RPL_WHOWASREAL
366, IrcCommand::RPL_ENDOFNAMES, 3, 3
361, IrcCommand::RPL_KILLDONE
362, IrcCommand::RPL_CLOSING
363, IrcCommand::RPL_CLOSEEND
364, IrcCommand::RPL_LINKS, 4, 4
365, IrcCommand::RPL_ENDOFLINKS, 3, 3
367, IrcCommand::RPL_BANLIST, 5, 5
368, IrcCommand::RPL_ENDOFBANLIST, 3, 3
371, IrcCommand::RPL_INFO, 2, 2
372, IrcCommand::RPL_MOTD, 2, 2
373, IrcCommand::RPL_INFOSTART
374, IrcCommand::RPL_ENDOFINFO, 2, 2
375, IrcCommand::RPL_MOTDSTART, 2, 2
376, IrcCommand::RPL_ENDOFMOTD, 2, 2
378, IrcCommand::RPL_WHOISHOST
381, IrcCommand::RPL_YOUREOPER, 2, 2
382, IrcCommand::RPL_REHASHING
384, IrcCommand::RPL_MYPORTIS
385, IrcCommand::RPL_NOTOPERANYMORE
386, IrcCommand::RPL_RSACHALLENGE
391, IrcCommand::RPL_TIME, 3, 3
392, IrcCommand::RPL_USERSSTART
393, IrcCommand::RPL_USERS
394, IrcCommand::RPL_ENDOFUSERS
395, IrcCommand::RPL_NOUSERS
396, IrcCommand::RPL_HOSTHIDDEN
401, IrcCommand::ERR_NOSUCHNICK, 3, 3
402, IrcCommand::ERR_NOSUCHSERVER, 3, 3
403, IrcCommand::ERR_NOSUCHCHANNEL
404, IrcCommand::ERR_CANNOTSENDTOCHAN
405, IrcCommand::ERR_TOOMANYCHANNELS
406, IrcCommand::ERR_WASNOSUCHNICK
407, IrcCommand::ERR_TOOMANYTARGETS
409, IrcCommand::ERR_NOORIGIN
410, IrcCommand::ERR_INVALIDCAPCMD
411, IrcCommand::ERR_NORECIPIENT
412, IrcCommand::ERR_NOTEXTTOSEND
413, IrcCommand::ERR_NOTOPLEVEL
414, IrcCommand::ERR_WILDTOPLEVEL
415, IrcCommand::ERR_MSGNEEDREGGEDNICK
416, IrcCommand::ERR_TOOMANYMATCHES
421, IrcCommand::ERR_UNKNOWNCOMMAND
422, IrcCommand::ERR_NOMOTD, 2, 2
423, IrcCommand::ERR_NOADMININFO
424, IrcCommand::ERR_FILEERROR
431, IrcCommand::ERR_NONICKNAMEGIVEN
432, IrcCommand::ERR_ERRONEUSNICKNAME
433, IrcCommand::ERR_NICKNAMEINUSE, 3, 3
435, IrcCommand::ERR_BANNICKCHANGE
436, IrcCommand::ERR_NICKCOLLISION
437, IrcCommand::ERR_UNAVAILRESOURCE
438, IrcCommand::ERR_NICKTOOFAST
440, IrcCommand::ERR_SERVICESDOWN
441, IrcCommand::ERR_USERNOTINCHANNEL
442, IrcCommand::ERR_NOTONCHANNEL, 3, 3
443, IrcCommand::ERR_USERONCHANNEL
444, IrcCommand::ERR_NOLOGIN
445, IrcCommand::ERR_SUMMONDISABLED
446, IrcCommand::ERR_USERSDISABLED
451, IrcCommand::ERR_NOTREGISTERED
456, IrcCommand::ERR_ACCEPTFULL
457, IrcCommand::ERR_ACCEPTEXIST
458, IrcCommand::ERR_ACCEPTNOT
461, IrcCommand::ERR_NEEDMOREPARAMS
462, IrcCommand::ERR_ALREADYREGISTRED
463, IrcCommand::ERR_NOPERMFORHOST
464, IrcCommand::ERR_PASSWDMISMATCH
465, IrcCommand::ERR_YOUREBANNEDCREEP
466, IrcCommand::ERR_YOUWILLBEBANNED
467, IrcCommand::ERR_KEYSET
470, IrcCommand::ERR_LINKCHANNEL
471, IrcCommand::ERR_CHANNELISFULL
472, IrcCommand::ERR_UNKNOWNMODE
473, IrcCommand::ERR_INVITEONLYCHAN
474, IrcCommand::ERR_BANNEDFROMCHAN
475, IrcCommand::ERR_BADCHANNELKEY
476, IrcCommand::ERR_BADCHANMASK
477, IrcCommand::ERR_NEEDREGGEDNICK
478, IrcCommand::ERR_BANLISTFULL
479, IrcCommand::ERR_BADCHANNAME
480, IrcCommand::ERR_THROTTLE
481, IrcCommand::ERR_NOPRIVILEGES
482, IrcCommand::ERR_CHANOPRIVSNEEDED
483, IrcCommand::ERR_CANTKILLSERVER
484, IrcCommand::ERR_ISCHANSERVICE
485, IrcCommand::ERR_BANNEDNICK
486, IrcCommand::ERR_NONONREG
489, IrcCommand::ERR_VOICENEEDED
491, IrcCommand::ERR_NOOPERHOST
492, IrcCommand::ERR_CANNOTSENDTOUSER
494, IrcCommand::ERR_OWNMODE
501, IrcCommand::ERR_UMODEUNKNOWNFLAG
502, IrcCommand::ERR_USERSDONTMATCH, 2, 2
503, IrcCommand::ERR_GHOSTEDCLIENT
504, IrcCommand::ERR_USERNOTONSERV
513, IrcCommand::ERR_WRONGPONG
517, IrcCommand::ERR_DISABLED
524, IrcCommand::ERR_HELPNOTFOUND
670, IrcCommand::RPL_STARTTLS
671, IrcCommand::RPL_WHOISSECURE, 3, 3
691, IrcCommand::ERR_STARTTLS
702, IrcCommand::RPL_MODLIST
703, IrcCommand::RPL_ENDOFMODLIST
704, IrcCommand::RPL_HELPSTART
705, IrcCommand::RPL_HELPTXT
706, IrcCommand::RPL_ENDOFHELP
707, IrcCommand::ERR_TARGCHANGE
708, IrcCommand::RPL_ETRACEFULL, 10, 10
709, IrcCommand::RPL_ETRACE
710, IrcCommand::RPL_KNOCK
711, IrcCommand::RPL_KNOCKDLVR
712, IrcCommand::ERR_TOOMANYKNOCK
713, IrcCommand::ERR_CHANOPEN
714, IrcCommand::ERR_KNOCKONCHAN
715, IrcCommand::ERR_KNOCKDISABLED
716, IrcCommand::ERR_TARGUMODEG
717, IrcCommand::RPL_TARGNOTIFY
718, IrcCommand::RPL_UMODEGMSG
720, IrcCommand::RPL_OMOTDSTART
721, IrcCommand::RPL_OMOTD
722, IrcCommand::RPL_ENDOFOMOTD
723, IrcCommand::ERR_NOPRIVS
724, IrcCommand::RPL_TESTMASK
725, IrcCommand::RPL_TESTLINE
726, IrcCommand::RPL_NOTESTLINE
727, IrcCommand::RPL_TESTMASKGECO, 6, 6
728, IrcCommand::RPL_QUIETLIST, 6, 6
729, IrcCommand::RPL_ENDOFQUIETLIS, 4, 4
730, IrcCommand::RPL_MONONLINE
731, IrcCommand::RPL_MONOFFLINE
732, IrcCommand::RPL_MONLIST
733, IrcCommand::RPL_ENDOFMONLIS, 2, 2
734, IrcCommand::ERR_MONLISTFULL
740, IrcCommand::RPL_RSACHALLENGE2, 2, 2
741, IrcCommand::RPL_ENDOFRSACHALLENGE2, 2, 2
742, IrcCommand::ERR_MLOCKRESTRICTE
743, IrcCommand::ERR_INVALIDBAN
744, IrcCommand::ERR_TOPICLOCK
750, IrcCommand::RPL_SCANMATCHED
751, IrcCommand::RPL_SCANUMODES
900, IrcCommand::RPL_LOGGEDIN, 4, 4
901, IrcCommand::RPL_LOGGEDOUT, 3, 3
902, IrcCommand::ERR_NICKLOCKED, 2, 2
903, IrcCommand::RPL_SASLSUCCESS, 2, 2
904, IrcCommand::ERR_SASLFAIL, 2, 2
905, IrcCommand::ERR_SASLTOOLONG, 2, 2
906, IrcCommand::ERR_SASLABORTED, 2, 2
907, IrcCommand::ERR_SASLALREADY, 2, 2
908, IrcCommand::RPL_SASLMECHS, 3, 3
ACCOUNT, IrcCommand::ACCOUNT, 1, 1
AUTHENTICATE, IrcCommand::AUTHENTICATE, 1, 1
AWAY, IrcCommand::AWAY, 0, 1
BATCH, IrcCommand::BATCH
BOUNCER, IrcCommand::BOUNCER
CAP, IrcCommand::CAP, 1, 15
CHGHOST, IrcCommand::CHGHOST, 2, 2
ERROR, IrcCommand::ERROR, 1, 1
JOIN, IrcCommand::JOIN, 1, 3
KICK, IrcCommand::KICK, 3, 3
MODE, IrcCommand::MODE, 2, 15
NICK, IrcCommand::NICK, 1, 1
NOTICE, IrcCommand::NOTICE, 2, 2
PART, IrcCommand::PART, 1, 2
PING, IrcCommand::PING, 1, 1
PRIVMSG, IrcCommand::PRIVMSG, 2, 2
QUIT, IrcCommand::QUIT, 1, 1
SETNAME, IrcCommand::SETNAME, 1, 1
TOPIC, IrcCommand::TOPIC, 2, 2

View File

@@ -1,20 +0,0 @@
#include "irc_coroutine.hpp"
auto irc_coroutine::is_running() -> bool
{
return promise().connection_ != nullptr;
}
auto irc_coroutine::exception() -> std::exception_ptr
{
return promise().exception_;
}
auto irc_coroutine::start(Connection &connection) -> void
{
promise().connection_ = connection.shared_from_this();
resume();
}
void wait_ircmsg::stop() { ircmsg_slot_.disconnect(); }
void wait_timeout::stop() { timer_.reset(); }

View File

@@ -1,101 +0,0 @@
#pragma once
/**
* @file linebuffer.hpp
* @author Eric Mertens <emertens@gmail.com>
* @brief A line buffering class
* @version 0.1
* @date 2023-08-22
*
* @copyright Copyright (c) 2023
*
*/
#include <boost/asio/buffer.hpp>
#include <algorithm>
#include <concepts>
#include <vector>
/**
* @brief Fixed-size buffer with line-oriented dispatch
*
*/
class LineBuffer
{
std::vector<char> buffer;
// [buffer.begin(), end_) contains buffered data
// [end_, buffer.end()) is available buffer space
std::vector<char>::iterator end_;
public:
/**
* @brief Construct a new Line Buffer object
*
* @param n Buffer size
*/
LineBuffer(std::size_t n)
: buffer(n)
, end_{buffer.begin()}
{
}
/**
* @brief Get the available buffer space
*
* @return boost::asio::mutable_buffer
*/
auto get_buffer() -> boost::asio::mutable_buffer
{
return boost::asio::buffer(&*end_, std::distance(end_, buffer.end()));
}
/**
* @brief Commit new buffer bytes and dispatch line callback
*
* The first n bytes of the buffer will be considered to be
* populated. The line callback function will be called once
* per completed line. Those lines are removed from the buffer
* and the is ready for additional calls to get_buffer and
* add_bytes.
*
* @param n Bytes written to the last call of get_buffer
* @param line_cb Callback function to run on each completed line
*/
auto add_bytes(std::size_t n, std::invocable<char *> auto line_cb) -> void
{
const auto start = end_;
std::advance(end_, n);
// new data is now located in [start, end_)
// cursor marks the beginning of the current line
auto cursor = buffer.begin();
for (auto nl = std::find(start, end_, '\n');
nl != end_;
nl = std::find(cursor, end_, '\n'))
{
// Null-terminate the line. Support both \n and \r\n
if (cursor < nl && *std::prev(nl) == '\r')
{
*std::prev(nl) = '\0';
}
else
{
*nl = '\0';
}
line_cb(&*cursor);
cursor = std::next(nl);
}
// If any lines were processed, move all processed lines to
// the front of the buffer
if (cursor != buffer.begin())
{
end_ = std::move(cursor, end_, buffer.begin());
}
}
};

View File

@@ -1,79 +0,0 @@
#include "connection.hpp"
#include "settings.hpp"
#include <boost/asio.hpp>
#include <boost/log/trivial.hpp>
#include <fstream>
#include <iostream>
#include <memory>
#include "bot.hpp"
#include "client.hpp"
#include "registration.hpp"
using namespace std::literals;
auto start(boost::asio::io_context &io, const Settings &settings) -> void
{
const auto connection = std::make_shared<Connection>(io);
const auto client = Client::start(*connection);
Registration::start(settings, client);
const auto bot = Bot::start(client);
connection->sig_snote.connect([](auto &match) {
std::cout << "SNOTE " << static_cast<int>(match.get_tag()) << std::endl;
for (auto c : match.get_results())
{
std::cout << " " << std::string_view{c.first, c.second} << std::endl;
}
});
client->sig_registered.connect([connection]() {
connection->send_join("##glguy"sv);
});
connection->sig_disconnect.connect(
[&io, &settings, client, bot]() {
client->shutdown();
bot->shutdown();
auto timer = std::make_shared<boost::asio::steady_timer>(io);
timer->expires_after(5s);
timer->async_wait([&io, &settings, timer](auto) { start(io, settings); });
}
);
bot->sig_command.connect([connection](const Command &cmd) {
std::cout << "COMMAND " << cmd.command << " from " << cmd.account << std::endl;
});
connection->start({
.tls = settings.use_tls,
.host = settings.host,
.port = settings.service,
.verify = settings.tls_hostname,
});
}
auto get_settings() -> Settings
{
if (auto config_stream = std::ifstream{"config.toml"})
{
return Settings::from_stream(config_stream);
}
else
{
BOOST_LOG_TRIVIAL(error) << "Unable to open config.toml";
std::exit(1);
}
}
auto main() -> int
{
const auto settings = get_settings();
auto io = boost::asio::io_context{};
start(io, settings);
io.run();
}

View File

@@ -9,8 +9,7 @@
#include <cstddef> #include <cstddef>
#include <string_view> #include <string_view>
namespace mybase64 namespace mybase64 {
{
inline constexpr auto encoded_size(std::size_t len) -> std::size_t inline constexpr auto encoded_size(std::size_t len) -> std::size_t
{ {
@@ -35,10 +34,8 @@ auto encode(std::string_view input, char* output) -> void;
* *
* @param input Base64 input text * @param input Base64 input text
* @param output Target buffer for decoded value * @param output Target buffer for decoded value
* @param outlen Output parameter for decoded length * @return pointer to end of output on success
* @return true success
* @return false failure
*/ */
auto decode(std::string_view input, char* output, std::size_t* outlen) -> bool; auto decode(std::string_view input, char* output) -> char*;
} // namespace } // namespace

View File

@@ -1,113 +1,102 @@
#include "mybase64.hpp" #include "mybase64.hpp"
#include <array>
#include <climits> #include <climits>
#include <cstdint> #include <cstdint>
#include <string_view> #include <string_view>
namespace mybase64 namespace mybase64 {
{
namespace {
constexpr std::array<char, 64> alphabet{
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};
constexpr std::array<std::int8_t, 256> alphabet_values = []() constexpr {
std::array<std::int8_t, 256> result;
result.fill(-1);
std::int8_t v = 0;
for (auto const k : alphabet)
{
result[k] = v++;
}
return result;
}();
}
static_assert(CHAR_BIT == 8); static_assert(CHAR_BIT == 8);
auto encode(std::string_view const input, char* output) -> void auto encode(std::string_view const input, char* output) -> void
{ {
static char const* const alphabet = auto cursor = std::begin(input);
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" auto const end = std::end(input);
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
auto cursor = std::begin(input); while (end - cursor >= 3)
auto const end = std::end(input); {
std::uint32_t buffer = std::uint8_t(*cursor++);
buffer <<= 8;
buffer |= std::uint8_t(*cursor++);
buffer <<= 8;
buffer |= std::uint8_t(*cursor++);
while (end - cursor >= 3) *output++ = alphabet[(buffer >> 6 * 3) % 64];
{ *output++ = alphabet[(buffer >> 6 * 2) % 64];
uint32_t buffer = uint8_t(*cursor++); *output++ = alphabet[(buffer >> 6 * 1) % 64];
buffer <<= 8; buffer |= uint8_t(*cursor++); *output++ = alphabet[(buffer >> 6 * 0) % 64];
buffer <<= 8; buffer |= uint8_t(*cursor++); }
*output++ = alphabet[(buffer >> 6 * 3) % 64]; if (cursor < end)
*output++ = alphabet[(buffer >> 6 * 2) % 64]; {
*output++ = alphabet[(buffer >> 6 * 1) % 64]; std::uint32_t buffer = std::uint8_t(*cursor++) << 10;
*output++ = alphabet[(buffer >> 6 * 0) % 64]; if (cursor < end)
} buffer |= std::uint8_t(*cursor) << 2;
if (cursor < end) *output++ = alphabet[(buffer >> 12) % 64];
{ *output++ = alphabet[(buffer >> 6) % 64];
uint32_t buffer = uint8_t(*cursor++) << 10; *output++ = cursor < end ? alphabet[(buffer % 64)] : '=';
if (cursor < end) buffer |= uint8_t(*cursor) << 2; *output++ = '=';
}
*output++ = alphabet[(buffer >> 12) % 64]; *output = '\0';
*output++ = alphabet[(buffer >> 6) % 64];
*output++ = cursor < end ? alphabet[(buffer % 64)] : '=';
*output++ = '=';
}
*output = '\0';
} }
auto decode(std::string_view const input, char* const output, std::size_t* const outlen) -> bool auto decode(std::string_view const input, char* output) -> char*
{ {
static int8_t const alphabet_values[] = { std::uint32_t buffer = 1;
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, 0x3e, -1, -1, -1, 0x3f,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
0x3c, 0x3d, -1, -1, -1, -1, -1, -1,
-1, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, -1, -1, -1, -1, -1,
-1, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32, 0x33, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
};
uint32_t buffer = 1; for (auto const c : input)
char* cursor = output; {
if (auto const value = alphabet_values[uint8_t(c)]; -1 != value)
for (char c : input) { {
int8_t const value = alphabet_values[uint8_t(c)]; buffer = (buffer << 6) | value;
if (-1 == value) continue; if (buffer & 1 << 6 * 4)
{
buffer = (buffer << 6) | value; *output++ = buffer >> 16;
*output++ = buffer >> 8;
if (buffer & 1<<6*4) { *output++ = buffer >> 0;
*cursor++ = buffer >> 8*2; buffer = 1;
*cursor++ = buffer >> 8*1; }
*cursor++ = buffer >> 8*0;
buffer = 1;
} }
} }
if (buffer & 1<<6*3) { if (buffer & 1 << 6 * 3)
*cursor++ = buffer >> 10; {
*cursor++ = buffer >> 2; *output++ = buffer >> 10;
} else if (buffer & 1<<6*2) { *output++ = buffer >> 2;
*cursor++ = buffer >> 4;
} else if (buffer & 1<<6*1) {
return false;
} }
*outlen = cursor - output; else if (buffer & 1 << 6 * 2)
return true; {
*output++ = buffer >> 4;
}
else if (buffer & 1 << 6 * 1)
{
return nullptr;
}
return output;
} }
} // namespace } // namespace

33
myirc/CMakeLists.txt Normal file
View File

@@ -0,0 +1,33 @@
add_custom_command(
OUTPUT irc_commands.inc
COMMAND
gperf
-C -Z IrcCommandHash -K text -L C++ -t
--output-file irc_commands.inc
${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
VERBATIM)
add_library(myirc STATIC
irc_commands.inc
bot.cpp
challenge.cpp
client.cpp
connection.cpp
ircmsg.cpp
openssl_utils.cpp
registration.cpp
ratelimit.cpp
sasl_mechanism.cpp
snote.cpp
linebuffer.cpp
)
target_include_directories(myirc PUBLIC include)
target_include_directories(myirc PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(myirc PRIVATE
OpenSSL::SSL
Boost::signals2 Boost::log Boost::asio
tomlplusplus_tomlplusplus
PkgConfig::LIBHS
mysocks5 mybase64)

View File

@@ -1,19 +1,22 @@
#include "bot.hpp" #include "myirc/bot.hpp"
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
namespace myirc {
auto Bot::start(std::shared_ptr<Client> self) -> std::shared_ptr<Bot> auto Bot::start(std::shared_ptr<Client> self) -> std::shared_ptr<Bot>
{ {
const auto thread = std::make_shared<Bot>(std::move(self)); const auto thread = std::make_shared<Bot>(std::move(self));
thread->self_->get_connection().sig_ircmsg.connect([thread](const auto cmd, auto &msg) {
thread->on_ircmsg(cmd, msg); thread->self_->sig_chat.connect([thread](auto &chat, bool) {
thread->on_chat(chat);
}); });
return thread; return thread;
} }
auto Bot::process_command(std::string_view message, const IrcMsg &msg) -> void auto Bot::process_command(std::string_view message, const Chat &chat) -> void
{ {
const auto cmdstart = message.find_first_not_of(' '); const auto cmdstart = message.find_first_not_of(' ');
if (cmdstart == message.npos) return; if (cmdstart == message.npos) return;
@@ -32,7 +35,7 @@ auto Bot::process_command(std::string_view message, const IrcMsg &msg) -> void
std::string_view oper; std::string_view oper;
std::string_view account; std::string_view account;
for (auto [key, value] : msg.tags) for (auto [key, value] : chat.tags)
{ {
if (key == "account") if (key == "account")
{ {
@@ -45,8 +48,8 @@ auto Bot::process_command(std::string_view message, const IrcMsg &msg) -> void
} }
sig_command({ sig_command({
.source = msg.args[0], .source = chat.source,
.target = msg.args[1], .target = chat.target,
.oper = oper, .oper = oper,
.account = account, .account = account,
.command = command, .command = command,
@@ -54,20 +57,20 @@ auto Bot::process_command(std::string_view message, const IrcMsg &msg) -> void
}); });
} }
auto Bot::on_ircmsg(const IrcCommand cmd, const IrcMsg &msg) -> void auto Bot::on_chat(const Chat &chat) -> void
{ {
if (cmd == IrcCommand::PRIVMSG) if (not chat.is_notice)
{ {
const auto target = msg.args[0]; if (self_->is_my_nick(chat.target))
const auto message = msg.args[1];
if (self_->is_my_nick(target))
{ {
process_command(message, msg); process_command(chat.message, chat);
} else if (self_->is_channel(target)) { }
const auto colon = message.find(':'); else if (self_->is_channel(chat.target))
if (colon == message.npos) return; {
if (not self_->is_my_nick(message.substr(0, colon))) return; const auto colon = chat.message.find(':');
process_command(message.substr(colon+1), msg); if (colon == chat.message.npos) return;
if (not self_->is_my_nick(chat.message.substr(0, colon))) return;
process_command(chat.message.substr(colon + 1), chat);
} }
} }
} }
@@ -76,3 +79,5 @@ auto Bot::shutdown() -> void
{ {
sig_command.disconnect_all_slots(); sig_command.disconnect_all_slots();
} }
} // namespace myirc

95
myirc/challenge.cpp Normal file
View File

@@ -0,0 +1,95 @@
#include "myirc/challenge.hpp"
#include "myirc/openssl_utils.hpp"
#include <mybase64.hpp>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <boost/log/trivial.hpp>
#include <memory>
#include <string>
namespace myirc {
Challenge::Challenge(Ref<EVP_PKEY> key, std::shared_ptr<Client> client)
: key_{std::move(key)}
, client_{std::move(client)}
{}
auto Challenge::on_ircmsg(IrcCommand cmd, const IrcMsg &msg) -> void {
switch (cmd) {
default: break;
case IrcCommand::RPL_RSACHALLENGE2:
buffer_ += msg.args[1];
break;
case IrcCommand::ERR_NOOPERHOST:
slot_.disconnect();
BOOST_LOG_TRIVIAL(error) << "Challenge: No oper host";
break;
case IrcCommand::RPL_YOUREOPER:
slot_.disconnect();
break;
case IrcCommand::RPL_ENDOFRSACHALLENGE2:
finish_challenge();
break;
}
}
auto Challenge::finish_challenge() -> void
{
unsigned int digestlen = EVP_MAX_MD_SIZE;
unsigned char digest[EVP_MAX_MD_SIZE];
size_t len = mybase64::decoded_size(buffer_.size());
std::vector<unsigned char> ciphertext(len, 0);
auto decode_end = mybase64::decode(buffer_, reinterpret_cast<char*>(ciphertext.data()));
if (decode_end == nullptr )
return log_openssl_errors("Challenge base64::decode: ");
ciphertext.resize(decode_end - reinterpret_cast<char*>(ciphertext.data()));
// Setup decryption context
Ref<EVP_PKEY_CTX> ctx{EVP_PKEY_CTX_new(key_.get(), nullptr)};
if (not ctx)
return log_openssl_errors("Challenge EVP_PKEY_CTX_new: ");
if (1 != EVP_PKEY_decrypt_init(ctx.get()))
return log_openssl_errors("Challenge EVP_PKEY_decrypt_init: ");
if (0 >= EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING))
return log_openssl_errors("Challenge EVP_PKEY_CTX_set_rsa_padding: ");
// Determine output size
if (1 != EVP_PKEY_decrypt(ctx.get(), nullptr, &len, ciphertext.data(), ciphertext.size()))
return log_openssl_errors("Challenge EVP_PKEY_decrypt (size): ");
buffer_.resize(len);
// Decrypt ciphertext
if (1 != EVP_PKEY_decrypt(ctx.get(), reinterpret_cast<unsigned char*>(buffer_.data()), &len, ciphertext.data(), ciphertext.size()))
return log_openssl_errors("Challenge EVP_PKEY_decrypt: ");
buffer_.resize(len);
// Hash the decrypted message
if (1 != EVP_Digest(buffer_.data(), buffer_.size(), digest, &digestlen, EVP_sha1(), nullptr))
return log_openssl_errors("Challenge EVP_Digest: ");
// Construct reply as '+' and base64 encoded digest
buffer_.resize(mybase64::encoded_size(digestlen) + 1);
buffer_[0] = '+';
mybase64::encode(std::string_view{(char*)digest, digestlen}, buffer_.data() + 1);
client_->send_challenge(buffer_);
buffer_.clear();
}
auto Challenge::start(std::shared_ptr<Client> client, const std::string_view user, Ref<EVP_PKEY> ref) -> std::shared_ptr<Challenge>
{
auto self = std::make_shared<Challenge>(std::move(ref), client);
self->slot_ = client->get_connection().sig_ircmsg.connect([self](auto cmd, auto &msg, bool) { self->on_ircmsg(cmd, msg); });
client->send_challenge(user);
return self;
}
} // namespace myirc

View File

@@ -1,12 +1,16 @@
#include "client.hpp" #include "myirc/client.hpp"
#include "connection.hpp" #include "myirc/connection.hpp"
#include "myirc/sasl_mechanism.hpp"
#include "myirc/snote.hpp"
#include <mybase64.hpp> #include <mybase64.hpp>
#include <boost/container/flat_map.hpp> #include <boost/container/flat_map.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
namespace myirc {
using namespace std::literals; using namespace std::literals;
auto Client::on_welcome(const IrcMsg &irc) -> void auto Client::on_welcome(const IrcMsg &irc) -> void
@@ -37,7 +41,7 @@ auto Client::on_join(const IrcMsg &irc) -> void
{ {
if (is_my_mask(irc.source)) if (is_my_mask(irc.source))
{ {
channels_.insert(std::string{irc.args[0]}); channels_.insert(casemap(irc.args[0]));
} }
} }
@@ -45,7 +49,7 @@ auto Client::on_kick(const IrcMsg &irc) -> void
{ {
if (is_my_nick(irc.args[1])) if (is_my_nick(irc.args[1]))
{ {
channels_.erase(std::string{irc.args[0]}); channels_.erase(casemap(irc.args[0]));
} }
} }
@@ -53,7 +57,7 @@ auto Client::on_part(const IrcMsg &irc) -> void
{ {
if (is_my_mask(irc.source)) if (is_my_mask(irc.source))
{ {
channels_.erase(std::string{irc.args[0]}); channels_.erase(casemap(irc.args[0]));
} }
} }
@@ -118,13 +122,43 @@ auto Client::on_isupport(const IrcMsg &msg) -> void
} }
} }
auto Client::start(Connection &connection) -> std::shared_ptr<Client> auto Client::on_chat(bool notice, const IrcMsg &irc, bool flush) -> void
{
char status_msg = '\0';
std::string_view target = irc.args[0];
if (not target.empty() && status_msg_.find(target[0]) != std::string::npos)
{
status_msg = target[0];
target = target.substr(1);
}
sig_chat({
.tags = irc.tags,
.is_notice = notice,
.status_msg = '\0',
.source = irc.source,
.target = irc.args[0],
.message = irc.args[1],
}, flush);
}
auto Client::start(std::shared_ptr<Connection> connection) -> std::shared_ptr<Client>
{ {
auto thread = std::make_shared<Client>(connection); auto thread = std::make_shared<Client>(connection);
connection.sig_ircmsg.connect([thread](auto cmd, auto &msg) { connection->sig_ircmsg.connect([thread](auto cmd, auto &msg, bool flush) {
switch (cmd) switch (cmd)
{ {
case IrcCommand::PRIVMSG:
thread->on_chat(false, msg, flush);
break;
case IrcCommand::NOTICE:
if (auto match = snoteCore.match(msg))
{
thread->sig_snote(*match, flush);
} else {
thread->on_chat(true, msg, flush);
}
break;
case IrcCommand::JOIN: case IrcCommand::JOIN:
thread->on_join(msg); thread->on_join(msg);
break; break;
@@ -150,24 +184,31 @@ auto Client::start(Connection &connection) -> std::shared_ptr<Client>
thread->on_welcome(msg); thread->on_welcome(msg);
break; break;
case IrcCommand::RPL_ENDOFMOTD: case IrcCommand::RPL_ENDOFMOTD:
case IrcCommand::ERR_NOMOTD:
thread->on_registered(); thread->on_registered();
break; break;
case IrcCommand::CAP: case IrcCommand::CAP:
thread->on_cap(msg); thread->on_cap(msg);
break; break;
case IrcCommand::AUTHENTICATE:
thread->on_authenticate_chunk(msg.args[0]);
break;
default: default:
break; break;
} }
}); });
connection.sig_authenticate.connect([thread](auto msg) { connection->sig_disconnect.connect([thread](auto) {
thread->on_authenticate(msg); thread->sig_registered.disconnect_all_slots();
thread->sig_cap_ls.disconnect_all_slots();
thread->sig_chat.disconnect_all_slots();
thread->sig_snote.disconnect_all_slots();
}); });
return thread; return thread;
} }
auto Client::get_my_nickname() const -> const std::string & auto Client::get_my_nick() const -> const std::string &
{ {
return nickname_; return nickname_;
} }
@@ -190,7 +231,7 @@ auto Client::is_my_nick(std::string_view nick) const -> bool
auto Client::is_my_mask(std::string_view mask) const -> bool auto Client::is_my_mask(std::string_view mask) const -> bool
{ {
const auto bang = mask.find('!'); const auto bang = mask.find('!');
return bang != std::string_view::npos && nickname_ == mask.substr(0, bang); return bang != std::string_view::npos && is_my_nick(mask.substr(0, bang));
} }
auto Client::is_channel(std::string_view name) const -> bool auto Client::is_channel(std::string_view name) const -> bool
@@ -198,25 +239,47 @@ auto Client::is_channel(std::string_view name) const -> bool
return not name.empty() && channel_prefix_.find(name[0]) != channel_prefix_.npos; return not name.empty() && channel_prefix_.find(name[0]) != channel_prefix_.npos;
} }
auto Client::is_on_channel(std::string_view name) const -> bool
{
return channels_.contains(casemap(name));
}
namespace {
template <class... Ts>
struct overloaded : Ts...
{
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
}
auto Client::on_authenticate(const std::string_view body) -> void auto Client::on_authenticate(const std::string_view body) -> void
{ {
if (not sasl_mechanism_) if (not sasl_mechanism_)
{ {
BOOST_LOG_TRIVIAL(warning) << "Unexpected AUTHENTICATE from server"sv; BOOST_LOG_TRIVIAL(warning) << "Unexpected AUTHENTICATE from server"sv;
connection_.send_authenticate_abort(); send_authenticate_abort();
return; return;
} }
if (auto reply = sasl_mechanism_->step(body)) std::visit(
overloaded{
[this](const std::string &reply) {
send_authenticate_encoded(reply);
},
[this](SaslMechanism::NoReply) {
send_authenticate("*"sv);
},
[this](SaslMechanism::Failure) {
send_authenticate_abort();
},
},
sasl_mechanism_->step(body));
if (sasl_mechanism_->is_complete())
{ {
sasl_mechanism_.reset();
connection_.send_authenticate_encoded(*reply);
// Clean up completed SASL transactions
if (sasl_mechanism_->is_complete())
{
sasl_mechanism_.reset();
}
} }
} }
@@ -224,11 +287,11 @@ auto Client::start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void
{ {
if (sasl_mechanism_) if (sasl_mechanism_)
{ {
connection_.send_authenticate("*"sv); // abort SASL send_authenticate("*"sv); // abort SASL
} }
sasl_mechanism_ = std::move(mechanism); sasl_mechanism_ = std::move(mechanism);
connection_.send_authenticate(sasl_mechanism_->mechanism_name()); send_authenticate(sasl_mechanism_->mechanism_name());
} }
static auto tolower_rfc1459(int c) -> int static auto tolower_rfc1459(int c) -> int
@@ -285,16 +348,10 @@ auto Client::casemap_compare(std::string_view lhs, std::string_view rhs) const -
} }
} }
auto Client::shutdown() -> void
{
sig_registered.disconnect_all_slots();
sig_cap_ls.disconnect_all_slots();
}
auto Client::list_caps() -> void auto Client::list_caps() -> void
{ {
caps_available_.clear(); caps_available_.clear();
connection_.send_cap_ls(); send_cap_ls();
} }
auto Client::on_cap(const IrcMsg &msg) -> void auto Client::on_cap(const IrcMsg &msg) -> void
@@ -388,3 +445,207 @@ auto Client::on_cap(const IrcMsg &msg) -> void
); );
} }
} }
auto Client::on_authenticate_chunk(const std::string_view chunk) -> void
{
if (chunk != "+"sv)
{
authenticate_buffer_ += chunk;
}
if (chunk.size() != 400)
{
std::string decoded;
decoded.resize(mybase64::decoded_size(authenticate_buffer_.size()));
std::size_t len;
if (auto decode_end = mybase64::decode(authenticate_buffer_, decoded.data()))
{
decoded.resize(decode_end - decoded.data());
on_authenticate(decoded);
}
else
{
BOOST_LOG_TRIVIAL(debug) << "Invalid AUTHENTICATE base64"sv;
send_authenticate("*"sv); // abort SASL
}
authenticate_buffer_.clear();
}
else if (authenticate_buffer_.size() > 1024)
{
BOOST_LOG_TRIVIAL(debug) << "AUTHENTICATE buffer overflow"sv;
authenticate_buffer_.clear();
send_authenticate("*"sv); // abort SASL
}
}
auto Client::send_ping(std::string_view txt) -> void
{
connection_->write_irc("PING", txt);
}
auto Client::send_pong(std::string_view txt) -> void
{
connection_->write_irc("PONG", txt);
}
auto Client::send_pass(std::string_view password) -> void
{
connection_->write_irc("PASS", password);
}
auto Client::send_user(std::string_view user, std::string_view real) -> void
{
connection_->write_irc("USER", user, "*", "*", real);
}
auto Client::send_nick(std::string_view nick) -> void
{
connection_->write_irc("NICK", nick);
}
auto Client::send_cap_ls() -> void
{
connection_->write_irc("CAP", "LS", "302");
}
auto Client::send_cap_end() -> void
{
connection_->write_irc("CAP", "END");
}
auto Client::send_cap_req(std::string_view caps) -> void
{
connection_->write_irc("CAP", "REQ", caps);
}
auto Client::send_privmsg(std::string_view target, std::string_view message) -> void
{
connection_->write_irc("PRIVMSG", target, message);
}
auto Client::send_notice(std::string_view target, std::string_view message) -> void
{
connection_->write_irc("NOTICE", target, message);
}
auto Client::send_wallops(std::string_view message) -> void
{
connection_->write_irc("WALLOPS", message);
}
auto Client::send_names(std::string_view channel) -> void
{
connection_->write_irc("NAMES", channel);
}
auto Client::send_map() -> void
{
connection_->write_irc("MAP");
}
auto Client::send_get_topic(std::string_view channel) -> void
{
connection_->write_irc("TOPIC", channel);
}
auto Client::send_set_topic(std::string_view channel, std::string_view message) -> void
{
connection_->write_irc("TOPIC", channel, message);
}
auto Client::send_testline(std::string_view target) -> void
{
connection_->write_irc("TESTLINE", target);
}
auto Client::send_masktrace_gecos(std::string_view target, std::string_view gecos) -> void
{
connection_->write_irc("MASKTRACE", target, gecos);
}
auto Client::send_masktrace(std::string_view target) -> void
{
connection_->write_irc("MASKTRACE", target);
}
auto Client::send_testmask_gecos(std::string_view target, std::string_view gecos) -> void
{
connection_->write_irc("TESTMASK", target, gecos);
}
auto Client::send_testmask(std::string_view target) -> void
{
connection_->write_irc("TESTMASK", target);
}
auto Client::send_authenticate(std::string_view message) -> void
{
connection_->write_irc("AUTHENTICATE", message);
}
auto Client::send_join(std::string_view channel) -> void
{
connection_->write_irc("JOIN", channel);
}
auto Client::send_challenge(std::string_view message) -> void
{
connection_->write_irc("CHALLENGE", message);
}
auto Client::send_oper(std::string_view user, std::string_view pass) -> void
{
connection_->write_irc("OPER", user, pass);
}
auto Client::send_kick(std::string_view channel, std::string_view nick, std::string_view reason) -> void
{
connection_->write_irc("KICK", channel, nick, reason);
}
auto Client::send_kill(std::string_view nick, std::string_view reason) -> void
{
connection_->write_irc("KILL", nick, reason);
}
auto Client::send_quit(std::string_view message) -> void
{
connection_->write_irc("QUIT", message);
}
auto Client::send_whois(std::string_view arg1) -> void
{
connection_->write_irc("WHOIS", arg1);
}
auto Client::send_whois_remote(std::string_view arg1, std::string_view arg2) -> void
{
connection_->write_irc("WHOIS", arg1, arg2);
}
auto Client::send_authenticate_abort() -> void
{
send_authenticate("*");
}
auto Client::send_authenticate_encoded(std::string_view body) -> void
{
std::string encoded(mybase64::encoded_size(body.size()), 0);
mybase64::encode(body, encoded.data());
for (size_t lo = 0; lo < encoded.size(); lo += 400)
{
const auto hi = std::min(lo + 400, encoded.size());
const std::string_view chunk{encoded.begin() + lo, encoded.begin() + hi};
send_authenticate(chunk);
}
if (encoded.size() % 400 == 0)
{
send_authenticate("+"sv);
}
}
} // namespace myirc

View File

@@ -1,18 +1,24 @@
#include "connection.hpp" #include "myirc/connection.hpp"
#include "linebuffer.hpp" #include "myirc/linebuffer.hpp"
#include <openssl/asn1.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <socks5.hpp>
#include <mybase64.hpp> #include <mybase64.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <sstream>
#include <iomanip>
namespace myirc {
namespace {
#include "irc_commands.inc" #include "irc_commands.inc"
using tcp_type = boost::asio::ip::tcp::socket; using tcp_type = boost::asio::ip::tcp::socket;
using tls_type = boost::asio::ssl::stream<tcp_type>; using tls_type = boost::asio::ssl::stream<tcp_type>;
} // namespace
using namespace std::literals; using namespace std::literals;
Connection::Connection(boost::asio::io_context &io) Connection::Connection(boost::asio::io_context &io)
@@ -25,16 +31,51 @@ Connection::Connection(boost::asio::io_context &io)
auto Connection::write_buffers() -> void auto Connection::write_buffers() -> void
{ {
const auto available = write_strings_.size();
const auto [delay, count]
= rate_limit
? rate_limit->query(available)
: std::pair{0ms, available};
if (delay > 0ms) {
auto timer = std::make_shared<boost::asio::steady_timer>(stream_.get_executor(), delay);
timer->async_wait([timer, count, self = weak_from_this()](auto) {
if (auto lock = self.lock()) {
lock->write_buffers(count);
}
});
} else {
write_buffers(count);
}
}
auto Connection::write_buffers(size_t n) -> void
{
std::list<std::string> strings;
std::vector<boost::asio::const_buffer> buffers; std::vector<boost::asio::const_buffer> buffers;
buffers.reserve(write_strings_.size());
for (const auto &elt : write_strings_) if (n == write_strings_.size()) {
strings = std::move(write_strings_);
write_strings_.clear();
} else {
strings.splice(
strings.begin(), // insert at
write_strings_, // remove from
write_strings_.begin(), // start removing at
std::next(write_strings_.begin(), n) // stop removing at
);
}
buffers.reserve(n);
for (const auto &elt : strings)
{ {
buffers.push_back(boost::asio::buffer(elt)); buffers.push_back(boost::asio::buffer(elt));
} }
boost::asio::async_write( boost::asio::async_write(
stream_, stream_,
buffers, buffers,
[this, strings = std::move(write_strings_)](const boost::system::error_code &error, std::size_t) { [this, strings = std::move(strings)](const boost::system::error_code &error, std::size_t) {
if (not error) if (not error)
{ {
if (write_strings_.empty()) if (write_strings_.empty())
@@ -48,7 +89,6 @@ auto Connection::write_buffers() -> void
} }
} }
); );
write_strings_.clear();
} }
auto Connection::watchdog() -> void auto Connection::watchdog() -> void
@@ -64,7 +104,7 @@ auto Connection::watchdog() -> void
} }
else else
{ {
send_ping("watchdog"); write_irc("PING", "watchdog");
stalled_ = true; stalled_ = true;
watchdog(); watchdog();
} }
@@ -79,7 +119,7 @@ auto Connection::watchdog_activity() -> void
} }
/// Parse IRC message line and dispatch it to the ircmsg slot. /// Parse IRC message line and dispatch it to the ircmsg slot.
auto Connection::dispatch_line(char *line) -> void auto Connection::dispatch_line(char *line, bool flush) -> void
{ {
const auto msg = parse_irc_message(line); const auto msg = parse_irc_message(line);
const auto recognized = IrcCommandHash::in_word_set(msg.command.data(), msg.command.size()); const auto recognized = IrcCommandHash::in_word_set(msg.command.data(), msg.command.size());
@@ -95,7 +135,7 @@ auto Connection::dispatch_line(char *line) -> void
// Respond to pings immediate and discard // Respond to pings immediate and discard
case IrcCommand::PING: case IrcCommand::PING:
send_pong(msg.args[0]); write_irc("PONG", msg.args[0]);
break; break;
// Unknown message generate warnings but do not dispatch // Unknown message generate warnings but do not dispatch
@@ -104,27 +144,19 @@ auto Connection::dispatch_line(char *line) -> void
BOOST_LOG_TRIVIAL(warning) << "Unrecognized command: " << msg.command << " " << msg.args.size(); BOOST_LOG_TRIVIAL(warning) << "Unrecognized command: " << msg.command << " " << msg.args.size();
break; break;
case IrcCommand::AUTHENTICATE:
on_authenticate(msg.args[0]);
break;
// Server notice generate snote events but not IRC command events
case IrcCommand::NOTICE:
if (auto match = snoteCore.match(msg))
{
sig_snote(*match);
break;
}
/* FALLTHROUGH */
// Normal IRC commands // Normal IRC commands
default: default:
sig_ircmsg(command, msg); sig_ircmsg(command, msg, flush);
break; break;
} }
} }
auto Connection::write_line(std::string message) -> void auto Connection::close() -> void
{
stream_.close();
}
auto Connection::write_irc(std::string message) -> void
{ {
BOOST_LOG_TRIVIAL(debug) << "SEND: " << message; BOOST_LOG_TRIVIAL(debug) << "SEND: " << message;
message += "\r\n"; message += "\r\n";
@@ -142,145 +174,21 @@ auto Connection::write_line(std::string message) -> void
} }
} }
auto Connection::close() -> void
{
stream_.close();
}
auto Connection::write_irc(std::string message) -> void
{
write_line(std::move(message));
}
auto Connection::write_irc(std::string front, std::string_view last) -> void auto Connection::write_irc(std::string front, std::string_view last) -> void
{ {
if (last.find_first_of("\r\n\0"sv) != last.npos) bool colon = last.starts_with(":");
{ for (const auto c : last) {
throw std::runtime_error{"bad irc argument"}; switch (c) {
case '\r': case '\n': case '\0': throw std::runtime_error{"bad irc argument"};
case ' ': colon = true;
default: break;
}
} }
front += colon ? " :" : " ";
front += " :";
front += last; front += last;
write_irc(std::move(front)); write_irc(std::move(front));
} }
auto Connection::send_ping(std::string_view txt) -> void
{
write_irc("PING", txt);
}
auto Connection::send_pong(std::string_view txt) -> void
{
write_irc("PONG", txt);
}
auto Connection::send_pass(std::string_view password) -> void
{
write_irc("PASS", password);
}
auto Connection::send_user(std::string_view user, std::string_view real) -> void
{
write_irc("USER", user, "*", "*", real);
}
auto Connection::send_nick(std::string_view nick) -> void
{
write_irc("NICK", nick);
}
auto Connection::send_cap_ls() -> void
{
write_irc("CAP", "LS", "302");
}
auto Connection::send_cap_end() -> void
{
write_irc("CAP", "END");
}
auto Connection::send_cap_req(std::string_view caps) -> void
{
write_irc("CAP", "REQ", caps);
}
auto Connection::send_privmsg(std::string_view target, std::string_view message) -> void
{
write_irc("PRIVMSG", target, message);
}
auto Connection::send_notice(std::string_view target, std::string_view message) -> void
{
write_irc("NOTICE", target, message);
}
auto Connection::send_authenticate(std::string_view message) -> void
{
write_irc("AUTHENTICATE", message);
}
auto Connection::send_join(std::string_view channel) -> void
{
write_irc("JOIN", channel);
}
auto Connection::on_authenticate(const std::string_view chunk) -> void
{
if (chunk != "+"sv)
{
authenticate_buffer_ += chunk;
}
if (chunk.size() != 400)
{
std::string decoded;
decoded.resize(mybase64::decoded_size(authenticate_buffer_.size()));
std::size_t len;
if (mybase64::decode(authenticate_buffer_, decoded.data(), &len))
{
decoded.resize(len);
sig_authenticate(decoded);
}
else
{
BOOST_LOG_TRIVIAL(debug) << "Invalid AUTHENTICATE base64"sv;
send_authenticate("*"sv); // abort SASL
}
authenticate_buffer_.clear();
}
else if (authenticate_buffer_.size() > 1024)
{
BOOST_LOG_TRIVIAL(debug) << "AUTHENTICATE buffer overflow"sv;
authenticate_buffer_.clear();
send_authenticate("*"sv); // abort SASL
}
}
auto Connection::send_authenticate_abort() -> void
{
send_authenticate("*");
}
auto Connection::send_authenticate_encoded(std::string_view body) -> void
{
std::string encoded(mybase64::encoded_size(body.size()), 0);
mybase64::encode(body, encoded.data());
for (size_t lo = 0; lo < encoded.size(); lo += 400)
{
const auto hi = std::min(lo + 400, encoded.size());
const std::string_view chunk{encoded.begin() + lo, encoded.begin() + hi};
send_authenticate(chunk);
}
if (encoded.size() % 400 == 0)
{
send_authenticate("+"sv);
}
}
static static
auto set_buffer_size(tls_type& stream, std::size_t const n) -> void auto set_buffer_size(tls_type& stream, std::size_t const n) -> void
{ {
@@ -381,8 +289,25 @@ auto build_ssl_context(
return ssl_context; return ssl_context;
} }
static auto peer_fingerprint(X509 *cer) -> std::string
{
std::ostringstream os;
std::vector<std::uint8_t> result;
EVP_MD *md_used;
if (auto digest = X509_digest_sig(cer, &md_used, nullptr))
{
os << EVP_MD_name(md_used) << ":" << std::hex << std::setfill('0');
EVP_MD_free(md_used);
for (int i = 0; i < digest->length; ++i) {
os << std::setw(2) << static_cast<unsigned>(digest->data[i]);
}
ASN1_OCTET_STRING_free(digest);
}
return os.str();
}
auto Connection::connect( auto Connection::connect(
ConnectSettings settings Settings settings
) -> boost::asio::awaitable<void> ) -> boost::asio::awaitable<void>
{ {
using namespace std::placeholders; using namespace std::placeholders;
@@ -391,20 +316,51 @@ auto Connection::connect(
const auto self = shared_from_this(); const auto self = shared_from_this();
const size_t irc_buffer_size = 32'768; const size_t irc_buffer_size = 32'768;
boost::asio::ip::tcp::endpoint socket_endpoint;
std::optional<boost::asio::ip::tcp::endpoint> socks_endpoint;
std::string fingerprint;
{ {
// Name resolution // Name resolution
auto resolver = boost::asio::ip::tcp::resolver{stream_.get_executor()}; auto resolver = boost::asio::ip::tcp::resolver{stream_.get_executor()};
const auto endpoints = co_await resolver.async_resolve(settings.host, std::to_string(settings.port), boost::asio::use_awaitable); const auto endpoints = co_await resolver.async_resolve(settings.host, std::to_string(settings.port), boost::asio::use_awaitable);
for (auto e : endpoints) {
BOOST_LOG_TRIVIAL(debug) << "DNS: " << e.endpoint();
}
// Connect to the IRC server // Connect to the IRC server
auto& socket = stream_.reset(); auto& socket = stream_.reset();
const auto endpoint = co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable);
BOOST_LOG_TRIVIAL(debug) << "CONNECTED: " << endpoint; // If we're going to use SOCKS then the TCP connection host is actually the socks
// server and then the IRC server gets passed over the SOCKS protocol
auto const use_socks = not settings.socks_host.empty() && settings.socks_port != 0;
if (use_socks)
{
std::swap(settings.host, settings.socks_host);
std::swap(settings.port, settings.socks_port);
}
socket_endpoint = co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable);
BOOST_LOG_TRIVIAL(debug) << "CONNECTED: " << socket_endpoint;
// Set socket options // Set socket options
socket.set_option(boost::asio::ip::tcp::no_delay(true)); socket.set_option(boost::asio::ip::tcp::no_delay(true));
set_buffer_size(socket, irc_buffer_size); set_buffer_size(socket, irc_buffer_size);
set_cloexec(socket.native_handle()); set_cloexec(socket.native_handle());
// Optionally negotiate SOCKS connection
if (use_socks)
{
auto auth = not settings.socks_user.empty() || not settings.socks_pass.empty()
? socks5::Auth{socks5::UsernamePasswordCredential{settings.socks_user, settings.socks_pass}}
: socks5::Auth{socks5::NoCredential{}};
socks_endpoint = co_await socks5::async_connect(
socket,
settings.socks_host, settings.socks_port, std::move(auth),
boost::asio::use_awaitable
);
}
} }
if (settings.tls) if (settings.tls)
@@ -429,32 +385,46 @@ auto Connection::connect(
} }
co_await stream.async_handshake(stream.client, boost::asio::use_awaitable); co_await stream.async_handshake(stream.client, boost::asio::use_awaitable);
const auto cer = SSL_get0_peer_certificate(stream.native_handle());
fingerprint = peer_fingerprint(cer);
} }
sig_connect(); sig_connect(socket_endpoint, socks_endpoint, std::move(fingerprint));
watchdog(); watchdog();
for (LineBuffer buffer{irc_buffer_size};;) for (LineBuffer buffer{irc_buffer_size};;)
{ {
boost::system::error_code error; boost::system::error_code error;
const auto n = co_await stream_.async_read_some(buffer.get_buffer(), boost::asio::redirect_error(boost::asio::use_awaitable, error)); auto const chunk = buffer.prepare();
if (chunk.size() == 0) break;
const auto n = co_await stream_.async_read_some(chunk, boost::asio::redirect_error(boost::asio::use_awaitable, error));
if (error) if (error)
{ {
break; break;
} }
buffer.add_bytes(n, [this](char *line) { buffer.commit(n);
BOOST_LOG_TRIVIAL(debug) << "RECV: " << line;
auto line = buffer.next_nonempty_line();
if (line)
{
watchdog_activity(); watchdog_activity();
dispatch_line(line); do
}); {
BOOST_LOG_TRIVIAL(debug) << "RECV: " << line;
const auto next_line = buffer.next_nonempty_line();
dispatch_line(line, next_line == nullptr);
line = next_line;
} while (line);
}
buffer.shift();
} }
watchdog_timer_.cancel(); watchdog_timer_.cancel();
stream_.close(); stream_.close();
} }
auto Connection::start(ConnectSettings settings) -> void auto Connection::start(Settings settings) -> void
{ {
boost::asio::co_spawn( boost::asio::co_spawn(
stream_.get_executor(), connect(std::move(settings)), stream_.get_executor(), connect(std::move(settings)),
@@ -471,13 +441,13 @@ auto Connection::start(ConnectSettings settings) -> void
BOOST_LOG_TRIVIAL(debug) << "TERMINATED: " << e.what(); BOOST_LOG_TRIVIAL(debug) << "TERMINATED: " << e.what();
} }
self->sig_disconnect();
// Disconnect all slots to avoid circular references // Disconnect all slots to avoid circular references
self->sig_connect.disconnect_all_slots(); self->sig_connect.disconnect_all_slots();
self->sig_ircmsg.disconnect_all_slots(); self->sig_ircmsg.disconnect_all_slots();
self->sig_disconnect(e);
self->sig_disconnect.disconnect_all_slots(); self->sig_disconnect.disconnect_all_slots();
self->sig_snote.disconnect_all_slots();
self->sig_authenticate.disconnect_all_slots();
}); });
} }
} // namespace myirc

View File

@@ -0,0 +1,49 @@
#pragma once
#include "client.hpp"
#include <boost/signals2.hpp>
#include <memory>
namespace myirc {
struct Bot : std::enable_shared_from_this<Bot>
{
struct Command
{
std::string_view source;
std::string_view target;
std::string_view oper;
std::string_view account;
std::string_view command;
std::string_view arguments;
auto nick() const -> std::string_view {
auto bang = source.find('!');
if (bang == std::string::npos) {
return "";
} else {
return source.substr(0, bang);
}
}
};
std::shared_ptr<Client> self_;
char command_prefix_;
boost::signals2::signal<void(const Command &)> sig_command;
Bot(std::shared_ptr<Client> self)
: self_{std::move(self)}
, command_prefix_{'!'}
{}
auto on_chat(const Chat &) -> void;
auto process_command(std::string_view message, const Chat &msg) -> void;
static auto start(std::shared_ptr<Client>) -> std::shared_ptr<Bot>;
auto shutdown() -> void;
};
} // namespace myirc

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
namespace myirc {
template <typename> struct CCallback_; template <typename> struct CCallback_;
template <typename F, typename R, typename... Ts> template <typename F, typename R, typename... Ts>
struct CCallback_<R (F::*) (Ts...) const> struct CCallback_<R (F::*) (Ts...) const>
@@ -10,5 +12,9 @@ struct CCallback_<R (F::*) (Ts...) const>
} }
}; };
/// @brief Wrapper for passing closures through C-style callbacks.
/// @tparam F Type of the closure
template <typename F> template <typename F>
using CCallback = CCallback_<decltype(&F::operator())>; using CCallback = CCallback_<decltype(&F::operator())>;
} // namespace myirc

View File

@@ -0,0 +1,35 @@
#pragma once
#include "myirc/client.hpp"
#include "myirc/ref.hpp"
#include <boost/signals2/connection.hpp>
#include <memory>
#include <string>
namespace myirc {
/// @brief Implements the CHALLENGE command protocol to identify as an operator.
class Challenge : std::enable_shared_from_this<Challenge>
{
Ref<EVP_PKEY> key_;
std::shared_ptr<Client> client_;
boost::signals2::scoped_connection slot_;
std::string buffer_;
auto on_ircmsg(IrcCommand cmd, const IrcMsg &msg) -> void;
auto finish_challenge() -> void;
public:
Challenge(Ref<EVP_PKEY>, std::shared_ptr<Client>);
/// @brief Starts the CHALLENGE protocol.
/// @param connection Registered connection.
/// @param user Operator username
/// @param key Operator private RSA key
/// @return Handle to the challenge object.
static auto start(std::shared_ptr<Client>, std::string_view user, Ref<EVP_PKEY> key) -> std::shared_ptr<Challenge>;
};
} // namespace myirc

View File

@@ -0,0 +1,145 @@
#pragma once
#include "myirc/connection.hpp"
#include "myirc/sasl_mechanism.hpp"
#include <string>
#include <unordered_set>
#include <span>
namespace myirc {
struct IrcMsg;
enum class Casemap
{
Rfc1459,
Rfc1459_Strict,
Ascii,
};
struct Chat {
std::span<const irctag> tags;
bool is_notice;
char status_msg;
std::string_view source;
std::string_view target;
std::string_view message;
};
/**
* @brief Thread to track this connection's identity, and IRC state.
*
*/
class Client
{
std::shared_ptr<Connection> connection_;
std::string nickname_;
std::string mode_;
std::unordered_set<std::string> channels_;
// RPL_ISUPPORT state
std::unordered_map<std::string, std::string> isupport_;
std::unique_ptr<SaslMechanism> sasl_mechanism_;
Casemap casemap_;
std::string channel_prefix_;
std::string status_msg_;
std::unordered_map<std::string, std::string> caps_available_;
std::unordered_set<std::string> caps_;
// AUTHENTICATE support
std::string authenticate_buffer_;
auto on_authenticate(std::string_view) -> void;
auto on_authenticate_chunk(std::string_view) -> void;
auto on_welcome(const IrcMsg &irc) -> void;
auto on_isupport(const IrcMsg &irc) -> void;
auto on_nick(const IrcMsg &irc) -> void;
auto on_umodeis(const IrcMsg &irc) -> void;
auto on_join(const IrcMsg &irc) -> void;
auto on_kick(const IrcMsg &irc) -> void;
auto on_part(const IrcMsg &irc) -> void;
auto on_mode(const IrcMsg &irc) -> void;
auto on_cap(const IrcMsg &irc) -> void;
auto on_registered() -> void;
auto on_chat(bool, const IrcMsg &irc, bool flush) -> void;
public:
boost::signals2::signal<void()> sig_registered;
boost::signals2::signal<void(const std::unordered_map<std::string, std::string> &)> sig_cap_ls;
boost::signals2::signal<void(const Chat &, bool flush)> sig_chat;
boost::signals2::signal<void(SnoteMatch &, bool flush)> sig_snote;
Client(std::shared_ptr<Connection> connection)
: connection_{std::move(connection)}
, casemap_{Casemap::Rfc1459}
, channel_prefix_{"#&"}
, status_msg_{"+@"}
{
}
auto get_connection() -> Connection & { return *connection_; }
static auto start(std::shared_ptr<Connection>) -> std::shared_ptr<Client>;
auto start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void;
auto get_connection() const -> std::shared_ptr<Connection>
{
return connection_->shared_from_this();
}
auto get_my_nick() const -> const std::string &;
auto get_my_mode() const -> const std::string &;
auto get_my_channels() const -> const std::unordered_set<std::string> &;
auto list_caps() -> void;
auto is_my_nick(std::string_view nick) const -> bool;
auto is_my_mask(std::string_view mask) const -> bool;
auto is_channel(std::string_view name) const -> bool;
auto is_on_channel(std::string_view name) const -> bool;
auto casemap(std::string_view) const -> std::string;
auto casemap_compare(std::string_view, std::string_view) const -> int;
auto shutdown() -> void;
auto send_ping(std::string_view) -> void;
auto send_pong(std::string_view) -> void;
auto send_pass(std::string_view) -> void;
auto send_user(std::string_view, std::string_view) -> void;
auto send_nick(std::string_view) -> void;
auto send_join(std::string_view) -> void;
auto send_names(std::string_view channel) -> void;
auto send_kick(std::string_view, std::string_view, std::string_view) -> void;
auto send_kill(std::string_view, std::string_view) -> void;
auto send_quit(std::string_view) -> void;
auto send_cap_ls() -> void;
auto send_cap_end() -> void;
auto send_map() -> void;
auto send_testline(std::string_view) -> void;
auto send_testmask(std::string_view) -> void;
auto send_testmask_gecos(std::string_view, std::string_view) -> void;
auto send_masktrace(std::string_view) -> void;
auto send_masktrace_gecos(std::string_view, std::string_view) -> void;
auto send_get_topic(std::string_view) -> void;
auto send_set_topic(std::string_view, std::string_view) -> void;
auto send_cap_req(std::string_view) -> void;
auto send_privmsg(std::string_view, std::string_view) -> void;
auto send_wallops(std::string_view) -> void;
auto send_notice(std::string_view, std::string_view) -> void;
auto send_authenticate(std::string_view message) -> void;
auto send_authenticate_encoded(std::string_view message) -> void;
auto send_authenticate_abort() -> void;
auto send_whois(std::string_view) -> void;
auto send_whois_remote(std::string_view, std::string_view) -> void;
auto send_challenge(std::string_view) -> void;
auto send_oper(std::string_view, std::string_view) -> void;
};
} // namespace myirc

View File

@@ -0,0 +1,107 @@
#pragma once
#include "irc_command.hpp"
#include "ircmsg.hpp"
#include "ratelimit.hpp"
#include "ref.hpp"
#include "stream.hpp"
#include <boost/asio.hpp>
#include <boost/signals2.hpp>
#include <list>
#include <memory>
#include <string>
namespace myirc {
struct SnoteMatch;
class Connection : public std::enable_shared_from_this<Connection>
{
public:
struct Settings
{
bool tls;
std::string host;
std::uint16_t port;
Ref<X509> client_cert;
Ref<EVP_PKEY> client_key;
std::string verify;
std::string sni;
std::string socks_host;
std::uint16_t socks_port;
std::string socks_user;
std::string socks_pass;
};
private:
Stream stream_;
boost::asio::steady_timer watchdog_timer_;
std::list<std::string> write_strings_;
bool write_posted_;
// Set true when watchdog triggers.
// Set false when message received.
bool stalled_;
/// write buffers after consulting with rate limit
auto write_buffers() -> void;
/// write a specific number of messages now
auto write_buffers(size_t) -> void;
auto dispatch_line(char *line, bool) -> void;
static constexpr std::chrono::seconds watchdog_duration = std::chrono::seconds{30};
auto watchdog() -> void;
auto watchdog_activity() -> void;
auto connect(Settings settings) -> boost::asio::awaitable<void>;
public:
boost::signals2::signal<void(
boost::asio::ip::tcp::endpoint,
std::optional<boost::asio::ip::tcp::endpoint>,
std::string
)> sig_connect;
boost::signals2::signal<void(std::exception_ptr)> sig_disconnect;
boost::signals2::signal<void(IrcCommand, const IrcMsg &, bool flush)> sig_ircmsg;
std::unique_ptr<RateLimit> rate_limit;
Connection(boost::asio::io_context &io);
auto get_executor() -> boost::asio::any_io_executor
{
return stream_.get_executor();
}
auto start(Settings) -> void;
auto close() -> void;
/// Build and send well-formed IRC message from individual parameters
auto write_irc(std::string) -> void;
auto write_irc(std::string, std::string_view) -> void;
template <typename... Args>
auto write_irc(std::string front, std::string_view next, Args... rest) -> void;
};
template <typename... Args>
auto Connection::write_irc(std::string front, std::string_view next, Args... rest) -> void
{
using namespace std::literals;
if (next.empty() || next.front() == ':' || next.find_first_of("\r\n \0"sv) != next.npos)
{
throw std::runtime_error{"bad irc argument"};
}
front += " ";
front += next;
write_irc(std::move(front), rest...);
}
} // namespace myirc

View File

@@ -1,3 +1,7 @@
#pragma once
namespace myirc {
enum class IrcCommand enum class IrcCommand
{ {
UNKNOWN, UNKNOWN,
@@ -9,7 +13,6 @@ enum class IrcCommand
RPL_SNOMASK, RPL_SNOMASK,
RPL_REDIR, RPL_REDIR,
RPL_MAP, RPL_MAP,
RPL_MAPMORE,
RPL_MAPEND, RPL_MAPEND,
RPL_SAVENICK, RPL_SAVENICK,
RPL_TRACELINK, RPL_TRACELINK,
@@ -24,7 +27,6 @@ enum class IrcCommand
RPL_STATSLINKINFO, RPL_STATSLINKINFO,
RPL_STATSCOMMANDS, RPL_STATSCOMMANDS,
RPL_STATSCLINE, RPL_STATSCLINE,
RPL_STATSNLINE,
RPL_STATSILINE, RPL_STATSILINE,
RPL_STATSKLINE, RPL_STATSKLINE,
RPL_STATSQLINE, RPL_STATSQLINE,
@@ -32,15 +34,11 @@ enum class IrcCommand
RPL_ENDOFSTATS, RPL_ENDOFSTATS,
RPL_STATSPLINE, RPL_STATSPLINE,
RPL_UMODEIS, RPL_UMODEIS,
RPL_STATSFLINE,
RPL_STATSDLINE, RPL_STATSDLINE,
RPL_SERVLIST,
RPL_SERVLISTEND,
RPL_STATSLLINE, RPL_STATSLLINE,
RPL_STATSUPTIME, RPL_STATSUPTIME,
RPL_STATSOLINE, RPL_STATSOLINE,
RPL_STATSHLINE, RPL_STATSHLINE,
RPL_STATSSLINE,
RPL_STATSXLINE, RPL_STATSXLINE,
RPL_STATSULINE, RPL_STATSULINE,
RPL_STATSDEBUG, RPL_STATSDEBUG,
@@ -54,7 +52,6 @@ enum class IrcCommand
RPL_ADMINLOC1, RPL_ADMINLOC1,
RPL_ADMINLOC2, RPL_ADMINLOC2,
RPL_ADMINEMAIL, RPL_ADMINEMAIL,
RPL_TRACELOG,
RPL_ENDOFTRACE, RPL_ENDOFTRACE,
RPL_LOAD2HI, RPL_LOAD2HI,
RPL_LOCALUSERS, RPL_LOCALUSERS,
@@ -63,11 +60,9 @@ enum class IrcCommand
RPL_WHOISCERTFP, RPL_WHOISCERTFP,
RPL_ACCEPTLIST, RPL_ACCEPTLIST,
RPL_ENDOFACCEPT, RPL_ENDOFACCEPT,
RPL_NONE,
RPL_AWAY, RPL_AWAY,
RPL_USERHOST, RPL_USERHOST,
RPL_ISON, RPL_ISON,
RPL_TEXT,
RPL_UNAWAY, RPL_UNAWAY,
RPL_NOWAWAY, RPL_NOWAWAY,
RPL_WHOISHELPOP, RPL_WHOISHELPOP,
@@ -76,7 +71,6 @@ enum class IrcCommand
RPL_WHOISOPERATOR, RPL_WHOISOPERATOR,
RPL_WHOWASUSER, RPL_WHOWASUSER,
RPL_ENDOFWHOWAS, RPL_ENDOFWHOWAS,
RPL_WHOISCHANOP,
RPL_WHOISIDLE, RPL_WHOISIDLE,
RPL_ENDOFWHOIS, RPL_ENDOFWHOIS,
RPL_WHOISCHANNELS, RPL_WHOISCHANNELS,
@@ -92,10 +86,8 @@ enum class IrcCommand
RPL_NOTOPIC, RPL_NOTOPIC,
RPL_TOPIC, RPL_TOPIC,
RPL_TOPICWHOTIME, RPL_TOPICWHOTIME,
RPL_WHOISTEXT,
RPL_WHOISACTUALLY, RPL_WHOISACTUALLY,
RPL_INVITING, RPL_INVITING,
RPL_SUMMONING,
RPL_INVITELIST, RPL_INVITELIST,
RPL_ENDOFINVITELIST, RPL_ENDOFINVITELIST,
RPL_EXCEPTLIST, RPL_EXCEPTLIST,
@@ -107,7 +99,6 @@ enum class IrcCommand
RPL_NAMREPLY, RPL_NAMREPLY,
RPL_WHOWASREAL, RPL_WHOWASREAL,
RPL_ENDOFNAMES, RPL_ENDOFNAMES,
RPL_KILLDONE,
RPL_CLOSING, RPL_CLOSING,
RPL_CLOSEEND, RPL_CLOSEEND,
RPL_LINKS, RPL_LINKS,
@@ -116,21 +107,14 @@ enum class IrcCommand
RPL_ENDOFBANLIST, RPL_ENDOFBANLIST,
RPL_INFO, RPL_INFO,
RPL_MOTD, RPL_MOTD,
RPL_INFOSTART,
RPL_ENDOFINFO, RPL_ENDOFINFO,
RPL_MOTDSTART, RPL_MOTDSTART,
RPL_ENDOFMOTD, RPL_ENDOFMOTD,
RPL_WHOISHOST, RPL_WHOISHOST,
RPL_YOUREOPER, RPL_YOUREOPER,
RPL_REHASHING, RPL_REHASHING,
RPL_MYPORTIS,
RPL_NOTOPERANYMORE,
RPL_RSACHALLENGE, RPL_RSACHALLENGE,
RPL_TIME, RPL_TIME,
RPL_USERSSTART,
RPL_USERS,
RPL_ENDOFUSERS,
RPL_NOUSERS,
RPL_HOSTHIDDEN, RPL_HOSTHIDDEN,
ERR_NOSUCHNICK, ERR_NOSUCHNICK,
ERR_NOSUCHSERVER, ERR_NOSUCHSERVER,
@@ -149,8 +133,6 @@ enum class IrcCommand
ERR_TOOMANYMATCHES, ERR_TOOMANYMATCHES,
ERR_UNKNOWNCOMMAND, ERR_UNKNOWNCOMMAND,
ERR_NOMOTD, ERR_NOMOTD,
ERR_NOADMININFO,
ERR_FILEERROR,
ERR_NONICKNAMEGIVEN, ERR_NONICKNAMEGIVEN,
ERR_ERRONEUSNICKNAME, ERR_ERRONEUSNICKNAME,
ERR_NICKNAMEINUSE, ERR_NICKNAMEINUSE,
@@ -162,27 +144,20 @@ enum class IrcCommand
ERR_USERNOTINCHANNEL, ERR_USERNOTINCHANNEL,
ERR_NOTONCHANNEL, ERR_NOTONCHANNEL,
ERR_USERONCHANNEL, ERR_USERONCHANNEL,
ERR_NOLOGIN,
ERR_SUMMONDISABLED,
ERR_USERSDISABLED,
ERR_NOTREGISTERED, ERR_NOTREGISTERED,
ERR_ACCEPTFULL, ERR_ACCEPTFULL,
ERR_ACCEPTEXIST, ERR_ACCEPTEXIST,
ERR_ACCEPTNOT, ERR_ACCEPTNOT,
ERR_NEEDMOREPARAMS, ERR_NEEDMOREPARAMS,
ERR_ALREADYREGISTRED, ERR_ALREADYREGISTRED,
ERR_NOPERMFORHOST,
ERR_PASSWDMISMATCH, ERR_PASSWDMISMATCH,
ERR_YOUREBANNEDCREEP, ERR_YOUREBANNEDCREEP,
ERR_YOUWILLBEBANNED,
ERR_KEYSET,
ERR_LINKCHANNEL, ERR_LINKCHANNEL,
ERR_CHANNELISFULL, ERR_CHANNELISFULL,
ERR_UNKNOWNMODE, ERR_UNKNOWNMODE,
ERR_INVITEONLYCHAN, ERR_INVITEONLYCHAN,
ERR_BANNEDFROMCHAN, ERR_BANNEDFROMCHAN,
ERR_BADCHANNELKEY, ERR_BADCHANNELKEY,
ERR_BADCHANMASK,
ERR_NEEDREGGEDNICK, ERR_NEEDREGGEDNICK,
ERR_BANLISTFULL, ERR_BANLISTFULL,
ERR_BADCHANNAME, ERR_BADCHANNAME,
@@ -191,7 +166,6 @@ enum class IrcCommand
ERR_CHANOPRIVSNEEDED, ERR_CHANOPRIVSNEEDED,
ERR_CANTKILLSERVER, ERR_CANTKILLSERVER,
ERR_ISCHANSERVICE, ERR_ISCHANSERVICE,
ERR_BANNEDNICK,
ERR_NONONREG, ERR_NONONREG,
ERR_VOICENEEDED, ERR_VOICENEEDED,
ERR_NOOPERHOST, ERR_NOOPERHOST,
@@ -199,7 +173,6 @@ enum class IrcCommand
ERR_OWNMODE, ERR_OWNMODE,
ERR_UMODEUNKNOWNFLAG, ERR_UMODEUNKNOWNFLAG,
ERR_USERSDONTMATCH, ERR_USERSDONTMATCH,
ERR_GHOSTEDCLIENT,
ERR_USERNOTONSERV, ERR_USERNOTONSERV,
ERR_WRONGPONG, ERR_WRONGPONG,
ERR_DISABLED, ERR_DISABLED,
@@ -228,10 +201,9 @@ enum class IrcCommand
RPL_OMOTD, RPL_OMOTD,
RPL_ENDOFOMOTD, RPL_ENDOFOMOTD,
ERR_NOPRIVS, ERR_NOPRIVS,
RPL_TESTMASK,
RPL_TESTLINE, RPL_TESTLINE,
RPL_NOTESTLINE, RPL_NOTESTLINE,
RPL_TESTMASKGECO, RPL_TESTMASKGECOS,
RPL_QUIETLIST, RPL_QUIETLIST,
RPL_ENDOFQUIETLIS, RPL_ENDOFQUIETLIS,
RPL_MONONLINE, RPL_MONONLINE,
@@ -243,7 +215,6 @@ enum class IrcCommand
RPL_ENDOFRSACHALLENGE2, RPL_ENDOFRSACHALLENGE2,
ERR_MLOCKRESTRICTE, ERR_MLOCKRESTRICTE,
ERR_INVALIDBAN, ERR_INVALIDBAN,
ERR_TOPICLOCK,
RPL_SCANMATCHED, RPL_SCANMATCHED,
RPL_SCANUMODES, RPL_SCANUMODES,
RPL_LOGGEDIN, RPL_LOGGEDIN,
@@ -263,15 +234,22 @@ enum class IrcCommand
CAP, CAP,
CHGHOST, CHGHOST,
ERROR, ERROR,
INVITE,
JOIN, JOIN,
KICK, KICK,
KILL,
MODE, MODE,
NICK, NICK,
NOTICE, NOTICE,
PART, PART,
PING, PING,
PONG,
PRIVMSG, PRIVMSG,
QUIT, QUIT,
SETNAME, SETNAME,
TAGMSG,
TOPIC, TOPIC,
WALLOPS,
}; };
} // namespace myirc

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "connection.hpp" #include "myirc/connection.hpp"
#include "myirc/snote.hpp"
#include <chrono> #include <chrono>
#include <coroutine> #include <coroutine>
@@ -8,6 +9,8 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
namespace myirc {
struct irc_promise; struct irc_promise;
/// A coroutine that can co_await on various IRC events /// A coroutine that can co_await on various IRC events
@@ -80,7 +83,28 @@ public:
template <size_t I, typename... Ts> template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void; auto start(Wait<Ts...> &command) -> void;
auto stop() -> void; auto stop() -> void { ircmsg_slot_.disconnect(); }
};
class wait_snote
{
// Vector of tags this wait is expecting. Leave empty to accept all messages.
std::vector<SnoteTag> want_tags_;
// Slot for the snote event
boost::signals2::scoped_connection snote_slot_;
public:
using result_type = SnoteMatch;
wait_snote(std::initializer_list<SnoteTag> want_tags)
: want_tags_{want_tags}
{
}
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void;
auto stop() -> void { snote_slot_.disconnect(); }
}; };
class wait_timeout class wait_timeout
@@ -99,13 +123,12 @@ public:
template <size_t I, typename... Ts> template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void; auto start(Wait<Ts...> &command) -> void;
auto stop() -> void; auto stop() -> void { timer_->cancel(); }
}; };
template <typename... Ts> template <typename... Ts>
class Wait class Wait
{ {
// State associated with each wait mode // State associated with each wait mode
std::tuple<Ts...> modes_; std::tuple<Ts...> modes_;
@@ -180,6 +203,19 @@ auto wait_ircmsg::start(Wait<Ts...> &command) -> void
}); });
} }
template <size_t I, typename... Ts>
auto wait_snote::start(Wait<Ts...> &command) -> void
{
snote_slot_ = command.get_connection().sig_snote.connect([this, &command](auto &match) {
const auto tag = match.get_tag();
const auto wanted = want_tags_.empty() || std::find(want_tags_.begin(), want_tags_.end(), tag) != want_tags_.end();
if (wanted)
{
command.template complete<I>(match);
}
});
}
template <size_t I, typename... Ts> template <size_t I, typename... Ts>
auto wait_timeout::start(Wait<Ts...> &command) -> void auto wait_timeout::start(Wait<Ts...> &command) -> void
{ {
@@ -202,7 +238,7 @@ auto Wait<Ts...>::await_suspend(std::coroutine_handle<irc_promise> handle) -> vo
const auto tuple_size = std::tuple_size_v<decltype(modes_)>; const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
start_modes(std::make_index_sequence<tuple_size>{}); start_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_ = get_connection().sig_disconnect.connect([this]() { disconnect_slot_ = get_connection().sig_disconnect.connect([this](auto) {
handle_.resume(); handle_.resume();
}); });
} }
@@ -224,3 +260,23 @@ auto Wait<Ts...>::await_resume() -> std::variant<typename Ts::result_type...>
throw std::runtime_error{"connection terminated"}; throw std::runtime_error{"connection terminated"};
} }
} }
/// Start the coroutine and associate it with a specific connection.
inline auto irc_coroutine::start(Connection &connection) -> void
{
promise().connection_ = connection.shared_from_this();
resume();
}
/// Returns true when this coroutine is still waiting on events
inline auto irc_coroutine::is_running() -> bool
{
return promise().connection_ != nullptr;
}
inline auto irc_coroutine::exception() -> std::exception_ptr
{
return promise().exception_;
}
} // namespace myirc

View File

@@ -4,6 +4,8 @@
#include <string_view> #include <string_view>
#include <vector> #include <vector>
namespace myirc {
struct irctag struct irctag
{ {
std::string_view key; std::string_view key;
@@ -72,3 +74,5 @@ struct irc_parse_error : public std::exception
auto parse_irc_message(char *msg) -> IrcMsg; auto parse_irc_message(char *msg) -> IrcMsg;
auto parse_irc_tags(char *msg) -> std::vector<irctag>; auto parse_irc_tags(char *msg) -> std::vector<irctag>;
} // namespace myirc

View File

@@ -0,0 +1,104 @@
#pragma once
/**
* @file linebuffer.hpp
* @author Eric Mertens <emertens@gmail.com>
* @brief A line buffering class
* @version 0.1
* @date 2023-08-22
*
* @copyright Copyright (c) 2023
*
*/
#include <boost/asio/buffer.hpp>
#include <vector>
namespace myirc {
/**
* @brief Fixed-size buffer with line-oriented dispatch
*
*/
class LineBuffer
{
std::vector<char> buffer_;
// [std::begin(buffer), end_) contains buffered data
// [end_, std::end(buffer)) is available buffer space
decltype(buffer_)::iterator start_;
decltype(buffer_)::iterator search_;
decltype(buffer_)::iterator end_;
public:
/**
* @brief Construct a new Line Buffer object
*
* @param n Buffer size
*/
LineBuffer(std::size_t n)
: buffer_(n)
, start_{buffer_.begin()}
, search_{buffer_.begin()}
, end_{buffer_.begin()}
{
}
// can't copy the iterator member safely
LineBuffer(LineBuffer const&) = delete;
LineBuffer(LineBuffer&&) = delete;
auto operator=(LineBuffer const&) -> LineBuffer& = delete;
auto operator=(LineBuffer&&) -> LineBuffer& = delete;
/**
* @brief Get the available buffer space
*
* @return boost::asio::mutable_buffer
*/
auto prepare() -> boost::asio::mutable_buffer
{
return boost::asio::buffer(&*end_, std::distance(end_, buffer_.end()));
}
/**
* @brief Commit new buffer bytes and dispatch line callback
*
* The first n bytes of the buffer will be considered to be
* populated. The line callback function will be called once
* per completed line. Those lines are removed from the buffer
* and the is ready for additional calls to prepare and
* commit.
*
* @param n Bytes written to the last call of prepare
* @param line_cb Callback function to run on each completed line
*/
auto commit(std::size_t const n) -> void
{
std::advance(end_, n);
}
/**
* @brief Return the next null-terminated line in the buffer
*
* This function should be repeatedly called until it returns
* nullptr. After that shift can be used to reclaim the
* previously used buffer.
*
* @return null-terminated line or nullptr if no line is ready
*/
auto next_line() -> char*;
/**
* @brief Return the next non-empty line if there is one.
*/
auto next_nonempty_line() -> char*;
/**
* @brief Reclaim used buffer space invalidating all previous
* next_line() results;
*
*/
auto shift() -> void;
};
} // namespace

View File

@@ -0,0 +1,13 @@
#pragma once
#include "ref.hpp"
#include <string_view>
namespace myirc {
auto log_openssl_errors(const std::string_view prefix) -> void;
auto key_from_file(const std::string &filename, const std::string_view password) -> Ref<EVP_PKEY>;
auto cert_from_file(const std::string &filename) -> Ref<X509>;
} // namespace myirc

View File

@@ -0,0 +1,24 @@
#pragma once
#include <chrono>
#include <utility>
namespace myirc {
struct RateLimit {
virtual ~RateLimit();
auto virtual query(size_t want_to_send) -> std::pair<std::chrono::milliseconds, size_t> = 0;
};
struct Rfc1459RateLimit final : RateLimit
{
using clock = std::chrono::steady_clock;
std::chrono::milliseconds cost_ {2'000};
std::chrono::milliseconds allowance_ {10'000};
clock::time_point horizon_{};
auto query(size_t want_to_send) -> std::pair<std::chrono::milliseconds, size_t> override;
};
} // namespace myirc

View File

@@ -0,0 +1,68 @@
#pragma once
#include <openssl/evp.h>
#include <openssl/x509.h>
#include <memory>
namespace myirc {
// Specializations must Free to release a reference
// Specializations can implement UpRef to increase a reference count on copy
template <typename> struct RefTraits {};
template <> struct RefTraits<EVP_PKEY> {
static constexpr void (*Free)(EVP_PKEY*) = EVP_PKEY_free;
static constexpr int (*UpRef)(EVP_PKEY*) = EVP_PKEY_up_ref;
};
template <> struct RefTraits<X509> {
static constexpr void (*Free)(X509*) = X509_free;
static constexpr int (*UpRef)(X509*) = X509_up_ref;
};
template <> struct RefTraits<EVP_PKEY_CTX> {
static constexpr void (*Free)(EVP_PKEY_CTX*) = EVP_PKEY_CTX_free;
// this type does not implement UpRef
};
template <typename T>
struct RefDeleter {
auto operator()(T *ptr) const -> void { RefTraits<T>::Free(ptr); }
};
template <typename T>
struct Ref : std::unique_ptr<T, RefDeleter<T>>
{
using base = std::unique_ptr<T, RefDeleter<T>>;
/// Owns nothing
Ref() noexcept = default;
/// Takes ownership of the pointer
explicit Ref(T *x) noexcept : base{x} {}
/// Takes ownership of the pointer
static auto borrow(T *x) -> Ref {
RefTraits<T>::UpRef(x);
return Ref{x};
}
Ref(Ref &&ref) noexcept = default;
Ref(const Ref &ref) noexcept : base{ref.get()} {
if (*this) {
RefTraits<T>::UpRef(this->get());
}
}
Ref &operator=(Ref&&) noexcept = default;
Ref &operator=(const Ref &ref) noexcept {
if (ref) {
RefTraits<T>::UpRef(ref.get());
}
this->reset(ref.get());
return *this;
}
};
} // namespace myirc

View File

@@ -2,16 +2,26 @@
#include "connection.hpp" #include "connection.hpp"
#include "client.hpp" #include "client.hpp"
#include "settings.hpp"
#include <memory> #include <memory>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
namespace myirc {
class Registration : public std::enable_shared_from_this<Registration> class Registration : public std::enable_shared_from_this<Registration>
{ {
const Settings &settings_; public:
struct Settings {
std::string nickname;
std::string username;
std::string realname;
std::string password;
std::unique_ptr<SaslMechanism> sasl_mechanism;
};
private:
Settings settings_;
std::shared_ptr<Client> client_; std::shared_ptr<Client> client_;
boost::signals2::scoped_connection slot_; boost::signals2::scoped_connection slot_;
@@ -27,12 +37,14 @@ class Registration : public std::enable_shared_from_this<Registration>
public: public:
Registration( Registration(
const Settings &, Settings,
std::shared_ptr<Client> std::shared_ptr<Client>
); );
static auto start( static auto start(
const Settings &, Settings,
std::shared_ptr<Client> std::shared_ptr<Client>
) -> std::shared_ptr<Registration>; ) -> std::shared_ptr<Registration>;
}; };
} // namespace myirc

View File

@@ -0,0 +1,114 @@
#pragma once
#include "ref.hpp"
#include <boost/signals2.hpp>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
namespace myirc {
class SaslMechanism
{
public:
struct NoReply {};
struct Failure {};
using StepResult = std::variant<std::string, NoReply, Failure>;
virtual ~SaslMechanism() {}
virtual auto mechanism_name() const -> std::string = 0;
virtual auto step(std::string_view msg) -> StepResult = 0;
virtual auto is_complete() const -> bool = 0;
};
class SaslPlain final : public SaslMechanism
{
std::string authcid_;
std::string authzid_;
std::string password_;
bool complete_;
public:
SaslPlain(std::string authcid, std::string authzid, std::string password)
: authcid_{std::move(authcid)}
, authzid_{std::move(authzid)}
, password_{std::move(password)}
, complete_{false}
{}
auto mechanism_name() const -> std::string override
{
return "PLAIN";
}
auto step(std::string_view msg) -> StepResult override;
auto is_complete() const -> bool override
{
return complete_;
}
};
class SaslExternal final : public SaslMechanism
{
std::string authzid_;
bool complete_;
public:
SaslExternal(std::string authzid)
: authzid_{std::move(authzid)}
, complete_{false}
{}
auto mechanism_name() const -> std::string override
{
return "EXTERNAL";
}
auto step(std::string_view msg) -> StepResult override;
auto is_complete() const -> bool override
{
return complete_;
}
};
class SaslEcdsa final : public SaslMechanism
{
std::string message1_;
Ref<EVP_PKEY> key_;
int stage_;
public:
SaslEcdsa(std::string authcid, std::string authzid, Ref<EVP_PKEY> key)
: message1_{std::move(authcid)}
, key_{std::move(key)}
, stage_{0}
{
if (not authzid.empty()) {
message1_.push_back(0);
message1_.append(authzid);
}
}
auto mechanism_name() const -> std::string override
{
return "ECDSA-NIST256P-CHALLENGE";
}
auto step(std::string_view msg) -> StepResult override;
auto is_complete() const -> bool override
{
return stage_ == 2;;
}
};
} // namespace myirc

View File

@@ -12,23 +12,50 @@
struct hs_database; struct hs_database;
struct hs_scratch; struct hs_scratch;
namespace myirc {
enum class SnoteTag enum class SnoteTag
{ {
ClientConnecting, ClientConnecting,
ClientExiting, ClientExiting,
RejectingKlined,
NickChange,
CreateChannel, CreateChannel,
TemporaryKlineExpired,
PropagatedBanExpired,
DisconnectingKlined, DisconnectingKlined,
DroppedChannel,
DroppedNick,
DroppedNickRename,
DroppedAccount,
FailedChallenge,
FailedChallengeFingerprintMismatch,
FailedChallengeHostMismatch,
FailedChallengeMissingSecure,
FailedChallengeNoBlock,
FailedChallengeTls,
Freeze,
IsNowOper,
IsNowOperGlobal,
JoinedJuped,
Killed,
KilledRemote,
KilledRemoteOper,
LoginAttempts,
NewPropagatedKline, NewPropagatedKline,
NewTemporaryKline, NewTemporaryKline,
LoginAttempts, NickChange,
NickCollision,
NickCollisionServices,
OperspyWhois,
OperspyWho,
PossibleFlooder, PossibleFlooder,
Killed, PropagatedBanExpired,
TooManyGlobalConnections, RejectingKlined,
SaveMessage,
SetVhostOnMarkedAccount, SetVhostOnMarkedAccount,
SighupReloadingConf,
Spambot,
TemporaryDline,
TemporaryKlineExpired,
TooManyGlobalConnections,
TooManyUserConnections,
}; };
class SnoteMatch class SnoteMatch
@@ -70,3 +97,5 @@ struct SnoteCore
}; };
extern SnoteCore snoteCore; extern SnoteCore snoteCore;
} // namespace myirc

View File

@@ -6,6 +6,8 @@
#include <cstddef> #include <cstddef>
#include <variant> #include <variant>
namespace myirc {
/// @brief Abstraction over plain-text and TLS streams. /// @brief Abstraction over plain-text and TLS streams.
class Stream : private class Stream : private
std::variant< std::variant<
@@ -112,3 +114,5 @@ public:
socket.lowest_layer().close(err); socket.lowest_layer().close(err);
} }
}; };
} // namespace myirc

252
myirc/irc_commands.gperf Normal file
View File

@@ -0,0 +1,252 @@
struct RecognizedCommand {
char const* text;
IrcCommand command;
std::size_t min_args;
std::size_t max_args;
};
%%
001, IrcCommand::RPL_WELCOME, 2, 2
002, IrcCommand::RPL_YOURHOST, 2, 2
003, IrcCommand::RPL_CREATED, 2, 2
004, IrcCommand::RPL_MYINFO, 5, 6
005, IrcCommand::RPL_ISUPPORT, 2, 15
008, IrcCommand::RPL_SNOMASK, 3, 3
010, IrcCommand::RPL_REDIR, 4, 4
015, IrcCommand::RPL_MAP, 2, 2
017, IrcCommand::RPL_MAPEND, 2, 2
043, IrcCommand::RPL_SAVENICK, 3, 3
200, IrcCommand::RPL_TRACELINK, 5, 5
201, IrcCommand::RPL_TRACECONNECTING, 4, 4
202, IrcCommand::RPL_TRACEHANDSHAKE, 4, 4
203, IrcCommand::RPL_TRACEUNKNOWN, 6, 6
204, IrcCommand::RPL_TRACEOPERATOR, 7, 7
205, IrcCommand::RPL_TRACEUSER, 7, 7
206, IrcCommand::RPL_TRACESERVER, 8, 8
208, IrcCommand::RPL_TRACENEWTYPE, 4, 4
209, IrcCommand::RPL_TRACECLASS, 4, 4
211, IrcCommand::RPL_STATSLINKINFO, 8, 8
212, IrcCommand::RPL_STATSCOMMANDS, 5, 5
213, IrcCommand::RPL_STATSCLINE, 8, 8
215, IrcCommand::RPL_STATSILINE, 8, 8
216, IrcCommand::RPL_STATSKLINE, 6, 6
217, IrcCommand::RPL_STATSQLINE, 5, 5
218, IrcCommand::RPL_STATSYLINE, 10, 10
219, IrcCommand::RPL_ENDOFSTATS, 3, 3
220, IrcCommand::RPL_STATSPLINE, 6, 6
221, IrcCommand::RPL_UMODEIS, 2, 2
225, IrcCommand::RPL_STATSDLINE, 4, 4
241, IrcCommand::RPL_STATSLLINE, 7, 7
242, IrcCommand::RPL_STATSUPTIME, 2, 2
243, IrcCommand::RPL_STATSOLINE, 7, 7
244, IrcCommand::RPL_STATSHLINE, 7, 7
247, IrcCommand::RPL_STATSXLINE, 5, 5
248, IrcCommand::RPL_STATSULINE, 5, 5
249, IrcCommand::RPL_STATSDEBUG, 3, 3
250, IrcCommand::RPL_STATSCONN, 2, 2
251, IrcCommand::RPL_LUSERCLIENT, 2, 2
252, IrcCommand::RPL_LUSEROP, 3, 3
253, IrcCommand::RPL_LUSERUNKNOWN, 3, 3
254, IrcCommand::RPL_LUSERCHANNELS, 3, 3
255, IrcCommand::RPL_LUSERME, 2, 2
256, IrcCommand::RPL_ADMINME, 3, 3
257, IrcCommand::RPL_ADMINLOC1, 2, 2
258, IrcCommand::RPL_ADMINLOC2, 2, 2
259, IrcCommand::RPL_ADMINEMAIL, 2, 2
262, IrcCommand::RPL_ENDOFTRACE, 3, 3
263, IrcCommand::RPL_LOAD2HI, 3, 3
265, IrcCommand::RPL_LOCALUSERS, 4, 4
266, IrcCommand::RPL_GLOBALUSERS, 4, 4
270, IrcCommand::RPL_PRIVS, 3, 3
276, IrcCommand::RPL_WHOISCERTFP, 3, 3
281, IrcCommand::RPL_ACCEPTLIST, 1, 15
282, IrcCommand::RPL_ENDOFACCEPT, 2, 2
301, IrcCommand::RPL_AWAY, 3, 3
302, IrcCommand::RPL_USERHOST, 2, 2
303, IrcCommand::RPL_ISON, 2, 2
305, IrcCommand::RPL_UNAWAY, 2, 2
306, IrcCommand::RPL_NOWAWAY, 2, 2
310, IrcCommand::RPL_WHOISHELPOP, 3, 3
311, IrcCommand::RPL_WHOISUSER, 6, 6
312, IrcCommand::RPL_WHOISSERVER, 4, 4
313, IrcCommand::RPL_WHOISOPERATOR, 3, 3
314, IrcCommand::RPL_WHOWASUSER, 6, 6
369, IrcCommand::RPL_ENDOFWHOWAS, 3, 3
317, IrcCommand::RPL_WHOISIDLE, 5, 5
318, IrcCommand::RPL_ENDOFWHOIS, 3, 3
319, IrcCommand::RPL_WHOISCHANNELS, 3, 3
320, IrcCommand::RPL_WHOISSPECIAL, 3, 3
321, IrcCommand::RPL_LISTSTART, 3, 3
322, IrcCommand::RPL_LIST, 4, 4
323, IrcCommand::RPL_LISTEND, 2, 2
324, IrcCommand::RPL_CHANNELMODEIS, 3, 3
325, IrcCommand::RPL_CHANNELMLOCK, 4, 4
328, IrcCommand::RPL_CHANNELURL, 2, 2
329, IrcCommand::RPL_CREATIONTIME, 3, 3
330, IrcCommand::RPL_WHOISLOGGEDIN, 4, 4
331, IrcCommand::RPL_NOTOPIC, 3, 3
332, IrcCommand::RPL_TOPIC, 3, 3
333, IrcCommand::RPL_TOPICWHOTIME, 4, 4
338, IrcCommand::RPL_WHOISACTUALLY, 4, 4
341, IrcCommand::RPL_INVITING, 3, 3
346, IrcCommand::RPL_INVITELIST, 5, 5
347, IrcCommand::RPL_ENDOFINVITELIST, 3, 3
348, IrcCommand::RPL_EXCEPTLIST, 5, 5
349, IrcCommand::RPL_ENDOFEXCEPTLIST, 3, 3
351, IrcCommand::RPL_VERSION, 4, 4
352, IrcCommand::RPL_WHOREPLY, 8, 8
354, IrcCommand::RPL_WHOSPCRPL, 1, 15
315, IrcCommand::RPL_ENDOFWHO, 3, 3
353, IrcCommand::RPL_NAMREPLY, 4, 4
360, IrcCommand::RPL_WHOWASREAL, 3, 3
366, IrcCommand::RPL_ENDOFNAMES, 3, 3
362, IrcCommand::RPL_CLOSING, 3, 3
363, IrcCommand::RPL_CLOSEEND, 3, 3
364, IrcCommand::RPL_LINKS, 4, 4
365, IrcCommand::RPL_ENDOFLINKS, 3, 3
367, IrcCommand::RPL_BANLIST, 5, 5
368, IrcCommand::RPL_ENDOFBANLIST, 3, 3
371, IrcCommand::RPL_INFO, 2, 2
372, IrcCommand::RPL_MOTD, 2, 2
374, IrcCommand::RPL_ENDOFINFO, 2, 2
375, IrcCommand::RPL_MOTDSTART, 2, 2
376, IrcCommand::RPL_ENDOFMOTD, 2, 2
378, IrcCommand::RPL_WHOISHOST, 3, 3
381, IrcCommand::RPL_YOUREOPER, 2, 2
382, IrcCommand::RPL_REHASHING, 3, 3
386, IrcCommand::RPL_RSACHALLENGE, 2, 2
391, IrcCommand::RPL_TIME, 3, 3
396, IrcCommand::RPL_HOSTHIDDEN, 2, 2
401, IrcCommand::ERR_NOSUCHNICK, 3, 3
402, IrcCommand::ERR_NOSUCHSERVER, 3, 3
403, IrcCommand::ERR_NOSUCHCHANNEL, 3, 3
404, IrcCommand::ERR_CANNOTSENDTOCHAN, 3, 3
405, IrcCommand::ERR_TOOMANYCHANNELS, 3, 3
406, IrcCommand::ERR_WASNOSUCHNICK, 3, 3
407, IrcCommand::ERR_TOOMANYTARGETS, 3, 3
409, IrcCommand::ERR_NOORIGIN, 2, 2
410, IrcCommand::ERR_INVALIDCAPCMD, 3, 3
411, IrcCommand::ERR_NORECIPIENT, 2, 2
412, IrcCommand::ERR_NOTEXTTOSEND, 2, 2
413, IrcCommand::ERR_NOTOPLEVEL, 3, 3
414, IrcCommand::ERR_WILDTOPLEVEL, 3, 3
415, IrcCommand::ERR_MSGNEEDREGGEDNICK, 3, 3
416, IrcCommand::ERR_TOOMANYMATCHES, 3, 3
421, IrcCommand::ERR_UNKNOWNCOMMAND, 3, 3
422, IrcCommand::ERR_NOMOTD, 2, 2
431, IrcCommand::ERR_NONICKNAMEGIVEN, 2, 2
432, IrcCommand::ERR_ERRONEUSNICKNAME, 3, 3
433, IrcCommand::ERR_NICKNAMEINUSE, 3, 3
435, IrcCommand::ERR_BANNICKCHANGE, 3, 3
436, IrcCommand::ERR_NICKCOLLISION, 3, 3
437, IrcCommand::ERR_UNAVAILRESOURCE, 3, 3
438, IrcCommand::ERR_NICKTOOFAST, 4, 4
440, IrcCommand::ERR_SERVICESDOWN, 3, 3
441, IrcCommand::ERR_USERNOTINCHANNEL, 4, 4
442, IrcCommand::ERR_NOTONCHANNEL, 3, 3
443, IrcCommand::ERR_USERONCHANNEL, 4, 4
451, IrcCommand::ERR_NOTREGISTERED, 2, 2
456, IrcCommand::ERR_ACCEPTFULL, 2, 2
457, IrcCommand::ERR_ACCEPTEXIST, 3, 3
458, IrcCommand::ERR_ACCEPTNOT, 3, 3
461, IrcCommand::ERR_NEEDMOREPARAMS, 3, 3
462, IrcCommand::ERR_ALREADYREGISTRED, 2, 2
464, IrcCommand::ERR_PASSWDMISMATCH, 2, 2
465, IrcCommand::ERR_YOUREBANNEDCREEP, 2, 2
470, IrcCommand::ERR_LINKCHANNEL, 4, 4
471, IrcCommand::ERR_CHANNELISFULL, 3, 3
472, IrcCommand::ERR_UNKNOWNMODE, 3, 3
473, IrcCommand::ERR_INVITEONLYCHAN, 3, 3
474, IrcCommand::ERR_BANNEDFROMCHAN, 3, 3
475, IrcCommand::ERR_BADCHANNELKEY, 3, 3
477, IrcCommand::ERR_NEEDREGGEDNICK, 3, 3
478, IrcCommand::ERR_BANLISTFULL, 4, 4
479, IrcCommand::ERR_BADCHANNAME, 3, 3
480, IrcCommand::ERR_THROTTLE, 3, 3
481, IrcCommand::ERR_NOPRIVILEGES, 2, 2
482, IrcCommand::ERR_CHANOPRIVSNEEDED, 3, 3
483, IrcCommand::ERR_CANTKILLSERVER, 2, 2
484, IrcCommand::ERR_ISCHANSERVICE, 4, 4
486, IrcCommand::ERR_NONONREG, 3, 3
489, IrcCommand::ERR_VOICENEEDED, 3, 3
491, IrcCommand::ERR_NOOPERHOST, 2, 2
492, IrcCommand::ERR_CANNOTSENDTOUSER, 2, 2
494, IrcCommand::ERR_OWNMODE, 3, 3
501, IrcCommand::ERR_UMODEUNKNOWNFLAG, 2, 2
502, IrcCommand::ERR_USERSDONTMATCH, 2, 2
504, IrcCommand::ERR_USERNOTONSERV, 3, 3
513, IrcCommand::ERR_WRONGPONG, 2, 2
517, IrcCommand::ERR_DISABLED, 3, 3
524, IrcCommand::ERR_HELPNOTFOUND, 3, 3
670, IrcCommand::RPL_STARTTLS, 2, 2
671, IrcCommand::RPL_WHOISSECURE, 3, 3
691, IrcCommand::ERR_STARTTLS, 2, 2
702, IrcCommand::RPL_MODLIST, 5, 5
703, IrcCommand::RPL_ENDOFMODLIST, 2, 2
704, IrcCommand::RPL_HELPSTART, 3, 3
705, IrcCommand::RPL_HELPTXT, 3, 3
706, IrcCommand::RPL_ENDOFHELP, 3, 3
707, IrcCommand::ERR_TARGCHANGE, 3, 3
708, IrcCommand::RPL_ETRACEFULL, 10, 10
709, IrcCommand::RPL_ETRACE, 8, 8
710, IrcCommand::RPL_KNOCK, 4, 4
711, IrcCommand::RPL_KNOCKDLVR, 3, 3
712, IrcCommand::ERR_TOOMANYKNOCK, 3, 3
713, IrcCommand::ERR_CHANOPEN, 3, 3
714, IrcCommand::ERR_KNOCKONCHAN, 3, 3
715, IrcCommand::ERR_KNOCKDISABLED, 2, 2
716, IrcCommand::ERR_TARGUMODEG, 3, 3
717, IrcCommand::RPL_TARGNOTIFY, 3, 3
718, IrcCommand::RPL_UMODEGMSG, 4, 4
720, IrcCommand::RPL_OMOTDSTART, 2, 2
721, IrcCommand::RPL_OMOTD, 2, 2
722, IrcCommand::RPL_ENDOFOMOTD, 2, 2
723, IrcCommand::ERR_NOPRIVS, 3, 3
725, IrcCommand::RPL_TESTLINE, 5, 5
726, IrcCommand::RPL_NOTESTLINE, 3, 3
727, IrcCommand::RPL_TESTMASKGECOS, 6, 6
728, IrcCommand::RPL_QUIETLIST, 6, 6
729, IrcCommand::RPL_ENDOFQUIETLIS, 4, 4
730, IrcCommand::RPL_MONONLINE, 2, 2
731, IrcCommand::RPL_MONOFFLINE, 2, 2
732, IrcCommand::RPL_MONLIST, 2, 2
733, IrcCommand::RPL_ENDOFMONLIS, 2, 2
734, IrcCommand::ERR_MONLISTFULL, 4, 4
740, IrcCommand::RPL_RSACHALLENGE2, 2, 2
741, IrcCommand::RPL_ENDOFRSACHALLENGE2, 2, 2
742, IrcCommand::ERR_MLOCKRESTRICTE, 5, 5
743, IrcCommand::ERR_INVALIDBAN, 5, 5
750, IrcCommand::RPL_SCANMATCHED, 3, 3
751, IrcCommand::RPL_SCANUMODES, 8, 8
900, IrcCommand::RPL_LOGGEDIN, 4, 4
901, IrcCommand::RPL_LOGGEDOUT, 3, 3
902, IrcCommand::ERR_NICKLOCKED, 2, 2
903, IrcCommand::RPL_SASLSUCCESS, 2, 2
904, IrcCommand::ERR_SASLFAIL, 2, 2
905, IrcCommand::ERR_SASLTOOLONG, 2, 2
906, IrcCommand::ERR_SASLABORTED, 2, 2
907, IrcCommand::ERR_SASLALREADY, 2, 2
908, IrcCommand::RPL_SASLMECHS, 3, 3
ACCOUNT, IrcCommand::ACCOUNT, 1, 1
AUTHENTICATE, IrcCommand::AUTHENTICATE, 1, 1
AWAY, IrcCommand::AWAY, 0, 1
BATCH, IrcCommand::BATCH, 1, 15
BOUNCER, IrcCommand::BOUNCER, 1, 15
CAP, IrcCommand::CAP, 1, 15
CHGHOST, IrcCommand::CHGHOST, 2, 2
ERROR, IrcCommand::ERROR, 1, 1
INVITE, IrcCommand::INVITE, 2, 2
JOIN, IrcCommand::JOIN, 1, 3
KICK, IrcCommand::KICK, 3, 3
KILL, IrcCommand::KILL, 2, 2
MODE, IrcCommand::MODE, 2, 15
NICK, IrcCommand::NICK, 1, 1
NOTICE, IrcCommand::NOTICE, 2, 2
PART, IrcCommand::PART, 1, 2
PING, IrcCommand::PING, 1, 1
PONG, IrcCommand::PONG, 2, 2
PRIVMSG, IrcCommand::PRIVMSG, 2, 2
QUIT, IrcCommand::QUIT, 1, 1
SETNAME, IrcCommand::SETNAME, 1, 1
TAGMSG, IrcCommand::TAGMSG, 1, 1
TOPIC, IrcCommand::TOPIC, 2, 2
WALLOPS, IrcCommand::WALLOPS, 1, 1

View File

@@ -1,8 +1,8 @@
#include "myirc/ircmsg.hpp"
#include <cstring> #include <cstring>
#include <optional> #include <optional>
#include "ircmsg.hpp"
namespace { namespace {
class Parser class Parser
{ {
@@ -104,6 +104,8 @@ std::string_view unescape_tag_value(char *const val)
} // namespace } // namespace
namespace myirc {
auto parse_irc_tags(char *str) -> std::vector<irctag> auto parse_irc_tags(char *str) -> std::vector<irctag>
{ {
std::vector<irctag> tags; std::vector<irctag> tags;
@@ -184,3 +186,5 @@ auto operator<<(std::ostream &out, irc_error_code code) -> std::ostream &
return out; return out;
} }
} }
} // namespace myirc

54
myirc/linebuffer.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include "myirc/linebuffer.hpp"
namespace myirc {
auto LineBuffer::next_line() -> char*
{
auto const nl = std::find(search_, end_, '\n');
if (nl == end_) // no newline found, line incomplete
{
search_ = end_;
return nullptr;
}
// Null-terminate the line. Support both \n and \r\n
*(start_ < nl && *std::prev(nl) == '\r' ? std::prev(nl) : nl) = '\0';
auto const result = start_;
start_ = search_ = std::next(nl);
return &*result;
}
// Get the next complete line skipping over empty lines
auto LineBuffer::next_nonempty_line() -> char*
{
char* line;
while ((line = next_line()))
{
while (*line == ' ')
{
line++;
}
if ('\0' != *line)
{
break;
}
}
return line;
}
auto LineBuffer::shift() -> void
{
auto const first = std::begin(buffer_);
auto const gap = std::distance(start_, first);
if (gap != 0) // relocate incomplete line to front of buffer
{
end_ = std::move(start_, end_, first);
start_ = first;
std::advance(search_, gap);
}
}
} // namespace myirc

71
myirc/openssl_utils.cpp Normal file
View File

@@ -0,0 +1,71 @@
#include "myirc/openssl_utils.hpp"
#include "myirc/c_callback.hpp"
#include <openssl/err.h>
#include <openssl/pem.h>
#include <boost/log/trivial.hpp>
#include <cstdio>
using namespace std::literals;
namespace myirc {
auto log_openssl_errors(const std::string_view prefix) -> void
{
auto err_cb = [prefix](const char *str, size_t len) -> int {
BOOST_LOG_TRIVIAL(error) << prefix << std::string_view{str, len};
return 0;
};
ERR_print_errors_cb(CCallback<decltype(err_cb)>::invoke, &err_cb);
}
auto cert_from_file(const std::string &filename) -> Ref<X509>
{
Ref<X509> cert;
if (const auto fp = fopen(filename.c_str(), "r"))
{
cert.reset(PEM_read_X509(fp, nullptr, nullptr, nullptr));
if (cert.get() == nullptr)
{
log_openssl_errors("Reading certificate: "sv);
}
fclose(fp);
}
else
{
const auto err = strerror(errno);
BOOST_LOG_TRIVIAL(error) << "Opening certificate: " << err;
}
return cert;
}
auto key_from_file(const std::string &filename, const std::string_view password) -> Ref<EVP_PKEY>
{
Ref<EVP_PKEY> key;
if (const auto fp = fopen(filename.c_str(), "r"))
{
auto cb = [password](char * const buf, int const size, int) -> int {
if (std::cmp_less(size, password.size())) { return -1; }
std::copy(password.begin(), password.end(), buf);
return static_cast<int>(password.size());
};
key.reset(PEM_read_PrivateKey(fp, nullptr, CCallback<decltype(cb)>::invoke, &cb));
if (key.get() == nullptr)
{
log_openssl_errors("Reading private key: "sv);
}
fclose(fp);
}
else
{
const auto err = strerror(errno);
BOOST_LOG_TRIVIAL(error) << "Opening private key: " << err;
}
return key;
}
} // namespace myirc

28
myirc/ratelimit.cpp Normal file
View File

@@ -0,0 +1,28 @@
#include "myirc/ratelimit.hpp"
#include <chrono>
namespace myirc {
using namespace std::literals;
using ms = std::chrono::milliseconds;
auto Rfc1459RateLimit::query(size_t want_to_send) -> std::pair<ms, size_t>
{
const auto now = clock::now();
if (horizon_ < now) horizon_ = now;
auto gap = std::chrono::floor<ms>(now + allowance_ - horizon_);
auto send = gap / cost_;
if (std::cmp_greater(send, want_to_send)) send = want_to_send;
if (send > 0) {
horizon_ += send * cost_;
return {0ms, send};
} else {
horizon_ += cost_;
return {cost_ - gap, 1};
}
}
} // namespace myirc

View File

@@ -1,18 +1,19 @@
#include "registration.hpp" #include "myirc/registration.hpp"
#include "connection.hpp" #include "myirc/connection.hpp"
#include "ircmsg.hpp" #include "myirc/ircmsg.hpp"
#include "sasl_mechanism.hpp"
#include <memory> #include <memory>
#include <random> #include <random>
#include <unordered_map> #include <unordered_map>
namespace myirc {
Registration::Registration( Registration::Registration(
const Settings &settings, Settings settings,
std::shared_ptr<Client> client std::shared_ptr<Client> client
) )
: settings_{settings} : settings_{std::move(settings)}
, client_{std::move(client)} , client_{std::move(client)}
{ {
} }
@@ -28,7 +29,7 @@ auto Registration::on_connect() -> void
}); });
slot_ = connection.sig_ircmsg.connect( slot_ = connection.sig_ircmsg.connect(
[self = shared_from_this()](const auto cmd, auto &msg) [self = shared_from_this()](const auto cmd, auto &msg, auto)
{ {
self->on_ircmsg(cmd, msg); self->on_ircmsg(cmd, msg);
} }
@@ -36,10 +37,10 @@ auto Registration::on_connect() -> void
if (not settings_.password.empty()) if (not settings_.password.empty())
{ {
connection.send_pass(settings_.password); client_->send_pass(settings_.password);
} }
connection.send_user(settings_.username, settings_.realname); client_->send_user(settings_.username, settings_.realname);
connection.send_nick(settings_.nickname); client_->send_nick(settings_.nickname);
} }
auto Registration::on_cap_list(const std::unordered_map<std::string, std::string> &caps) -> void auto Registration::on_cap_list(const std::unordered_map<std::string, std::string> &caps) -> void
@@ -70,7 +71,7 @@ auto Registration::on_cap_list(const std::unordered_map<std::string, std::string
} }
} }
bool do_sasl = not settings_.sasl_mechanism.empty() && caps.contains("sasl"); bool do_sasl = settings_.sasl_mechanism && caps.contains("sasl");
if (do_sasl) { if (do_sasl) {
request.append("sasl "); request.append("sasl ");
} }
@@ -78,28 +79,24 @@ auto Registration::on_cap_list(const std::unordered_map<std::string, std::string
if (not request.empty()) if (not request.empty())
{ {
request.pop_back(); // trailing space request.pop_back(); // trailing space
client_->get_connection().send_cap_req(request); client_->send_cap_req(request);
} }
if (do_sasl) { if (do_sasl) {
client_->start_sasl( client_->start_sasl(std::move(settings_.sasl_mechanism));
std::make_unique<SaslPlain>(
settings_.sasl_authcid,
settings_.sasl_authzid,
settings_.sasl_password));
} else { } else {
client_->get_connection().send_cap_end(); client_->send_cap_end();
} }
} }
auto Registration::start( auto Registration::start(
const Settings &settings, Settings settings,
std::shared_ptr<Client> client std::shared_ptr<Client> client
) -> std::shared_ptr<Registration> ) -> std::shared_ptr<Registration>
{ {
const auto thread = std::make_shared<Registration>(std::move(settings), std::move(client)); const auto thread = std::make_shared<Registration>(std::move(settings), std::move(client));
thread->slot_ = thread->client_->get_connection().sig_connect.connect([thread]() { thread->slot_ = thread->client_->get_connection().sig_connect.connect([thread](auto, auto, auto) {
thread->slot_.disconnect(); thread->slot_.disconnect();
thread->on_connect(); thread->on_connect();
}); });
@@ -109,8 +106,7 @@ auto Registration::start(
auto Registration::randomize_nick() -> void auto Registration::randomize_nick() -> void
{ {
std::string new_nick; std::string new_nick = settings_.nickname.substr(0, 8);
new_nick += settings_.nickname.substr(0, 8);
std::random_device rd; std::random_device rd;
std::mt19937 gen{rd()}; std::mt19937 gen{rd()};
@@ -121,7 +117,7 @@ auto Registration::randomize_nick() -> void
new_nick += x < 10 ? '0' + x : 'A' + (x-10); new_nick += x < 10 ? '0' + x : 'A' + (x-10);
} }
client_->get_connection().send_nick(new_nick); client_->send_nick(new_nick);
} }
auto Registration::on_ircmsg(const IrcCommand cmd, const IrcMsg &msg) -> void auto Registration::on_ircmsg(const IrcCommand cmd, const IrcMsg &msg) -> void
@@ -143,7 +139,9 @@ auto Registration::on_ircmsg(const IrcCommand cmd, const IrcMsg &msg) -> void
case IrcCommand::RPL_SASLSUCCESS: case IrcCommand::RPL_SASLSUCCESS:
case IrcCommand::ERR_SASLFAIL: case IrcCommand::ERR_SASLFAIL:
client_->get_connection().send_cap_end(); client_->send_cap_end();
break; break;
} }
} }
} // namespace myirc

79
myirc/sasl_mechanism.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include "myirc/sasl_mechanism.hpp"
#include "myirc/openssl_utils.hpp"
#include <openssl/evp.h>
namespace myirc {
auto SaslPlain::step(std::string_view msg) -> StepResult {
if (complete_) {
return Failure{};
} else {
std::string reply;
reply += authzid_;
reply += '\0';
reply += authcid_;
reply += '\0';
reply += password_;
complete_ = true;
return std::move(reply);
}
}
auto SaslExternal::step(std::string_view msg) -> StepResult {
if (complete_) {
return Failure{};
} else {
complete_ = true;
return std::move(authzid_);
}
}
auto SaslEcdsa::step(std::string_view msg) -> StepResult {
switch (stage_) {
case 0:
stage_ = 1;
return std::move(message1_);
case 1:
{
stage_ = 2;
Ref<EVP_PKEY_CTX> ctx {EVP_PKEY_CTX_new(key_.get(), nullptr)};
if (not ctx) {
log_openssl_errors("ECDSA new context: ");
return Failure{};
}
if (0 >= EVP_PKEY_sign_init(ctx.get()))
{
log_openssl_errors("ECDSA init: ");
return Failure{};
}
const auto input = reinterpret_cast<const unsigned char *>(msg.data());
size_t siglen;
if (0 >= EVP_PKEY_sign(ctx.get(), nullptr, &siglen, input, msg.size()))
{
log_openssl_errors("ECDSA signature (presize): ");
return Failure{};
}
std::string result(siglen, '\0');
const auto output = reinterpret_cast<unsigned char *>(result.data());
if (0 >= EVP_PKEY_sign(ctx.get(), output, &siglen, input, msg.size()))
{
log_openssl_errors("ECDSA signature: ");
return Failure{};
}
result.resize(siglen);
return std::move(result);
}
default:
return Failure{};
}
}
} // namespace myirc

View File

@@ -1,6 +1,6 @@
#include "snote.hpp" #include "myirc/snote.hpp"
#include "c_callback.hpp" #include "myirc/c_callback.hpp"
#include <hs.h> #include <hs.h>
@@ -13,11 +13,14 @@
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>
namespace myirc {
namespace { namespace {
struct SnotePattern struct SnotePattern
{ {
SnotePattern(SnoteTag tag, const char *expression, unsigned flags = 0) SnotePattern(SnoteTag tag, const char *expression)
: tag{tag} : tag{tag}
, expression{expression} , expression{expression}
, regex{expression, std::regex_constants::ECMAScript | std::regex_constants::optimize} , regex{expression, std::regex_constants::ECMAScript | std::regex_constants::optimize}
@@ -29,6 +32,8 @@ struct SnotePattern
std::regex regex; std::regex regex;
}; };
using namespace std::literals;
const SnotePattern static patterns[] = { const SnotePattern static patterns[] = {
{SnoteTag::ClientConnecting, {SnoteTag::ClientConnecting,
R"(^Client connecting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \{([^ ]*)\} <([^ ]*)> \[(.*)\]$)"}, R"(^Client connecting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \{([^ ]*)\} <([^ ]*)> \[(.*)\]$)"},
@@ -66,14 +71,90 @@ const SnotePattern static patterns[] = {
{SnoteTag::PossibleFlooder, {SnoteTag::PossibleFlooder,
R"(^Possible Flooder ([^ ]+)\[([^ ]+)@[^ ]+\] on ([^ ]+) target: ([^ ]+)$)"}, R"(^Possible Flooder ([^ ]+)\[([^ ]+)@[^ ]+\] on ([^ ]+) target: ([^ ]+)$)"},
{SnoteTag::Killed, {SnoteTag::KilledRemote,
R"(^Received KILL message for ([^ ]+)!([^ ]+)@([^ ]+)\. From ([^ ]+) Path: ([^ ]+) \((.*)\)$)"}, R"(^Received KILL message for ([^ ]+)!([^ ]+)@([^ ]+)\. From ([^ ]+) Path: ([^ ]+) \((.*)\)$)"},
{SnoteTag::KilledRemoteOper,
R"(^Received KILL message for ([^ ]+)!([^ ]+)@([^ ]+)\. From ([^ ]+) Path: ([^ ]+)!([^ ]+)!([^ ]+)!([^ ]+) (.*)$)"},
{SnoteTag::Killed,
R"(^Received KILL message for ([^ ]+)!([^ ]+)@([^ ]+)\. From ([^ ]+) (.*)$)"},
{SnoteTag::TooManyGlobalConnections, {SnoteTag::TooManyGlobalConnections,
R"(^Too many global connections for ([^ ]+)\[([^ ]+)@([^ ]+)\] \[(.*)\]$)"}, R"(^Too many global connections for ([^ ]+)\[([^ ]+)@([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::TooManyUserConnections,
R"(^Too many user connections for ([^ ]+)\[([^ ]+)@([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::SetVhostOnMarkedAccount, {SnoteTag::SetVhostOnMarkedAccount,
"^\x02([^ ]+)\x02 set vhost ([^ ]+) on the \x02MARKED\x02 account ([^ ]+).$"}, "^\x02([^ ]+)\x02 set vhost ([^ ]+) on the \x02MARKED\x02 account ([^ ]+).$"},
{SnoteTag::IsNowOper,
R"(^([^ ]+) \(([^ ]+)!([^ ]+)@([^ ]+)\) is now an operator$)"},
{SnoteTag::IsNowOperGlobal,
R"(^([^ ]+) \(([^ ]+)@([^ ]+)\) is now an operator$)"},
{SnoteTag::OperspyWhois,
R"(^OPERSPY ([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} WHOIS ([^ ]+)!([^ ]+)@([^ ]+) ([^ ]+) $)"}, // trailing space intentional
{SnoteTag::OperspyWho,
R"(^OPERSPY ([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} WHO ([^ ]+)$)"},
{SnoteTag::Freeze,
"^\x02([^ ]+)\x02 froze the account \x02([^ ]+)\x02 \\((.*)\\)\\.$"},
{SnoteTag::DroppedChannel,
"^\x02([^ ]+)\x02 dropped the channel \x02([^ ]+)\x02$"},
{SnoteTag::DroppedAccount,
"^\x02([^ ]+)\x02 dropped the account \x02([^ ]+)\x02$"},
{SnoteTag::DroppedNick,
"^\x02([^ ]+)\x02 dropped the nick \x02([^ ]+)\x02 from ([^ ]+)$"},
{SnoteTag::DroppedNickRename,
"^\x02([^ ]+)\x02 dropped the nick \x02([^ ]+)\x02 from ([^ ]+), changing account name to \x02([^ ]+)\x02$"},
{SnoteTag::Spambot,
R"(^User ([^ ]+) \(([^ ]+)@([^ ]+)\) trying to join ([^ ]+) is a possible spambot$)"},
{SnoteTag::SaveMessage,
R"(^Received SAVE message for ([^ ]+) from ([^ ]+)$)"},
{SnoteTag::NickCollisionServices,
R"(^Nick collision due to services forced nick change on ([^ ]+)$)"},
{SnoteTag::NickCollision,
R"(^Nick collision on ([^ ]+)\(([^ ]+) <- ([^ ]+)\)\(([^ ]+) ([^ ]+)\)$)"},
{SnoteTag::TemporaryDline,
R"(^([^ ]+) added temporary ([^ ]+) min\. D-Line for \[([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::FailedChallengeMissingSecure,
R"(^Failed CHALLENGE attempt - missing secure connection by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallenge,
R"(^Failed CHALLENGE attempt by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallengeHostMismatch,
R"(^Failed CHALLENGE attempt - host mismatch by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallengeNoBlock,
R"(^Failed CHALLENGE attempt - user@host mismatch or no operator block for ([^ ]+) by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallengeTls,
R"(^Failed CHALLENGE attempt - missing SSL/TLS by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallengeFingerprintMismatch,
R"(^Failed CHALLENGE attempt - client certificate fingerprint mismatch by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::SighupReloadingConf,
R"(^Got signal SIGHUP, reloading ircd conf\. file$)"},
{SnoteTag::JoinedJuped,
R"(^User ([^ ]+) \(([^ ]+)@([^ ]+)\) is attempting to join locally juped channel ([^ ]+) \((.*)\)$)"},
}; };
static auto setup_database() -> hs_database_t * static auto setup_database() -> hs_database_t *
@@ -86,7 +167,7 @@ static auto setup_database() -> hs_database_t *
expressions.reserve(n); expressions.reserve(n);
ids.reserve(n); ids.reserve(n);
for (std::size_t i = 0; i < n; i++) for (unsigned i = 0; i < n; i++)
{ {
expressions.push_back(patterns[i].expression); expressions.push_back(patterns[i].expression);
ids.push_back(i); ids.push_back(i);
@@ -95,10 +176,10 @@ static auto setup_database() -> hs_database_t *
hs_database_t *db; hs_database_t *db;
hs_compile_error *error; hs_compile_error *error;
hs_platform_info_t *platform = nullptr; // target current platform hs_platform_info_t *platform = nullptr; // target current platform
switch (hs_compile_multi(expressions.data(), flags.data(), ids.data(), expressions.size(), HS_MODE_BLOCK, platform, &db, &error)) switch (hs_compile_multi(expressions.data(), flags.data(), ids.data(), static_cast<unsigned>(expressions.size()), HS_MODE_BLOCK, platform, &db, &error))
{ {
case HS_COMPILER_ERROR: { case HS_COMPILER_ERROR: {
std::string msg = error->message; std::string msg = std::to_string(error->expression) + ": " + error->message;
hs_free_compile_error(error); hs_free_compile_error(error);
throw std::runtime_error{std::move(msg)}; throw std::runtime_error{std::move(msg)};
} }
@@ -144,7 +225,8 @@ auto SnoteCore::match(const IrcMsg &msg) -> std::optional<SnoteMatch>
const auto scan_result = hs_scan( const auto scan_result = hs_scan(
db_.get(), db_.get(),
message.data(), message.size(), message.data(),
static_cast<unsigned>(message.size()),
0, // no flags 0, // no flags
scratch_.get(), scratch_.get(),
CCallback<decltype(cb)>::invoke, &cb CCallback<decltype(cb)>::invoke, &cb
@@ -200,3 +282,5 @@ auto SnoteCore::ScratchDeleter::operator()(hs_scratch_t *scratch) const -> void
} }
SnoteCore snoteCore; SnoteCore snoteCore;
} // namespace myirc

View File

@@ -1,19 +0,0 @@
#include "sasl_mechanism.hpp"
auto SaslPlain::step(std::string_view msg) -> std::optional<std::string> {
if (complete_) {
return std::nullopt;
} else {
std::string reply;
reply += authzid_;
reply += '\0';
reply += authcid_;
reply += '\0';
reply += password_;
complete_ = true;
return {std::move(reply)};
}
}

View File

@@ -1,46 +0,0 @@
#pragma once
#include <boost/signals2.hpp>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
class SaslMechanism
{
public:
virtual ~SaslMechanism() {}
virtual auto mechanism_name() const -> std::string = 0;
virtual auto step(std::string_view msg) -> std::optional<std::string> = 0;
virtual auto is_complete() const -> bool = 0;
};
class SaslPlain final : public SaslMechanism
{
std::string authcid_;
std::string authzid_;
std::string password_;
bool complete_;
public:
SaslPlain(std::string authcid, std::string authzid, std::string password)
: authcid_{std::move(authcid)}
, authzid_{std::move(authzid)}
, password_{std::move(password)}
, complete_{false}
{}
auto mechanism_name() const -> std::string override
{
return "PLAIN";
}
auto step(std::string_view msg) -> std::optional<std::string> override;
auto is_complete() const -> bool override
{
return complete_;
}
};

View File

@@ -1,23 +0,0 @@
#include "settings.hpp"
#define TOML_ENABLE_FORMATTERS 0
#include <toml++/toml.hpp>
auto Settings::from_stream(std::istream &in) -> Settings
{
const auto config = toml::parse(in);
return Settings{
.host = config["host"].value_or(std::string{}),
.service = config["port"].value_or(std::uint16_t{6667}),
.password = config["password"].value_or(std::string{}),
.username = config["username"].value_or(std::string{}),
.realname = config["realname"].value_or(std::string{}),
.nickname = config["nickname"].value_or(std::string{}),
.sasl_mechanism = config["sasl_mechanism"].value_or(std::string{}),
.sasl_authcid = config["sasl_authcid"].value_or(std::string{}),
.sasl_authzid = config["sasl_authzid"].value_or(std::string{}),
.sasl_password = config["sasl_password"].value_or(std::string{}),
.tls_hostname = config["tls_hostname"].value_or(std::string{}),
.use_tls = config["use_tls"].value_or(false),
};
}