X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fcore%2Fcorenetwork.cpp;h=fcafde47935ea5326bb0d0c9188df4d04fc143d9;hp=76f430cbc7a68b4453620f97453b98d8f7c77a4d;hb=cdc6091a2e02b84a48937cda287a0769ceb8726a;hpb=5013eef8eb17221e8f5866977f02e970e30ec0ac diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index 76f430cb..fcafde47 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-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" @@ -44,11 +45,12 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) _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())); @@ -63,6 +65,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))); @@ -72,7 +79,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))); @@ -84,6 +91,13 @@ 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))); @@ -91,8 +105,17 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) 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))); } } @@ -174,6 +197,10 @@ QByteArray CoreNetwork::userEncode(const QString &userNick, const QString &strin void CoreNetwork::connectToIrc(bool reconnecting) { + if (Core::instance()->identServer()) { + _socketId = Core::instance()->identServer()->addWaitingSocket(); + } + if (!reconnecting && useAutoReconnect() && _autoReconnectCount == 0) { _autoReconnectTimer.setInterval(autoReconnectInterval() * 1000); if (unlimitedReconnectRetries()) @@ -207,12 +234,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)); @@ -228,10 +259,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(); @@ -301,12 +338,18 @@ void CoreNetwork::userInput(BufferInfo buf, QString msg) 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 { + // 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); } } @@ -422,6 +465,7 @@ void CoreNetwork::setCipherKey(const QString &target, const QByteArray &key) CoreIrcChannel *c = qobject_cast(ircChannel(target)); if (c) { c->setEncrypted(c->cipher()->setKey(key)); + coreSession()->setBufferCipher(networkId(), target, key); return; } @@ -431,6 +475,7 @@ void CoreNetwork::setCipherKey(const QString &target, const QByteArray &key) if (u) { u->setEncrypted(u->cipher()->setKey(key)); + coreSession()->setBufferCipher(networkId(), target, key); return; } } @@ -516,7 +561,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()) { @@ -524,16 +569,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 @@ -552,7 +601,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()))); } @@ -578,7 +630,7 @@ 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) { @@ -628,6 +680,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(); @@ -635,8 +691,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(); @@ -870,12 +929,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. @@ -885,8 +949,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()); + } } } @@ -897,6 +965,7 @@ void CoreNetwork::enablePingTimeout(bool enable) disablePingTimeout(); else { resetPingTimeout(); + resetPongReplyPending(); if (networkConfig()->pingTimeoutEnabled()) _pingTimer.start(); } @@ -908,6 +977,7 @@ void CoreNetwork::disablePingTimeout() _pingTimer.stop(); _sendPings = false; resetPingTimeout(); + resetPongReplyPending(); } @@ -916,6 +986,94 @@ void CoreNetwork::setPingInterval(int interval) _pingTimer.setInterval(interval * 1000); } + +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 { + // 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) @@ -950,7 +1108,7 @@ 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 { @@ -960,7 +1118,7 @@ void CoreNetwork::serverCapAcknowledged(const QString &capability) } } 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")); @@ -1097,9 +1255,10 @@ void CoreNetwork::beginCapNegotiation() 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. + // comma separator between the two if needed (both individual and bundled caps exist). QString queuedCapsDisplay = - (!_capsQueuedIndividual.empty() ? _capsQueuedIndividual.join(", ") + ", " : "") + _capsQueuedIndividual.join(", ") + + ((!_capsQueuedIndividual.empty() && !_capsQueuedBundled.empty()) ? ", " : "") + _capsQueuedBundled.join(", "); displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay)); @@ -1216,11 +1375,23 @@ void CoreNetwork::sendAutoWho() } 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)))); + // 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; @@ -1274,12 +1445,30 @@ void CoreNetwork::sslErrors(const QList &sslErrors) #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()); } @@ -1288,9 +1477,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--; + } }