core: connectToIrc: Prevent DNS leaks on connection when using proxy
[quassel.git] / src / core / corenetwork.cpp
index 76f430c..f77b29a 100644 (file)
@@ -72,7 +72,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 +84,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)));
@@ -207,12 +214,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));
@@ -230,8 +241,9 @@ void CoreNetwork::connectToIrc(bool reconnecting)
 
     // 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) {
+               QHostInfo::fromName(server.host);
+       }
 #ifdef HAVE_SSL
     if (server.useSsl) {
         CoreIdentity *identity = identityPtr();
@@ -301,12 +313,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);
         }
     }
@@ -529,11 +547,15 @@ void CoreNetwork::socketInitialized()
 
     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
@@ -628,6 +650,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 +661,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();
 
@@ -916,6 +945,88 @@ void CoreNetwork::setPingInterval(int interval)
     _pingTimer.setInterval(interval * 1000);
 }
 
+
+/******** 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 +1061,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 +1071,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"));
@@ -1274,12 +1385,30 @@ void CoreNetwork::sslErrors(const QList<QSslError> &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());
     }
@@ -1290,7 +1419,10 @@ void CoreNetwork::writeToSocket(const QByteArray &data)
 {
     socket.write(data);
     socket.write("\r\n");
-    _tokenBucket--;
+    if (!_skipMessageRates) {
+        // Only subtract from the token bucket if message rate limiting is enabled
+        _tokenBucket--;
+    }
 }