xbot/myirc/connection.cpp

454 lines
13 KiB
C++
Raw Permalink Normal View History

2025-02-01 11:04:33 -08:00
#include "myirc/connection.hpp"
2023-11-22 19:59:34 -08:00
2025-02-01 11:04:33 -08:00
#include "myirc/linebuffer.hpp"
2025-02-05 09:24:47 -08:00
#include <openssl/asn1.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <socks5.hpp>
2023-11-27 19:09:45 -08:00
2025-01-25 12:25:38 -08:00
#include <mybase64.hpp>
2025-02-01 11:04:33 -08:00
#include <boost/asio/steady_timer.hpp>
2023-11-29 13:13:48 -08:00
#include <boost/log/trivial.hpp>
2025-02-05 09:24:47 -08:00
#include <sstream>
#include <iomanip>
2023-11-29 13:13:48 -08:00
2025-02-01 20:57:57 -08:00
namespace myirc {
2025-01-22 23:49:48 -08:00
#include "irc_commands.inc"
2025-01-26 14:38:13 -08:00
using tcp_type = boost::asio::ip::tcp::socket;
using tls_type = boost::asio::ssl::stream<tcp_type>;
2025-01-24 14:48:15 -08:00
using namespace std::literals;
2025-01-25 15:45:31 -08:00
Connection::Connection(boost::asio::io_context &io)
: stream_{io}
, watchdog_timer_{io}
, write_posted_{false}
, stalled_{false}
2023-11-25 09:22:55 -08:00
{
}
2025-01-24 16:57:34 -08:00
auto Connection::write_buffers() -> void
2023-11-22 19:59:34 -08:00
{
2025-01-31 16:14:13 -08:00
const auto available = write_strings_.size();
const auto [delay, count]
= rate_limit
? rate_limit->query(available)
: std::pair{0ms, available};
if (delay > 0ms) {
auto timer = std::make_shared<boost::asio::steady_timer>(stream_.get_executor(), delay);
timer->async_wait([timer, count, self = weak_from_this()](auto) {
if (auto lock = self.lock()) {
lock->write_buffers(count);
}
});
} else {
write_buffers(count);
}
}
auto Connection::write_buffers(size_t n) -> void
{
std::list<std::string> strings;
2023-11-22 19:59:34 -08:00
std::vector<boost::asio::const_buffer> buffers;
2025-01-31 16:14:13 -08:00
if (n == write_strings_.size()) {
strings = std::move(write_strings_);
write_strings_.clear();
} else {
strings.splice(
strings.begin(), // insert at
write_strings_, // remove from
write_strings_.begin(), // start removing at
std::next(write_strings_.begin(), n) // stop removing at
);
}
buffers.reserve(n);
for (const auto &elt : strings)
2023-11-22 19:59:34 -08:00
{
buffers.push_back(boost::asio::buffer(elt));
}
2025-01-31 16:14:13 -08:00
2023-11-22 19:59:34 -08:00
boost::asio::async_write(
stream_,
buffers,
2025-01-31 16:14:13 -08:00
[this, strings = std::move(strings)](const boost::system::error_code &error, std::size_t) {
2025-01-25 15:45:31 -08:00
if (not error)
{
if (write_strings_.empty())
{
2025-01-24 16:57:34 -08:00
write_posted_ = false;
2025-01-25 15:45:31 -08:00
}
else
{
2025-01-24 16:57:34 -08:00
write_buffers();
2023-11-22 19:59:34 -08:00
}
}
2025-01-25 15:45:31 -08:00
}
);
2023-11-22 19:59:34 -08:00
}
2025-01-24 14:48:15 -08:00
auto Connection::watchdog() -> void
{
watchdog_timer_.expires_after(watchdog_duration);
2025-01-25 15:45:31 -08:00
watchdog_timer_.async_wait([this](const auto &error) {
2025-01-24 14:48:15 -08:00
if (not error)
{
if (stalled_)
{
BOOST_LOG_TRIVIAL(debug) << "Watchdog timer elapsed, closing stream";
close();
}
else
{
2025-02-05 09:24:47 -08:00
write_irc("PING", "watchdog");
2025-01-24 14:48:15 -08:00
stalled_ = true;
watchdog();
}
}
});
}
auto Connection::watchdog_activity() -> void
{
stalled_ = false;
watchdog_timer_.expires_after(watchdog_duration);
}
2025-01-23 12:46:52 -08:00
/// Parse IRC message line and dispatch it to the ircmsg slot.
2025-02-05 09:24:47 -08:00
auto Connection::dispatch_line(char *line, bool flush) -> void
2025-01-23 12:46:52 -08:00
{
2025-01-25 15:45:31 -08:00
const auto msg = parse_irc_message(line);
const auto recognized = IrcCommandHash::in_word_set(msg.command.data(), msg.command.size());
const auto command
2025-01-23 12:46:52 -08:00
= recognized
2025-01-25 15:45:31 -08:00
&& recognized->min_args <= msg.args.size()
&& recognized->max_args >= msg.args.size()
? recognized->command
: IrcCommand::UNKNOWN;
2025-01-23 12:46:52 -08:00
2025-01-25 15:45:31 -08:00
switch (command)
{
2025-01-23 21:23:32 -08:00
// Respond to pings immediate and discard
case IrcCommand::PING:
2025-02-05 09:24:47 -08:00
write_irc("PONG", msg.args[0]);
2025-01-23 21:23:32 -08:00
break;
// Unknown message generate warnings but do not dispatch
// Messages can be unknown due to bad command or bad argument count
case IrcCommand::UNKNOWN:
2025-01-23 12:46:52 -08:00
BOOST_LOG_TRIVIAL(warning) << "Unrecognized command: " << msg.command << " " << msg.args.size();
2025-01-23 21:23:32 -08:00
break;
// Normal IRC commands
default:
2025-02-05 09:24:47 -08:00
sig_ircmsg(command, msg, flush);
2025-01-23 21:23:32 -08:00
break;
2025-01-23 12:46:52 -08:00
}
}
2025-02-05 09:47:52 -08:00
auto Connection::close() -> void
{
stream_.close();
}
auto Connection::write_irc(std::string message) -> void
2023-11-22 19:59:34 -08:00
{
2023-11-29 13:13:48 -08:00
BOOST_LOG_TRIVIAL(debug) << "SEND: " << message;
2023-11-26 19:59:12 -08:00
message += "\r\n";
2023-11-22 19:59:34 -08:00
write_strings_.push_back(std::move(message));
2025-01-24 16:57:34 -08:00
2025-01-25 15:45:31 -08:00
if (not write_posted_)
{
2025-01-24 16:57:34 -08:00
write_posted_ = true;
2025-01-25 15:45:31 -08:00
boost::asio::post(stream_.get_executor(), [weak = weak_from_this()]() {
2025-01-24 16:57:34 -08:00
if (auto self = weak.lock())
{
self->write_buffers();
}
});
2023-11-22 19:59:34 -08:00
}
}
2025-01-23 21:23:32 -08:00
auto Connection::write_irc(std::string front, std::string_view last) -> void
{
2025-01-31 16:14:13 -08:00
bool colon = last.starts_with(":");
for (const auto c : last) {
switch (c) {
case '\r': case '\n': case '\0': throw std::runtime_error{"bad irc argument"};
case ' ': colon = true;
default: break;
}
2025-01-23 21:23:32 -08:00
}
2025-01-31 16:14:13 -08:00
front += colon ? " :" : " ";
2025-01-23 21:23:32 -08:00
front += last;
2025-02-05 09:47:52 -08:00
write_irc(std::move(front));
2025-01-23 21:23:32 -08:00
}
2025-01-26 14:38:13 -08:00
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;
}
2025-02-05 09:24:47 -08:00
static auto peer_fingerprint(X509 *cer) -> std::string
{
std::ostringstream os;
std::vector<std::uint8_t> result;
EVP_MD *md_used;
if (auto digest = X509_digest_sig(cer, &md_used, nullptr))
{
os << EVP_MD_name(md_used) << ":" << std::hex << std::setfill('0');
EVP_MD_free(md_used);
for (int i = 0; i < digest->length; ++i) {
os << std::setw(2) << static_cast<unsigned>(digest->data[i]);
}
ASN1_OCTET_STRING_free(digest);
}
return os.str();
}
2025-01-26 14:38:13 -08:00
auto Connection::connect(
2025-01-30 09:28:28 -08:00
Settings settings
2025-01-26 14:38:13 -08:00
) -> 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;
2025-02-05 09:24:47 -08:00
boost::asio::ip::tcp::endpoint socket_endpoint;
std::optional<boost::asio::ip::tcp::endpoint> socks_endpoint;
std::string fingerprint;
2025-01-26 14:38:13 -08:00
{
2025-01-26 14:43:26 -08:00
// Name resolution
2025-01-26 14:51:44 -08:00
auto resolver = boost::asio::ip::tcp::resolver{stream_.get_executor()};
2025-01-26 14:38:13 -08:00
const auto endpoints = co_await resolver.async_resolve(settings.host, std::to_string(settings.port), boost::asio::use_awaitable);
2025-02-01 11:04:33 -08:00
for (auto e : endpoints) {
BOOST_LOG_TRIVIAL(debug) << "DNS: " << e.endpoint();
}
2025-01-26 14:43:26 -08:00
// Connect to the IRC server
auto& socket = stream_.reset();
2025-02-05 09:24:47 -08:00
// If we're going to use SOCKS then the TCP connection host is actually the socks
// server and then the IRC server gets passed over the SOCKS protocol
auto const use_socks = not settings.socks_host.empty() && settings.socks_port != 0;
if (use_socks)
{
std::swap(settings.host, settings.socks_host);
std::swap(settings.port, settings.socks_port);
}
socket_endpoint = co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable);
BOOST_LOG_TRIVIAL(debug) << "CONNECTED: " << socket_endpoint;
2025-01-26 14:38:13 -08:00
2025-01-26 14:43:26 -08:00
// Set socket options
2025-01-26 14:38:13 -08:00
socket.set_option(boost::asio::ip::tcp::no_delay(true));
set_buffer_size(socket, irc_buffer_size);
set_cloexec(socket.native_handle());
2025-02-05 09:24:47 -08:00
// Optionally negotiate SOCKS connection
if (use_socks)
{
auto auth = not settings.socks_user.empty() || not settings.socks_pass.empty()
? socks5::Auth{socks5::UsernamePasswordCredential{settings.socks_user, settings.socks_pass}}
: socks5::Auth{socks5::NoCredential{}};
socks_endpoint = co_await socks5::async_connect(
socket,
settings.socks_host, settings.socks_port, std::move(auth),
boost::asio::use_awaitable
);
}
2025-01-26 14:38:13 -08:00
}
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);
2025-02-05 09:24:47 -08:00
const auto cer = SSL_get0_peer_certificate(stream.native_handle());
fingerprint = peer_fingerprint(cer);
2025-01-26 14:38:13 -08:00
}
2025-02-05 09:24:47 -08:00
sig_connect(socket_endpoint, socks_endpoint, std::move(fingerprint));
2025-01-26 14:38:13 -08:00
watchdog();
for (LineBuffer buffer{irc_buffer_size};;)
{
boost::system::error_code error;
2025-02-05 09:24:47 -08:00
auto const chunk = buffer.prepare();
if (chunk.size() == 0) break;
const auto n = co_await stream_.async_read_some(chunk, boost::asio::redirect_error(boost::asio::use_awaitable, error));
2025-01-26 14:38:13 -08:00
if (error)
{
break;
}
2025-02-05 09:24:47 -08:00
buffer.commit(n);
auto line = buffer.next_nonempty_line();
if (line)
{
2025-01-26 14:38:13 -08:00
watchdog_activity();
2025-02-05 09:24:47 -08:00
do
{
BOOST_LOG_TRIVIAL(debug) << "RECV: " << line;
const auto next_line = buffer.next_nonempty_line();
dispatch_line(line, next_line == nullptr);
line = next_line;
} while (line);
}
buffer.shift();
2025-01-26 14:38:13 -08:00
}
watchdog_timer_.cancel();
stream_.close();
}
2025-01-26 14:51:44 -08:00
2025-01-30 09:28:28 -08:00
auto Connection::start(Settings settings) -> void
2025-01-26 14:51:44 -08:00
{
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();
}
2025-01-26 19:35:56 -08:00
// Disconnect all slots to avoid circular references
self->sig_connect.disconnect_all_slots();
self->sig_ircmsg.disconnect_all_slots();
2025-01-31 08:38:14 -08:00
2025-02-05 09:24:47 -08:00
self->sig_disconnect(e);
2025-01-31 08:38:14 -08:00
self->sig_disconnect.disconnect_all_slots();
2025-01-26 14:51:44 -08:00
});
}
2025-02-01 11:04:33 -08:00
} // namespace myirc