X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fcore%2Fcorenetwork.cpp;h=76f430cbc7a68b4453620f97453b98d8f7c77a4d;hp=ca935d96eecfc1e0de53b0ebe7c4b0538579c065;hb=5013eef8eb17221e8f5866977f02e970e30ec0ac;hpb=a2e096ed11221866f9707ad61265a9d359a21de3 diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index ca935d96..76f430cb 100644 --- a/src/core/corenetwork.cpp +++ b/src/core/corenetwork.cpp @@ -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(); } @@ -268,16 +299,21 @@ void CoreNetwork::userInput(BufferInfo buf, QString msg) } -void CoreNetwork::putRawLine(QByteArray s) +void CoreNetwork::putRawLine(const QByteArray s, const bool prepend) { - if (_tokenBucket > 0) + if (_tokenBucket > 0) { writeToSocket(s); - else - _msgQueue.append(s); + } else { + if (prepend) { + _msgQueue.prepend(s); + } else { + _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; @@ -294,16 +330,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); } } @@ -449,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())); @@ -541,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); @@ -585,6 +625,7 @@ void CoreNetwork::networkInitialized() { setConnectionState(Network::Initialized); setConnected(true); + _disconnectExpected = false; _quitRequested = false; if (useAutoReconnect()) { @@ -949,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 @@ -996,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; } } @@ -1108,9 +1242,33 @@ 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(); + } }