X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fcore%2Fcorenetwork.cpp;h=e6699c1251c77abeaba944c2cfdb50bd3bcc9620;hp=e3963c4e71d7704eb0f02e4e48356dc1bc802439;hb=edc55b1f86cf613a332eeeb5d85537a54120dfa5;hpb=36d432bc4ee56dd9a89850a8dbcc58ea12d8ee33 diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index e3963c4e..e6699c12 100644 --- a/src/core/corenetwork.cpp +++ b/src/core/corenetwork.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2005-2016 by the Quassel Project * + * Copyright (C) 2005-2019 by the Quassel Project * * devel@quassel-irc.org * * * * This program is free software; you can redistribute it and/or modify * @@ -18,107 +18,112 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ -#include - #include "corenetwork.h" +#include +#include + #include "core.h" #include "coreidentity.h" #include "corenetworkconfig.h" #include "coresession.h" #include "coreuserinputhandler.h" -#include "networkevent.h" - -// IRCv3 capabilities +#include "ircencoder.h" #include "irccap.h" +#include "irctag.h" +#include "networkevent.h" -INIT_SYNCABLE_OBJECT(CoreNetwork) -CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) - : Network(networkid, session), - _coreSession(session), - _userInputHandler(new CoreUserInputHandler(this)), - _autoReconnectCount(0), - _quitRequested(false), - _disconnectExpected(false), - - _previousConnectionAttemptFailed(false), - _lastUsedServerIndex(0), +CoreNetwork::CoreNetwork(const NetworkId& networkid, CoreSession* session) + : Network(networkid, session) + , _coreSession(session) + , _userInputHandler(new CoreUserInputHandler(this)) + , _autoReconnectCount(0) + , _quitRequested(false) + , _disconnectExpected(false) + , _previousConnectionAttemptFailed(false) + , _lastUsedServerIndex(0) + , _requestedUserModes('-') +{ + // Check if raw IRC logging is enabled + _debugLogRawIrc = (Quassel::isOptionSet("debug-irc") || Quassel::isOptionSet("debug-irc-id")); + _debugLogRawNetId = Quassel::optionValue("debug-irc-id").toInt(); - _lastPingTime(0), - _pingCount(0), - _sendPings(false), - _requestedUserModes('-') -{ _autoReconnectTimer.setSingleShot(true); - connect(&_socketCloseTimer, SIGNAL(timeout()), this, SLOT(socketCloseTimeout())); + connect(&_socketCloseTimer, &QTimer::timeout, this, &CoreNetwork::onSocketCloseTimeout); setPingInterval(networkConfig()->pingInterval()); - connect(&_pingTimer, SIGNAL(timeout()), this, SLOT(sendPing())); + connect(&_pingTimer, &QTimer::timeout, this, &CoreNetwork::sendPing); setAutoWhoDelay(networkConfig()->autoWhoDelay()); setAutoWhoInterval(networkConfig()->autoWhoInterval()); QHash channels = coreSession()->persistentChannels(networkId()); - foreach(QString chan, channels.keys()) { + for (const QString& chan : channels.keys()) { _channelKeys[chan.toLower()] = channels[chan]; } - connect(networkConfig(), SIGNAL(pingTimeoutEnabledSet(bool)), SLOT(enablePingTimeout(bool))); - connect(networkConfig(), SIGNAL(pingIntervalSet(int)), SLOT(setPingInterval(int))); - connect(networkConfig(), SIGNAL(autoWhoEnabledSet(bool)), SLOT(setAutoWhoEnabled(bool))); - connect(networkConfig(), SIGNAL(autoWhoIntervalSet(int)), SLOT(setAutoWhoInterval(int))); - connect(networkConfig(), SIGNAL(autoWhoDelaySet(int)), SLOT(setAutoWhoDelay(int))); + QHash bufferCiphers = coreSession()->bufferCiphers(networkId()); + for (const QString& buffer : bufferCiphers.keys()) { + storeChannelCipherKey(buffer.toLower(), bufferCiphers[buffer]); + } + + connect(networkConfig(), &NetworkConfig::pingTimeoutEnabledSet, this, &CoreNetwork::enablePingTimeout); + connect(networkConfig(), &NetworkConfig::pingIntervalSet, this, &CoreNetwork::setPingInterval); + connect(networkConfig(), &NetworkConfig::autoWhoEnabledSet, this, &CoreNetwork::setAutoWhoEnabled); + connect(networkConfig(), &NetworkConfig::autoWhoIntervalSet, this, &CoreNetwork::setAutoWhoInterval); + connect(networkConfig(), &NetworkConfig::autoWhoDelaySet, this, &CoreNetwork::setAutoWhoDelay); - connect(&_autoReconnectTimer, SIGNAL(timeout()), this, SLOT(doAutoReconnect())); - connect(&_autoWhoTimer, SIGNAL(timeout()), this, SLOT(sendAutoWho())); - connect(&_autoWhoCycleTimer, SIGNAL(timeout()), this, SLOT(startAutoWhoCycle())); - connect(&_tokenBucketTimer, SIGNAL(timeout()), this, SLOT(fillBucketAndProcessQueue())); + connect(&_autoReconnectTimer, &QTimer::timeout, this, &CoreNetwork::doAutoReconnect); + connect(&_autoWhoTimer, &QTimer::timeout, this, &CoreNetwork::sendAutoWho); + connect(&_autoWhoCycleTimer, &QTimer::timeout, this, &CoreNetwork::startAutoWhoCycle); + connect(&_tokenBucketTimer, &QTimer::timeout, this, &CoreNetwork::checkTokenBucket); - connect(&socket, SIGNAL(connected()), this, SLOT(socketInitialized())); - connect(&socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError))); - connect(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(socketStateChanged(QAbstractSocket::SocketState))); - connect(&socket, SIGNAL(readyRead()), this, SLOT(socketHasData())); + connect(&socket, &QAbstractSocket::connected, this, &CoreNetwork::onSocketInitialized); + connect(&socket, selectOverload(&QAbstractSocket::error), this, &CoreNetwork::onSocketError); + connect(&socket, &QAbstractSocket::stateChanged, this, &CoreNetwork::onSocketStateChanged); + connect(&socket, &QIODevice::readyRead, this, &CoreNetwork::onSocketHasData); #ifdef HAVE_SSL - connect(&socket, SIGNAL(encrypted()), this, SLOT(socketInitialized())); - connect(&socket, SIGNAL(sslErrors(const QList &)), this, SLOT(sslErrors(const QList &))); + connect(&socket, &QSslSocket::encrypted, this, &CoreNetwork::onSocketInitialized); + connect(&socket, selectOverload&>(&QSslSocket::sslErrors), this, &CoreNetwork::onSslErrors); #endif - connect(this, SIGNAL(newEvent(Event *)), coreSession()->eventManager(), SLOT(postEvent(Event *))); + connect(this, &CoreNetwork::newEvent, coreSession()->eventManager(), &EventManager::postEvent); + + // Custom rate limiting + // These react to the user changing settings in the client + connect(this, &Network::useCustomMessageRateSet, this, &CoreNetwork::updateRateLimiting); + connect(this, &Network::messageRateBurstSizeSet, this, &CoreNetwork::updateRateLimiting); + connect(this, &Network::messageRateDelaySet, this, &CoreNetwork::updateRateLimiting); + connect(this, &Network::unlimitedMessageRateSet, this, &CoreNetwork::updateRateLimiting); // IRCv3 capability handling // These react to CAP messages from the server - connect(this, SIGNAL(capAdded(QString)), this, SLOT(serverCapAdded(QString))); - connect(this, SIGNAL(capAcknowledged(QString)), this, SLOT(serverCapAcknowledged(QString))); - connect(this, SIGNAL(capRemoved(QString)), this, SLOT(serverCapRemoved(QString))); + connect(this, &Network::capAdded, this, &CoreNetwork::serverCapAdded); + connect(this, &Network::capAcknowledged, this, &CoreNetwork::serverCapAcknowledged); + connect(this, &Network::capRemoved, this, &CoreNetwork::serverCapRemoved); if (Quassel::isOptionSet("oidentd")) { - connect(this, SIGNAL(socketInitialized(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Core::instance()->oidentdConfigGenerator(), SLOT(addSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Qt::BlockingQueuedConnection); - connect(this, SIGNAL(socketDisconnected(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Core::instance()->oidentdConfigGenerator(), SLOT(removeSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16))); + connect(this, &CoreNetwork::socketInitialized, Core::instance()->oidentdConfigGenerator(), &OidentdConfigGenerator::addSocket); + connect(this, &CoreNetwork::socketDisconnected, Core::instance()->oidentdConfigGenerator(), &OidentdConfigGenerator::removeSocket); } -} + if (Quassel::isOptionSet("ident-daemon")) { + connect(this, &CoreNetwork::socketInitialized, Core::instance()->identServer(), &IdentServer::addSocket); + connect(this, &CoreNetwork::socketDisconnected, Core::instance()->identServer(), &IdentServer::removeSocket); + } +} CoreNetwork::~CoreNetwork() { - // Request a proper disconnect, but don't count as user-requested disconnect - if (socketConnected()) { - // Only try if the socket's fully connected (not initializing or disconnecting). - // Force an immediate disconnect, jumping the command queue. Ensures the proper QUIT is - // shown even if other messages are queued. - disconnectFromIrc(false, QString(), false, true); - // Process the putCmd events that trigger the quit. Without this, shutting down the core - // results in abrubtly closing the socket rather than sending the QUIT as expected. - QCoreApplication::processEvents(); - // Wait briefly for each network to disconnect. Sometimes it takes a little while to send. - if (!forceDisconnect()) { - qWarning() << "Timed out quitting network" << networkName() << - "(user ID " << userId() << ")"; - } + // Ensure we don't get any more signals from the socket while shutting down + disconnect(&socket, nullptr, this, nullptr); + if (!forceDisconnect()) { + qWarning() << QString{"Could not disconnect from network %1 (network ID: %2, user ID: %3)"} + .arg(networkName()) + .arg(networkId().toInt()) + .arg(userId().toInt()); } - disconnect(&socket, 0, this, 0); // this keeps the socket from triggering events during clean up - delete _userInputHandler; } - bool CoreNetwork::forceDisconnect(int msecs) { if (socket.state() == QAbstractSocket::UnconnectedState) { @@ -127,53 +132,58 @@ bool CoreNetwork::forceDisconnect(int msecs) } // Request a socket-level disconnect if not already happened socket.disconnectFromHost(); - // Return the result of waiting for disconnect; true if successful, otherwise false - return socket.waitForDisconnected(msecs); + if (socket.state() != QAbstractSocket::UnconnectedState) { + return socket.waitForDisconnected(msecs); + } + return true; } - -QString CoreNetwork::channelDecode(const QString &bufferName, const QByteArray &string) const +QString CoreNetwork::channelDecode(const QString& bufferName, const QByteArray& string) const { if (!bufferName.isEmpty()) { - IrcChannel *channel = ircChannel(bufferName); + IrcChannel* channel = ircChannel(bufferName); if (channel) return channel->decodeString(string); } return decodeString(string); } - -QString CoreNetwork::userDecode(const QString &userNick, const QByteArray &string) const +QString CoreNetwork::userDecode(const QString& userNick, const QByteArray& string) const { - IrcUser *user = ircUser(userNick); + IrcUser* user = ircUser(userNick); if (user) return user->decodeString(string); return decodeString(string); } - -QByteArray CoreNetwork::channelEncode(const QString &bufferName, const QString &string) const +QByteArray CoreNetwork::channelEncode(const QString& bufferName, const QString& string) const { if (!bufferName.isEmpty()) { - IrcChannel *channel = ircChannel(bufferName); + IrcChannel* channel = ircChannel(bufferName); if (channel) return channel->encodeString(string); } return encodeString(string); } - -QByteArray CoreNetwork::userEncode(const QString &userNick, const QString &string) const +QByteArray CoreNetwork::userEncode(const QString& userNick, const QString& string) const { - IrcUser *user = ircUser(userNick); + IrcUser* user = ircUser(userNick); if (user) return user->encodeString(string); return encodeString(string); } - void CoreNetwork::connectToIrc(bool reconnecting) { + if (_shuttingDown) { + return; + } + + if (Core::instance()->identServer()) { + _socketId = Core::instance()->identServer()->addWaitingSocket(); + } + if (!reconnecting && useAutoReconnect() && _autoReconnectCount == 0) { _autoReconnectTimer.setInterval(autoReconnectInterval() * 1000); if (unlimitedReconnectRetries()) @@ -185,7 +195,7 @@ void CoreNetwork::connectToIrc(bool reconnecting) qWarning() << "Server list empty, ignoring connect request!"; return; } - CoreIdentity *identity = identityPtr(); + CoreIdentity* identity = identityPtr(); if (!identity) { qWarning() << "Invalid identity configures, ignoring connect request!"; return; @@ -195,7 +205,8 @@ void CoreNetwork::connectToIrc(bool reconnecting) _quitReason.clear(); // Reset capability negotiation tracking, also handling server changes during reconnect - _capsQueued.clear(); + _capsQueuedIndividual.clear(); + _capsQueuedBundled.clear(); clearCaps(); _capNegotiationActive = false; _capInitialNegotiationEnded = false; @@ -206,19 +217,33 @@ void CoreNetwork::connectToIrc(bool reconnecting) } else if (_previousConnectionAttemptFailed) { // cycle to next server if previous connection attempt failed - displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Connection failed. Cycling to next Server")); + _previousConnectionAttemptFailed = false; + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("Connection failed. Cycling to next server...") + )); if (++_lastUsedServerIndex >= serverList().size()) { _lastUsedServerIndex = 0; } } - _previousConnectionAttemptFailed = false; + else { + // Start out with the top server in the list + _lastUsedServerIndex = 0; + } Server server = usedServer(); displayStatusMsg(tr("Connecting to %1:%2...").arg(server.host).arg(server.port)); - displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Connecting to %1:%2...").arg(server.host).arg(server.port)); + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("Connecting to %1:%2...").arg(server.host).arg(server.port) + )); if (server.useProxy) { - QNetworkProxy proxy((QNetworkProxy::ProxyType)server.proxyType, server.proxyHost, server.proxyPort, server.proxyUser, server.proxyPass); + QNetworkProxy proxy((QNetworkProxy::ProxyType) server.proxyType, server.proxyHost, server.proxyPort, server.proxyUser, server.proxyPass); socket.setProxy(proxy); } else { @@ -227,13 +252,19 @@ void CoreNetwork::connectToIrc(bool reconnecting) enablePingTimeout(); + // Reset tracking for valid timestamps in PONG replies + setPongTimestampValid(false); + // Qt caches DNS entries for a minute, resulting in round-robin (e.g. for chat.freenode.net) not working if several users // connect at a similar time. QHostInfo::fromName(), however, always performs a fresh lookup, overwriting the cache entry. - QHostInfo::fromName(server.host); - + if (!server.useProxy) { + // Avoid hostname lookups when a proxy is specified. The lookups won't use the proxy and may therefore leak the DNS + // hostname of the server. Qt's DNS cache also isn't used by the proxy so we don't need to refresh the entry. + QHostInfo::fromName(server.host); + } #ifdef HAVE_SSL if (server.useSsl) { - CoreIdentity *identity = identityPtr(); + CoreIdentity* identity = identityPtr(); if (identity) { socket.setLocalCertificate(identity->sslCert()); socket.setPrivateKey(identity->sslKey()); @@ -248,21 +279,19 @@ void CoreNetwork::connectToIrc(bool reconnecting) #endif } - -void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool withReconnect, - bool forceImmediate) +void CoreNetwork::disconnectFromIrc(bool requested, const QString& reason, bool withReconnect) { // Disconnecting from the network, should expect a socket close or error _disconnectExpected = true; - _quitRequested = requested; // see socketDisconnected(); + _quitRequested = requested; // see socketDisconnected(); if (!withReconnect) { _autoReconnectTimer.stop(); - _autoReconnectCount = 0; // prohibiting auto reconnect + _autoReconnectCount = 0; // prohibiting auto reconnect } disablePingTimeout(); _msgQueue.clear(); - IrcUser *me_ = me(); + IrcUser* me_ = me(); if (me_) { QString awayMsg; if (me_->isAway()) @@ -275,84 +304,94 @@ void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool else _quitReason = reason; - displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason)); + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason) + )); if (socket.state() == QAbstractSocket::UnconnectedState) { - socketDisconnected(); - } else { + onSocketDisconnected(); + } + else { if (socket.state() == QAbstractSocket::ConnectedState) { - userInputHandler()->issueQuit(_quitReason, forceImmediate); - } else { + // If shutting down, prioritize the QUIT command + userInputHandler()->issueQuit(_quitReason, _shuttingDown); + } + else { socket.close(); } - if (requested || withReconnect) { - // the irc server has 10 seconds to close the socket + if (socket.state() != QAbstractSocket::UnconnectedState) { + // Wait for up to 10 seconds for the socket to close cleanly, then it will be forcefully aborted _socketCloseTimer.start(10000); } } } +void CoreNetwork::onSocketCloseTimeout() +{ + qWarning() << QString{"Timed out quitting network %1 (network ID: %2, user ID: %3)"} + .arg(networkName()) + .arg(networkId().toInt()) + .arg(userId().toInt()); + socket.abort(); +} -void CoreNetwork::userInput(BufferInfo buf, QString msg) +void CoreNetwork::shutdown() { - userInputHandler()->handleUserInput(buf, msg); + _shuttingDown = true; + disconnectFromIrc(false, {}, false); } +void CoreNetwork::userInput(const BufferInfo& buf, QString msg) +{ + userInputHandler()->handleUserInput(buf, msg); +} -void CoreNetwork::putRawLine(const QByteArray s, const bool prepend) +void CoreNetwork::putRawLine(const QByteArray& s, bool prepend) { - if (_tokenBucket > 0) { + if (_tokenBucket > 0 || (_skipMessageRates && _msgQueue.isEmpty())) { + // If there's tokens remaining, ... + // Or rate limits don't apply AND no messages are in queue (to prevent out-of-order), ... + // Send the message now. writeToSocket(s); - } else { + } + else { + // Otherwise, queue the message for later if (prepend) { + // Jump to the start, skipping other messages _msgQueue.prepend(s); - } else { + } + else { + // Add to back, waiting in order _msgQueue.append(s); } } } - -void CoreNetwork::putCmd(const QString &cmd, const QList ¶ms, const QByteArray &prefix, const bool prepend) +void CoreNetwork::putCmd(const QString& cmd, const QList& params, const QByteArray& prefix, const QHash& tags, bool prepend) { - QByteArray msg; - - if (!prefix.isEmpty()) - msg += ":" + prefix + " "; - msg += cmd.toUpper().toLatin1(); - - for (int i = 0; i < params.size(); i++) { - msg += " "; - - if (i == params.size() - 1 && (params[i].contains(' ') || (!params[i].isEmpty() && params[i][0] == ':'))) - msg += ":"; - - msg += params[i]; - } - - putRawLine(msg, prepend); + putRawLine(IrcEncoder::writeMessage(tags, prefix, cmd, params), prepend); } - -void CoreNetwork::putCmd(const QString &cmd, const QList> ¶ms, const QByteArray &prefix, const bool prependAll) +void CoreNetwork::putCmd(const QString& cmd, const QList>& params, const QByteArray& prefix, const QHash& tags, bool prependAll) { QListIterator> i(params); while (i.hasNext()) { QList msg = i.next(); - putCmd(cmd, msg, prefix, prependAll); + putCmd(cmd, msg, prefix, tags, prependAll); } } - -void CoreNetwork::setChannelJoined(const QString &channel) +void CoreNetwork::setChannelJoined(const QString& channel) { - queueAutoWhoOneshot(channel); // check this new channel first + queueAutoWhoOneshot(channel); // check this new channel first Core::setChannelPersistent(userId(), networkId(), channel, true); Core::setPersistentChannelKey(userId(), networkId(), channel, _channelKeys[channel.toLower()]); } - -void CoreNetwork::setChannelParted(const QString &channel) +void CoreNetwork::setChannelParted(const QString& channel) { removeChannelKey(channel); _autoWhoQueue.removeAll(channel.toLower()); @@ -361,8 +400,7 @@ void CoreNetwork::setChannelParted(const QString &channel) Core::setChannelPersistent(userId(), networkId(), channel, false); } - -void CoreNetwork::addChannelKey(const QString &channel, const QString &key) +void CoreNetwork::addChannelKey(const QString& channel, const QString& key) { if (key.isEmpty()) { removeChannelKey(channel); @@ -372,75 +410,73 @@ void CoreNetwork::addChannelKey(const QString &channel, const QString &key) } } - -void CoreNetwork::removeChannelKey(const QString &channel) +void CoreNetwork::removeChannelKey(const QString& channel) { _channelKeys.remove(channel.toLower()); } - #ifdef HAVE_QCA2 -Cipher *CoreNetwork::cipher(const QString &target) +Cipher* CoreNetwork::cipher(const QString& target) { if (target.isEmpty()) - return 0; + return nullptr; if (!Cipher::neededFeaturesAvailable()) - return 0; + return nullptr; - CoreIrcChannel *channel = qobject_cast(ircChannel(target)); + auto* channel = qobject_cast(ircChannel(target)); if (channel) { return channel->cipher(); } - CoreIrcUser *user = qobject_cast(ircUser(target)); + auto* user = qobject_cast(ircUser(target)); if (user) { return user->cipher(); - } else if (!isChannelName(target)) { + } + else if (!isChannelName(target)) { return qobject_cast(newIrcUser(target))->cipher(); } - return 0; + return nullptr; } - -QByteArray CoreNetwork::cipherKey(const QString &target) const +QByteArray CoreNetwork::cipherKey(const QString& target) const { - CoreIrcChannel *c = qobject_cast(ircChannel(target)); + auto* c = qobject_cast(ircChannel(target)); if (c) return c->cipher()->key(); - CoreIrcUser *u = qobject_cast(ircUser(target)); + auto* u = qobject_cast(ircUser(target)); if (u) return u->cipher()->key(); return QByteArray(); } - -void CoreNetwork::setCipherKey(const QString &target, const QByteArray &key) +void CoreNetwork::setCipherKey(const QString& target, const QByteArray& key) { - CoreIrcChannel *c = qobject_cast(ircChannel(target)); + auto* c = qobject_cast(ircChannel(target)); if (c) { c->setEncrypted(c->cipher()->setKey(key)); + coreSession()->setBufferCipher(networkId(), target, key); return; } - CoreIrcUser *u = qobject_cast(ircUser(target)); + auto* u = qobject_cast(ircUser(target)); if (!u && !isChannelName(target)) u = qobject_cast(newIrcUser(target)); if (u) { u->setEncrypted(u->cipher()->setKey(key)); + coreSession()->setBufferCipher(networkId(), target, key); return; } } - -bool CoreNetwork::cipherUsesCBC(const QString &target) +bool CoreNetwork::cipherUsesCBC(const QString& target) { - CoreIrcChannel *c = qobject_cast(ircChannel(target)); + auto* c = qobject_cast(ircChannel(target)); if (c) return c->cipher()->usesCBC(); - CoreIrcUser *u = qobject_cast(ircUser(target)); + auto* u = qobject_cast(ircUser(target)); if (u) return u->cipher()->usesCBC(); @@ -448,26 +484,24 @@ bool CoreNetwork::cipherUsesCBC(const QString &target) } #endif /* HAVE_QCA2 */ -bool CoreNetwork::setAutoWhoDone(const QString &channel) +bool CoreNetwork::setAutoWhoDone(const QString& name) { - QString chan = channel.toLower(); - if (_autoWhoPending.value(chan, 0) <= 0) + QString chanOrNick = name.toLower(); + if (_autoWhoPending.value(chanOrNick, 0) <= 0) return false; - if (--_autoWhoPending[chan] <= 0) - _autoWhoPending.remove(chan); + if (--_autoWhoPending[chanOrNick] <= 0) + _autoWhoPending.remove(chanOrNick); return true; } - -void CoreNetwork::setMyNick(const QString &mynick) +void CoreNetwork::setMyNick(const QString& mynick) { Network::setMyNick(mynick); if (connectionState() == Network::Initializing) networkInitialized(); } - -void CoreNetwork::socketHasData() +void CoreNetwork::onSocketHasData() { while (socket.canReadLine()) { QByteArray s = socket.readLine(); @@ -475,14 +509,13 @@ void CoreNetwork::socketHasData() s.chop(2); else if (s.endsWith("\n")) s.chop(1); - NetworkDataEvent *event = new NetworkDataEvent(EventManager::NetworkIncoming, this, s); + NetworkDataEvent* event = new NetworkDataEvent(EventManager::NetworkIncoming, this, s); event->setTimestamp(QDateTime::currentDateTimeUtc()); emit newEvent(event); } } - -void CoreNetwork::socketError(QAbstractSocket::SocketError error) +void CoreNetwork::onSocketError(QAbstractSocket::SocketError error) { // Ignore socket closed errors if expected if (_disconnectExpected && error == QAbstractSocket::RemoteHostClosedError) { @@ -492,17 +525,21 @@ void CoreNetwork::socketError(QAbstractSocket::SocketError error) _previousConnectionAttemptFailed = true; qWarning() << qPrintable(tr("Could not connect to %1 (%2)").arg(networkName(), socket.errorString())); emit connectionError(socket.errorString()); - displayMsg(Message::Error, BufferInfo::StatusBuffer, "", tr("Connection failure: %1").arg(socket.errorString())); + showMessage(NetworkInternalMessage( + Message::Error, + BufferInfo::StatusBuffer, + "", + tr("Connection failure: %1").arg(socket.errorString()) + )); emitConnectionError(socket.errorString()); if (socket.state() < QAbstractSocket::ConnectedState) { - socketDisconnected(); + onSocketDisconnected(); } } - -void CoreNetwork::socketInitialized() +void CoreNetwork::onSocketInitialized() { - CoreIdentity *identity = identityPtr(); + CoreIdentity* identity = identityPtr(); if (!identity) { qCritical() << "Identity invalid!"; disconnectFromIrc(); @@ -515,7 +552,7 @@ void CoreNetwork::socketInitialized() // Non-SSL connections enter here only once, always emit socketInitialized(...) in these cases // SSL connections call socketInitialized() twice, only emit socketInitialized(...) on the first (not yet encrypted) run if (!server.useSsl || !socket.isEncrypted()) { - emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort()); + emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort(), _socketId); } if (server.useSsl && !socket.isEncrypted()) { @@ -523,20 +560,29 @@ void CoreNetwork::socketInitialized() return; } #else - emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort()); + emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort(), _socketId); #endif socket.setSocketOption(QAbstractSocket::KeepAliveOption, true); - // TokenBucket to avoid sending too much at once - _messageDelay = 2200; // this seems to be a safe value (2.2 seconds delay) - _burstSize = 5; - _tokenBucket = _burstSize; // init with a full bucket - _tokenBucketTimer.start(_messageDelay); + // Update the TokenBucket, force-enabling unlimited message rates for initial registration and + // capability negotiation. networkInitialized() will call updateRateLimiting() without the + // force flag to apply user preferences. When making changes, ensure that this still happens! + // As Quassel waits for CAP ACK/NAK and AUTHENTICATE replies, this shouldn't ever fill the IRC + // server receive queue and cause a kill. "Shouldn't" being the operative word; the real world + // is a scary place. + updateRateLimiting(true); + // Fill up the token bucket as we're connecting from scratch + resetTokenBucket(); // Request capabilities as per IRCv3.2 specifications // Older servers should ignore this; newer servers won't downgrade to RFC1459 - displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Requesting capability list...")); + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("Requesting capability list...") + )); putRawLine(serverEncode(QString("CAP LS 302"))); if (!server.password.isEmpty()) { @@ -551,11 +597,11 @@ void CoreNetwork::socketInitialized() nick = identity->nicks()[0]; } putRawLine(serverEncode(QString("NICK %1").arg(nick))); - putRawLine(serverEncode(QString("USER %1 8 * :%2").arg(identity->ident(), identity->realName()))); + // Only allow strict-compliant idents when strict mode is enabled + putRawLine(serverEncode(QString("USER %1 8 * :%2").arg(coreSession()->strictCompliantIdent(identity), identity->realName()))); } - -void CoreNetwork::socketDisconnected() +void CoreNetwork::onSocketDisconnected() { disablePingTimeout(); _msgQueue.clear(); @@ -569,15 +615,21 @@ void CoreNetwork::socketDisconnected() _tokenBucketTimer.stop(); - IrcUser *me_ = me(); + IrcUser* me_ = me(); if (me_) { - foreach(QString channel, me_->channels()) - displayMsg(Message::Quit, BufferInfo::ChannelBuffer, channel, _quitReason, me_->hostmask()); + for (const QString& channel : me_->channels()) { + showMessage(NetworkInternalMessage( + Message::Quit, + BufferInfo::ChannelBuffer, + channel, + _quitReason, me_->hostmask() + )); + } } setConnected(false); emit disconnected(networkId()); - emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort()); + emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort(), _socketId); // Reset disconnect expectations _disconnectExpected = false; if (_quitRequested) { @@ -594,14 +646,13 @@ void CoreNetwork::socketDisconnected() } } - -void CoreNetwork::socketStateChanged(QAbstractSocket::SocketState socketState) +void CoreNetwork::onSocketStateChanged(QAbstractSocket::SocketState socketState) { Network::ConnectionState state; switch (socketState) { case QAbstractSocket::UnconnectedState: state = Network::Disconnected; - socketDisconnected(); + onSocketDisconnected(); break; case QAbstractSocket::HostLookupState: case QAbstractSocket::ConnectingState: @@ -619,7 +670,6 @@ void CoreNetwork::socketStateChanged(QAbstractSocket::SocketState socketState) setConnectionState(state); } - void CoreNetwork::networkInitialized() { setConnectionState(Network::Initialized); @@ -627,6 +677,10 @@ void CoreNetwork::networkInitialized() _disconnectExpected = false; _quitRequested = false; + // Update the TokenBucket with specified rate-limiting settings, removing the force-unlimited + // flag used for initial registration and capability negotiation. + updateRateLimiting(); + if (useAutoReconnect()) { // reset counter _autoReconnectCount = unlimitedReconnectRetries() ? -1 : autoReconnectRetries(); @@ -634,8 +688,11 @@ void CoreNetwork::networkInitialized() // restore away state QString awayMsg = Core::awayMessage(userId(), networkId()); - if (!awayMsg.isEmpty()) - userInputHandler()->handleAway(BufferInfo(), Core::awayMessage(userId(), networkId())); + if (!awayMsg.isEmpty()) { + // Don't re-apply any timestamp formatting in order to preserve escaped percent signs, e.g. + // '%%%%%%%%' -> '%%%%' If processed again, it'd result in '%%'. + userInputHandler()->handleAway(BufferInfo(), awayMsg, true); + } sendPerform(); @@ -644,14 +701,13 @@ void CoreNetwork::networkInitialized() if (networkConfig()->autoWhoEnabled()) { _autoWhoCycleTimer.start(); _autoWhoTimer.start(); - startAutoWhoCycle(); // FIXME wait for autojoin to be completed + startAutoWhoCycle(); // FIXME wait for autojoin to be completed } - Core::bufferInfo(userId(), networkId(), BufferInfo::StatusBuffer); // create status buffer + Core::bufferInfo(userId(), networkId(), BufferInfo::StatusBuffer); // create status buffer Core::setNetworkConnected(userId(), networkId(), true); } - void CoreNetwork::sendPerform() { BufferInfo statusBuf = BufferInfo::fakeStatusBuffer(networkId()); @@ -662,26 +718,27 @@ void CoreNetwork::sendPerform() } // restore old user modes if server default mode is set. - IrcUser *me_ = me(); + IrcUser* me_ = me(); if (me_) { if (!me_->userModes().isEmpty()) { restoreUserModes(); } else { - connect(me_, SIGNAL(userModesSet(QString)), this, SLOT(restoreUserModes())); - connect(me_, SIGNAL(userModesAdded(QString)), this, SLOT(restoreUserModes())); + connect(me_, &IrcUser::userModesSet, this, &CoreNetwork::restoreUserModes); + connect(me_, &IrcUser::userModesAdded, this, &CoreNetwork::restoreUserModes); } } // send perform list - foreach(QString line, perform()) { - if (!line.isEmpty()) userInput(statusBuf, line); + for (const QString& line : perform()) { + if (!line.isEmpty()) + userInput(statusBuf, line); } // rejoin channels we've been in if (rejoinChannels()) { QStringList channels, keys; - foreach(QString chan, coreSession()->persistentChannels(networkId()).keys()) { + for (const QString& chan : coreSession()->persistentChannels(networkId()).keys()) { QString key = channelKey(chan); if (!key.isEmpty()) { channels.prepend(chan); @@ -697,14 +754,13 @@ void CoreNetwork::sendPerform() } } - void CoreNetwork::restoreUserModes() { - IrcUser *me_ = me(); + IrcUser* me_ = me(); Q_ASSERT(me_); - disconnect(me_, SIGNAL(userModesSet(QString)), this, SLOT(restoreUserModes())); - disconnect(me_, SIGNAL(userModesAdded(QString)), this, SLOT(restoreUserModes())); + disconnect(me_, &IrcUser::userModesSet, this, &CoreNetwork::restoreUserModes); + disconnect(me_, &IrcUser::userModesAdded, this, &CoreNetwork::restoreUserModes); QString modesDelta = Core::userModes(userId(), networkId()); QString currentModes = me_->userModes(); @@ -736,45 +792,43 @@ void CoreNetwork::restoreUserModes() putRawLine(serverEncode(QString("MODE %1 %2%3").arg(me_->nick()).arg(addModes).arg(removeModes))); } - -void CoreNetwork::updateIssuedModes(const QString &requestedModes) +void CoreNetwork::updateIssuedModes(const QString& requestedModes) { QString addModes; QString removeModes; bool addMode = true; - for (int i = 0; i < requestedModes.length(); i++) { - if (requestedModes[i] == '+') { + for (auto requestedMode : requestedModes) { + if (requestedMode == '+') { addMode = true; continue; } - if (requestedModes[i] == '-') { + if (requestedMode == '-') { addMode = false; continue; } if (addMode) { - addModes += requestedModes[i]; + addModes += requestedMode; } else { - removeModes += requestedModes[i]; + removeModes += requestedMode; } } QString addModesOld = _requestedUserModes.section('-', 0, 0); QString removeModesOld = _requestedUserModes.section('-', 1); - addModes.remove(QRegExp(QString("[%1]").arg(addModesOld))); // deduplicate - addModesOld.remove(QRegExp(QString("[%1]").arg(removeModes))); // update + addModes.remove(QRegExp(QString("[%1]").arg(addModesOld))); // deduplicate + addModesOld.remove(QRegExp(QString("[%1]").arg(removeModes))); // update addModes += addModesOld; - removeModes.remove(QRegExp(QString("[%1]").arg(removeModesOld))); // deduplicate - removeModesOld.remove(QRegExp(QString("[%1]").arg(addModes))); // update + removeModes.remove(QRegExp(QString("[%1]").arg(removeModesOld))); // deduplicate + removeModesOld.remove(QRegExp(QString("[%1]").arg(addModes))); // update removeModes += removeModesOld; _requestedUserModes = QString("%1-%2").arg(addModes).arg(removeModes); } - void CoreNetwork::updatePersistentModes(QString addModes, QString removeModes) { QString persistentUserModes = Core::userModes(userId(), networkId()); @@ -820,14 +874,12 @@ void CoreNetwork::updatePersistentModes(QString addModes, QString removeModes) Core::setUserModes(userId(), networkId(), QString("%1-%2").arg(persistentAdd).arg(persistentRemove)); } - void CoreNetwork::resetPersistentModes() { _requestedUserModes = QString('-'); Core::setUserModes(userId(), networkId(), QString()); } - void CoreNetwork::setUseAutoReconnect(bool use) { Network::setUseAutoReconnect(use); @@ -835,14 +887,12 @@ void CoreNetwork::setUseAutoReconnect(bool use) _autoReconnectTimer.stop(); } - void CoreNetwork::setAutoReconnectInterval(quint32 interval) { Network::setAutoReconnectInterval(interval); _autoReconnectTimer.setInterval(interval * 1000); } - void CoreNetwork::setAutoReconnectRetries(quint16 retries) { Network::setAutoReconnectRetries(retries); @@ -854,7 +904,6 @@ void CoreNetwork::setAutoReconnectRetries(quint16 retries) } } - void CoreNetwork::doAutoReconnect() { if (connectionState() != Network::Disconnected && connectionState() != Network::Reconnecting) { @@ -866,58 +915,153 @@ void CoreNetwork::doAutoReconnect() connectToIrc(true); } - void CoreNetwork::sendPing() { - uint now = QDateTime::currentDateTime().toTime_t(); + qint64 now = QDateTime::currentDateTime().toMSecsSinceEpoch(); if (_pingCount != 0) { qDebug() << "UserId:" << userId() << "Network:" << networkName() << "missed" << _pingCount << "pings." << "BA:" << socket.bytesAvailable() << "BTW:" << socket.bytesToWrite(); } - if ((int)_pingCount >= networkConfig()->maxPingCount() && now - _lastPingTime <= (uint)(_pingTimer.interval() / 1000) + 1) { + if ((int) _pingCount >= networkConfig()->maxPingCount() && (now - _lastPingTime) <= (_pingTimer.interval() + (1 * 1000))) { + // In transitioning to 64-bit time, the interval no longer needs converted down to seconds. + // However, to reduce the risk of breaking things by changing past behavior, we still allow + // up to 1 second missed instead of enforcing a stricter 1 millisecond allowance. + // // the second check compares the actual elapsed time since the last ping and the pingTimer interval // if the interval is shorter then the actual elapsed time it means that this thread was somehow blocked // and unable to even handle a ping answer. So we ignore those misses. - disconnectFromIrc(false, QString("No Ping reply in %1 seconds.").arg(_pingCount * _pingTimer.interval() / 1000), true /* withReconnect */); + disconnectFromIrc(false, + QString("No Ping reply in %1 seconds.").arg(_pingCount * _pingTimer.interval() / 1000), + true /* withReconnect */); } else { _lastPingTime = now; _pingCount++; // Don't send pings until the network is initialized - if(_sendPings) + if (_sendPings) { + // Mark as waiting for a reply + _pongReplyPending = true; + // Send default timestamp ping userInputHandler()->handlePing(BufferInfo(), QString()); + } } } - void CoreNetwork::enablePingTimeout(bool enable) { if (!enable) disablePingTimeout(); else { resetPingTimeout(); + resetPongReplyPending(); if (networkConfig()->pingTimeoutEnabled()) _pingTimer.start(); } } - void CoreNetwork::disablePingTimeout() { _pingTimer.stop(); _sendPings = false; resetPingTimeout(); + resetPongReplyPending(); } - void CoreNetwork::setPingInterval(int interval) { _pingTimer.setInterval(interval * 1000); } +void CoreNetwork::setPongTimestampValid(bool validTimestamp) +{ + _pongTimestampValid = validTimestamp; +} + +/******** Custom Rate Limiting ********/ + +void CoreNetwork::updateRateLimiting(bool forceUnlimited) +{ + // Verify and apply custom rate limiting options, always resetting the delay and burst size + // (safe-guarding against accidentally starting the timer), but don't reset the token bucket as + // this may be called while connected to a server. + + if (useCustomMessageRate() || forceUnlimited) { + // Custom message rates enabled, or chosen by means of forcing unlimited. Let's go for it! + + _messageDelay = messageRateDelay(); + + _burstSize = messageRateBurstSize(); + if (_burstSize < 1) { + qWarning() << "Invalid messageRateBurstSize data, cannot have zero message burst size!" << _burstSize; + // Can't go slower than one message at a time + _burstSize = 1; + } + + if (_tokenBucket > _burstSize) { + // Don't let the token bucket exceed the maximum + _tokenBucket = _burstSize; + // To fill up the token bucket, use resetRateLimiting(). Don't do that here, otherwise + // changing the rate-limit settings while connected to a server will incorrectly reset + // the token bucket. + } + + // Toggle the timer according to whether or not rate limiting is enabled + // If we're here, either useCustomMessageRate or forceUnlimited is true. Thus, the logic is + // _skipMessageRates = ((useCustomMessageRate && unlimitedMessageRate) || forceUnlimited) + // Override user preferences if called with force unlimited, only used during connect. + _skipMessageRates = (unlimitedMessageRate() || forceUnlimited); + if (_skipMessageRates) { + // If the message queue already contains messages, they need sent before disabling the + // timer. Set the timer to a rapid pace and let it disable itself. + if (!_msgQueue.isEmpty()) { + qDebug() << "Outgoing message queue contains messages while disabling rate " + "limiting. Sending remaining queued messages..."; + // Promptly run the timer again to clear the messages. Rate limiting is disabled, + // so nothing should cause this to block.. in theory. However, don't directly call + // fillBucketAndProcessQueue() in order to keep it on a separate thread. + // + // TODO If testing shows this isn't needed, it can be simplified to a direct call. + // Hesitant to change it without a wide variety of situations to verify behavior. + _tokenBucketTimer.start(100); + } + else { + // No rate limiting, disable the timer + _tokenBucketTimer.stop(); + } + } + else { + // Rate limiting enabled, enable the timer + _tokenBucketTimer.start(_messageDelay); + } + } + else { + // Custom message rates disabled. Go for the default. + + _skipMessageRates = false; // Enable rate-limiting by default + _messageDelay = 2200; // This seems to be a safe value (2.2 seconds delay) + _burstSize = 5; // 5 messages at once + if (_tokenBucket > _burstSize) { + // TokenBucket to avoid sending too much at once. Don't let the token bucket exceed the + // maximum. + _tokenBucket = _burstSize; + // To fill up the token bucket, use resetRateLimiting(). Don't do that here, otherwise + // changing the rate-limit settings while connected to a server will incorrectly reset + // the token bucket. + } + // Rate limiting enabled, enable the timer + _tokenBucketTimer.start(_messageDelay); + } +} + +void CoreNetwork::resetTokenBucket() +{ + // Fill up the token bucket to the maximum + _tokenBucket = _burstSize; +} + /******** IRCv3 Capability Negotiation ********/ -void CoreNetwork::serverCapAdded(const QString &capability) +void CoreNetwork::serverCapAdded(const QString& capability) { // Check if it's a known capability; if so, add it to the list // Handle special cases first @@ -925,13 +1069,14 @@ void CoreNetwork::serverCapAdded(const QString &capability) // Only request SASL if it's enabled if (networkInfo().useSasl) queueCap(capability); - } else if (IrcCap::knownCaps.contains(capability)) { + } + else if (IrcCap::knownCaps.contains(capability)) { // Handling for general known capabilities queueCap(capability); } } -void CoreNetwork::serverCapAcknowledged(const QString &capability) +void CoreNetwork::serverCapAcknowledged(const QString& capability) { // This may be called multiple times in certain situations. @@ -949,23 +1094,34 @@ void CoreNetwork::serverCapAcknowledged(const QString &capability) // FIXME use event #ifdef HAVE_SSL if (!identityPtr()->sslCert().isNull()) { - if (IrcCap::SaslMech::maybeSupported(capValue(IrcCap::SASL), IrcCap::SaslMech::EXTERNAL)) { + if (saslMaybeSupports(IrcCap::SaslMech::EXTERNAL)) { // EXTERNAL authentication supported, send request putRawLine(serverEncode("AUTHENTICATE EXTERNAL")); - } else { - displayMsg(Message::Error, BufferInfo::StatusBuffer, "", - tr("SASL EXTERNAL authentication not supported")); + } + else { + showMessage(NetworkInternalMessage( + Message::Error, + BufferInfo::StatusBuffer, + "", + tr("SASL EXTERNAL authentication not supported") + )); sendNextCap(); } - } else { + } + else { #endif - if (IrcCap::SaslMech::maybeSupported(capValue(IrcCap::SASL), IrcCap::SaslMech::PLAIN)) { + if (saslMaybeSupports(IrcCap::SaslMech::PLAIN)) { // PLAIN authentication supported, send request // Only working with PLAIN atm, blowfish later putRawLine(serverEncode("AUTHENTICATE PLAIN")); - } else { - displayMsg(Message::Error, BufferInfo::StatusBuffer, "", - tr("SASL PLAIN authentication not supported")); + } + else { + showMessage(NetworkInternalMessage( + Message::Error, + BufferInfo::StatusBuffer, + "", + tr("SASL PLAIN authentication not supported") + )); sendNextCap(); } #ifdef HAVE_SSL @@ -974,7 +1130,7 @@ void CoreNetwork::serverCapAcknowledged(const QString &capability) } } -void CoreNetwork::serverCapRemoved(const QString &capability) +void CoreNetwork::serverCapRemoved(const QString& capability) { // This may be called multiple times in certain situations. @@ -985,59 +1141,171 @@ void CoreNetwork::serverCapRemoved(const QString &capability) } } -void CoreNetwork::queueCap(const QString &capability) +void CoreNetwork::queueCap(const QString& capability) { // IRCv3 specs all use lowercase capability names QString _capLowercase = capability.toLower(); - if (!_capsQueued.contains(_capLowercase)) { - _capsQueued.append(_capLowercase); + + if (capsRequiringConfiguration.contains(_capLowercase)) { + // The capability requires additional configuration before being acknowledged (e.g. SASL), + // so we should negotiate it separately from all other capabilities. Otherwise new + // capabilities will be requested while still configuring the previous one. + if (!_capsQueuedIndividual.contains(_capLowercase)) { + _capsQueuedIndividual.append(_capLowercase); + } + } + else { + // The capability doesn't need any special configuration, so it should be safe to try + // bundling together with others. "Should" being the imperative word, as IRC servers can do + // anything. + if (!_capsQueuedBundled.contains(_capLowercase)) { + _capsQueuedBundled.append(_capLowercase); + } } } -QString CoreNetwork::takeQueuedCap() -{ - if (!_capsQueued.empty()) { - return _capsQueued.takeFirst(); - } else { +QString CoreNetwork::takeQueuedCaps() +{ + // Clear the record of the most recently negotiated capability bundle. Does nothing if the list + // is empty. + _capsQueuedLastBundle.clear(); + + // First, negotiate all the standalone capabilities that require additional configuration. + if (!_capsQueuedIndividual.empty()) { + // We have an individual capability available. Take the first and pass it back. + return _capsQueuedIndividual.takeFirst(); + } + else if (!_capsQueuedBundled.empty()) { + // We have capabilities available that can be grouped. Try to fit in as many as within the + // maximum length. + // See CoreNetwork::maxCapRequestLength + + // Response must have at least one capability regardless of max length for anything to + // happen. + QString capBundle = _capsQueuedBundled.takeFirst(); + QString nextCap(""); + while (!_capsQueuedBundled.empty()) { + // As long as capabilities remain, get the next... + nextCap = _capsQueuedBundled.first(); + if ((capBundle.length() + 1 + nextCap.length()) <= maxCapRequestLength) { + // [capability + 1 for a space + this new capability] fit within length limits + // Add it to formatted list + capBundle.append(" " + nextCap); + // Add it to most recent bundle of requested capabilities (simplifies retry logic) + _capsQueuedLastBundle.append(nextCap); + // Then remove it from the queue + _capsQueuedBundled.removeFirst(); + } + else { + // We've reached the length limit for a single capability request, stop adding more + break; + } + } + // Return this space-separated set of capabilities, removing any extra spaces + return capBundle.trimmed(); + } + else { + // No capabilities left to negotiate, return an empty string. return QString(); } } +void CoreNetwork::retryCapsIndividually() +{ + // The most recent set of capabilities got denied by the IRC server. As we don't know what got + // denied, try each capability individually. + if (_capsQueuedLastBundle.empty()) { + // No most recently tried capability set, just return. + return; + // Note: there's little point in retrying individually requested caps during negotiation. + // We know the individual capability was the one that failed, and it's not likely it'll + // suddenly start working within a few seconds. 'cap-notify' provides a better system for + // handling capability removal and addition. + } + + // This should be fairly rare, e.g. services restarting during negotiation, so simplicity wins + // over efficiency. If this becomes an issue, implement a binary splicing system instead, + // keeping track of which halves of the group fail, dividing the set each time. + + // Add most recently tried capability set to individual list, re-requesting them one at a time + _capsQueuedIndividual.append(_capsQueuedLastBundle); + // Warn of this issue to explain the slower login. Servers usually shouldn't trigger this. + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("Could not negotiate some capabilities, retrying individually (%1)...").arg(_capsQueuedLastBundle.join(", ")) + )); + // Capabilities are already removed from the capability bundle queue via takeQueuedCaps(), no + // need to remove them here. + // Clear the most recently tried set to reduce risk that mistakes elsewhere causes retrying + // indefinitely. + _capsQueuedLastBundle.clear(); +} + void CoreNetwork::beginCapNegotiation() { // Don't begin negotiation if no capabilities are queued to request if (!capNegotiationInProgress()) { // If the server doesn't have any capabilities, but supports CAP LS, continue on with the // normal connection. - displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("No capabilities available")); + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("No capabilities available") + )); endCapNegotiation(); return; } _capNegotiationActive = true; - displayMsg(Message::Server, BufferInfo::StatusBuffer, "", - tr("Ready to negotiate (found: %1)").arg(caps().join(", "))); - displayMsg(Message::Server, BufferInfo::StatusBuffer, "", - tr("Negotiating capabilities (requesting: %1)...").arg(_capsQueued.join(", "))); + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("Ready to negotiate (found: %1)").arg(caps().join(", ")) + )); + + // Build a list of queued capabilities, starting with individual, then bundled, only adding the + // comma separator between the two if needed (both individual and bundled caps exist). + QString queuedCapsDisplay = _capsQueuedIndividual.join(", ") + + ((!_capsQueuedIndividual.empty() && !_capsQueuedBundled.empty()) ? ", " : "") + + _capsQueuedBundled.join(", "); + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay) + )); + sendNextCap(); } void CoreNetwork::sendNextCap() { if (capNegotiationInProgress()) { - // Request the next capability and remove it from the list - // Handle one at a time so one capability failing won't NAK all of 'em - putRawLine(serverEncode(QString("CAP REQ :%1").arg(takeQueuedCap()))); - } else { + // Request the next set of capabilities and remove them from the list + putRawLine(serverEncode(QString("CAP REQ :%1").arg(takeQueuedCaps()))); + } + else { // No pending desired capabilities, capability negotiation finished // If SASL requested but not available, print a warning if (networkInfo().useSasl && !capEnabled(IrcCap::SASL)) - displayMsg(Message::Error, BufferInfo::StatusBuffer, "", - tr("SASL authentication currently not supported by server")); + showMessage(NetworkInternalMessage( + Message::Error, + BufferInfo::StatusBuffer, + "", + tr("SASL authentication currently not supported by server") + )); if (_capNegotiationActive) { - displayMsg(Message::Server, BufferInfo::StatusBuffer, "", - tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", "))); + showMessage(NetworkInternalMessage( + Message::Server, + BufferInfo::StatusBuffer, + "", + tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", ")) + )); _capNegotiationActive = false; } @@ -1065,12 +1333,12 @@ void CoreNetwork::startAutoWhoCycle() _autoWhoQueue = channels(); } -void CoreNetwork::queueAutoWhoOneshot(const QString &channelOrNick) +void CoreNetwork::queueAutoWhoOneshot(const QString& name) { // Prepend so these new channels/nicks are the first to be checked // Don't allow duplicates - if (!_autoWhoQueue.contains(channelOrNick.toLower())) { - _autoWhoQueue.prepend(channelOrNick.toLower()); + if (!_autoWhoQueue.contains(name.toLower())) { + _autoWhoQueue.prepend(name.toLower()); } if (capEnabled(IrcCap::AWAY_NOTIFY)) { // When away-notify is active, the timer's stopped. Start a new cycle to who this channel. @@ -1078,19 +1346,16 @@ void CoreNetwork::queueAutoWhoOneshot(const QString &channelOrNick) } } - void CoreNetwork::setAutoWhoDelay(int delay) { _autoWhoTimer.setInterval(delay * 1000); } - void CoreNetwork::setAutoWhoInterval(int interval) { _autoWhoCycleTimer.setInterval(interval * 1000); } - void CoreNetwork::setAutoWhoEnabled(bool enabled) { if (enabled && isConnected() && !_autoWhoTimer.isActive()) @@ -1101,7 +1366,6 @@ void CoreNetwork::setAutoWhoEnabled(bool enabled) } } - void CoreNetwork::sendAutoWho() { // Don't send autowho if there are still some pending @@ -1111,52 +1375,66 @@ void CoreNetwork::sendAutoWho() while (!_autoWhoQueue.isEmpty()) { QString chanOrNick = _autoWhoQueue.takeFirst(); // Check if it's a known channel or nick - IrcChannel *ircchan = ircChannel(chanOrNick); - IrcUser *ircuser = ircUser(chanOrNick); + IrcChannel* ircchan = ircChannel(chanOrNick); + IrcUser* ircuser = ircUser(chanOrNick); if (ircchan) { // Apply channel limiting rules // If using away-notify, don't impose channel size limits in order to capture away // state of everyone. Auto-who won't run on a timer so network impact is minimal. - if (networkConfig()->autoWhoNickLimit() > 0 - && ircchan->ircUsers().count() >= networkConfig()->autoWhoNickLimit() + if (networkConfig()->autoWhoNickLimit() > 0 && ircchan->ircUsers().count() >= networkConfig()->autoWhoNickLimit() && !capEnabled(IrcCap::AWAY_NOTIFY)) continue; _autoWhoPending[chanOrNick.toLower()]++; - } else if (ircuser) { + } + else if (ircuser) { // Checking a nick, add it to the pending list _autoWhoPending[ircuser->nick().toLower()]++; - } else { + } + else { // Not a channel or a nick, skip it qDebug() << "Skipping who polling of unknown channel or nick" << chanOrNick; continue; } if (supports("WHOX")) { // Use WHO extended to poll away users and/or user accounts + // Explicitly only match on nickname ("n"), don't rely on server defaults + // + // WHO n%chtsunfra, + // // See http://faerion.sourceforge.net/doc/irc/whox.var - // And https://github.com/hexchat/hexchat/blob/c874a9525c9b66f1d5ddcf6c4107d046eba7e2c5/src/common/proto-irc.c#L750 - putRawLine(serverEncode(QString("WHO %1 %%chtsunfra,%2") - .arg(serverEncode(chanOrNick), QString::number(IrcCap::ACCOUNT_NOTIFY_WHOX_NUM)))); - } else { + // And https://github.com/quakenet/snircd/blob/master/doc/readme.who + // And https://github.com/hexchat/hexchat/blob/57478b65758e6b697b1d82ce21075e74aa475efc/src/common/proto-irc.c#L752 + putRawLine(serverEncode( + QString("WHO %1 n%chtsunfra,%2") + .arg(chanOrNick, QString::number(IrcCap::ACCOUNT_NOTIFY_WHOX_NUM)) + )); + } + else { + // Fall back to normal WHO + // + // Note: According to RFC 1459, "WHO " can fall back to searching realname, + // hostmask, etc. There's nothing we can do about that :( + // + // See https://tools.ietf.org/html/rfc1459#section-4.5.1 putRawLine(serverEncode(QString("WHO %1").arg(chanOrNick))); } break; } - if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive() - && !capEnabled(IrcCap::AWAY_NOTIFY)) { + if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive() && !capEnabled(IrcCap::AWAY_NOTIFY)) { // Timer was stopped, means a new cycle is due immediately // Don't run a new cycle if using away-notify; server will notify as appropriate _autoWhoCycleTimer.start(); startAutoWhoCycle(); - } else if (capEnabled(IrcCap::AWAY_NOTIFY) && _autoWhoCycleTimer.isActive()) { + } + else if (capEnabled(IrcCap::AWAY_NOTIFY) && _autoWhoCycleTimer.isActive()) { // Don't run another who cycle if away-notify is enabled _autoWhoCycleTimer.stop(); } } - #ifdef HAVE_SSL -void CoreNetwork::sslErrors(const QList &sslErrors) +void CoreNetwork::onSslErrors(const QList& sslErrors) { Server server = usedServer(); if (server.sslVerify) { @@ -1167,12 +1445,18 @@ void CoreNetwork::sslErrors(const QList &sslErrors) // Add the error reason if known sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString())); } - displayMsg(Message::Error, BufferInfo::StatusBuffer, "", sslErrorMessage); + showMessage(NetworkInternalMessage( + Message::Error, + BufferInfo::StatusBuffer, + "", + sslErrorMessage + )); // Disconnect, triggering a reconnect in case it's a temporary issue with certificate // validity, network trouble, etc. disconnectFromIrc(false, QString("Encrypted connection not verified"), true /* withReconnect */); - } else { + } + else { // Treat the SSL error as a warning, continue to connect anyways QString sslErrorMessage = tr("Encrypted connection couldn't be verified, continuing " "since verification is not required"); @@ -1180,36 +1464,63 @@ void CoreNetwork::sslErrors(const QList &sslErrors) // Add the error reason if known sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString())); } - displayMsg(Message::Info, BufferInfo::StatusBuffer, "", sslErrorMessage); + showMessage(NetworkInternalMessage( + Message::Info, + BufferInfo::StatusBuffer, + "", + sslErrorMessage + )); // Proceed with the connection socket.ignoreSslErrors(); } } - #endif // HAVE_SSL +void CoreNetwork::checkTokenBucket() +{ + if (_skipMessageRates) { + if (_msgQueue.empty()) { + // Message queue emptied; stop the timer and bail out + _tokenBucketTimer.stop(); + return; + } + // Otherwise, we're emptying the queue, continue on as normal + } + + // Process whatever messages are pending + fillBucketAndProcessQueue(); +} + void CoreNetwork::fillBucketAndProcessQueue() { + // If there's less tokens than burst size, refill the token bucket by 1 if (_tokenBucket < _burstSize) { _tokenBucket++; } - while (_msgQueue.size() > 0 && _tokenBucket > 0) { + // As long as there's tokens available and messages remaining, sending messages from the queue + while (!_msgQueue.empty() && _tokenBucket > 0) { writeToSocket(_msgQueue.takeFirst()); } } - -void CoreNetwork::writeToSocket(const QByteArray &data) +void CoreNetwork::writeToSocket(const QByteArray& data) { + // Log the message if enabled and network ID matches or allows all + if (_debugLogRawIrc && (_debugLogRawNetId == -1 || networkId().toInt() == _debugLogRawNetId)) { + // Include network ID + qDebug() << "IRC net" << networkId() << ">>" << data; + } socket.write(data); socket.write("\r\n"); - _tokenBucket--; + if (!_skipMessageRates) { + // Only subtract from the token bucket if message rate limiting is enabled + _tokenBucket--; + } } - Network::Server CoreNetwork::usedServer() const { if (_lastUsedServerIndex < serverList().count()) @@ -1221,19 +1532,23 @@ Network::Server CoreNetwork::usedServer() const return Network::Server(); } - void CoreNetwork::requestConnect() const { + if (_shuttingDown) { + return; + } if (connectionState() != Disconnected) { qWarning() << "Requesting connect while already being connected!"; return; } - QMetaObject::invokeMethod(const_cast(this), "connectToIrc", Qt::QueuedConnection); + QMetaObject::invokeMethod(const_cast(this), "connectToIrc", Qt::QueuedConnection); } - void CoreNetwork::requestDisconnect() const { + if (_shuttingDown) { + return; + } if (connectionState() == Disconnected) { qWarning() << "Requesting disconnect while not being connected!"; return; @@ -1241,8 +1556,7 @@ void CoreNetwork::requestDisconnect() const userInputHandler()->handleQuit(BufferInfo(), QString()); } - -void CoreNetwork::requestSetNetworkInfo(const NetworkInfo &info) +void CoreNetwork::requestSetNetworkInfo(const NetworkInfo& info) { Network::Server currentServer = usedServer(); setNetworkInfo(info); @@ -1260,8 +1574,9 @@ void CoreNetwork::requestSetNetworkInfo(const NetworkInfo &info) } } - -QList> CoreNetwork::splitMessage(const QString &cmd, const QString &message, std::function(QString &)> cmdGenerator) +QList> CoreNetwork::splitMessage(const QString& cmd, + const QString& message, + const std::function(QString&)>& cmdGenerator) { QString wrkMsg(message); QList> msgsToSend; @@ -1325,7 +1640,7 @@ QList> CoreNetwork::splitMessage(const QString &cmd, const QSt wrkMsg.remove(0, splitPos); msgsToSend.append(splitMsgEnc); } - else{ + else { // If the entire remaining message is short enough to be sent all at once, remove // it from the wrkMsg and add it to the list of messages to be sent. wrkMsg.remove(0, splitPos);