The complete source code of IdealIRC http://www.idealirc.org/
 
 
 
 
idealirc/IRCClient/IRCBase.cpp

1421 lines
36 KiB

#include "IRCBase.h"
#include "Utilities.h"
#include "Commands.h"
#include "Numeric.h"
#include "IRCMember.h"
#include "IRCChannel.h"
#include "DCC.h"
#include <asio.hpp>
#include <asio/ssl.hpp>
#include <fmt/format.h>
#include <ctime>
#include <iostream>
using asio::ip::tcp;
namespace {
/* Debug/development options */
constexpr bool DumpReadData = true;
constexpr bool DumpWriteData = false;
const std::vector<std::string> 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<tcp::socket> sock;
std::optional< asio::ssl::stream<asio::ip::tcp::socket> > sslsock;
std::vector<IRCError> 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<std::shared_ptr<DCC>> dcclist;
std::unordered_map<std::string,std::string> isupport;
std::vector<std::string> registeredV3support;
std::vector<std::string> serverV3support;
std::vector<std::shared_ptr<IRCMember>> allMembers;
std::vector<std::shared_ptr<IRCChannel>> 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<std::string>& 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<std::string>& 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<IRCMember>(prefix);
allMembers.emplace_back(member);
}
auto chanentry = super.getChannel(channel);
if (!chanentry) {
chanentry = std::make_shared<IRCChannel>(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<IRCChannel> chan, const std::string& modes, const std::vector<std::string>& 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<std::string>& 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<std::string>& IRCBase::clientV3Support()
{
return V3Support;
}
const std::vector<std::string>& IRCBase::registeredV3Support() const
{
return mp->registeredV3support;
}
const std::vector<std::string>& IRCBase::serverV3Support() const
{
return mp->serverV3support;
}
const std::unordered_map<std::string, std::string>& IRCBase::isupport() const
{
return mp->isupport;
}
const std::vector<std::shared_ptr<IRCChannel>>& IRCBase::channels() const
{
return mp->channels;
}
std::shared_ptr<IRCChannel> IRCBase::getChannel(const std::string& name) const
{
for (auto chanp : mp->channels)
if (chanp->name() == name)
return chanp;
return nullptr;
}
std::shared_ptr<IRCMember> IRCBase::getMember(const std::string& nickname) const
{
for (auto memp : mp->allMembers)
if (memp->prefix().nickname() == nickname)
return memp;
return nullptr;
}
std::pair<std::shared_ptr<DCC>, IRCError> IRCBase::initiateDCC(const std::string& /*port*/)
{
return std::pair<std::shared_ptr<DCC>, IRCError>();
}
IRCError IRCBase::declineDCC(std::shared_ptr<DCC> /*dcc*/)
{
return IRCError::NetworkUnreachable;
}
void IRCBasePriv::parseIncoming(const std::string& line)
{
std::string command;
std::vector<std::string> 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<IRCMember>( IRCPrefix::fromNickname(nickname) ));
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::string>(std::istream_iterator<std::string>{ss},
std::istream_iterator<std::string>());
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<IRCPrefix> 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<IRCMember>(*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<std::string> 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<std::shared_ptr<IRCChannel>> channelsAffected;
std::vector<std::string> 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<IRCChannel>(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<IRCChannel>(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<std::string> dccMsgTks;
{
std::stringstream ss(dccMsg);
dccMsgTks = std::vector<std::string>(std::istream_iterator<std::string>{ss},
std::istream_iterator<std::string>());
}
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<DCC>(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::string>(std::istream_iterator<std::string>{ss},
std::istream_iterator<std::string>());
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<std::string> 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);
}
}