#include "IRCBase.h" #include "Utilities.h" #include "Commands.h" #include "Numeric.h" #include "IRCMember.h" #include "IRCChannel.h" #include "DCC.h" #include #include #include #include #include using asio::ip::tcp; namespace { /* Debug/development options */ constexpr bool DumpReadData = false; constexpr bool DumpWriteData = false; const std::vector V3Support { "account-notify", "extended-join", "away-notify", "invite-notify", "multi-prefix", "userhost-in-names" /* * TODO https://ircv3.net/irc/ * batch * chghost * server-time * sasl */ }; } // end anonymous namespace struct IRCBasePriv { explicit IRCBasePriv(IRCBase& super_) : super(super_) , sslctx(asio::ssl::context::tls) , resolver(ioctx) , keepaliveTimer(ioctx) { readbuf.reserve(1024); setDefaults(); } IRCBase& super; std::string hostname; std::string port; std::string realname; std::string ident; std::string password; std::string nickname; struct { bool selfSigned{ false }; bool CNMismactch{ false }; bool expired{ false }; } sslExcept; bool useSSL{ false }; asio::io_context ioctx; asio::ssl::context sslctx; tcp::resolver resolver; tcp::resolver::results_type endpoints; std::string readbuf; std::optional sock; std::optional< asio::ssl::stream > sslsock; std::vector sslExceptions; // List of SSL verification errors that were ignored. bool isConnected{ false }; // When the socket itself is successfully connected. bool isOnline{ false }; // When the client itself is successfully registered. IRCError lastError{ IRCError::NoError }; std::chrono::seconds keepaliveFreq{ 0 }; asio::steady_timer keepaliveTimer; std::vector> dcclist; std::unordered_map isupport; std::vector registeredV3support; std::vector serverV3support; std::vector> allMembers; std::vector> channels; std::string validPrivilegeModes; // ohv etc. Ordered by most significant first. std::string validPrivilegeSymbols; // @%+ etc. Ordered by most significant first. char channelModeGroup(char m) const { char group[] = {'A', 'B', 'C', 'D', 'M', 0}; int gi = 0; // group index const std::string& cm = isupport.at("CHANMODES"); for (char c : cm) { if (c == ',') { ++gi; continue; } if (c == m) return group[gi]; } ++gi; // No match yet... Test for channel-member modes if (validPrivilegeModes.find(m) == std::string::npos) ++gi; // Not found, advance to the last "group" (numeric zero, failure). return group[gi]; } bool isChannelSymbol(char c) { const std::string& ct = isupport.at("CHANTYPES"); return ct.find(c) != std::string::npos; } bool isMemberPrivilegeSymbol(char m) const { bool ret = validPrivilegeSymbols.find(m) != std::string::npos; return ret; } char memberPrivilegeSymbolToMode(char s) const { auto pos = validPrivilegeSymbols.find(s); if (pos == std::string::npos) return '\0'; else return validPrivilegeModes[pos]; } void setDefaults() { readbuf.clear(); isConnected = false; isOnline = false; isupport.clear(); /* * Setting defaults as found in the IRC standard. * Do not remove any of these entries. */ isupport.emplace("CHANMODES", "b,k,l,imnpstr"); // Channel modes isupport.emplace("CHANTYPES", "#"); // Channel prefix characters isupport.emplace("PREFIX", "(ov)@+"); // Usermode in channel, modes and prefix characters isupport.emplace("MODES", "3"); // Max modes to set in a channel per MODE message validPrivilegeModes = "ov"; validPrivilegeSymbols = "@+"; } void startKeepaliveTimer() { keepaliveTimer.expires_after(keepaliveFreq); keepaliveTimer.async_wait([this](const asio::error_code& ec){ keepaliveTimeout(ec); }); } void stopKeepaliveTimer() { keepaliveTimer.cancel(); } void keepaliveTimeout(const asio::error_code& ec) { if (ec != asio::error::operation_aborted && isOnline) { auto ts = std::chrono::steady_clock::now().time_since_epoch().count(); write(Command::IRC::PING, fmt::format("KeepAlive {}", ts)); startKeepaliveTimer(); } } void disconnectHandler() { // Just to ensure we're actually closed... might throw something and we really do not care. if (useSSL) try { sslsock->lowest_layer().close(); } catch(...) {} else try { sock->close(); } catch(...) {} readbuf.clear(); channels.clear(); allMembers.clear(); setDefaults(); super.onDisconnected(); } void read(const asio::error_code& ec, std::size_t /*size*/) { lastError = SystemErrorToIRCError(ec); if (lastError != IRCError::NoError) { disconnectHandler(); return; } auto crIt = std::find(readbuf.begin(), readbuf.end(), '\r'); std::string line(readbuf.begin(), crIt); readbuf.erase(readbuf.begin(), crIt + 2); parseIncoming(line); if (useSSL) { asio::async_read_until(sslsock.value(), asio::dynamic_buffer(readbuf), "\r\n", [this](const asio::error_code& e, std::size_t size){ read(e, size); }); } else { asio::async_read_until(sock.value(), asio::dynamic_buffer(readbuf), "\r\n", [this](const asio::error_code& e, std::size_t size){ read(e, size); }); } } void connected(const asio::error_code& /*ec*/) { if (useSSL) { asio::async_read_until(sslsock.value(), asio::dynamic_buffer(readbuf), "\r\n", [this](const asio::error_code& e, std::size_t size){ read(e, size); }); } else { asio::async_read_until(sock.value(), asio::dynamic_buffer(readbuf), "\r\n", [this](const asio::error_code& e, std::size_t size){ read(e, size); }); } isConnected = true; writeNoMsg(Command::IRCv3::CAP, { Command::IRCv3::LS }); if (!password.empty()) write(Command::IRC::PASS, password); write(Command::IRC::NICK, nickname); write(Command::IRC::USER, {ident, "0", "*"}, realname); if (useSSL) { /* TODO test if these two below must be set _before_ ::connect happens, on Windows platform. */ sslsock->next_layer().set_option(asio::socket_base::reuse_address(true)); sslsock->next_layer().set_option(asio::socket_base::keep_alive(true)); } else { /* TODO test if these two below must be set _before_ ::connect happens, on Windows platform. */ sock->set_option(asio::socket_base::reuse_address(true)); sock->set_option(asio::socket_base::keep_alive(true)); } if (sslExceptions.empty()) super.onConnected(); else super.onConnectedWithSSLExceptions(sslExceptions); } void write(const std::string& command, const std::vector& args, const std::string& msg) { std::string out = command; if constexpr (DumpWriteData) std::cout << fmt::format("[SND] cmd=\"{}\" msg=\"{}\" argc={}", command, msg, args.size()); int c = 0; for (const auto& arg : args) { out.append(" " + arg); if constexpr (DumpWriteData) { std::cout << fmt::format(" arg({})=\"{}\"", c, arg); ++c; } } if constexpr (DumpWriteData) std::cout << std::endl; out.append(" :" + msg); out.append("\r\n"); if (useSSL) sslsock->write_some(asio::buffer(out)); else sock->write_some(asio::buffer(out)); } void writeNoMsg(const std::string& command, const std::vector& args) { std::string out = command; if constexpr (DumpWriteData) std::cout << fmt::format("[SND] cmd=\"{}\" argc={}", command, args.size()); int c = 0; for (const auto& arg : args) { out.append(" " + arg); if constexpr (DumpWriteData) { std::cout << fmt::format(" arg({})=\"{}\"", c, arg); ++c; } } if constexpr (DumpWriteData) std::cout << std::endl; out.append("\r\n"); if (useSSL) sslsock->write_some(asio::buffer(out)); else sock->write_some(asio::buffer(out)); } void write(const std::string& command, const std::string& msg) { std::string out = fmt::format("{} :{}\r\n", command, msg); if constexpr (DumpWriteData) { std::cout << fmt::format("[SND] cmd=\"{}\" msg=\"{}\"", command, msg) << std::endl; } if (useSSL) sslsock->write_some(asio::buffer(out)); else sock->write_some(asio::buffer(out)); } void ctcp(const std::string& messageType, const std::string& target, const std::string& command, const std::string& message = "") { if (!isConnected) return; std::string ctcpdata; ctcpdata.push_back(CTCPflag); ctcpdata.append(command); if (!message.empty()) ctcpdata.append(" " + message); ctcpdata.push_back(CTCPflag); write(messageType, { target }, ctcpdata); } void addMemberToChannel(const IRCPrefix& prefix, const std::string& channel) { auto member = super.getMember(prefix.nickname()); if (!member) { member = std::make_shared(prefix); allMembers.emplace_back(member); } auto chanentry = super.getChannel(channel); if (!chanentry) { chanentry = std::make_shared(channel, super); channels.emplace_back(chanentry); } member->addChannel(chanentry); chanentry->addMember(member); } void delMemberFromChannel(const IRCPrefix& prefix, const std::string& channel) { auto member = super.getMember(prefix.nickname()); auto chanentry = super.getChannel(channel); if (member && chanentry) { member->delChannel(chanentry); /* Delete only if not us. */ if (member->channels().empty() && prefix.nickname() != nickname) { chanentry->delMember(member); auto it = std::find_if(allMembers.begin(), allMembers.end(), [&prefix](auto mem) { return prefix == mem->prefix(); }); if (it != allMembers.end()) allMembers.erase(it); } /* If us, delete everything we know about this channel. */ if (prefix.nickname() == nickname) { /* We also need to remove this channel from every member in that channel. */ for (auto m : chanentry->members()) { IRCPrefix mp = m.member()->prefix(); if (mp.nickname() == nickname) continue; delMemberFromChannel(mp, channel); } chanentry->delMember(member); /* Erase this channel */ auto it = std::find_if(channels.begin(), channels.end(), [&channel](auto cptr) { return channel == cptr->name(); }); if (it != channels.end()) channels.erase(it); } } } IRCError verify_X509(X509* cert) { char sn_char[256]; X509_NAME_oneline(X509_get_subject_name(cert), sn_char, 256); std::string subject(sn_char); std::string CN; { auto pos = subject.find("/CN="); if (pos != std::string::npos) { auto begin = subject.begin() + pos; auto end = std::find(begin + 1, subject.end(), '/'); CN = std::string(begin, end); } } if (X509_check_issued(cert, cert) == X509_V_OK) { if (sslExcept.selfSigned) sslExceptions.emplace_back(IRCError::SSL_SelfSigned); else return IRCError::SSL_SelfSigned; } const auto notbefore = X509_getm_notBefore(cert); if (X509_cmp_current_time(notbefore) > -1) { if (sslExcept.expired) sslExceptions.emplace_back(IRCError::SSL_NotYetValid); else return IRCError::SSL_NotYetValid; } const auto notafter = X509_getm_notAfter(cert); if (X509_cmp_current_time(notafter) < 1) { if (sslExcept.expired) sslExceptions.emplace_back(IRCError::SSL_Expired); else return IRCError::SSL_Expired; } if (CN.empty()) { if (sslExcept.CNMismactch) sslExceptions.emplace_back(IRCError::SSL_CN_Mismatch); else return IRCError::SSL_CN_Missing; } /* * CN wildcard matching. * Rules picked from: https://en.wikipedia.org/wiki/Wildcard_certificate#Examples * TODO There are probably more rules! Remember this is IRC and not HTTP so the rules may be more relaxed. */ if (std::find(CN.begin(), CN.end(), '*') != CN.end()) { // Lambda: Count occurrences of character in a string auto strCount = [](const std::string& str, char c) -> std::size_t { auto count = 0; std::for_each(str.begin(), str.end(), [&count,c](char ch) { if (c == ch) ++count; }); return count; }; // Reject "*" and wildcard on the end of CN if (CN.back() == '*') return IRCError::SSL_CN_WildcardIllegal; // Reject multiple wildcards if (strCount(CN, '*') > 1) return IRCError::SSL_CN_WildcardIllegal; /* * Reject "*.tld" * Actually, reject if there is only a single dot */ if (strCount(CN, '.') == 1) return IRCError::SSL_CN_WildcardIllegal; // Reject sub.*.domain.com auto wcit = std::find(CN.begin(), CN.end(), '*'); if (wcit != CN.begin()) { if (*(wcit-1) == '.' && *(wcit+1) == '.') return IRCError::SSL_CN_WildcardIllegal; } if (!singleWildcardMatch(hostname, CN)) { if (sslExcept.CNMismactch) sslExceptions.emplace_back(IRCError::SSL_CN_Mismatch); else return IRCError::SSL_CN_Mismatch; } } /* CN exact hostname match */ else if (CN != hostname) { if (sslExcept.CNMismactch) sslExceptions.emplace_back(IRCError::SSL_CN_Mismatch); else return IRCError::SSL_CN_Mismatch; } return IRCError::NoError; } void parseChannelModeMessage(std::shared_ptr chan, const std::string& modes, const std::vector& args) const { char sign; // + or - int argidx = 0; for (char m : modes) { if (m == '+' || m == '-') { sign = m; continue; } char group = channelModeGroup(m); if (group == 'A') ++argidx; else if (group == 'B') { if (sign == '+') chan->setMode(m, args[argidx]); else chan->delMode(m); ++argidx; } else if (group == 'C') { if (sign == '+') { chan->setMode(m, args[argidx]); ++argidx; } else chan->delMode(m); } else if (group == 'D') { if (sign == '+') chan->setMode(m, ""); else chan->delMode(m); } else if (group == 'M') { auto member = chan->getMember(args[argidx]); ++argidx; if (sign == '+') member->get().addMode(m); else member->get().delMode(m); } } } void parseIncoming(const std::string& line); }; // IRCBasePriv /* ********************************************************************************************** * * * * IRCBase implementation * * * * ********************************************************************************************** */ IRCBase::IRCBase() : mp(new IRCBasePriv(*this)) {} IRCBase::IRCBase(IRCBase&& other) : mp(std::move(other.mp)) {} IRCBase::~IRCBase() = default; bool IRCBase::poll() { asio::error_code ioctxerr; bool ioDidSomething = false; try { ioDidSomething = mp->ioctx.poll_one(ioctxerr) > 0; } catch (const std::system_error& e) { mp->lastError = SystemErrorToIRCError(e); mp->disconnectHandler(); return false; } if (ioctxerr.value() != 0) std::cout << fmt::format("IOCTX err {}: {}", ioctxerr.value(), ioctxerr.message()) << std::endl; return ioDidSomething; } const std::string& IRCBase::getHostname() const { return mp->hostname; } IRCError IRCBase::setHostname(const std::string& hostname, bool SSL) { if (isOnline()) return IRCError::CannotChangeWhenConnected; mp->useSSL = SSL; mp->hostname = hostname; return IRCError::NoError; } const std::string& IRCBase::getPort() const { return mp->port; } IRCError IRCBase::setPort(const std::string& port) { if (isOnline()) return IRCError::CannotChangeWhenConnected; mp->port = port; return IRCError::NoError; } const std::string& IRCBase::getRealname() const { return mp->realname; } IRCError IRCBase::setRealname(const std::string& realname) { if (isOnline()) return IRCError::CannotChangeWhenConnected; mp->realname = realname; return IRCError::NoError; } const std::string& IRCBase::getIdent() const { return mp->ident; } IRCError IRCBase::setIdent(const std::string& ident) { if (isOnline()) return IRCError::CannotChangeWhenConnected; mp->ident = ident; return IRCError::NoError; } const std::string& IRCBase::getNickname() const { return mp->nickname; } void IRCBase::setNickname(const std::string& nickname) { if (mp->isConnected) { if (!isOnline()) mp->nickname = nickname; mp->write(Command::IRC::NICK, nickname); } else mp->nickname = nickname; } const std::string& IRCBase::getPassword() const { return mp->password; } void IRCBase::setPassword(const std::string& password) { mp->password = password; } void IRCBase::exceptSSL_SelfSigned(bool except) { mp->sslExcept.selfSigned = except; } void IRCBase::exceptSSL_CNMismatch(bool except) { mp->sslExcept.CNMismactch = except; } void IRCBase::exceptSSL_Expired(bool except) { mp->sslExcept.expired = except; } void IRCBase::command(const std::string& command, const std::vector& args, const std::string& msg) { if (!mp->isConnected) return; if (msg.empty()) mp->writeNoMsg(command, args); else mp->write(command, args, msg); } void IRCBase::command(const std::string& command, const std::string& msg) { if (!mp->isConnected) return; mp->write(command, msg); } void IRCBase::raw(const std::string& data) { if (!mp->isConnected) return; mp->writeNoMsg(data, {}); } void IRCBase::ctcpRequest(const std::string& target, const std::string& command, const std::string& message) { mp->ctcp(Command::IRC::PRIVMSG, target, command, message); } void IRCBase::ctcpResponse(const std::string& target, const std::string& command, const std::string& message) { mp->ctcp(Command::IRC::NOTICE, target, command, message); } void IRCBase::setManualKeepalive(std::chrono::seconds freq) { mp->keepaliveFreq = freq; if (mp->isOnline) { if (freq > std::chrono::seconds(0)) mp->startKeepaliveTimer(); else mp->stopKeepaliveTimer(); } } std::chrono::milliseconds IRCBase::getManualKeepaliveFreq() const { return mp->keepaliveFreq; } asio::io_context& IRCBase::getIOCTX() { return mp->ioctx; } std::string IRCBase::toMemberPrefix(const std::string& modes) const { // PREFIX=(qaohv)~&@%+ const auto& prefixdef = mp->isupport.at("PREFIX"); // PREFIX is always present. const auto prefixmodes = prefixdef.substr(1, prefixdef.find_first_of(')') - 1); const auto prefix = prefixdef.substr(prefixdef.find_first_of(')') + 1); std::string ret; for (const char m : modes) { // Only accept a-zA-Z if (!(m > 'a' && m < 'z' || m > 'A' && m < 'Z')) { // De Morgan pls halp ret += ':'; continue; } const auto ctpos = prefixmodes.find_first_of(m); if (ctpos == std::string::npos) ret += ':'; else ret += prefix[ctpos]; } return ret; } bool IRCBase::isChannelSymbol(char c) { return mp->isChannelSymbol(c); } char IRCBase::channelModeGroup(char m) const { return mp->channelModeGroup(m); } IRCError IRCBase::tryConnect() { if (isOnline()) return IRCError::AlreadyConnected; if (mp->hostname.empty()) return IRCError::HostNotSet; if (mp->port.empty()) return IRCError::PortNotSet; if (mp->ident.empty()) return IRCError::IdentNotSet; if (mp->nickname.empty()) return IRCError::NicknameNotSet; if (mp->realname.empty()) return IRCError::RealnameNotSet; try { mp->sslExceptions.clear(); mp->ioctx.restart(); mp->endpoints = mp->resolver.resolve(mp->hostname, mp->port); if (mp->endpoints.empty()) return IRCError::CannotResolveAddress; if (mp->useSSL) { mp->sslsock.emplace(mp->ioctx, mp->sslctx); mp->sslsock->set_verify_mode(asio::ssl::verify_peer); mp->sslsock->set_verify_callback([this](bool preverified, asio::ssl::verify_context& vc){ if (preverified) return true; X509* cert = X509_STORE_CTX_get_current_cert(vc.native_handle()); mp->lastError = mp->verify_X509(cert); if (mp->lastError != IRCError::NoError) { onConnectionError(mp->lastError); return false; } else return true; }); asio::async_connect(mp->sslsock->lowest_layer(), mp->endpoints, [this](const asio::error_code& ec, const tcp::endpoint&) { std::cout << "ssl connect error: " << ec.value() << " " << ec.message() << std::endl; mp->lastError = SystemErrorToIRCError(ec); if (ec) { onConnectionError(mp->lastError); return; } mp->sslsock->async_handshake(asio::ssl::stream_base::client, [this](asio::error_code ec) { mp->lastError = SystemErrorToIRCError(ec); if (ec) { onConnectionError(mp->lastError); return; } // TODO error codes! std::cout << "ssl handshake error: " << ec.value() << " " << ec.message() << std::endl; mp->connected(ec); }); }); } else { mp->sock.emplace(mp->ioctx); asio::async_connect(mp->sock.value(), mp->endpoints, [this](const asio::error_code& ec, const tcp::endpoint&) { mp->lastError = SystemErrorToIRCError(ec); if (ec) { onConnectionError(mp->lastError); return; } mp->connected(ec); } ); } } catch (const std::system_error& e) { return SystemErrorToIRCError(e); } return IRCError::NoError; } IRCError IRCBase::disconnectFromServer(const std::string& quitMessage) { if (!mp->isConnected) return IRCError::NotConnected; if (mp->isOnline) { mp->write(Command::IRC::QUIT, quitMessage); // TODO Quit timeout; if we never get disconnected by server, we forcefully must do so. } else { if (mp->useSSL) mp->sslsock->lowest_layer().close(); else mp->sock->close(); } if (mp->keepaliveFreq > std::chrono::seconds(0)) mp->stopKeepaliveTimer(); return IRCError::NoError; } IRCError IRCBase::lastErrorCode() const { return mp->lastError; } bool IRCBase::isOnline() const { return mp->isOnline; } bool IRCBase::isConnected() const { return mp->isConnected; } bool IRCBase::isSSL() const { return mp->useSSL; } const std::vector& IRCBase::clientV3Support() { return V3Support; } const std::vector& IRCBase::registeredV3Support() const { return mp->registeredV3support; } const std::vector& IRCBase::serverV3Support() const { return mp->serverV3support; } const std::unordered_map& IRCBase::isupport() const { return mp->isupport; } const std::vector>& IRCBase::channels() const { return mp->channels; } std::shared_ptr IRCBase::getChannel(const std::string& name) const { for (auto chanp : mp->channels) if (chanp->name() == name) return chanp; return nullptr; } std::shared_ptr IRCBase::getMember(const std::string& nickname) const { for (auto memp : mp->allMembers) if (memp->prefix().nickname() == nickname) return memp; return nullptr; } std::pair, IRCError> IRCBase::initiateDCC(const std::string& /*port*/) { return std::pair, IRCError>(); } IRCError IRCBase::declineDCC(std::shared_ptr /*dcc*/) { return IRCError::NetworkUnreachable; } void IRCBasePriv::parseIncoming(const std::string& line) { std::string command; std::vector args; std::string msg; IRCPrefix sender(hostname); if (line[0] == ':') { // :server.addr cmd arg :msg enum class S { Prefix, Command, Argument, Message } parseState = S::Prefix; auto it = line.begin() + 1; auto next = [&it, &line] { return std::find(it, line.end(), ' '); }; while (it != line.end()) { auto end = next(); switch (parseState) { case S::Prefix: sender = IRCPrefix(std::string(it, end)); parseState = S::Command; break; case S::Command: command = std::string(it, end); parseState = S::Argument; break; case S::Argument: if (*it == ':') { ++it; parseState = S::Message; [[fallthrough]]; } else { args.emplace_back(it, end); break; } case S::Message: end = line.end(); msg = std::string(it, end); break; } it = (end == line.end()) ? end : end + 1; } } else { // cmd :msg auto wsIt = std::find(line.begin(), line.end(), ' '); auto colonIt = std::find(wsIt, line.end(), ':'); command = std::string(line.begin(), wsIt); if (colonIt != line.end()) { msg = std::string(colonIt + 1, line.end()); } } if constexpr (DumpReadData) { std::cout << fmt::format("[RCV] cmd=\"{}\" msg=\"{}\" argc={} ", command, msg, args.size()); int c = 0; for (const auto& arg : args) { std::cout << fmt::format("arg({})=\"{}\" ", c, arg); ++c; } std::cout << std::endl; } using namespace Command::IRC; using namespace Numeric; /* * The 'args' vector and 'msg' string may be modified inside any of the following conditionals. * Do not rely on them after the last else-if, but use the below copies. */ const auto arguments = args; const auto message = msg; /* * Numeric reply */ if (command[0] >= '0' && command[0] <= '9') { if (command == RPL_ISUPPORT && !isOnline) { args.erase(args.begin()); // First arg is always our nickname. for (const auto& a : args) { std::string key, val; auto delim = std::find(a.begin(), a.end(), '='); if (delim != a.end()) { key = std::string(a.begin(), delim); val = std::string(delim + 1, a.end()); } else key = a; isupport.insert_or_assign(key, val); } const std::string& prefix = isupport.find("PREFIX")->second; validPrivilegeModes = std::string(std::find(prefix.begin(), prefix.end(), '(') + 1, std::find(prefix.begin(), prefix.end(), ')')); validPrivilegeSymbols = std::string(std::find(prefix.begin(), prefix.end(), ')') + 1, prefix.end()); } else if (command == RPL_ENDOFMOTD && !isOnline) { nickname = args[0]; isOnline = true; if (keepaliveFreq > std::chrono::seconds(0)) startKeepaliveTimer(); // Emplace ourself in the all-members list. allMembers.emplace_back(std::make_shared(sender)); super.onRegistered(); } else if (command == RPL_NAMREPLY) { auto chan = super.getChannel(args[2]); if (chan && chan->isPopulating()) { std::istringstream ss(msg); /* * Note these things: * A member may contain a usermode. * IRCv3 may enable multiple usermodes (multi-prefix). * IRCv3 may enable each member entry to be a full hostmask (userhost-in-names) and not only a nickname. */ auto members = std::vector(std::istream_iterator{ss}, std::istream_iterator()); for (std::string& namem : members) { /* Extract mode symbols */ auto memBegin = std::find_if_not(namem.begin(), namem.end(), [this](char ms){ return isMemberPrivilegeSymbol(ms); }); std::string modesymbols = std::string(namem.begin(), memBegin); namem.erase(namem.begin(), memBegin); std::optional memprefix; /* IRC standard reply */ if (namem.find('!') == std::string::npos) memprefix.emplace(IRCPrefix::fromNickname(namem)); /* IRCv3 reply */ else memprefix.emplace(namem); auto mem = super.getMember(memprefix->nickname()); if (!mem) { mem = std::make_shared(*memprefix); allMembers.push_back(mem); } if (mem->prefix().host().empty() && !memprefix->host().empty()) mem->setPrefix(*memprefix); mem->addChannel(chan); auto& entry = chan->addMember(mem); for (char s : modesymbols) { char letter = memberPrivilegeSymbolToMode(s); if (letter != '\0') entry.addMode(letter); } } } } else if (command == RPL_ENDOFNAMES) { auto chan = super.getChannel(args[1]); if (chan && chan->isPopulating()) chan->donePopulating(); } else if (command == RPL_TOPIC) { auto chan = super.getChannel(args[1]); if (chan) chan->setTopic(msg); } else if (command == RPL_CHANNELMODEIS) { auto chan = super.getChannel(args[1]); const std::string& modes = args[2]; args.erase(args.begin(), args.begin() + 3); if (chan) parseChannelModeMessage(chan, modes, args); } super.onMsgNumeric(sender, command, arguments, message); } // End numeric message else if (command == NICK) { std::vector channelsAffected; channelsAffected.reserve(16); auto member = super.getMember(sender.toString()); if (member) { const auto& chans = member->channels(); for (const auto& c : chans) channelsAffected.push_back(c.lock()->name()); member->setNickname(msg); } /* We changed our nickname */ if (sender.toString() == nickname) nickname = msg; super.onMsgNick(sender, msg, channelsAffected); } else if (command == MODE) { const std::string target = args[0]; std::string modes; if (isChannelSymbol(target[0])) { modes = args[1]; args.erase(args.begin(), args.begin() + 2); auto chan = super.getChannel(target); if (chan) parseChannelModeMessage(chan, modes, args); } else { modes = msg; args.erase(args.begin(), args.begin() + 1); } super.onMsgMode(sender, target, modes, args); } else if (command == QUIT) { std::vector> channelsAffected; std::vector channelsAffectedStr; auto member = super.getMember(sender.nickname()); if (member) { const auto& chans = member->channels(); for (const auto& c : chans) { channelsAffected.push_back(c.lock()); channelsAffectedStr.push_back(c.lock()->name()); } auto it = std::find(allMembers.begin(), allMembers.end(), member); allMembers.erase(it); } super.onMsgQuit(sender, msg, channelsAffectedStr); for (const auto& c : channelsAffected) c->delMember(member); } else if (command == JOIN) { if (args.size() == 2) { std::string accountname = args[1]; if (accountname[0] == '*') accountname.clear(); /* We are joining a channel */ if (sender.nickname() == nickname) { auto chanentry = super.getChannel(args[0]); if (!chanentry) channels.emplace_back(std::make_shared(args[0], super)); } else addMemberToChannel(sender, args[0]); super.v3onMsgJoin(sender, args[0], accountname, msg); } else { /* We are joining a channel */ if (sender.nickname() == nickname) { auto chanentry = super.getChannel(msg); if (!chanentry) channels.emplace_back(std::make_shared(msg, super)); } else addMemberToChannel(sender, msg); super.onMsgJoin(sender, msg); } } else if (command == PART) { super.onMsgPart(sender, args[0], msg); delMemberFromChannel(sender, args[0]); } else if (command == TOPIC) { super.onMsgTopic(sender, args[0], msg); } else if (command == INVITE) { super.onMsgInvite(sender, msg); } else if (command == KICK) { super.onMsgKick(sender, args[0], args[1], msg); auto prefix = IRCPrefix::fromNickname(args[1]); delMemberFromChannel(prefix, args[0]); } else if (command == PRIVMSG) { if (msg[0] == CTCPflag) { auto[cmd,ctcpMsg] = FormatCTCPLine(msg); if (cmd == "DCC") { auto[dccCmd,dccMsg] = FormatCTCPLine(ctcpMsg); std::vector dccMsgTks; { std::stringstream ss(dccMsg); dccMsgTks = std::vector(std::istream_iterator{ss}, std::istream_iterator()); } if (dccCmd == "CHAT" && dccMsgTks.size() >= 3 && dccMsgTks[0] == "chat") { const auto& dccLongIp = dccMsgTks[1]; const auto& dccPort = dccMsgTks[2]; if (port == "0") { // nat stuff std::cout << "DCC CHAT TODO" << std::endl; } else { const auto ip = longIpToNormal(dccLongIp); dcclist.emplace_back(std::make_shared(super, ioctx, ip, dccPort)); super.onMsgDCCRequest(dcclist.back(), sender, args[0], dccCmd, dccMsg); } } } else super.onMsgCTCPRequest(sender, args[0], cmd, ctcpMsg); } else super.onMsgPrivmsg(sender, args[0], msg); } else if (command == NOTICE) { if (msg[0] == CTCPflag) { auto[cmd,ctcpMsg] = FormatCTCPLine(msg); super.onMsgCTCPResponse(sender, args[0], cmd, ctcpMsg); } else super.onMsgNotice(sender, args[0], msg); } else if (command == KILL) { super.onMsgKill(sender, msg); } else if (command == PING) { write(PONG, msg); super.onMsgPing(msg); } else if (command == PONG) { super.onMsgPong(msg); } // In Windows, wingdi.h, this is defined and we don't use it in this here source file. #if defined(ERROR) #undef ERROR #endif else if (command == ERROR) { super.onMsgError(msg); } else if (command == WALLOPS) { super.onMsgWallops(sender, msg); } /* ******************************************************************* * IRCv3 section * * All commands below here is not part of the IRC standard itself but * is a part of the IRCv3 effort from https://ircv3.net/ * *******************************************************************/ else if (command == Command::IRCv3::CAP && !isOnline) { /* * List of IRCv3 capabilities from the server. * Match it with what we support and request those. */ if (args[1] == Command::IRCv3::LS) { std::istringstream ss(msg); serverV3support = std::vector(std::istream_iterator{ss}, std::istream_iterator()); for (const auto& cap : serverV3support) { auto myIt = std::find(V3Support.begin(), V3Support.end(), cap); if (myIt != V3Support.end()) registeredV3support.push_back(cap); } std::string requestStr; for (const auto& cap : registeredV3support) { if (!requestStr.empty()) requestStr += ' '; requestStr += cap; } write(Command::IRCv3::CAP, { Command::IRCv3::REQ }, requestStr); } /* * Server accepted our desires. */ else if (args[1] == Command::IRCv3::ACK) { writeNoMsg(Command::IRCv3::CAP, { Command::IRCv3::END }); } /* * Server replied NAK on our IRCv3 capabilities, even though it seems it supported them... * Resetting and continuing with our lives. */ else if (args[1] == Command::IRCv3::NAK) { registeredV3support.clear(); writeNoMsg(Command::IRCv3::CAP, { Command::IRCv3::END }); // TODO erase the offending modules instead. } } /* IRCv3 capability "away-notify" */ else if (command == Command::IRC::AWAY) { std::vector channelsAffected; auto member = super.getMember(sender.nickname()); if (member) { const auto& chans = member->channels(); for (const auto& c : chans) { const auto cptr = c.lock(); channelsAffected.push_back(cptr->name()); cptr->delMember(member); } } super.v3onMsgAway(sender, msg, channelsAffected); } /* IRCv3 capability "account-notify" */ else if (command == Command::Extension::ACCOUNT) { if (args[0] == "*") super.v3onMsgAccountLogout(sender); else super.v3onMsgAccountLogin(sender, args[0]); } /* **************************************** * * Catch-all, message was not handled! * * ***************************************/ else { super.onMsgUnhandled(sender, command, arguments, message); } }