split up driver and library

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

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

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,131 @@
#pragma once
#include "irc_command.hpp"
#include "ircmsg.hpp"
#include "ref.hpp"
#include "snote.hpp"
#include "stream.hpp"
#include <boost/asio.hpp>
#include <boost/signals2.hpp>
#include <list>
#include <memory>
#include <string>
class Connection : public std::enable_shared_from_this<Connection>
{
public:
struct Settings
{
bool tls;
std::string host;
std::uint16_t port;
X509_Ref client_cert;
EVP_PKEY_Ref client_key;
std::string verify;
std::string sni;
std::string socks_host;
std::uint16_t socks_port;
std::string socks_user;
std::string socks_pass;
};
private:
Stream stream_;
boost::asio::steady_timer watchdog_timer_;
std::list<std::string> write_strings_;
bool write_posted_;
// Set true when watchdog triggers.
// Set false when message received.
bool stalled_;
// AUTHENTICATE support
std::string authenticate_buffer_;
auto write_buffers() -> void;
auto dispatch_line(char *line) -> void;
static constexpr std::chrono::seconds watchdog_duration = std::chrono::seconds{30};
auto watchdog() -> void;
auto watchdog_activity() -> void;
auto connect(Settings settings) -> boost::asio::awaitable<void>;
auto on_authenticate(std::string_view) -> void;
/// Build and send well-formed IRC message from individual parameters
auto write_irc(std::string) -> void;
auto write_irc(std::string, std::string_view) -> void;
template <typename... Args>
auto write_irc(std::string front, std::string_view next, Args... rest) -> void;
public:
boost::signals2::signal<void()> sig_connect;
boost::signals2::signal<void()> sig_disconnect;
boost::signals2::signal<void(IrcCommand, const IrcMsg &)> sig_ircmsg;
boost::signals2::signal<void(SnoteMatch &)> sig_snote;
boost::signals2::signal<void(std::string_view)> sig_authenticate;
Connection(boost::asio::io_context &io);
/// Write bytes into the socket.
auto write_line(std::string message) -> void;
auto get_executor() -> boost::asio::any_io_executor
{
return stream_.get_executor();
}
auto start(Settings) -> void;
auto close() -> void;
auto send_ping(std::string_view) -> void;
auto send_pong(std::string_view) -> void;
auto send_pass(std::string_view) -> void;
auto send_user(std::string_view, std::string_view) -> void;
auto send_nick(std::string_view) -> void;
auto send_join(std::string_view) -> void;
auto send_names(std::string_view channel) -> void;
auto send_kick(std::string_view, std::string_view, std::string_view) -> void;
auto send_kill(std::string_view, std::string_view) -> void;
auto send_quit(std::string_view) -> void;
auto send_cap_ls() -> void;
auto send_cap_end() -> void;
auto send_map() -> void;
auto send_testline(std::string_view) -> void;
auto send_testmask(std::string_view) -> void;
auto send_testmask_gecos(std::string_view, std::string_view) -> void;
auto send_masktrace(std::string_view) -> void;
auto send_masktrace_gecos(std::string_view, std::string_view) -> void;
auto send_get_topic(std::string_view) -> void;
auto send_set_topic(std::string_view, std::string_view) -> void;
auto send_cap_req(std::string_view) -> void;
auto send_privmsg(std::string_view, std::string_view) -> void;
auto send_wallops(std::string_view) -> void;
auto send_notice(std::string_view, std::string_view) -> void;
auto send_authenticate(std::string_view message) -> void;
auto send_authenticate_encoded(std::string_view message) -> void;
auto send_authenticate_abort() -> void;
auto send_whois(std::string_view) -> void;
auto send_whois_remote(std::string_view, std::string_view) -> void;
auto send_challenge(std::string_view) -> void;
auto send_oper(std::string_view, std::string_view) -> void;
};
template <typename... Args>
auto Connection::write_irc(std::string front, std::string_view next, Args... rest) -> void
{
using namespace std::literals;
if (next.empty() || next.front() == ':' || next.find_first_of("\r\n \0"sv) != next.npos)
{
throw std::runtime_error{"bad irc argument"};
}
front += " ";
front += next;
write_irc(std::move(front), rest...);
}

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

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

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