X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fcore%2Fcorenetwork.cpp;h=29f10f06af865b9c834d0b738653d72d25435219;hp=4784edb7fe8bebc18c00bb5a954e4d2a41f1e5e1;hb=714b39660fe19e7f092880019429c8da76ee2bd5;hpb=de8cb8bb24c3bfd5600726e99acccb080a5beb87 diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index 4784edb7..29f10f06 100644 --- a/src/core/corenetwork.cpp +++ b/src/core/corenetwork.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2005-2015 by the Quassel Project * + * Copyright (C) 2005-2018 by the Quassel Project * * devel@quassel-irc.org * * * * This program is free software; you can redistribute it and/or modify * @@ -18,10 +18,11 @@ * 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" @@ -29,22 +30,26 @@ #include "coreuserinputhandler.h" #include "networkevent.h" -INIT_SYNCABLE_OBJECT(CoreNetwork) +// IRCv3 capabilities +#include "irccap.h" + 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), - _lastPingTime(0), - _pingCount(0), - _sendPings(false), _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(); + _autoReconnectTimer.setSingleShot(true); connect(&_socketCloseTimer, SIGNAL(timeout()), this, SLOT(socketCloseTimeout())); @@ -59,6 +64,11 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) _channelKeys[chan.toLower()] = channels[chan]; } + QHash bufferCiphers = coreSession()->bufferCiphers(networkId()); + foreach(QString buffer, bufferCiphers.keys()) { + storeChannelCipherKey(buffer.toLower(), bufferCiphers[buffer]); + } + connect(networkConfig(), SIGNAL(pingTimeoutEnabledSet(bool)), SLOT(enablePingTimeout(bool))); connect(networkConfig(), SIGNAL(pingIntervalSet(int)), SLOT(setPingInterval(int))); connect(networkConfig(), SIGNAL(autoWhoEnabledSet(bool)), SLOT(setAutoWhoEnabled(bool))); @@ -68,7 +78,7 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) 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(&_tokenBucketTimer, SIGNAL(timeout()), this, SLOT(checkTokenBucket())); connect(&socket, SIGNAL(connected()), this, SLOT(socketInitialized())); connect(&socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError))); @@ -80,19 +90,58 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) #endif connect(this, SIGNAL(newEvent(Event *)), coreSession()->eventManager(), SLOT(postEvent(Event *))); + // Custom rate limiting + // These react to the user changing settings in the client + connect(this, SIGNAL(useCustomMessageRateSet(bool)), SLOT(updateRateLimiting())); + connect(this, SIGNAL(messageRateBurstSizeSet(quint32)), SLOT(updateRateLimiting())); + connect(this, SIGNAL(messageRateDelaySet(quint32)), SLOT(updateRateLimiting())); + connect(this, SIGNAL(unlimitedMessageRateSet(bool)), SLOT(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))); + 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, SIGNAL(socketInitialized(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)), + Core::instance()->oidentdConfigGenerator(), SLOT(addSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)), Qt::BlockingQueuedConnection); + connect(this, SIGNAL(socketDisconnected(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)), + Core::instance()->oidentdConfigGenerator(), SLOT(removeSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64))); + } + + if (Quassel::isOptionSet("ident-daemon")) { + connect(this, SIGNAL(socketInitialized(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)), + Core::instance()->identServer(), SLOT(addSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)), Qt::BlockingQueuedConnection); + connect(this, SIGNAL(socketDisconnected(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64)), + Core::instance()->identServer(), SLOT(removeSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16, qint64))); } } CoreNetwork::~CoreNetwork() { - if (connectionState() != Disconnected && connectionState() != Network::Reconnecting) - disconnectFromIrc(false); // clean up, but this does not count as requested disconnect! - disconnect(&socket, 0, this, 0); // this keeps the socket from triggering events during clean up - delete _userInputHandler; + // 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()); + } +} + + +bool CoreNetwork::forceDisconnect(int msecs) +{ + if (socket.state() == QAbstractSocket::UnconnectedState) { + // Socket already disconnected. + return true; + } + // Request a socket-level disconnect if not already happened + socket.disconnectFromHost(); + if (socket.state() != QAbstractSocket::UnconnectedState) { + return socket.waitForDisconnected(msecs); + } + return true; } @@ -138,6 +187,14 @@ QByteArray CoreNetwork::userEncode(const QString &userNick, const QString &strin 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()) @@ -158,10 +215,12 @@ void CoreNetwork::connectToIrc(bool reconnecting) // cleaning up old quit reason _quitReason.clear(); - // reset capability negotiation in case server changes during a reconnect - _capsQueued.clear(); - _capsPending.clear(); - _capsSupported.clear(); + // Reset capability negotiation tracking, also handling server changes during reconnect + _capsQueuedIndividual.clear(); + _capsQueuedBundled.clear(); + clearCaps(); + _capNegotiationActive = false; + _capInitialNegotiationEnded = false; // use a random server? if (useRandomServer()) { @@ -169,12 +228,16 @@ void CoreNetwork::connectToIrc(bool reconnecting) } else if (_previousConnectionAttemptFailed) { // cycle to next server if previous connection attempt failed + _previousConnectionAttemptFailed = false; displayMsg(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)); @@ -190,10 +253,16 @@ 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(); @@ -214,6 +283,8 @@ void CoreNetwork::connectToIrc(bool reconnecting) 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(); if (!withReconnect) { _autoReconnectTimer.stop(); @@ -238,36 +309,65 @@ void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason)); if (socket.state() == QAbstractSocket::UnconnectedState) { socketDisconnected(); - } else { + } + else { if (socket.state() == QAbstractSocket::ConnectedState) { - userInputHandler()->issueQuit(_quitReason); - } 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::socketCloseTimeout() +{ + 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::shutdown() +{ + _shuttingDown = true; + disconnectFromIrc(false, {}, false); +} + + void CoreNetwork::userInput(BufferInfo buf, QString msg) { userInputHandler()->handleUserInput(buf, msg); } -void CoreNetwork::putRawLine(QByteArray s) +void CoreNetwork::putRawLine(const QByteArray s, const bool prepend) { - if (_tokenBucket > 0) + if (_tokenBucket > 0 || (_skipMessageRates && _msgQueue.size() == 0)) { + // 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 - _msgQueue.append(s); + } else { + // Otherwise, queue the message for later + if (prepend) { + // Jump to the start, skipping other messages + _msgQueue.prepend(s); + } else { + // Add to back, waiting in order + _msgQueue.append(s); + } + } } -void CoreNetwork::putCmd(const QString &cmd, const QList ¶ms, const QByteArray &prefix) +void CoreNetwork::putCmd(const QString &cmd, const QList ¶ms, const QByteArray &prefix, const bool prepend) { QByteArray msg; @@ -284,16 +384,16 @@ void CoreNetwork::putCmd(const QString &cmd, const QList ¶ms, co msg += params[i]; } - putRawLine(msg); + putRawLine(msg, prepend); } -void CoreNetwork::putCmd(const QString &cmd, const QList> ¶ms, const QByteArray &prefix) +void CoreNetwork::putCmd(const QString &cmd, const QList> ¶ms, const QByteArray &prefix, const bool prependAll) { QListIterator> i(params); while (i.hasNext()) { QList msg = i.next(); - putCmd(cmd, msg, prefix); + putCmd(cmd, msg, prefix, prependAll); } } @@ -338,32 +438,32 @@ void CoreNetwork::removeChannelKey(const QString &channel) 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)) { return qobject_cast(newIrcUser(target))->cipher(); } - return 0; + return nullptr; } 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(); @@ -373,18 +473,20 @@ QByteArray CoreNetwork::cipherKey(const QString &target) const 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; } } @@ -392,10 +494,10 @@ void CoreNetwork::setCipherKey(const QString &target, const QByteArray &key) 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(); @@ -403,13 +505,13 @@ 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; } @@ -439,8 +541,10 @@ void CoreNetwork::socketHasData() void CoreNetwork::socketError(QAbstractSocket::SocketError error) { - if (_quitRequested && error == QAbstractSocket::RemoteHostClosedError) + // Ignore socket closed errors if expected + if (_disconnectExpected && error == QAbstractSocket::RemoteHostClosedError) { return; + } _previousConnectionAttemptFailed = true; qWarning() << qPrintable(tr("Could not connect to %1 (%2)").arg(networkName(), socket.errorString())); @@ -468,7 +572,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()) { @@ -476,16 +580,20 @@ 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 @@ -504,7 +612,10 @@ 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()))); } @@ -530,7 +641,9 @@ void CoreNetwork::socketDisconnected() 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) { _quitRequested = false; setConnectionState(Network::Disconnected); @@ -575,8 +688,13 @@ void CoreNetwork::networkInitialized() { setConnectionState(Network::Initialized); setConnected(true); + _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(); @@ -584,8 +702,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(); @@ -693,20 +814,20 @@ void CoreNetwork::updateIssuedModes(const QString &requestedModes) 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; } } @@ -819,12 +940,17 @@ void CoreNetwork::doAutoReconnect() 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. @@ -834,8 +960,12 @@ void CoreNetwork::sendPing() _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()); + } } } @@ -846,6 +976,7 @@ void CoreNetwork::enablePingTimeout(bool enable) disablePingTimeout(); else { resetPingTimeout(); + resetPongReplyPending(); if (networkConfig()->pingTimeoutEnabled()) _pingTimer.start(); } @@ -857,6 +988,7 @@ void CoreNetwork::disablePingTimeout() _pingTimer.stop(); _sendPings = false; resetPingTimeout(); + resetPongReplyPending(); } @@ -865,79 +997,314 @@ void CoreNetwork::setPingInterval(int interval) _pingTimer.setInterval(interval * 1000); } -/******** IRCv3 Capability Negotiation ********/ -void CoreNetwork::addCap(const QString &capability, const QString &value) -{ - // Clear from pending list, add to supported list - if (!_capsSupported.contains(capability)) { - if (value != "") { - // Value defined, just use it - _capsSupported[capability] = value; - } else if (_capsPending.contains(capability)) { - // Value not defined, but a pending capability had a value. - // E.g. CAP * LS :sasl=PLAIN multi-prefix - // Preserve the capability value for later use. - _capsSupported[capability] = _capsPending[capability]; +void CoreNetwork::setPongTimestampValid(bool validTimestamp) +{ + _pongTimestampValid = validTimestamp; +} + + +/******** Custom Rate Limiting ********/ + +void CoreNetwork::updateRateLimiting(const 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.size() > 0) { + 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 { - // No value ever given, assign to blank - _capsSupported[capability] = QString(); + // 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) +{ + // Check if it's a known capability; if so, add it to the list + // Handle special cases first + if (capability == IrcCap::SASL) { + // Only request SASL if it's enabled + if (networkInfo().useSasl) + queueCap(capability); + } else if (IrcCap::knownCaps.contains(capability)) { + // Handling for general known capabilities + queueCap(capability); } - if (_capsPending.contains(capability)) - _capsPending.remove(capability); +} - // Handle special cases here - // TODO Use events if it makes sense - if (capability == "away-notify") { - // away-notify enabled, stop the automatic timers, handle manually +void CoreNetwork::serverCapAcknowledged(const QString &capability) +{ + // This may be called multiple times in certain situations. + + // Handle core-side configuration + if (capability == IrcCap::AWAY_NOTIFY) { + // away-notify enabled, stop the autoWho timers, handle manually setAutoWhoEnabled(false); } + + // Handle capabilities that require further messages sent to the IRC server + // If you change this list, ALSO change the list in CoreNetwork::capsRequiringServerMessages + if (capability == IrcCap::SASL) { + // If SASL mechanisms specified, limit to what's accepted for authentication + // if the current identity has a cert set, use SASL EXTERNAL + // FIXME use event +#ifdef HAVE_SSL + if (!identityPtr()->sslCert().isNull()) { + 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")); + sendNextCap(); + } + } else { +#endif + 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")); + sendNextCap(); + } +#ifdef HAVE_SSL + } +#endif + } } -void CoreNetwork::removeCap(const QString &capability) +void CoreNetwork::serverCapRemoved(const QString &capability) { - // Clear from pending list, remove from supported list - if (_capsPending.contains(capability)) - _capsPending.remove(capability); - if (_capsSupported.contains(capability)) - _capsSupported.remove(capability); + // This may be called multiple times in certain situations. // Handle special cases here - // TODO Use events if it makes sense - if (capability == "away-notify") { - // away-notify disabled, enable autowho according to configuration + if (capability == IrcCap::AWAY_NOTIFY) { + // away-notify disabled, enable autoWho according to configuration setAutoWhoEnabled(networkConfig()->autoWhoEnabled()); } } -QString CoreNetwork::capValue(const QString &capability) const +void CoreNetwork::queueCap(const QString &capability) { - // If a supported capability exists, good; if not, return pending value. - // If capability isn't supported after all, the pending entry will be removed. - if (_capsSupported.contains(capability)) - return _capsSupported[capability]; - else if (_capsPending.contains(capability)) - return _capsPending[capability]; - else + // IRCv3 specs all use lowercase capability names + QString _capLowercase = capability.toLower(); + + 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::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. + displayMsg(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::queuePendingCap(const QString &capability, const QString &value) +void CoreNetwork::beginCapNegotiation() { - if (!_capsQueued.contains(capability)) { - _capsQueued.append(capability); - // Some capabilities may have values attached, preserve them as pending - _capsPending[capability] = value; + // 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")); + endCapNegotiation(); + return; } + + _capNegotiationActive = true; + displayMsg(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(", "); + displayMsg(Message::Server, BufferInfo::StatusBuffer, "", + tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay)); + + sendNextCap(); } -QString CoreNetwork::takeQueuedCap() +void CoreNetwork::sendNextCap() { - if (!_capsQueued.empty()) { - return _capsQueued.takeFirst(); + if (capNegotiationInProgress()) { + // Request the next set of capabilities and remove them from the list + putRawLine(serverEncode(QString("CAP REQ :%1").arg(takeQueuedCaps()))); } else { - return QString(); + // 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")); + + if (_capNegotiationActive) { + displayMsg(Message::Server, BufferInfo::StatusBuffer, "", + tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", "))); + _capNegotiationActive = false; + } + + endCapNegotiation(); + } +} + +void CoreNetwork::endCapNegotiation() +{ + // If nick registration is already complete, CAP END is not required + if (!_capInitialNegotiationEnded) { + putRawLine(serverEncode(QString("CAP END"))); + _capInitialNegotiationEnded = true; } } @@ -952,14 +1319,14 @@ 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 (useCapAwayNotify()) { + if (capEnabled(IrcCap::AWAY_NOTIFY)) { // When away-notify is active, the timer's stopped. Start a new cycle to who this channel. setAutoWhoEnabled(true); } @@ -1006,7 +1373,7 @@ void CoreNetwork::sendAutoWho() // 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() - && !useCapAwayNotify()) + && !capEnabled(IrcCap::AWAY_NOTIFY)) continue; _autoWhoPending[chanOrNick.toLower()]++; } else if (ircuser) { @@ -1017,20 +1384,37 @@ void CoreNetwork::sendAutoWho() qDebug() << "Skipping who polling of unknown channel or nick" << chanOrNick; continue; } - // TODO Use WHO extended to poll away users and/or user accounts - // If a server supports it, supports("WHOX") will be true - // See: http://faerion.sourceforge.net/doc/irc/whox.var and HexChat - putRawLine("WHO " + serverEncode(chanOrNick)); + 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/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(serverEncode(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() - && !useCapAwayNotify()) { + && !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 (useCapAwayNotify() && _autoWhoCycleTimer.isActive()) { + } else if (capEnabled(IrcCap::AWAY_NOTIFY) && _autoWhoCycleTimer.isActive()) { // Don't run another who cycle if away-notify is enabled _autoWhoCycleTimer.stop(); } @@ -1040,20 +1424,62 @@ void CoreNetwork::sendAutoWho() #ifdef HAVE_SSL void CoreNetwork::sslErrors(const QList &sslErrors) { - Q_UNUSED(sslErrors) - socket.ignoreSslErrors(); - // TODO errorhandling + Server server = usedServer(); + if (server.sslVerify) { + // Treat the SSL error as a hard error + QString sslErrorMessage = tr("Encrypted connection couldn't be verified, disconnecting " + "since verification is required"); + if (!sslErrors.empty()) { + // Add the error reason if known + sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString())); + } + displayMsg(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 { + // 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"); + if (!sslErrors.empty()) { + // Add the error reason if known + sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString())); + } + displayMsg(Message::Info, BufferInfo::StatusBuffer, "", sslErrorMessage); + + // Proceed with the connection + socket.ignoreSslErrors(); + } } #endif // HAVE_SSL +void CoreNetwork::checkTokenBucket() +{ + if (_skipMessageRates) { + if (_msgQueue.size() == 0) { + // 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++; } + // As long as there's tokens available and messages remaining, sending messages from the queue while (_msgQueue.size() > 0 && _tokenBucket > 0) { writeToSocket(_msgQueue.takeFirst()); } @@ -1062,9 +1488,18 @@ void CoreNetwork::fillBucketAndProcessQueue() 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--; + } } @@ -1082,6 +1517,9 @@ Network::Server CoreNetwork::usedServer() const void CoreNetwork::requestConnect() const { + if (_shuttingDown) { + return; + } if (connectionState() != Disconnected) { qWarning() << "Requesting connect while already being connected!"; return; @@ -1092,6 +1530,9 @@ void CoreNetwork::requestConnect() const void CoreNetwork::requestDisconnect() const { + if (_shuttingDown) { + return; + } if (connectionState() == Disconnected) { qWarning() << "Requesting disconnect while not being connected!"; return;