#include "myirc/client.hpp" #include "myirc/connection.hpp" #include "myirc/sasl_mechanism.hpp" #include "myirc/snote.hpp" #include #include #include 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) -> std::shared_ptr { auto thread = std::make_shared(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 & { 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; } auto Client::is_on_channel(std::string_view name) const -> bool { return channels_.contains(casemap(name)); } namespace { template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; } 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 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 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(str); case Casemap::Rfc1459: return casemap_impl(str); case Casemap::Rfc1459_Strict: return casemap_impl(str); } } template 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(lhs, rhs); case Casemap::Rfc1459: return casemap_compare_impl(lhs, rhs); case Casemap::Rfc1459_Strict: return casemap_compare_impl(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{in}, std::istream_iterator{}, [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{in}, std::istream_iterator{}, [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{in}, std::istream_iterator{}, [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{in}, std::istream_iterator{}, [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{in}, std::istream_iterator{}, [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