Batch request capabilities during negotiation
[quassel.git] / src / core / corenetwork.cpp
index f622eea..76f430c 100644 (file)
@@ -39,6 +39,7 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
     _userInputHandler(new CoreUserInputHandler(this)),
     _autoReconnectCount(0),
     _quitRequested(false),
+    _disconnectExpected(false),
 
     _previousConnectionAttemptFailed(false),
     _lastUsedServerIndex(0),
@@ -98,13 +99,39 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
 
 CoreNetwork::~CoreNetwork()
 {
-    if (connectionState() != Disconnected && connectionState() != Network::Reconnecting)
-        disconnectFromIrc(false);  // clean up, but this does not count as requested disconnect!
+    // Request a proper disconnect, but don't count as user-requested disconnect
+    if (socketConnected()) {
+        // Only try if the socket's fully connected (not initializing or disconnecting).
+        // Force an immediate disconnect, jumping the command queue.  Ensures the proper QUIT is
+        // shown even if other messages are queued.
+        disconnectFromIrc(false, QString(), false, true);
+        // Process the putCmd events that trigger the quit.  Without this, shutting down the core
+        // results in abrubtly closing the socket rather than sending the QUIT as expected.
+        QCoreApplication::processEvents();
+        // Wait briefly for each network to disconnect.  Sometimes it takes a little while to send.
+        if (!forceDisconnect()) {
+            qWarning() << "Timed out quitting network" << networkName() <<
+                          "(user ID " << userId() << ")";
+        }
+    }
     disconnect(&socket, 0, this, 0); // this keeps the socket from triggering events during clean up
     delete _userInputHandler;
 }
 
 
+bool CoreNetwork::forceDisconnect(int msecs)
+{
+    if (socket.state() == QAbstractSocket::UnconnectedState) {
+        // Socket already disconnected.
+        return true;
+    }
+    // Request a socket-level disconnect if not already happened
+    socket.disconnectFromHost();
+    // Return the result of waiting for disconnect; true if successful, otherwise false
+    return socket.waitForDisconnected(msecs);
+}
+
+
 QString CoreNetwork::channelDecode(const QString &bufferName, const QByteArray &string) const
 {
     if (!bufferName.isEmpty()) {
@@ -168,7 +195,8 @@ void CoreNetwork::connectToIrc(bool reconnecting)
     _quitReason.clear();
 
     // Reset capability negotiation tracking, also handling server changes during reconnect
-    _capsQueued.clear();
+    _capsQueuedIndividual.clear();
+    _capsQueuedBundled.clear();
     clearCaps();
     _capNegotiationActive = false;
     _capInitialNegotiationEnded = false;
@@ -222,8 +250,11 @@ void CoreNetwork::connectToIrc(bool reconnecting)
 }
 
 
-void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool withReconnect)
+void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool withReconnect,
+                                    bool forceImmediate)
 {
+    // Disconnecting from the network, should expect a socket close or error
+    _disconnectExpected = true;
     _quitRequested = requested; // see socketDisconnected();
     if (!withReconnect) {
         _autoReconnectTimer.stop();
@@ -250,7 +281,7 @@ void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool
         socketDisconnected();
     } else {
         if (socket.state() == QAbstractSocket::ConnectedState) {
-            userInputHandler()->issueQuit(_quitReason);
+            userInputHandler()->issueQuit(_quitReason, forceImmediate);
         } else {
             socket.close();
         }
@@ -454,8 +485,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()));
@@ -546,6 +579,8 @@ void CoreNetwork::socketDisconnected()
     setConnected(false);
     emit disconnected(networkId());
     emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort());
+    // Reset disconnect expectations
+    _disconnectExpected = false;
     if (_quitRequested) {
         _quitRequested = false;
         setConnectionState(Network::Disconnected);
@@ -590,6 +625,7 @@ void CoreNetwork::networkInitialized()
 {
     setConnectionState(Network::Initialized);
     setConnected(true);
+    _disconnectExpected = false;
     _quitRequested = false;
 
     if (useAutoReconnect()) {
@@ -954,40 +990,128 @@ void CoreNetwork::queueCap(const QString &capability)
 {
     // IRCv3 specs all use lowercase capability names
     QString _capLowercase = capability.toLower();
-    if (!_capsQueued.contains(_capLowercase)) {
-        _capsQueued.append(_capLowercase);
+
+    if(capsRequiringConfiguration.contains(_capLowercase)) {
+        // The capability requires additional configuration before being acknowledged (e.g. SASL),
+        // so we should negotiate it separately from all other capabilities.  Otherwise new
+        // capabilities will be requested while still configuring the previous one.
+        if (!_capsQueuedIndividual.contains(_capLowercase)) {
+            _capsQueuedIndividual.append(_capLowercase);
+        }
+    } else {
+        // The capability doesn't need any special configuration, so it should be safe to try
+        // bundling together with others.  "Should" being the imperative word, as IRC servers can do
+        // anything.
+        if (!_capsQueuedBundled.contains(_capLowercase)) {
+            _capsQueuedBundled.append(_capLowercase);
+        }
     }
 }
 
-QString CoreNetwork::takeQueuedCap()
+QString CoreNetwork::takeQueuedCaps()
 {
-    if (!_capsQueued.empty()) {
-        return _capsQueued.takeFirst();
+    // 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::beginCapNegotiation()
 {
     // Don't begin negotiation if no capabilities are queued to request
-    if (!capNegotiationInProgress())
+    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.
+    QString queuedCapsDisplay =
+            (!_capsQueuedIndividual.empty() ? _capsQueuedIndividual.join(", ") + ", " : "")
+            + _capsQueuedBundled.join(", ");
     displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
-               tr("Negotiating capabilities (requesting: %1)...").arg(_capsQueued.join(", ")));
+               tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay));
+
     sendNextCap();
 }
 
 void CoreNetwork::sendNextCap()
 {
     if (capNegotiationInProgress()) {
-        // Request the next capability and remove it from the list
-        // Handle one at a time so one capability failing won't NAK all of 'em
-        putRawLine(serverEncode(QString("CAP REQ :%1").arg(takeQueuedCap())));
+        // Request the next set of capabilities and remove them from the list
+        putRawLine(serverEncode(QString("CAP REQ :%1").arg(takeQueuedCaps())));
     } else {
         // No pending desired capabilities, capability negotiation finished
         // If SASL requested but not available, print a warning
@@ -1001,11 +1125,16 @@ void CoreNetwork::sendNextCap()
             _capNegotiationActive = false;
         }
 
-        // If nick registration is already complete, CAP END is not required
-        if (!_capInitialNegotiationEnded) {
-            putRawLine(serverEncode(QString("CAP END")));
-            _capInitialNegotiationEnded = true;
-        }
+        endCapNegotiation();
+    }
+}
+
+void CoreNetwork::endCapNegotiation()
+{
+    // If nick registration is already complete, CAP END is not required
+    if (!_capInitialNegotiationEnded) {
+        putRawLine(serverEncode(QString("CAP END")));
+        _capInitialNegotiationEnded = true;
     }
 }
 
@@ -1113,9 +1242,33 @@ void CoreNetwork::sendAutoWho()
 #ifdef HAVE_SSL
 void CoreNetwork::sslErrors(const QList<QSslError> &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();
+    }
 }