split up driver and library

This commit is contained in:
2025-01-30 09:28:28 -08:00
parent 5218ea0892
commit 281937e2c5
31 changed files with 176 additions and 105 deletions

31
myirc/CMakeLists.txt Normal file
View File

@@ -0,0 +1,31 @@
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
sasl_mechanism.cpp
snote.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)

79
myirc/bot.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include "bot.hpp"
#include <boost/log/trivial.hpp>
auto Bot::start(std::shared_ptr<Client> self) -> std::shared_ptr<Bot>
{
const auto thread = std::make_shared<Bot>(std::move(self));
thread->self_->sig_chat.connect([thread](auto &chat) {
thread->on_chat(chat);
});
return thread;
}
auto Bot::process_command(std::string_view message, const Chat &chat) -> void
{
const auto cmdstart = message.find_first_not_of(' ');
if (cmdstart == message.npos) return;
message = message.substr(cmdstart);
if (not message.starts_with(command_prefix_)) return;
message = message.substr(1); // discard the prefix
auto cmdend = message.find(' ');
std::string_view command =
cmdend == message.npos ? message : message.substr(0, cmdend);
std::string_view arguments =
cmdend == message.npos ? std::string_view{} : message.substr(cmdend + 1);
std::string_view oper;
std::string_view account;
for (auto [key, value] : chat.tags)
{
if (key == "account")
{
account = value;
}
else if (key == "solanum.chat/oper")
{
oper = value;
}
}
sig_command({
.source = chat.source,
.target = chat.target,
.oper = oper,
.account = account,
.command = command,
.arguments = arguments
});
}
auto Bot::on_chat(const Chat &chat) -> void
{
if (not chat.is_notice)
{
if (self_->is_my_nick(chat.target))
{
process_command(chat.message, chat);
}
else if (self_->is_channel(chat.target))
{
const auto colon = chat.message.find(':');
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);
}
}
}
auto Bot::shutdown() -> void
{
sig_command.disconnect_all_slots();
}

92
myirc/challenge.cpp Normal file
View File

@@ -0,0 +1,92 @@
#include "challenge.hpp"
#include "openssl_utils.hpp"
#include <mybase64.hpp>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <boost/log/trivial.hpp>
#include <memory>
#include <string>
Challenge::Challenge(EVP_PKEY_Ref key, Connection & connection)
: key_{std::move(key)}
, connection_{connection}
{}
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();
connection_.send_ping("mitigation");
break;
case IrcCommand::RPL_ENDOFRSACHALLENGE2:
finish_challenge();
break;
}
}
auto Challenge::finish_challenge() -> void
{
EVP_PKEY_CTX_Ref ctx;
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);
if (not mybase64::decode(buffer_, reinterpret_cast<char*>(ciphertext.data()), &len))
return log_openssl_errors("Challenge base64::decode: ");
ciphertext.resize(len);
// Setup decryption context
ctx.reset(EVP_PKEY_CTX_new(key_.get(), nullptr));
if (ctx.get() == nullptr)
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);
connection_.send_challenge(buffer_);
buffer_.clear();
}
auto Challenge::start(Connection &connection, const std::string_view user, EVP_PKEY_Ref ref) -> std::shared_ptr<Challenge>
{
auto self = std::make_shared<Challenge>(std::move(ref), connection);
self->slot_ = connection.sig_ircmsg.connect([self](auto cmd, auto &msg) { self->on_ircmsg(cmd, msg); });
connection.send_challenge(user);
return self;
}

433
myirc/client.cpp Normal file
View File

@@ -0,0 +1,433 @@
#include "client.hpp"
#include "connection.hpp"
#include <mybase64.hpp>
#include <boost/container/flat_map.hpp>
#include <boost/log/trivial.hpp>
using namespace std::literals;
auto Client::on_welcome(const IrcMsg &irc) -> void
{
nickname_ = irc.args[0];
}
auto Client::on_registered() -> void
{
sig_registered();
sig_registered.disconnect_all_slots();
}
auto Client::on_nick(const IrcMsg &irc) -> void
{
if (is_my_mask(irc.source))
{
nickname_ = irc.args[0];
}
}
auto Client::on_umodeis(const IrcMsg &irc) -> void
{
mode_ = irc.args[1];
}
auto Client::on_join(const IrcMsg &irc) -> void
{
if (is_my_mask(irc.source))
{
channels_.insert(casemap(irc.args[0]));
}
}
auto Client::on_kick(const IrcMsg &irc) -> void
{
if (is_my_nick(irc.args[1]))
{
channels_.erase(casemap(irc.args[0]));
}
}
auto Client::on_part(const IrcMsg &irc) -> void
{
if (is_my_mask(irc.source))
{
channels_.erase(casemap(irc.args[0]));
}
}
auto Client::on_mode(const IrcMsg &irc) -> void
{
if (is_my_nick(irc.args[0]))
{
auto polarity = true;
for (const char c : irc.args[1])
{
switch (c)
{
case '+':
polarity = true;
break;
case '-':
polarity = false;
break;
default:
if (polarity)
{
mode_ += c;
}
else
{
const auto ix = mode_.find(c);
if (ix != std::string::npos)
{
mode_.erase(ix, 1);
}
}
break;
}
}
}
}
auto Client::on_isupport(const IrcMsg &msg) -> void
{
const auto hi = msg.args.size() - 1;
for (int i = 1; i < hi; ++i)
{
auto &entry = msg.args[i];
// Leading minus means to stop support
if (entry.starts_with("-"))
{
const auto key = std::string{entry.substr(1)};
if (auto cursor = isupport_.find(key); cursor != isupport_.end())
{
isupport_.erase(cursor);
}
}
else if (const auto cursor = entry.find('='); cursor != entry.npos)
{
isupport_.emplace(entry.substr(0, cursor), entry.substr(cursor + 1));
}
else
{
isupport_.emplace(entry, std::string{});
}
}
}
auto Client::on_chat(bool notice, const IrcMsg &irc) -> 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],
});
}
auto Client::start(Connection &connection) -> std::shared_ptr<Client>
{
auto thread = std::make_shared<Client>(connection);
connection.sig_ircmsg.connect([thread](auto cmd, auto &msg) {
switch (cmd)
{
case IrcCommand::PRIVMSG:
thread->on_chat(false, msg);
break;
case IrcCommand::NOTICE:
thread->on_chat(true, msg);
break;
case IrcCommand::JOIN:
thread->on_join(msg);
break;
case IrcCommand::KICK:
thread->on_kick(msg);
break;
case IrcCommand::MODE:
thread->on_mode(msg);
break;
case IrcCommand::NICK:
thread->on_nick(msg);
break;
case IrcCommand::PART:
thread->on_part(msg);
break;
case IrcCommand::RPL_ISUPPORT:
thread->on_isupport(msg);
break;
case IrcCommand::RPL_UMODEIS:
thread->on_umodeis(msg);
break;
case IrcCommand::RPL_WELCOME:
thread->on_welcome(msg);
break;
case IrcCommand::RPL_ENDOFMOTD:
case IrcCommand::ERR_NOMOTD:
thread->on_registered();
break;
case IrcCommand::CAP:
thread->on_cap(msg);
break;
default:
break;
}
});
connection.sig_authenticate.connect([thread](auto msg) {
thread->on_authenticate(msg);
});
return thread;
}
auto Client::get_my_nick() const -> const std::string &
{
return nickname_;
}
auto Client::get_my_mode() const -> const std::string &
{
return mode_;
}
auto Client::get_my_channels() const -> const std::unordered_set<std::string> &
{
return channels_;
}
auto Client::is_my_nick(std::string_view nick) const -> bool
{
return casemap_compare(nick, nickname_) == 0;
}
auto Client::is_my_mask(std::string_view mask) const -> bool
{
const auto bang = mask.find('!');
return bang != std::string_view::npos && is_my_nick(mask.substr(0, bang));
}
auto Client::is_channel(std::string_view name) const -> bool
{
return not name.empty() && channel_prefix_.find(name[0]) != channel_prefix_.npos;
}
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
{
if (not sasl_mechanism_)
{
BOOST_LOG_TRIVIAL(warning) << "Unexpected AUTHENTICATE from server"sv;
connection_.send_authenticate_abort();
return;
}
std::visit(
overloaded{
[this](const std::string &reply) {
connection_.send_authenticate_encoded(reply);
},
[this](SaslMechanism::NoReply) {
connection_.send_authenticate("*"sv);
},
[this](SaslMechanism::Failure) {
connection_.send_authenticate_abort();
},
},
sasl_mechanism_->step(body));
if (sasl_mechanism_->is_complete())
{
sasl_mechanism_.reset();
}
}
auto Client::start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void
{
if (sasl_mechanism_)
{
connection_.send_authenticate("*"sv); // abort SASL
}
sasl_mechanism_ = std::move(mechanism);
connection_.send_authenticate(sasl_mechanism_->mechanism_name());
}
static auto tolower_rfc1459(int c) -> int
{
return 97 <= c && c <= 126 ? c - 32 : c;
}
static auto tolower_rfc1459_strict(int c) -> int
{
return 97 <= c && c <= 125 ? c - 32 : c;
}
template <int lower(int)>
static auto casemap_impl(std::string_view str) -> std::string
{
std::string result;
result.reserve(str.size());
for (auto c : str) {
result.push_back(lower(c));
}
return result;
}
auto Client::casemap(std::string_view str) const -> std::string
{
switch (casemap_) {
case Casemap::Ascii: return casemap_impl<tolower>(str);
case Casemap::Rfc1459: return casemap_impl<tolower_rfc1459>(str);
case Casemap::Rfc1459_Strict: return casemap_impl<tolower_rfc1459_strict>(str);
}
}
template <int lower(int)>
static auto casemap_compare_impl(std::string_view lhs, std::string_view rhs) -> int
{
size_t n1 = lhs.size();
size_t n2 = rhs.size();
size_t n = std::min(n1, n2);
for (size_t i = 0; i < n; ++i) {
if (lower(lhs[i]) < lower(rhs[i])) return -1;
if (lower(lhs[i]) > lower(rhs[i])) return 1;
}
if (n1 < n2) return -1;
if (n1 > n2) return 1;
return 0;
}
auto Client::casemap_compare(std::string_view lhs, std::string_view rhs) const -> int
{
switch (casemap_) {
case Casemap::Ascii: return casemap_compare_impl<tolower>(lhs, rhs);
case Casemap::Rfc1459: return casemap_compare_impl<tolower_rfc1459>(lhs, rhs);
case Casemap::Rfc1459_Strict: return casemap_compare_impl<tolower_rfc1459_strict>(lhs, rhs);
}
}
auto Client::shutdown() -> void
{
sig_registered.disconnect_all_slots();
sig_cap_ls.disconnect_all_slots();
}
auto Client::list_caps() -> void
{
caps_available_.clear();
connection_.send_cap_ls();
}
auto Client::on_cap(const IrcMsg &msg) -> void
{
if ("ACK" == msg.args[1] && msg.args.size() == 3) {
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
caps_.insert(std::move(x));
}
);
} else if ("NAK" == msg.args[1] && msg.args.size() == 3) {
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
caps_.erase(x);
}
);
} else if ("LS" == msg.args[1]) {
const std::string_view *kvs;
bool last;
if (3 == msg.args.size())
{
kvs = &msg.args[2];
last = true;
}
else if (4 == msg.args.size() && "*" == msg.args[2])
{
kvs = &msg.args[3];
last = false;
}
else
{
BOOST_LOG_TRIVIAL(warning) << "Malformed CAP LS";
return;
}
auto in = std::istringstream{std::string{*kvs}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
const auto eq = x.find('=');
if (eq == x.npos)
{
caps_available_.emplace(x, std::string{});
}
else
{
caps_available_.emplace(std::string{x, 0, eq}, std::string{x, eq + 1, x.npos});
}
}
);
if (last)
{
sig_cap_ls(caps_available_);
}
} else if ("NEW" == msg.args[1] && msg.args.size() == 3) {
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
const auto eq = x.find('=');
if (eq == x.npos)
{
caps_available_.emplace(x, std::string{});
}
else
{
caps_available_.emplace(std::string{x, 0, eq}, std::string{x, eq + 1, x.npos});
}
}
);
} else if ("DEL" == msg.args[1] && msg.args.size() == 3) {
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
caps_available_.erase(x);
caps_.erase(x);
}
);
}
}

568
myirc/connection.cpp Normal file
View File

@@ -0,0 +1,568 @@
#include "connection.hpp"
#include "linebuffer.hpp"
#include <mybase64.hpp>
#include <boost/log/trivial.hpp>
namespace {
#include "irc_commands.inc"
using tcp_type = boost::asio::ip::tcp::socket;
using tls_type = boost::asio::ssl::stream<tcp_type>;
} // namespace
using namespace std::literals;
Connection::Connection(boost::asio::io_context &io)
: stream_{io}
, watchdog_timer_{io}
, write_posted_{false}
, stalled_{false}
{
}
auto Connection::write_buffers() -> void
{
std::vector<boost::asio::const_buffer> buffers;
buffers.reserve(write_strings_.size());
for (const auto &elt : write_strings_)
{
buffers.push_back(boost::asio::buffer(elt));
}
boost::asio::async_write(
stream_,
buffers,
[this, strings = std::move(write_strings_)](const boost::system::error_code &error, std::size_t) {
if (not error)
{
if (write_strings_.empty())
{
write_posted_ = false;
}
else
{
write_buffers();
}
}
}
);
write_strings_.clear();
}
auto Connection::watchdog() -> void
{
watchdog_timer_.expires_after(watchdog_duration);
watchdog_timer_.async_wait([this](const auto &error) {
if (not error)
{
if (stalled_)
{
BOOST_LOG_TRIVIAL(debug) << "Watchdog timer elapsed, closing stream";
close();
}
else
{
send_ping("watchdog");
stalled_ = true;
watchdog();
}
}
});
}
auto Connection::watchdog_activity() -> void
{
stalled_ = false;
watchdog_timer_.expires_after(watchdog_duration);
}
/// Parse IRC message line and dispatch it to the ircmsg slot.
auto Connection::dispatch_line(char *line) -> void
{
const auto msg = parse_irc_message(line);
const auto recognized = IrcCommandHash::in_word_set(msg.command.data(), msg.command.size());
const auto command
= recognized
&& recognized->min_args <= msg.args.size()
&& recognized->max_args >= msg.args.size()
? recognized->command
: IrcCommand::UNKNOWN;
switch (command)
{
// Respond to pings immediate and discard
case IrcCommand::PING:
send_pong(msg.args[0]);
break;
// Unknown message generate warnings but do not dispatch
// Messages can be unknown due to bad command or bad argument count
case IrcCommand::UNKNOWN:
BOOST_LOG_TRIVIAL(warning) << "Unrecognized command: " << msg.command << " " << msg.args.size();
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
default:
sig_ircmsg(command, msg);
break;
}
}
auto Connection::write_line(std::string message) -> void
{
BOOST_LOG_TRIVIAL(debug) << "SEND: " << message;
message += "\r\n";
write_strings_.push_back(std::move(message));
if (not write_posted_)
{
write_posted_ = true;
boost::asio::post(stream_.get_executor(), [weak = weak_from_this()]() {
if (auto self = weak.lock())
{
self->write_buffers();
}
});
}
}
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
{
if (last.find_first_of("\r\n\0"sv) != last.npos)
{
throw std::runtime_error{"bad irc argument"};
}
front += " :";
front += last;
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_wallops(std::string_view message) -> void
{
write_irc("WALLOPS", message);
}
auto Connection::send_names(std::string_view channel) -> void
{
write_irc("NAMES", channel);
}
auto Connection::send_map() -> void
{
write_irc("MAP");
}
auto Connection::send_get_topic(std::string_view channel) -> void
{
write_irc("TOPIC", channel);
}
auto Connection::send_set_topic(std::string_view channel, std::string_view message) -> void
{
write_irc("TOPIC", channel, message);
}
auto Connection::send_testline(std::string_view target) -> void
{
write_irc("TESTLINE", target);
}
auto Connection::send_masktrace_gecos(std::string_view target, std::string_view gecos) -> void
{
write_irc("MASKTRACE", target, gecos);
}
auto Connection::send_masktrace(std::string_view target) -> void
{
write_irc("MASKTRACE", target);
}
auto Connection::send_testmask_gecos(std::string_view target, std::string_view gecos) -> void
{
write_irc("TESTMASK", target, gecos);
}
auto Connection::send_testmask(std::string_view target) -> void
{
write_irc("TESTMASK", target);
}
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::send_challenge(std::string_view message) -> void
{
write_irc("CHALLENGE", message);
}
auto Connection::send_oper(std::string_view user, std::string_view pass) -> void
{
write_irc("OPER", user, pass);
}
auto Connection::send_kick(std::string_view channel, std::string_view nick, std::string_view reason) -> void
{
write_irc("KICK", channel, nick, reason);
}
auto Connection::send_kill(std::string_view nick, std::string_view reason) -> void
{
write_irc("KILL", nick, reason);
}
auto Connection::send_quit(std::string_view message) -> void
{
write_irc("QUIT", message);
}
auto Connection::send_whois(std::string_view arg1) -> void
{
write_irc("WHOIS", arg1);
}
auto Connection::send_whois_remote(std::string_view arg1, std::string_view arg2) -> void
{
write_irc("WHOIS", arg1, arg2);
}
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
auto set_buffer_size(tls_type& stream, std::size_t const n) -> void
{
auto const ssl = stream.native_handle();
BIO_set_buffer_size(SSL_get_rbio(ssl), n);
BIO_set_buffer_size(SSL_get_wbio(ssl), n);
}
static
auto set_buffer_size(tcp_type& socket, std::size_t const n) -> void
{
socket.set_option(tcp_type::send_buffer_size{static_cast<int>(n)});
socket.set_option(tcp_type::receive_buffer_size{static_cast<int>(n)});
}
static
auto set_cloexec(int const fd) -> void
{
auto const flags = fcntl(fd, F_GETFD);
if (-1 == flags)
{
throw std::system_error{errno, std::generic_category(), "failed to get file descriptor flags"};
}
if (-1 == fcntl(fd, F_SETFD, flags | FD_CLOEXEC))
{
throw std::system_error{errno, std::generic_category(), "failed to set file descriptor flags"};
}
}
template <std::size_t... Ns>
static
auto constexpr sum() -> std::size_t { return (0 + ... + Ns); }
/**
* @brief Build's the string format required for the ALPN extension
*
* @tparam Ns sizes of each protocol name
* @param protocols array of the names of the supported protocols
* @return encoded protocol names
*/
template <std::size_t... Ns>
static
auto constexpr alpn_encode(char const (&... protocols)[Ns]) -> std::array<unsigned char, sum<Ns...>()>
{
auto result = std::array<unsigned char, sum<Ns...>()>{};
auto cursor = std::begin(result);
auto const encode = [&cursor]<std::size_t N>(char const(&protocol)[N]) {
static_assert(N > 0, "Protocol name must be null-terminated");
static_assert(N < 256, "Protocol name too long");
if (protocol[N - 1] != '\0')
throw "Protocol name not null-terminated";
// Prefixed length byte
*cursor++ = N - 1;
// Add string skipping null terminator
cursor = std::copy(std::begin(protocol), std::end(protocol) - 1, cursor);
};
(encode(protocols), ...);
return result;
}
/**
* @brief Configure the TLS stream to request the IRC protocol.
*
* @param stream TLS stream
*/
static
auto set_alpn(tls_type& stream) -> void
{
auto constexpr protos = alpn_encode("irc");
SSL_set_alpn_protos(stream.native_handle(), protos.data(), protos.size());
}
static
auto build_ssl_context(
X509* client_cert,
EVP_PKEY* client_key
) -> boost::asio::ssl::context
{
boost::asio::ssl::context ssl_context{boost::asio::ssl::context::method::tls_client};
ssl_context.set_default_verify_paths();
if (nullptr != client_cert)
{
if (1 != SSL_CTX_use_certificate(ssl_context.native_handle(), client_cert))
{
throw std::runtime_error{"certificate file"};
}
}
if (nullptr != client_key)
{
if (1 != SSL_CTX_use_PrivateKey(ssl_context.native_handle(), client_key))
{
throw std::runtime_error{"private key"};
}
}
return ssl_context;
}
auto Connection::connect(
Settings settings
) -> boost::asio::awaitable<void>
{
using namespace std::placeholders;
// keep connection alive while coroutine is active
const auto self = shared_from_this();
const size_t irc_buffer_size = 32'768;
{
// Name resolution
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);
// Connect to the IRC server
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;
// Set socket options
socket.set_option(boost::asio::ip::tcp::no_delay(true));
set_buffer_size(socket, irc_buffer_size);
set_cloexec(socket.native_handle());
}
if (settings.tls)
{
auto cxt = build_ssl_context(settings.client_cert.get(), settings.client_key.get());
// Upgrade stream_ to use TLS and invalidate socket
auto& stream = stream_.upgrade(cxt);
set_buffer_size(stream, irc_buffer_size);
set_alpn(stream);
if (not settings.verify.empty())
{
stream.set_verify_mode(boost::asio::ssl::verify_peer);
stream.set_verify_callback(boost::asio::ssl::host_name_verification(settings.verify));
}
if (not settings.sni.empty())
{
SSL_set_tlsext_host_name(stream.native_handle(), settings.sni.c_str());
}
co_await stream.async_handshake(stream.client, boost::asio::use_awaitable);
}
sig_connect();
watchdog();
for (LineBuffer buffer{irc_buffer_size};;)
{
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));
if (error)
{
break;
}
buffer.add_bytes(n, [this](char *line) {
BOOST_LOG_TRIVIAL(debug) << "RECV: " << line;
watchdog_activity();
dispatch_line(line);
});
}
watchdog_timer_.cancel();
stream_.close();
}
auto Connection::start(Settings settings) -> void
{
boost::asio::co_spawn(
stream_.get_executor(), connect(std::move(settings)),
[self = shared_from_this()](std::exception_ptr e) {
try
{
if (e)
std::rethrow_exception(e);
BOOST_LOG_TRIVIAL(debug) << "DISCONNECTED";
}
catch (const std::exception &e)
{
BOOST_LOG_TRIVIAL(debug) << "TERMINATED: " << e.what();
}
self->sig_disconnect();
// Disconnect all slots to avoid circular references
self->sig_connect.disconnect_all_slots();
self->sig_ircmsg.disconnect_all_slots();
self->sig_disconnect.disconnect_all_slots();
self->sig_snote.disconnect_all_slots();
self->sig_authenticate.disconnect_all_slots();
});
}

36
myirc/include/bot.hpp Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
#include "client.hpp"
#include <boost/signals2.hpp>
#include <memory>
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;
};
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;
};

View File

@@ -0,0 +1,16 @@
#pragma once
template <typename> struct CCallback_;
template <typename F, typename R, typename... Ts>
struct CCallback_<R (F::*) (Ts...) const>
{
static R invoke(Ts... args, void* u)
{
return (*reinterpret_cast<F*>(u))(args...);
}
};
/// @brief Wrapper for passing closures through C-style callbacks.
/// @tparam F Type of the closure
template <typename F>
using CCallback = CCallback_<decltype(&F::operator())>;

View File

@@ -0,0 +1,31 @@
#pragma once
#include "connection.hpp"
#include "ref.hpp"
#include <boost/signals2/connection.hpp>
#include <memory>
#include <string>
/// @brief Implements the CHALLENGE command protocol to identify as an operator.
class Challenge : std::enable_shared_from_this<Challenge>
{
EVP_PKEY_Ref key_;
Connection &connection_;
boost::signals2::scoped_connection slot_;
std::string buffer_;
auto on_ircmsg(IrcCommand cmd, const IrcMsg &msg) -> void;
auto finish_challenge() -> void;
public:
Challenge(EVP_PKEY_Ref, Connection &);
/// @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(Connection &, std::string_view user, EVP_PKEY_Ref key) -> std::shared_ptr<Challenge>;
};

103
myirc/include/client.hpp Normal file
View File

@@ -0,0 +1,103 @@
#pragma once
#include "connection.hpp"
#include "sasl_mechanism.hpp"
#include <string>
#include <unordered_set>
#include <span>
struct Connection;
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
{
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_;
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;
auto on_chat(bool, const IrcMsg &irc) -> 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 &)> sig_chat;
Client(Connection &connection)
: connection_{connection}
, casemap_{Casemap::Rfc1459}
, channel_prefix_{"#&"}
, status_msg_{"+@"}
{
}
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_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 casemap(std::string_view) const -> std::string;
auto casemap_compare(std::string_view, std::string_view) const -> int;
auto shutdown() -> void;
};

View File

@@ -0,0 +1,131 @@
#pragma once
#include "irc_command.hpp"
#include "ircmsg.hpp"
#include "ref.hpp"
#include "snote.hpp"
#include "stream.hpp"
#include <boost/asio.hpp>
#include <boost/signals2.hpp>
#include <list>
#include <memory>
#include <string>
class Connection : public std::enable_shared_from_this<Connection>
{
public:
struct Settings
{
bool tls;
std::string host;
std::uint16_t port;
X509_Ref client_cert;
EVP_PKEY_Ref 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_;
// 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(Settings 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(Settings) -> 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_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;
};
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...);
}

View File

@@ -0,0 +1,282 @@
enum class IrcCommand
{
UNKNOWN,
RPL_WELCOME,
RPL_YOURHOST,
RPL_CREATED,
RPL_MYINFO,
RPL_ISUPPORT,
RPL_SNOMASK,
RPL_REDIR,
RPL_MAP,
RPL_MAPMORE,
RPL_MAPEND,
RPL_SAVENICK,
RPL_TRACELINK,
RPL_TRACECONNECTING,
RPL_TRACEHANDSHAKE,
RPL_TRACEUNKNOWN,
RPL_TRACEOPERATOR,
RPL_TRACEUSER,
RPL_TRACESERVER,
RPL_TRACENEWTYPE,
RPL_TRACECLASS,
RPL_STATSLINKINFO,
RPL_STATSCOMMANDS,
RPL_STATSCLINE,
RPL_STATSNLINE,
RPL_STATSILINE,
RPL_STATSKLINE,
RPL_STATSQLINE,
RPL_STATSYLINE,
RPL_ENDOFSTATS,
RPL_STATSPLINE,
RPL_UMODEIS,
RPL_STATSFLINE,
RPL_STATSDLINE,
RPL_SERVLIST,
RPL_SERVLISTEND,
RPL_STATSLLINE,
RPL_STATSUPTIME,
RPL_STATSOLINE,
RPL_STATSHLINE,
RPL_STATSSLINE,
RPL_STATSXLINE,
RPL_STATSULINE,
RPL_STATSDEBUG,
RPL_STATSCONN,
RPL_LUSERCLIENT,
RPL_LUSEROP,
RPL_LUSERUNKNOWN,
RPL_LUSERCHANNELS,
RPL_LUSERME,
RPL_ADMINME,
RPL_ADMINLOC1,
RPL_ADMINLOC2,
RPL_ADMINEMAIL,
RPL_TRACELOG,
RPL_ENDOFTRACE,
RPL_LOAD2HI,
RPL_LOCALUSERS,
RPL_GLOBALUSERS,
RPL_PRIVS,
RPL_WHOISCERTFP,
RPL_ACCEPTLIST,
RPL_ENDOFACCEPT,
RPL_NONE,
RPL_AWAY,
RPL_USERHOST,
RPL_ISON,
RPL_TEXT,
RPL_UNAWAY,
RPL_NOWAWAY,
RPL_WHOISHELPOP,
RPL_WHOISUSER,
RPL_WHOISSERVER,
RPL_WHOISOPERATOR,
RPL_WHOWASUSER,
RPL_ENDOFWHOWAS,
RPL_WHOISCHANOP,
RPL_WHOISIDLE,
RPL_ENDOFWHOIS,
RPL_WHOISCHANNELS,
RPL_WHOISSPECIAL,
RPL_LISTSTART,
RPL_LIST,
RPL_LISTEND,
RPL_CHANNELMODEIS,
RPL_CHANNELMLOCK,
RPL_CHANNELURL,
RPL_CREATIONTIME,
RPL_WHOISLOGGEDIN,
RPL_NOTOPIC,
RPL_TOPIC,
RPL_TOPICWHOTIME,
RPL_WHOISTEXT,
RPL_WHOISACTUALLY,
RPL_INVITING,
RPL_SUMMONING,
RPL_INVITELIST,
RPL_ENDOFINVITELIST,
RPL_EXCEPTLIST,
RPL_ENDOFEXCEPTLIST,
RPL_VERSION,
RPL_WHOREPLY,
RPL_WHOSPCRPL,
RPL_ENDOFWHO,
RPL_NAMREPLY,
RPL_WHOWASREAL,
RPL_ENDOFNAMES,
RPL_KILLDONE,
RPL_CLOSING,
RPL_CLOSEEND,
RPL_LINKS,
RPL_ENDOFLINKS,
RPL_BANLIST,
RPL_ENDOFBANLIST,
RPL_INFO,
RPL_MOTD,
RPL_INFOSTART,
RPL_ENDOFINFO,
RPL_MOTDSTART,
RPL_ENDOFMOTD,
RPL_WHOISHOST,
RPL_YOUREOPER,
RPL_REHASHING,
RPL_MYPORTIS,
RPL_NOTOPERANYMORE,
RPL_RSACHALLENGE,
RPL_TIME,
RPL_USERSSTART,
RPL_USERS,
RPL_ENDOFUSERS,
RPL_NOUSERS,
RPL_HOSTHIDDEN,
ERR_NOSUCHNICK,
ERR_NOSUCHSERVER,
ERR_NOSUCHCHANNEL,
ERR_CANNOTSENDTOCHAN,
ERR_TOOMANYCHANNELS,
ERR_WASNOSUCHNICK,
ERR_TOOMANYTARGETS,
ERR_NOORIGIN,
ERR_INVALIDCAPCMD,
ERR_NORECIPIENT,
ERR_NOTEXTTOSEND,
ERR_NOTOPLEVEL,
ERR_WILDTOPLEVEL,
ERR_MSGNEEDREGGEDNICK,
ERR_TOOMANYMATCHES,
ERR_UNKNOWNCOMMAND,
ERR_NOMOTD,
ERR_NOADMININFO,
ERR_FILEERROR,
ERR_NONICKNAMEGIVEN,
ERR_ERRONEUSNICKNAME,
ERR_NICKNAMEINUSE,
ERR_BANNICKCHANGE,
ERR_NICKCOLLISION,
ERR_UNAVAILRESOURCE,
ERR_NICKTOOFAST,
ERR_SERVICESDOWN,
ERR_USERNOTINCHANNEL,
ERR_NOTONCHANNEL,
ERR_USERONCHANNEL,
ERR_NOLOGIN,
ERR_SUMMONDISABLED,
ERR_USERSDISABLED,
ERR_NOTREGISTERED,
ERR_ACCEPTFULL,
ERR_ACCEPTEXIST,
ERR_ACCEPTNOT,
ERR_NEEDMOREPARAMS,
ERR_ALREADYREGISTRED,
ERR_NOPERMFORHOST,
ERR_PASSWDMISMATCH,
ERR_YOUREBANNEDCREEP,
ERR_YOUWILLBEBANNED,
ERR_KEYSET,
ERR_LINKCHANNEL,
ERR_CHANNELISFULL,
ERR_UNKNOWNMODE,
ERR_INVITEONLYCHAN,
ERR_BANNEDFROMCHAN,
ERR_BADCHANNELKEY,
ERR_BADCHANMASK,
ERR_NEEDREGGEDNICK,
ERR_BANLISTFULL,
ERR_BADCHANNAME,
ERR_THROTTLE,
ERR_NOPRIVILEGES,
ERR_CHANOPRIVSNEEDED,
ERR_CANTKILLSERVER,
ERR_ISCHANSERVICE,
ERR_BANNEDNICK,
ERR_NONONREG,
ERR_VOICENEEDED,
ERR_NOOPERHOST,
ERR_CANNOTSENDTOUSER,
ERR_OWNMODE,
ERR_UMODEUNKNOWNFLAG,
ERR_USERSDONTMATCH,
ERR_GHOSTEDCLIENT,
ERR_USERNOTONSERV,
ERR_WRONGPONG,
ERR_DISABLED,
ERR_HELPNOTFOUND,
RPL_STARTTLS,
RPL_WHOISSECURE,
ERR_STARTTLS,
RPL_MODLIST,
RPL_ENDOFMODLIST,
RPL_HELPSTART,
RPL_HELPTXT,
RPL_ENDOFHELP,
ERR_TARGCHANGE,
RPL_ETRACEFULL,
RPL_ETRACE,
RPL_KNOCK,
RPL_KNOCKDLVR,
ERR_TOOMANYKNOCK,
ERR_CHANOPEN,
ERR_KNOCKONCHAN,
ERR_KNOCKDISABLED,
ERR_TARGUMODEG,
RPL_TARGNOTIFY,
RPL_UMODEGMSG,
RPL_OMOTDSTART,
RPL_OMOTD,
RPL_ENDOFOMOTD,
ERR_NOPRIVS,
RPL_TESTMASK,
RPL_TESTLINE,
RPL_NOTESTLINE,
RPL_TESTMASKGECO,
RPL_QUIETLIST,
RPL_ENDOFQUIETLIS,
RPL_MONONLINE,
RPL_MONOFFLINE,
RPL_MONLIST,
RPL_ENDOFMONLIS,
ERR_MONLISTFULL,
RPL_RSACHALLENGE2,
RPL_ENDOFRSACHALLENGE2,
ERR_MLOCKRESTRICTE,
ERR_INVALIDBAN,
ERR_TOPICLOCK,
RPL_SCANMATCHED,
RPL_SCANUMODES,
RPL_LOGGEDIN,
RPL_LOGGEDOUT,
ERR_NICKLOCKED,
RPL_SASLSUCCESS,
ERR_SASLFAIL,
ERR_SASLTOOLONG,
ERR_SASLABORTED,
ERR_SASLALREADY,
RPL_SASLMECHS,
ACCOUNT,
AUTHENTICATE,
AWAY,
BATCH,
BOUNCER,
CAP,
CHGHOST,
ERROR,
INVITE,
JOIN,
KICK,
KILL,
MODE,
NICK,
NOTICE,
PART,
PING,
PONG,
PRIVMSG,
QUIT,
SETNAME,
TAGMSG,
TOPIC,
WALLOPS,
};

View File

@@ -0,0 +1,277 @@
#pragma once
#include "connection.hpp"
#include <chrono>
#include <coroutine>
#include <initializer_list>
#include <utility>
#include <vector>
struct irc_promise;
/// A coroutine that can co_await on various IRC events
struct irc_coroutine : std::coroutine_handle<irc_promise>
{
using promise_type = irc_promise;
/// Start the coroutine and associate it with a specific connection.
auto start(Connection &connection) -> void;
/// Returns true when this coroutine is still waiting on events
auto is_running() -> bool;
/// Returns the exception that terminated this coroutine, if there is one.
auto exception() -> std::exception_ptr;
};
struct irc_promise
{
// Pointer to the connection while running. Cleared on termination.
std::shared_ptr<Connection> connection_;
// Pointer to exception that terminated this coroutine if there is one.
std::exception_ptr exception_;
auto get_return_object() -> irc_coroutine
{
return {irc_coroutine::from_promise(*this)};
}
// Suspend waiting for start() to initialize connection_
auto initial_suspend() noexcept -> std::suspend_always { return {}; }
// Suspend so that is_running() and exception() work
auto final_suspend() noexcept -> std::suspend_always { return {}; }
// Normal termination
auto return_void() -> void
{
connection_.reset();
}
// Abnormal termination - remember the exception
auto unhandled_exception() -> void
{
connection_.reset();
exception_ = std::current_exception();
}
};
template <typename... Ts>
class Wait;
/// Argument to a Wait that expects one or more IRC messages
class wait_ircmsg
{
// Vector of commands this wait is expecting. Leave empty to accept all messages.
std::vector<IrcCommand> want_cmds_;
// Slot for the ircmsg event
boost::signals2::scoped_connection ircmsg_slot_;
public:
using result_type = std::pair<IrcCommand, const IrcMsg &>;
wait_ircmsg(std::initializer_list<IrcCommand> want_cmds)
: want_cmds_{want_cmds}
{
}
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> 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
{
std::optional<boost::asio::steady_timer> timer_;
std::chrono::milliseconds timeout_;
public:
struct result_type
{
};
wait_timeout(std::chrono::milliseconds timeout)
: timeout_{timeout}
{
}
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void;
auto stop() -> void { timer_->cancel(); }
};
template <typename... Ts>
class Wait
{
// State associated with each wait mode
std::tuple<Ts...> modes_;
// Result from any one of the wait modes
std::optional<std::variant<typename Ts::result_type...>> result_;
// Handle of the continuation to be resumed when one of the wait
// modes is ready.
std::coroutine_handle<irc_promise> handle_;
// Slot for tearing down the irc_coroutine in case the connection
// fails before any wait modes complete.
boost::signals2::scoped_connection disconnect_slot_;
template <size_t I>
auto start_mode() -> void
{
std::get<I>(modes_).template start<I, Ts...>(*this);
}
template <std::size_t... Indices>
auto start_modes(std::index_sequence<Indices...>) -> void
{
(start_mode<Indices>(), ...);
}
template <std::size_t... Indices>
auto stop_modes(std::index_sequence<Indices...>) -> void
{
(std::get<Indices>(modes_).stop(), ...);
}
public:
Wait(Ts &&...modes)
: modes_{std::forward<Ts>(modes)...}
{
}
// Get the connection that this coroutine was started with.
auto get_connection() const -> Connection &
{
return *handle_.promise().connection_;
}
// Store a successful result and resume the coroutine
template <size_t I, typename... Args>
auto complete(Args &&...args) -> void
{
result_.emplace(std::in_place_index<I>, std::forward<Args>(args)...);
handle_.resume();
}
// The coroutine always needs to wait for a message. It will never
// be ready immediately.
auto await_ready() noexcept -> bool { return false; }
/// Install event handles in the connection that will resume this coroutine.
auto await_suspend(std::coroutine_handle<irc_promise> handle) -> void;
auto await_resume() -> std::variant<typename Ts::result_type...>;
};
template <size_t I, typename... Ts>
auto wait_ircmsg::start(Wait<Ts...> &command) -> void
{
ircmsg_slot_ = command.get_connection().sig_ircmsg.connect([this, &command](auto cmd, auto &msg) {
const auto wanted = want_cmds_.empty() || std::find(want_cmds_.begin(), want_cmds_.end(), cmd) != want_cmds_.end();
if (wanted)
{
command.template complete<I>(cmd, msg);
}
});
}
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>
auto wait_timeout::start(Wait<Ts...> &command) -> void
{
timer_.emplace(command.get_connection().get_executor());
timer_->expires_after(timeout_);
timer_->async_wait([this, &command](const auto &error) {
if (not error)
{
timer_.reset();
command.template complete<I>();
}
});
}
template <typename... Ts>
auto Wait<Ts...>::await_suspend(std::coroutine_handle<irc_promise> handle) -> void
{
handle_ = handle;
const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
start_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_ = get_connection().sig_disconnect.connect([this]() {
handle_.resume();
});
}
template <typename... Ts>
auto Wait<Ts...>::await_resume() -> std::variant<typename Ts::result_type...>
{
const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
stop_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_.disconnect();
if (result_)
{
return std::move(*result_);
}
else
{
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_;
}

74
myirc/include/ircmsg.hpp Normal file
View File

@@ -0,0 +1,74 @@
#pragma once
#include <iostream>
#include <string_view>
#include <vector>
struct irctag
{
std::string_view key;
std::string_view val;
irctag(std::string_view key, std::string_view val)
: key{key}
, val{val}
{
}
friend auto operator==(const irctag &, const irctag &) -> bool = default;
};
struct IrcMsg
{
std::vector<irctag> tags;
std::vector<std::string_view> args;
std::string_view source;
std::string_view command;
IrcMsg() = default;
IrcMsg(
std::vector<irctag> &&tags,
std::string_view source,
std::string_view command,
std::vector<std::string_view> &&args
)
: tags(std::move(tags))
, args(std::move(args))
, source{source}
, command{command}
{
}
bool hassource() const;
friend bool operator==(const IrcMsg &, const IrcMsg &) = default;
};
enum class irc_error_code
{
MISSING_TAG,
MISSING_COMMAND,
};
auto operator<<(std::ostream &out, irc_error_code) -> std::ostream &;
struct irc_parse_error : public std::exception
{
irc_error_code code;
irc_parse_error(irc_error_code code)
: code(code)
{
}
};
/**
* Parses the given IRC message into a structured format.
* The original message is mangled to store string fragments
* that are pointed to by the structured message type.
*
* Returns zero for success, non-zero for parse error.
*/
auto parse_irc_message(char *msg) -> IrcMsg;
auto parse_irc_tags(char *msg) -> std::vector<irctag>;

View File

@@ -0,0 +1,101 @@
#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

@@ -0,0 +1,9 @@
#pragma once
#include "ref.hpp"
#include <string_view>
auto log_openssl_errors(const std::string_view prefix) -> void;
auto key_from_file(const std::string &filename, const std::string_view password) -> EVP_PKEY_Ref;
auto cert_from_file(const std::string &filename) -> X509_Ref;

46
myirc/include/ref.hpp Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <openssl/evp.h>
#include <openssl/x509.h>
#include <memory>
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;
};
template <typename T>
struct FnDeleter {
auto operator()(T *ptr) const -> void { RefTraits<T>::Free(ptr); }
};
template <typename T>
struct Ref : std::unique_ptr<T, FnDeleter<T>> {
using std::unique_ptr<T, FnDeleter<T>>::unique_ptr;
Ref(const Ref &ref) {
*this = ref;
}
Ref &operator=(const Ref &ref) {
if (ref) {
RefTraits<T>::UpRef(ref.get());
this->reset(ref.get());
}
return *this;
}
};
using EVP_PKEY_CTX_Ref = Ref<EVP_PKEY_CTX>;
using X509_Ref = Ref<X509>;
using EVP_PKEY_Ref = Ref<EVP_PKEY>;

View File

@@ -0,0 +1,52 @@
#pragma once
#include "connection.hpp"
#include "client.hpp"
#include "ref.hpp"
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
class Registration : public std::enable_shared_from_this<Registration>
{
public:
struct Settings {
std::string nickname;
std::string username;
std::string realname;
std::string password;
std::string sasl_mechanism;
std::string sasl_authcid;
std::string sasl_authzid;
std::string sasl_password;
EVP_PKEY_Ref sasl_key;
};
private:
Settings settings_;
std::shared_ptr<Client> client_;
boost::signals2::scoped_connection slot_;
boost::signals2::scoped_connection caps_slot_;
auto on_connect() -> void;
auto on_cap_list(const std::unordered_map<std::string, std::string> &) -> void;
auto on_ircmsg(IrcCommand, const IrcMsg &msg) -> void;
auto send_req() -> void;
auto randomize_nick() -> void;
public:
Registration(
Settings,
std::shared_ptr<Client>
);
static auto start(
Settings,
std::shared_ptr<Client>
) -> std::shared_ptr<Registration>;
};

View File

@@ -0,0 +1,76 @@
#pragma once
#include <boost/signals2.hpp>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
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_;
}
};

92
myirc/include/snote.hpp Normal file
View File

@@ -0,0 +1,92 @@
#pragma once
#include "ircmsg.hpp"
#include <memory>
#include <optional>
#include <regex>
#include <string_view>
#include <utility>
#include <variant>
struct hs_database;
struct hs_scratch;
enum class SnoteTag
{
ClientConnecting,
ClientExiting,
CreateChannel,
DisconnectingKlined,
DroppedChannel,
FailedChallenge,
FailedChallengeFingerprintMismatch,
FailedChallengeHostMismatch,
FailedChallengeMissingSecure,
FailedChallengeNoBlock,
FailedChallengeTls,
Freeze,
IsNowOper,
JoinedJuped,
Killed,
KilledRemote,
KilledRemoteOper,
LoginAttempts,
NewPropagatedKline,
NewTemporaryKline,
NickChange,
NickCollision,
NickCollisionServices,
OperspyWhois,
PossibleFlooder,
PropagatedBanExpired,
RejectingKlined,
SaveMessage,
SetVhostOnMarkedAccount,
SighupReloadingConf,
Spambot,
TemporaryDline,
TemporaryKlineExpired,
TooManyGlobalConnections,
TooManyUserConnections,
};
class SnoteMatch
{
SnoteTag tag_;
std::variant<std::pair<const std::regex &, std::string_view>, std::match_results<std::string_view::const_iterator>> components_;
public:
SnoteMatch(SnoteTag tag, const std::regex &regex, std::string_view full)
: tag_{tag}
, components_{std::make_pair(std::ref(regex), full)}
{
}
auto get_tag() -> SnoteTag { return tag_; }
auto get_results() -> const std::match_results<std::string_view::const_iterator> &;
};
struct SnoteCore
{
struct DbDeleter
{
auto operator()(hs_database *db) const -> void;
};
struct ScratchDeleter
{
auto operator()(hs_scratch *scratch) const -> void;
};
/// @brief Database of server notice patterns
std::unique_ptr<hs_database, DbDeleter> db_;
/// @brief HyperScan scratch space
std::unique_ptr<hs_scratch, ScratchDeleter> scratch_;
SnoteCore();
auto match(const IrcMsg &msg) -> std::optional<SnoteMatch>;
};
extern SnoteCore snoteCore;

114
myirc/include/stream.hpp Normal file
View File

@@ -0,0 +1,114 @@
#pragma once
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <cstddef>
#include <variant>
/// @brief Abstraction over plain-text and TLS streams.
class Stream : private
std::variant<
boost::asio::ip::tcp::socket,
boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>
{
public:
using tcp_socket = boost::asio::ip::tcp::socket;
using tls_stream = boost::asio::ssl::stream<tcp_socket>;
/// @brief The type of the executor associated with the stream.
using executor_type = boost::asio::any_io_executor;
/// @brief Type of the lowest layer of this stream
using lowest_layer_type = tcp_socket::lowest_layer_type;
private:
using base_type = std::variant<tcp_socket, tls_stream>;
auto base() -> base_type& { return *this; }
auto base() const -> base_type const& { return *this; }
public:
/// @brief Initialize stream with a plain TCP socket
/// @param ioc IO context of stream
template <typename T>
Stream(T&& executor) : base_type{std::in_place_type<tcp_socket>, std::forward<T>(executor)} {}
/// @brief Reset stream to a plain TCP socket
/// @return Reference to internal socket object
auto reset() -> tcp_socket&
{
return base().emplace<tcp_socket>(get_executor());
}
/// @brief Upgrade a plain TCP socket into a TLS stream.
/// @param ctx TLS context used for handshake
/// @return Reference to internal stream object
auto upgrade(boost::asio::ssl::context& ctx) -> tls_stream&
{
auto socket = std::move(std::get<tcp_socket>(base()));
return base().emplace<tls_stream>(std::move(socket), ctx);
}
/// @brief Get underlying basic socket
/// @return Reference to underlying socket
auto lowest_layer() -> lowest_layer_type&
{
return std::visit([](auto&& x) -> decltype(auto) { return x.lowest_layer(); }, base());
}
/// @brief Get underlying basic socket
/// @return Reference to underlying socket
auto lowest_layer() const -> lowest_layer_type const&
{
return std::visit([](auto&& x) -> decltype(auto) { return x.lowest_layer(); }, base());
}
/// @brief Get the executor associated with this stream.
/// @return The executor associated with the stream.
auto get_executor() -> executor_type const&
{
return lowest_layer().get_executor();
}
/// @brief Initiates an asynchronous read operation.
/// @tparam MutableBufferSequence Type of the buffer sequence.
/// @tparam Token Type of the completion token.
/// @param buffers The buffer sequence into which data will be read.
/// @param token The completion token for the read operation.
/// @return The result determined by the completion token.
template <
typename MutableBufferSequence,
boost::asio::completion_token_for<void(boost::system::error_code, std::size_t)> Token>
auto async_read_some(MutableBufferSequence&& buffers, Token&& token) -> decltype(auto)
{
return std::visit([&buffers, &token](auto&& x) -> decltype(auto) {
return x.async_read_some(std::forward<MutableBufferSequence>(buffers), std::forward<Token>(token));
}, base());
}
/// @brief Initiates an asynchronous write operation.
/// @tparam ConstBufferSequence Type of the buffer sequence.
/// @tparam Token Type of the completion token.
/// @param buffers The buffer sequence from which data will be written.
/// @param token The completion token for the write operation.
/// @return The result determined by the completion token.
template <
typename ConstBufferSequence,
boost::asio::completion_token_for<void(boost::system::error_code, std::size_t)> Token>
auto async_write_some(ConstBufferSequence&& buffers, Token&& token) -> decltype(auto)
{
return std::visit([&buffers, &token](auto&& x) -> decltype(auto) {
return x.async_write_some(std::forward<ConstBufferSequence>(buffers), std::forward<Token>(token));
}, base());
}
/// @brief Tear down the network stream
auto close() -> void
{
boost::system::error_code err;
auto& socket = lowest_layer();
socket.shutdown(socket.shutdown_both, err);
socket.lowest_layer().close(err);
}
};

283
myirc/irc_commands.gperf Normal file
View File

@@ -0,0 +1,283 @@
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
212, IrcCommand::RPL_STATSCOMMANDS, 5, 5
213, IrcCommand::RPL_STATSCLINE, 8, 8
214, IrcCommand::RPL_STATSNLINE
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
224, IrcCommand::RPL_STATSFLINE
225, IrcCommand::RPL_STATSDLINE, 4, 4
234, IrcCommand::RPL_SERVLIST
235, IrcCommand::RPL_SERVLISTEND
241, IrcCommand::RPL_STATSLLINE, 7, 7
242, IrcCommand::RPL_STATSUPTIME, 2, 2
243, IrcCommand::RPL_STATSOLINE, 7, 7
244, IrcCommand::RPL_STATSHLINE, 7, 7
245, IrcCommand::RPL_STATSSLINE
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
261, IrcCommand::RPL_TRACELOG
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
300, IrcCommand::RPL_NONE
301, IrcCommand::RPL_AWAY, 3, 3
302, IrcCommand::RPL_USERHOST, 2, 2
303, IrcCommand::RPL_ISON, 2, 2
304, IrcCommand::RPL_TEXT
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
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, 4, 4
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, 4, 4
341, IrcCommand::RPL_INVITING, 3, 3
342, IrcCommand::RPL_SUMMONING
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
315, IrcCommand::RPL_ENDOFWHO, 3, 3
353, IrcCommand::RPL_NAMREPLY, 4, 4
360, IrcCommand::RPL_WHOWASREAL, 3, 3
366, IrcCommand::RPL_ENDOFNAMES, 3, 3
361, IrcCommand::RPL_KILLDONE
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
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, 3, 3
381, IrcCommand::RPL_YOUREOPER, 2, 2
382, IrcCommand::RPL_REHASHING, 3, 3
384, IrcCommand::RPL_MYPORTIS
385, IrcCommand::RPL_NOTOPERANYMORE
386, IrcCommand::RPL_RSACHALLENGE, 2, 2
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, 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
423, IrcCommand::ERR_NOADMININFO
424, IrcCommand::ERR_FILEERROR
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
444, IrcCommand::ERR_NOLOGIN
445, IrcCommand::ERR_SUMMONDISABLED
446, IrcCommand::ERR_USERSDISABLED
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
463, IrcCommand::ERR_NOPERMFORHOST
464, IrcCommand::ERR_PASSWDMISMATCH, 2, 2
465, IrcCommand::ERR_YOUREBANNEDCREEP, 2, 2
466, IrcCommand::ERR_YOUWILLBEBANNED
467, IrcCommand::ERR_KEYSET
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
476, IrcCommand::ERR_BADCHANMASK
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
485, IrcCommand::ERR_BANNEDNICK
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
503, IrcCommand::ERR_GHOSTEDCLIENT
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
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
724, IrcCommand::RPL_TESTMASK
725, IrcCommand::RPL_TESTLINE, 5, 5
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, 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
744, IrcCommand::ERR_TOPICLOCK
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
BOUNCER, IrcCommand::BOUNCER
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, 1, 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

186
myirc/ircmsg.cpp Normal file
View File

@@ -0,0 +1,186 @@
#include <cstring>
#include <optional>
#include "ircmsg.hpp"
namespace {
class Parser
{
char *msg_;
inline static char empty[1];
inline void trim()
{
while (*msg_ == ' ')
msg_++;
}
public:
Parser(char *msg)
: msg_(msg)
{
if (msg_ == nullptr)
{
msg_ = empty;
}
else
{
trim();
}
}
char *word()
{
const auto start = msg_;
while (*msg_ != '\0' && *msg_ != ' ')
msg_++;
if (*msg_ != '\0')
{ // prepare for next token
*msg_++ = '\0';
trim();
}
return start;
}
bool match(char c)
{
if (c == *msg_)
{
msg_++;
return true;
}
return false;
}
bool isempty() const { return *msg_ == '\0'; }
const char *peek() { return msg_; }
};
std::string_view unescape_tag_value(char *const val)
{
// only start copying at the first escape character
// skip everything before that
auto cursor = strchr(val, '\\');
if (cursor == nullptr)
{
return {val};
}
auto write = cursor;
for (; *cursor; cursor++)
{
if (*cursor == '\\')
{
cursor++;
switch (*cursor)
{
default:
*write++ = *cursor;
break;
case ':':
*write++ = ';';
break;
case 's':
*write++ = ' ';
break;
case 'r':
*write++ = '\r';
break;
case 'n':
*write++ = '\n';
break;
case '\0':
return {val, write};
}
}
else
{
*write++ = *cursor;
}
}
return {val, write};
}
} // namespace
auto parse_irc_tags(char *str) -> std::vector<irctag>
{
std::vector<irctag> tags;
do
{
auto val = strsep(&str, ";");
auto key = strsep(&val, "=");
if ('\0' == *key)
{
throw irc_parse_error(irc_error_code::MISSING_TAG);
}
if (nullptr == val)
{
tags.emplace_back(key, "");
}
else
{
tags.emplace_back(std::string_view{key, val - 1}, unescape_tag_value(val));
}
}
while (nullptr != str);
return tags;
}
auto parse_irc_message(char *msg) -> IrcMsg
{
Parser p{msg};
IrcMsg out;
/* MESSAGE TAGS */
if (p.match('@'))
{
out.tags = parse_irc_tags(p.word());
}
/* MESSAGE SOURCE */
if (p.match(':'))
{
out.source = p.word();
}
/* MESSAGE COMMANDS */
out.command = p.word();
if (out.command.empty())
{
throw irc_parse_error{irc_error_code::MISSING_COMMAND};
}
/* MESSAGE ARGUMENTS */
while (!p.isempty())
{
if (p.match(':'))
{
out.args.push_back(p.peek());
break;
}
out.args.push_back(p.word());
}
return out;
}
auto IrcMsg::hassource() const -> bool { return source.data() != nullptr; }
auto operator<<(std::ostream &out, irc_error_code code) -> std::ostream &
{
switch (code)
{
case irc_error_code::MISSING_COMMAND:
out << "MISSING COMMAND";
return out;
case irc_error_code::MISSING_TAG:
out << "MISSING TAG";
return out;
default:
return out;
}
}

67
myirc/openssl_utils.cpp Normal file
View File

@@ -0,0 +1,67 @@
#include "openssl_utils.hpp"
#include "c_callback.hpp"
#include <openssl/err.h>
#include <openssl/pem.h>
#include <boost/log/trivial.hpp>
#include <cstdio>
using namespace std::literals;
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) -> X509_Ref
{
X509_Ref 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) -> EVP_PKEY_Ref
{
EVP_PKEY_Ref key;
if (const auto fp = fopen(filename.c_str(), "r"))
{
auto cb = [password](char * const buf, int const size, int) -> int {
if (size < password.size()) { return -1; }
std::copy(password.begin(), password.end(), buf);
return 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;
}

151
myirc/registration.cpp Normal file
View File

@@ -0,0 +1,151 @@
#include "registration.hpp"
#include "connection.hpp"
#include "ircmsg.hpp"
#include "sasl_mechanism.hpp"
#include <memory>
#include <random>
#include <unordered_map>
Registration::Registration(
Settings settings,
std::shared_ptr<Client> client
)
: settings_{std::move(settings)}
, client_{std::move(client)}
{
}
auto Registration::on_connect() -> void
{
auto &connection = client_->get_connection();
client_->list_caps();
caps_slot_ = client_->sig_cap_ls.connect([self = shared_from_this()](auto &caps) {
self->caps_slot_.disconnect();
self->on_cap_list(caps);
});
slot_ = connection.sig_ircmsg.connect(
[self = shared_from_this()](const auto cmd, auto &msg)
{
self->on_ircmsg(cmd, msg);
}
);
if (not settings_.password.empty())
{
connection.send_pass(settings_.password);
}
connection.send_user(settings_.username, settings_.realname);
connection.send_nick(settings_.nickname);
}
auto Registration::on_cap_list(const std::unordered_map<std::string, std::string> &caps) -> void
{
std::string request;
static const char * const want [] {
"account-notify",
"account-tag",
"batch",
"chghost",
"draft/chathistory",
"extended-join",
"invite-notify",
"server-time",
"setname",
"soju.im/no-implicit-names",
"solanum.chat/identify-msg",
"solanum.chat/oper",
"solanum.chat/realhost",
};
for (const auto cap : want)
{
if (caps.contains(cap))
{
request.append(cap);
request.push_back(' ');
}
}
bool do_sasl = not settings_.sasl_mechanism.empty() && caps.contains("sasl");
if (do_sasl) {
request.append("sasl ");
}
if (not request.empty())
{
request.pop_back(); // trailing space
client_->get_connection().send_cap_req(request);
}
if (do_sasl && settings_.sasl_mechanism == "PLAIN") {
client_->start_sasl(
std::make_unique<SaslPlain>(
settings_.sasl_authcid,
settings_.sasl_authzid,
settings_.sasl_password));
} else if (do_sasl && settings_.sasl_mechanism == "EXTERNAL") {
client_->start_sasl(std::make_unique<SaslExternal>(settings_.sasl_authzid));
} else {
client_->get_connection().send_cap_end();
}
}
auto Registration::start(
Settings settings,
std::shared_ptr<Client> client
) -> std::shared_ptr<Registration>
{
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_.disconnect();
thread->on_connect();
});
return thread;
}
auto Registration::randomize_nick() -> void
{
std::string new_nick;
new_nick += settings_.nickname.substr(0, 8);
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<> distrib(0, 35);
for (int i = 0; i < 8; ++i) {
const auto x = distrib(gen);
new_nick += x < 10 ? '0' + x : 'A' + (x-10);
}
client_->get_connection().send_nick(new_nick);
}
auto Registration::on_ircmsg(const IrcCommand cmd, const IrcMsg &msg) -> void
{
switch (cmd)
{
default: break;
case IrcCommand::ERR_NICKNAMEINUSE:
case IrcCommand::ERR_ERRONEUSNICKNAME:
case IrcCommand::ERR_UNAVAILRESOURCE:
randomize_nick();
break;
case IrcCommand::RPL_WELCOME:
slot_.disconnect();
caps_slot_.disconnect();
break;
case IrcCommand::RPL_SASLSUCCESS:
case IrcCommand::ERR_SASLFAIL:
client_->get_connection().send_cap_end();
break;
}
}

27
myirc/sasl_mechanism.cpp Normal file
View File

@@ -0,0 +1,27 @@
#include "sasl_mechanism.hpp"
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 {
return std::move(authzid_);
}
}

265
myirc/snote.cpp Normal file
View File

@@ -0,0 +1,265 @@
#include "snote.hpp"
#include "c_callback.hpp"
#include <hs.h>
#include <boost/log/trivial.hpp>
#include <cstdlib>
#include <cstring>
#include <optional>
#include <regex>
#include <stdexcept>
#include <utility>
namespace {
struct SnotePattern
{
SnotePattern(SnoteTag tag, const char *expression)
: tag{tag}
, expression{expression}
, regex{expression, std::regex_constants::ECMAScript | std::regex_constants::optimize}
{
}
SnoteTag tag;
const char *expression;
std::regex regex;
};
using namespace std::literals;
const SnotePattern static patterns[] = {
{SnoteTag::ClientConnecting,
R"(^Client connecting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \{([^ ]*)\} <([^ ]*)> \[(.*)\]$)"},
{SnoteTag::ClientExiting,
R"(^Client exiting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \[(.*)\]$)"},
{SnoteTag::RejectingKlined,
R"(^Rejecting K-Lined user ([^ ]+)\[([^@]+)@([^\]]+)\] \[([^\] ]+)\] \((.*)\)$)"},
{SnoteTag::NickChange,
R"(^Nick change: From ([^ ]+) to ([^ ]+) \[([^@]+)@([^ ]+)\]$)"},
{SnoteTag::CreateChannel,
R"(^([^ ]+) is creating new channel ([^ ]+)$)"},
{SnoteTag::TemporaryKlineExpired,
R"(^Temporary K-line for \[([^ ]+)\] expired$)"},
{SnoteTag::PropagatedBanExpired,
R"(^Propagated ban for \[([^ ]+)\] expired$)"},
{SnoteTag::DisconnectingKlined,
R"(^Disconnecting K-Lined user ([^ ]+)\[([^@]+)@([^ ]+)\] \((.*)\)$)"},
{SnoteTag::NewPropagatedKline,
R"(^([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} added global ([^ ]+) min\. K-Line for \[([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::NewTemporaryKline,
R"(^([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} added temporary ([^ ]+) min\. K-Line for \[([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::LoginAttempts,
"^Warning: \x02([^ ]+)\x02 failed login attempts to \x02([^ ]+)\x02\\. Last attempt received from \x02(.+)\x02.*$"},
{SnoteTag::PossibleFlooder,
R"(^Possible Flooder ([^ ]+)\[([^ ]+)@[^ ]+\] on ([^ ]+) target: ([^ ]+)$)"},
{SnoteTag::KilledRemote,
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,
R"(^Too many global connections for ([^ ]+)\[([^ ]+)@([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::TooManyUserConnections,
R"(^Too many user connections for ([^ ]+)\[([^ ]+)@([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::SetVhostOnMarkedAccount,
"^\x02([^ ]+)\x02 set vhost ([^ ]+) on the \x02MARKED\x02 account ([^ ]+).$"},
{SnoteTag::IsNowOper,
R"(^([^ ]+) \(([^ ]+)!([^ ]+)@([^ ]+)\) is now an operator$)"},
{SnoteTag::OperspyWhois,
R"(^OPERSPY ([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} WHOIS ([^ ]+)!([^ ]+)@([^ ]+) ([^ ]+) $)"}, // trailing space intentional
{SnoteTag::Freeze,
"^\x02([^ ]+)\x02 froze the account \x02([^ ]+)\x02 \\((.*)\\)\\.$"},
{SnoteTag::DroppedChannel,
"^\x02([^ ]+)\x02 dropped the channel \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 *
{
const auto n = std::size(patterns);
std::vector<const char *> expressions;
std::vector<unsigned> flags(n, HS_FLAG_SINGLEMATCH);
std::vector<unsigned> ids;
expressions.reserve(n);
ids.reserve(n);
for (std::size_t i = 0; i < n; i++)
{
expressions.push_back(patterns[i].expression);
ids.push_back(i);
}
hs_database_t *db;
hs_compile_error *error;
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))
{
case HS_COMPILER_ERROR: {
std::string msg = std::to_string(error->expression) + ": " + error->message;
hs_free_compile_error(error);
throw std::runtime_error{std::move(msg)};
}
case HS_SUCCESS:
break;
default:
abort();
}
return db;
}
} // namespace
SnoteCore::SnoteCore()
{
db_.reset(setup_database());
hs_scratch_t *scratch = nullptr;
if (HS_SUCCESS != hs_alloc_scratch(db_.get(), &scratch))
{
abort();
}
scratch_.reset(scratch);
}
auto SnoteCore::match(const IrcMsg &msg) -> std::optional<SnoteMatch>
{
static const char *const prefix = "*** Notice -- ";
auto &args = msg.args;
if ("*" != args[0] || !args[1].starts_with(prefix))
{
return std::nullopt;
}
const auto message = args[1].substr(strlen(prefix));
unsigned match_id;
auto cb = [&match_id](unsigned id, unsigned long long, unsigned long long, unsigned) -> int {
match_id = id;
return 1; // stop scanning
};
const auto scan_result = hs_scan(
db_.get(),
message.data(), message.size(),
0, // no flags
scratch_.get(),
CCallback<decltype(cb)>::invoke, &cb
);
switch (scan_result)
{
case HS_SUCCESS:
BOOST_LOG_TRIVIAL(warning) << "Unknown snote: " << message;
return std::nullopt;
case HS_SCAN_TERMINATED: {
auto &pattern = patterns[match_id];
return SnoteMatch{pattern.tag, pattern.regex, message};
}
default:
abort();
}
}
auto SnoteMatch::get_results() -> const std::match_results<std::string_view::const_iterator> &
{
if (auto results = std::get_if<1>(&components_))
{
return *results;
}
auto [regex, message] = std::get<0>(components_);
auto &results = components_.emplace<1>();
if (not std::regex_match(message.begin(), message.end(), results, regex))
{
// something went wrong - hyperscan disagrees with std::regex
abort();
}
return results;
}
auto SnoteCore::DbDeleter::operator()(hs_database_t *db) const -> void
{
if (HS_SUCCESS != hs_free_database(db))
{
abort();
}
}
auto SnoteCore::ScratchDeleter::operator()(hs_scratch_t *scratch) const -> void
{
if (HS_SUCCESS != hs_free_scratch(scratch))
{
abort();
}
}
SnoteCore snoteCore;