/***************************************************************************
- * 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 *
_previousConnectionAttemptFailed(false),
_lastUsedServerIndex(0),
- _lastPingTime(0),
- _pingCount(0),
- _sendPings(false),
_requestedUserModes('-')
{
_autoReconnectTimer.setSingleShot(true);
_channelKeys[chan.toLower()] = channels[chan];
}
+ QHash<QString, QByteArray> 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)));
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)));
#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(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)));
}
}
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())
}
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));
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();
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);
}
}
CoreIrcChannel *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
if (c) {
c->setEncrypted(c->cipher()->setKey(key));
+ coreSession()->setBufferCipher(networkId(), target, key);
return;
}
if (u) {
u->setEncrypted(u->cipher()->setKey(key));
+ coreSession()->setBufferCipher(networkId(), target, key);
return;
}
}
// 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()) {
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
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())));
}
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) {
_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();
// 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();
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.
_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());
+ }
}
}
disablePingTimeout();
else {
resetPingTimeout();
+ resetPongReplyPending();
if (networkConfig()->pingTimeoutEnabled())
_pingTimer.start();
}
_pingTimer.stop();
_sendPings = false;
resetPingTimeout();
+ resetPongReplyPending();
}
_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)
// 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 {
}
} 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"));
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));
#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());
}
{
socket.write(data);
socket.write("\r\n");
- _tokenBucket--;
+ if (!_skipMessageRates) {
+ // Only subtract from the token bucket if message rate limiting is enabled
+ _tokenBucket--;
+ }
}