split up driver and library
This commit is contained in:
31
myirc/CMakeLists.txt
Normal file
31
myirc/CMakeLists.txt
Normal 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
79
myirc/bot.cpp
Normal 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
92
myirc/challenge.cpp
Normal 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
433
myirc/client.cpp
Normal 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
568
myirc/connection.cpp
Normal 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
36
myirc/include/bot.hpp
Normal 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;
|
||||
};
|
16
myirc/include/c_callback.hpp
Normal file
16
myirc/include/c_callback.hpp
Normal 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())>;
|
31
myirc/include/challenge.hpp
Normal file
31
myirc/include/challenge.hpp
Normal 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
103
myirc/include/client.hpp
Normal 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;
|
||||
};
|
131
myirc/include/connection.hpp
Normal file
131
myirc/include/connection.hpp
Normal 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...);
|
||||
}
|
282
myirc/include/irc_command.hpp
Normal file
282
myirc/include/irc_command.hpp
Normal 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,
|
||||
};
|
277
myirc/include/irc_coroutine.hpp
Normal file
277
myirc/include/irc_coroutine.hpp
Normal 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
74
myirc/include/ircmsg.hpp
Normal 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>;
|
101
myirc/include/linebuffer.hpp
Normal file
101
myirc/include/linebuffer.hpp
Normal 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());
|
||||
}
|
||||
}
|
||||
};
|
9
myirc/include/openssl_utils.hpp
Normal file
9
myirc/include/openssl_utils.hpp
Normal 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
46
myirc/include/ref.hpp
Normal 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>;
|
52
myirc/include/registration.hpp
Normal file
52
myirc/include/registration.hpp
Normal 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>;
|
||||
};
|
76
myirc/include/sasl_mechanism.hpp
Normal file
76
myirc/include/sasl_mechanism.hpp
Normal 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
92
myirc/include/snote.hpp
Normal 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 ®ex, 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
114
myirc/include/stream.hpp
Normal 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
283
myirc/irc_commands.gperf
Normal 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
186
myirc/ircmsg.cpp
Normal 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
67
myirc/openssl_utils.cpp
Normal 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
151
myirc/registration.cpp
Normal 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
27
myirc/sasl_mechanism.cpp
Normal 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
265
myirc/snote.cpp
Normal 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;
|
Reference in New Issue
Block a user