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

1304 lines
33 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 = false;
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 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);
chanentry->delMember(member);
/* If not us and the member isn't known on any channels anymore, remove it. */
if (prefix.nickname() != nickname && member->channels().empty()) {
auto it = std::find(allMembers.begin(), allMembers.end(), member);
allMembers.erase(it);
}
/* If us, delete everything we know about this channel */
else if (prefix.nickname() == nickname)
channels.erase(
std::find_if(channels.begin(), channels.end(),
[&channel](std::shared_ptr<IRCChannel> cptr) {
return channel == cptr->name();
}
));
}
}
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 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)
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 (msg.empty())
mp->writeNoMsg(command, args);
else
mp->write(command, args, msg);
}
void IRCBase::command(const std::string& command, const std::string& msg)
{
mp->write(command, msg);
}
void IRCBase::raw(const std::string& data)
{
mp->writeNoMsg(data, {});
}
void IRCBase::ctcp(const std::string& target, const std::string& command, const std::string& message)
{
std::string ctcpdata;
ctcpdata.push_back(CTCPflag);
ctcpdata.append(command);
if (!message.empty())
ctcpdata.append(" " + message);
ctcpdata.push_back(CTCPflag);
mp->write(Command::IRC::PRIVMSG, { target }, ctcpdata);
}
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;
}
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
auto wsPrefixIt = std::find(line.begin(), line.end(), ' ');
std::string prefix(line.begin() + 1, wsPrefixIt);
sender = IRCPrefix(prefix);
auto wsCmdIt = std::find(wsPrefixIt + 1, line.end(), ' ');
command = std::string(wsPrefixIt + 1, wsCmdIt);
auto endargsPos = line.find(" :");
auto endargsIt = (endargsPos == std::string::npos) ? line.end()
: line.begin() + endargsPos;
if (endargsIt != line.end())
msg = std::string(endargsIt + 2, line.end());
auto argStartIt = wsCmdIt + 1;
auto argEndIt = std::find(argStartIt, endargsIt, ' ');
if (argStartIt - 1 != endargsIt) {
std::string arg(argStartIt, argEndIt);
while (arg[0] != ':') {
if (!arg.empty())
args.push_back(arg);
if (argEndIt == endargsIt)
break;
argStartIt = argEndIt + 1;
argEndIt = std::find(argStartIt, endargsIt, ' ');
arg = std::string(argStartIt, argEndIt);
}
}
if (msg.empty() && !args.empty())
msg = args.back();
}
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.
*/
/*
* 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());
}
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>(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::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[2]);
if (chan && chan->isPopulating())
chan->donePopulating();
}
super.onMsgNumeric(sender, command, args, msg);
} // End numeric message
else if (command == NICK) {
std::vector<std::string> channelsAffected;
auto member = super.getMember(sender.nickname());
const auto& chans = member->channels();
for (auto c : chans)
channelsAffected.push_back(c->name());
member->setNickname(msg);
super.onMsgNick(sender, msg, channelsAffected);
}
else if (command == MODE) {
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) {
char sign; // + or -
int argidx = 0;
for (char m : modes) {
if (m == '+' || m == '-') {
sign = m;
continue;
}
char group = channelModeGroup(m);
if (group == 'A' || group == 'B' || (group == 'C' && sign == '+'))
++argidx;
else if (group == 'M') {
auto member = chan->getMember(args[argidx]);
++argidx;
if (sign == '+')
member->get().addMode(m);
else
member->get().delMode(m);
}
}
}
}
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 (auto c : chans) {
channelsAffected.push_back(c);
channelsAffectedStr.push_back(c->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]);
if (args[0] == msg) {
msg.clear();
}
}
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);
delMemberFromChannel(sender, 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 (auto c : chans) {
channelsAffected.push_back(c->name());
c->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, args, msg);
}
}