X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fcore%2Fcorenetwork.cpp;h=a928d5b370217827b1b23ea797657829ac4cda37;hp=b8a5ab8d1b98583b89ba37bb85335fb85e0d8111;hb=ff2aeb5dad907f833b4311243213e5ee9fe12dfc;hpb=0a43227b8cd44625f4881cc1545d42c8c8a4876c diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index b8a5ab8d..a928d5b3 100644 --- a/src/core/corenetwork.cpp +++ b/src/core/corenetwork.cpp @@ -29,6 +29,9 @@ #include "coreuserinputhandler.h" #include "networkevent.h" +// IRCv3 capabilities +#include "irccap.h" + INIT_SYNCABLE_OBJECT(CoreNetwork) CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) : Network(networkid, session), @@ -36,6 +39,7 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) _userInputHandler(new CoreUserInputHandler(this)), _autoReconnectCount(0), _quitRequested(false), + _disconnectExpected(false), _previousConnectionAttemptFailed(false), _lastUsedServerIndex(0), @@ -80,6 +84,12 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session) #endif connect(this, SIGNAL(newEvent(Event *)), coreSession()->eventManager(), SLOT(postEvent(Event *))); + // IRCv3 capability handling + // These react to CAP messages from the server + connect(this, SIGNAL(capAdded(QString)), this, SLOT(serverCapAdded(QString))); + connect(this, SIGNAL(capAcknowledged(QString)), this, SLOT(serverCapAcknowledged(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))); @@ -158,10 +168,11 @@ void CoreNetwork::connectToIrc(bool reconnecting) // cleaning up old quit reason _quitReason.clear(); - // reset capability negotiation in case server changes during a reconnect + // Reset capability negotiation tracking, also handling server changes during reconnect _capsQueued.clear(); - _capsPending.clear(); - _capsSupported.clear(); + clearCaps(); + _capNegotiationActive = false; + _capInitialNegotiationEnded = false; // use a random server? if (useRandomServer()) { @@ -212,8 +223,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(); @@ -240,7 +254,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(); } @@ -258,16 +272,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; @@ -284,16 +303,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); } } @@ -439,8 +458,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())); @@ -531,6 +552,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); @@ -575,6 +598,7 @@ void CoreNetwork::networkInitialized() { setConnectionState(Network::Initialized); setConnected(true); + _disconnectExpected = false; _quitRequested = false; if (useAutoReconnect()) { @@ -867,68 +891,80 @@ void CoreNetwork::setPingInterval(int interval) /******** IRCv3 Capability Negotiation ********/ -void CoreNetwork::addCap(const QString &capability, const QString &value) +void CoreNetwork::serverCapAdded(const QString &capability) { - // Clear from pending list, add to supported list - if (!_capsSupported.contains(capability)) { - if (value != "") { - // Value defined, just use it - _capsSupported[capability] = value; - } else if (_capsPending.contains(capability)) { - // Value not defined, but a pending capability had a value. - // E.g. CAP * LS :sasl=PLAIN multi-prefix - // Preserve the capability value for later use. - _capsSupported[capability] = _capsPending[capability]; - } else { - // No value ever given, assign to blank - _capsSupported[capability] = QString(); - } + // Check if it's a known capability; if so, add it to the list + // Handle special cases first + if (capability == IrcCap::SASL) { + // Only request SASL if it's enabled + if (networkInfo().useSasl) + queueCap(capability); + } else if (IrcCap::knownCaps.contains(capability)) { + // Handling for general known capabilities + queueCap(capability); } - if (_capsPending.contains(capability)) - _capsPending.remove(capability); +} - // Handle special cases here - // TODO Use events if it makes sense - if (capability == "away-notify") { - // away-notify enabled, stop the automatic timers, handle manually +void CoreNetwork::serverCapAcknowledged(const QString &capability) +{ + // This may be called multiple times in certain situations. + + // Handle core-side configuration + if (capability == IrcCap::AWAY_NOTIFY) { + // away-notify enabled, stop the autoWho timers, handle manually setAutoWhoEnabled(false); } + + // Handle capabilities that require further messages sent to the IRC server + // If you change this list, ALSO change the list in CoreNetwork::capsRequiringServerMessages + if (capability == IrcCap::SASL) { + // If SASL mechanisms specified, limit to what's accepted for authentication + // if the current identity has a cert set, use SASL EXTERNAL + // FIXME use event +#ifdef HAVE_SSL + if (!identityPtr()->sslCert().isNull()) { + if (IrcCap::SaslMech::maybeSupported(capValue(IrcCap::SASL), IrcCap::SaslMech::EXTERNAL)) { + // EXTERNAL authentication supported, send request + putRawLine(serverEncode("AUTHENTICATE EXTERNAL")); + } else { + displayMsg(Message::Error, BufferInfo::StatusBuffer, "", + tr("SASL EXTERNAL authentication not supported")); + sendNextCap(); + } + } else { +#endif + if (IrcCap::SaslMech::maybeSupported(capValue(IrcCap::SASL), IrcCap::SaslMech::PLAIN)) { + // PLAIN authentication supported, send request + // Only working with PLAIN atm, blowfish later + putRawLine(serverEncode("AUTHENTICATE PLAIN")); + } else { + displayMsg(Message::Error, BufferInfo::StatusBuffer, "", + tr("SASL PLAIN authentication not supported")); + sendNextCap(); + } +#ifdef HAVE_SSL + } +#endif + } } -void CoreNetwork::removeCap(const QString &capability) +void CoreNetwork::serverCapRemoved(const QString &capability) { - // Clear from pending list, remove from supported list - if (_capsPending.contains(capability)) - _capsPending.remove(capability); - if (_capsSupported.contains(capability)) - _capsSupported.remove(capability); + // This may be called multiple times in certain situations. // Handle special cases here - // TODO Use events if it makes sense - if (capability == "away-notify") { - // away-notify disabled, enable autowho according to configuration + if (capability == IrcCap::AWAY_NOTIFY) { + // away-notify disabled, enable autoWho according to configuration setAutoWhoEnabled(networkConfig()->autoWhoEnabled()); } } -QString CoreNetwork::capValue(const QString &capability) const +void CoreNetwork::queueCap(const QString &capability) { - // If a supported capability exists, good; if not, return pending value. - // If capability isn't supported after all, the pending entry will be removed. - if (_capsSupported.contains(capability)) - return _capsSupported[capability]; - else if (_capsPending.contains(capability)) - return _capsPending[capability]; - else - return QString(); -} - -void CoreNetwork::queuePendingCap(const QString &capability, const QString &value) -{ - if (!_capsQueued.contains(capability)) { - _capsQueued.append(capability); - // Some capabilities may have values attached, preserve them as pending - _capsPending[capability] = value; + // IRCv3 specs all use lowercase capability names + QString _capLowercase = capability.toLower(); + if (!_capsQueued.contains(_capLowercase)) { + _capsQueued.append(_capLowercase); } } @@ -941,6 +977,47 @@ QString CoreNetwork::takeQueuedCap() } } +void CoreNetwork::beginCapNegotiation() +{ + // Don't begin negotiation if no capabilities are queued to request + if (!capNegotiationInProgress()) + return; + + _capNegotiationActive = true; + displayMsg(Message::Server, BufferInfo::StatusBuffer, "", + tr("Ready to negotiate (found: %1)").arg(caps().join(", "))); + displayMsg(Message::Server, BufferInfo::StatusBuffer, "", + tr("Negotiating capabilities (requesting: %1)...").arg(_capsQueued.join(", "))); + 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()))); + } else { + // No pending desired capabilities, capability negotiation finished + // If SASL requested but not available, print a warning + if (networkInfo().useSasl && !capEnabled(IrcCap::SASL)) + displayMsg(Message::Error, BufferInfo::StatusBuffer, "", + tr("SASL authentication currently not supported by server")); + + if (_capNegotiationActive) { + displayMsg(Message::Server, BufferInfo::StatusBuffer, "", + tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", "))); + _capNegotiationActive = false; + } + + // If nick registration is already complete, CAP END is not required + if (!_capInitialNegotiationEnded) { + putRawLine(serverEncode(QString("CAP END"))); + _capInitialNegotiationEnded = true; + } + } +} + /******** AutoWHO ********/ void CoreNetwork::startAutoWhoCycle() @@ -959,7 +1036,7 @@ void CoreNetwork::queueAutoWhoOneshot(const QString &channelOrNick) if (!_autoWhoQueue.contains(channelOrNick.toLower())) { _autoWhoQueue.prepend(channelOrNick.toLower()); } - if (useCapAwayNotify()) { + if (capEnabled(IrcCap::AWAY_NOTIFY)) { // When away-notify is active, the timer's stopped. Start a new cycle to who this channel. setAutoWhoEnabled(true); } @@ -1006,7 +1083,7 @@ void CoreNetwork::sendAutoWho() // state of everyone. Auto-who won't run on a timer so network impact is minimal. if (networkConfig()->autoWhoNickLimit() > 0 && ircchan->ircUsers().count() >= networkConfig()->autoWhoNickLimit() - && !useCapAwayNotify()) + && !capEnabled(IrcCap::AWAY_NOTIFY)) continue; _autoWhoPending[chanOrNick.toLower()]++; } else if (ircuser) { @@ -1017,20 +1094,25 @@ void CoreNetwork::sendAutoWho() qDebug() << "Skipping who polling of unknown channel or nick" << chanOrNick; continue; } - // TODO Use WHO extended to poll away users and/or user accounts - // If a server supports it, supports("WHOX") will be true - // See: http://faerion.sourceforge.net/doc/irc/whox.var and HexChat - putRawLine("WHO " + serverEncode(chanOrNick)); + if (supports("WHOX")) { + // Use WHO extended to poll away users and/or user accounts + // 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)))); + } else { + putRawLine(serverEncode(QString("WHO %1").arg(chanOrNick))); + } break; } if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive() - && !useCapAwayNotify()) { + && !capEnabled(IrcCap::AWAY_NOTIFY)) { // Timer was stopped, means a new cycle is due immediately // Don't run a new cycle if using away-notify; server will notify as appropriate _autoWhoCycleTimer.start(); startAutoWhoCycle(); - } else if (useCapAwayNotify() && _autoWhoCycleTimer.isActive()) { + } else if (capEnabled(IrcCap::AWAY_NOTIFY) && _autoWhoCycleTimer.isActive()) { // Don't run another who cycle if away-notify is enabled _autoWhoCycleTimer.stop(); } @@ -1040,9 +1122,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(); + } }