647 lines
16 KiB
C++
647 lines
16 KiB
C++
#include "myirc/client.hpp"
|
|
|
|
#include "myirc/connection.hpp"
|
|
#include "myirc/sasl_mechanism.hpp"
|
|
#include "myirc/snote.hpp"
|
|
|
|
#include <mybase64.hpp>
|
|
|
|
#include <boost/container/flat_map.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
namespace myirc {
|
|
|
|
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, bool flush) -> void
|
|
{
|
|
char status_msg = '\0';
|
|
std::string_view target = irc.args[0];
|
|
if (not target.empty() && status_msg_.find(target[0]) != std::string::npos)
|
|
{
|
|
status_msg = target[0];
|
|
target = target.substr(1);
|
|
}
|
|
sig_chat({
|
|
.tags = irc.tags,
|
|
.is_notice = notice,
|
|
.status_msg = '\0',
|
|
.source = irc.source,
|
|
.target = irc.args[0],
|
|
.message = irc.args[1],
|
|
}, flush);
|
|
}
|
|
|
|
auto Client::start(std::shared_ptr<Connection> connection) -> std::shared_ptr<Client>
|
|
{
|
|
auto thread = std::make_shared<Client>(connection);
|
|
|
|
connection->sig_ircmsg.connect([thread](auto cmd, auto &msg, bool flush) {
|
|
switch (cmd)
|
|
{
|
|
case IrcCommand::PRIVMSG:
|
|
thread->on_chat(false, msg, flush);
|
|
break;
|
|
case IrcCommand::NOTICE:
|
|
if (auto match = snoteCore.match(msg))
|
|
{
|
|
thread->sig_snote(*match, flush);
|
|
} else {
|
|
thread->on_chat(true, msg, flush);
|
|
}
|
|
break;
|
|
case IrcCommand::JOIN:
|
|
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;
|
|
case IrcCommand::AUTHENTICATE:
|
|
thread->on_authenticate_chunk(msg.args[0]);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
|
|
connection->sig_disconnect.connect([thread](auto) {
|
|
thread->sig_registered.disconnect_all_slots();
|
|
thread->sig_cap_ls.disconnect_all_slots();
|
|
thread->sig_chat.disconnect_all_slots();
|
|
thread->sig_snote.disconnect_all_slots();
|
|
});
|
|
|
|
return thread;
|
|
}
|
|
|
|
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;
|
|
send_authenticate_abort();
|
|
return;
|
|
}
|
|
|
|
std::visit(
|
|
overloaded{
|
|
[this](const std::string &reply) {
|
|
send_authenticate_encoded(reply);
|
|
},
|
|
[this](SaslMechanism::NoReply) {
|
|
send_authenticate("*"sv);
|
|
},
|
|
[this](SaslMechanism::Failure) {
|
|
send_authenticate_abort();
|
|
},
|
|
},
|
|
sasl_mechanism_->step(body));
|
|
|
|
if (sasl_mechanism_->is_complete())
|
|
{
|
|
sasl_mechanism_.reset();
|
|
}
|
|
}
|
|
|
|
auto Client::start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void
|
|
{
|
|
if (sasl_mechanism_)
|
|
{
|
|
send_authenticate("*"sv); // abort SASL
|
|
}
|
|
|
|
sasl_mechanism_ = std::move(mechanism);
|
|
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::list_caps() -> void
|
|
{
|
|
caps_available_.clear();
|
|
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);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
auto Client::on_authenticate_chunk(const std::string_view chunk) -> void
|
|
{
|
|
if (chunk != "+"sv)
|
|
{
|
|
authenticate_buffer_ += chunk;
|
|
}
|
|
|
|
if (chunk.size() != 400)
|
|
{
|
|
std::string decoded;
|
|
decoded.resize(mybase64::decoded_size(authenticate_buffer_.size()));
|
|
std::size_t len;
|
|
|
|
if (auto decode_end = mybase64::decode(authenticate_buffer_, decoded.data()))
|
|
{
|
|
decoded.resize(decode_end - decoded.data());
|
|
on_authenticate(decoded);
|
|
}
|
|
else
|
|
{
|
|
BOOST_LOG_TRIVIAL(debug) << "Invalid AUTHENTICATE base64"sv;
|
|
send_authenticate("*"sv); // abort SASL
|
|
}
|
|
|
|
authenticate_buffer_.clear();
|
|
}
|
|
else if (authenticate_buffer_.size() > 1024)
|
|
{
|
|
BOOST_LOG_TRIVIAL(debug) << "AUTHENTICATE buffer overflow"sv;
|
|
authenticate_buffer_.clear();
|
|
send_authenticate("*"sv); // abort SASL
|
|
}
|
|
}
|
|
|
|
auto Client::send_ping(std::string_view txt) -> void
|
|
{
|
|
connection_->write_irc("PING", txt);
|
|
}
|
|
|
|
auto Client::send_pong(std::string_view txt) -> void
|
|
{
|
|
connection_->write_irc("PONG", txt);
|
|
}
|
|
|
|
auto Client::send_pass(std::string_view password) -> void
|
|
{
|
|
connection_->write_irc("PASS", password);
|
|
}
|
|
|
|
auto Client::send_user(std::string_view user, std::string_view real) -> void
|
|
{
|
|
connection_->write_irc("USER", user, "*", "*", real);
|
|
}
|
|
|
|
auto Client::send_nick(std::string_view nick) -> void
|
|
{
|
|
connection_->write_irc("NICK", nick);
|
|
}
|
|
|
|
auto Client::send_cap_ls() -> void
|
|
{
|
|
connection_->write_irc("CAP", "LS", "302");
|
|
}
|
|
|
|
auto Client::send_cap_end() -> void
|
|
{
|
|
connection_->write_irc("CAP", "END");
|
|
}
|
|
|
|
auto Client::send_cap_req(std::string_view caps) -> void
|
|
{
|
|
connection_->write_irc("CAP", "REQ", caps);
|
|
}
|
|
|
|
auto Client::send_privmsg(std::string_view target, std::string_view message) -> void
|
|
{
|
|
connection_->write_irc("PRIVMSG", target, message);
|
|
}
|
|
|
|
auto Client::send_notice(std::string_view target, std::string_view message) -> void
|
|
{
|
|
connection_->write_irc("NOTICE", target, message);
|
|
}
|
|
|
|
auto Client::send_wallops(std::string_view message) -> void
|
|
{
|
|
connection_->write_irc("WALLOPS", message);
|
|
}
|
|
|
|
auto Client::send_names(std::string_view channel) -> void
|
|
{
|
|
connection_->write_irc("NAMES", channel);
|
|
}
|
|
|
|
auto Client::send_map() -> void
|
|
{
|
|
connection_->write_irc("MAP");
|
|
}
|
|
|
|
auto Client::send_get_topic(std::string_view channel) -> void
|
|
{
|
|
connection_->write_irc("TOPIC", channel);
|
|
}
|
|
|
|
auto Client::send_set_topic(std::string_view channel, std::string_view message) -> void
|
|
{
|
|
connection_->write_irc("TOPIC", channel, message);
|
|
}
|
|
|
|
auto Client::send_testline(std::string_view target) -> void
|
|
{
|
|
connection_->write_irc("TESTLINE", target);
|
|
}
|
|
|
|
auto Client::send_masktrace_gecos(std::string_view target, std::string_view gecos) -> void
|
|
{
|
|
connection_->write_irc("MASKTRACE", target, gecos);
|
|
}
|
|
|
|
auto Client::send_masktrace(std::string_view target) -> void
|
|
{
|
|
connection_->write_irc("MASKTRACE", target);
|
|
}
|
|
|
|
auto Client::send_testmask_gecos(std::string_view target, std::string_view gecos) -> void
|
|
{
|
|
connection_->write_irc("TESTMASK", target, gecos);
|
|
}
|
|
|
|
auto Client::send_testmask(std::string_view target) -> void
|
|
{
|
|
connection_->write_irc("TESTMASK", target);
|
|
}
|
|
|
|
auto Client::send_authenticate(std::string_view message) -> void
|
|
{
|
|
connection_->write_irc("AUTHENTICATE", message);
|
|
}
|
|
|
|
auto Client::send_join(std::string_view channel) -> void
|
|
{
|
|
connection_->write_irc("JOIN", channel);
|
|
}
|
|
|
|
auto Client::send_challenge(std::string_view message) -> void
|
|
{
|
|
connection_->write_irc("CHALLENGE", message);
|
|
}
|
|
|
|
auto Client::send_oper(std::string_view user, std::string_view pass) -> void
|
|
{
|
|
connection_->write_irc("OPER", user, pass);
|
|
}
|
|
|
|
auto Client::send_kick(std::string_view channel, std::string_view nick, std::string_view reason) -> void
|
|
{
|
|
connection_->write_irc("KICK", channel, nick, reason);
|
|
}
|
|
|
|
auto Client::send_kill(std::string_view nick, std::string_view reason) -> void
|
|
{
|
|
connection_->write_irc("KILL", nick, reason);
|
|
}
|
|
|
|
auto Client::send_quit(std::string_view message) -> void
|
|
{
|
|
connection_->write_irc("QUIT", message);
|
|
}
|
|
|
|
auto Client::send_whois(std::string_view arg1) -> void
|
|
{
|
|
connection_->write_irc("WHOIS", arg1);
|
|
}
|
|
|
|
auto Client::send_whois_remote(std::string_view arg1, std::string_view arg2) -> void
|
|
{
|
|
connection_->write_irc("WHOIS", arg1, arg2);
|
|
}
|
|
|
|
auto Client::send_authenticate_abort() -> void
|
|
{
|
|
send_authenticate("*");
|
|
}
|
|
|
|
auto Client::send_authenticate_encoded(std::string_view body) -> void
|
|
{
|
|
std::string encoded(mybase64::encoded_size(body.size()), 0);
|
|
mybase64::encode(body, encoded.data());
|
|
|
|
for (size_t lo = 0; lo < encoded.size(); lo += 400)
|
|
{
|
|
const auto hi = std::min(lo + 400, encoded.size());
|
|
const std::string_view chunk{encoded.begin() + lo, encoded.begin() + hi};
|
|
send_authenticate(chunk);
|
|
}
|
|
|
|
if (encoded.size() % 400 == 0)
|
|
{
|
|
send_authenticate("+"sv);
|
|
}
|
|
}
|
|
|
|
} // namespace myirc
|