From: Shane Synan Date: Tue, 7 Jun 2016 15:05:55 +0000 (-0400) Subject: Sync caps, use signal/slot, CAP NEW/DEL, polish X-Git-Tag: travis-deploy-test~470^2~6 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=dccbef488a2299cf08146cdfc11933a8fe28a387 Sync caps, use signal/slot, CAP NEW/DEL, polish Expose IRCv3 capabilities in the synced Network object. In testing, this works fine with older/newer cores/clients. Migrate capability negotiation to Qt Slots/Signals (events), allowing easier future scriptability and general reacting to IRCv3. In the future, the client could detect if IrcCap::SASL is present and in the settings UI recommend SASL over NickServ, it could warn if IrcCap::SaslMech::EXTERNAL is not available, etc. Handle CAP NEW and CAP DEL, to comply with cap-notify and IRCv3.2 requirements. Might make ZNC users happier, too. Show available, requested, and successful capabilities during capability negotiation. Move IRCv3 list of capabilities and SASL handling to new header, irccap.h This reduces clutter in network.h, and via new QString constants typos are now compile-time errors. Update CMakeLists.txt for automoc and Qt Creator to know of the new file. Preliminary test results on Ubuntu: > New core, new client - works fine > New core, old client - client prints warnings to console but works (Complains of no matching slot for sync call. Old client doesn't need the new information, though, so it shouldn't cause issues.) > New client, old core - works fine, simply assuming no IRCv3 (Quassel didn't support IRCv3 cap negotiation before now, so that's a safe assumption to make) > New core, Quasseldroid alpha - works fine, warnings in debug log --- diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index bb3aef27..b2aa2008 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -43,6 +43,7 @@ set(SOURCES # needed for automoc coreinfo.h + irccap.h ) if (USE_QT5) diff --git a/src/common/irccap.h b/src/common/irccap.h new file mode 100644 index 00000000..3398adab --- /dev/null +++ b/src/common/irccap.h @@ -0,0 +1,137 @@ +/*************************************************************************** + * Copyright (C) 2005-2016 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef IRCCAP_H +#define IRCCAP_H + +#include +#include + +// Why a namespace instead of a class? Seems to be a better fit for C++ than a 'static' class, as +// compared to C# or Java. However, feel free to change if needed. +// See https://stackoverflow.com/questions/482745/namespaces-for-enum-types-best-practices +/** + * IRCv3 capability names and values + */ +namespace IrcCap { + + // NOTE: If you add or modify the constants below, update the knownCaps list. + + /** + * Account change notification. + * + * http://ircv3.net/specs/extensions/account-notify-3.1.html + */ + const QString ACCOUNT_NOTIFY = "account-notify"; + + /** + * Away change notification. + * + * http://ircv3.net/specs/extensions/away-notify-3.1.html + */ + const QString AWAY_NOTIFY = "away-notify"; + + /** + * Capability added/removed notification. + * + * This is implicitly enabled via CAP LS 302, and is here for servers that only partially + * support IRCv3.2. + * + * http://ircv3.net/specs/extensions/cap-notify-3.2.html + */ + const QString CAP_NOTIFY = "cap-notify"; + + /** + * Extended join information. + * + * http://ircv3.net/specs/extensions/extended-join-3.1.html + */ + const QString EXTENDED_JOIN = "extended-join"; + + /** + * Multiple mode prefixes in MODE and WHO replies. + * + * http://ircv3.net/specs/extensions/multi-prefix-3.1.html + */ + const QString MULTI_PREFIX = "multi-prefix"; + + /** + * SASL authentication. + * + * http://ircv3.net/specs/extensions/sasl-3.2.html + */ + const QString SASL = "sasl"; + + /** + * Userhost in names replies. + * + * http://ircv3.net/specs/extensions/userhost-in-names-3.2.html + */ + const QString USERHOST_IN_NAMES = "userhost-in-names"; + + /** + * List of capabilities currently implemented and requested during capability negotiation. + */ + const QStringList knownCaps = QStringList { + ACCOUNT_NOTIFY, + AWAY_NOTIFY, + CAP_NOTIFY, + EXTENDED_JOIN, + MULTI_PREFIX, + SASL, + USERHOST_IN_NAMES + }; + // NOTE: If you modify the knownCaps list, update the constants above as needed. + + /** + * SASL authentication mechanisms + * + * http://ircv3.net/specs/extensions/sasl-3.1.html + */ + namespace SaslMech { + + /** + * Check if the given authentication mechanism is likely to be supported. + * + * @param[in] saslCapValue QString of SASL capability value, e.g. capValue(IrcCap::SASL) + * @param[in] saslMechanism Desired SASL mechanism + * @return True if mechanism supported or unknown, otherwise false + */ + inline bool maybeSupported(const QString &saslCapValue, const QString &saslMechanism) { return + ((saslCapValue.length() == 0) || (saslCapValue.contains(saslMechanism, Qt::CaseInsensitive))); } + // SASL mechanisms are only specified in capability values as part of SASL 3.2. In + // SASL 3.1, it's handled differently. If we don't know via capability value, assume it's + // supported to reduce the risk of breaking existing setups. + // See: http://ircv3.net/specs/extensions/sasl-3.1.html + // And: http://ircv3.net/specs/extensions/sasl-3.2.html + + /** + * PLAIN authentication, e.g. hashed password + */ + const QString PLAIN = "PLAIN"; + + /** + * EXTERNAL authentication, e.g. SSL certificate and keys + */ + const QString EXTERNAL = "EXTERNAL"; + } +} + +#endif // IRCCAP_H diff --git a/src/common/network.cpp b/src/common/network.cpp index b60d9447..ef24220a 100644 --- a/src/common/network.cpp +++ b/src/common/network.cpp @@ -705,6 +705,73 @@ QVariantMap Network::initSupports() const return supports; } +void Network::addCap(const QString &capability, const QString &value) +{ + // IRCv3 specs all use lowercase capability names + QString _capLowercase = capability.toLower(); + if (!_caps.contains(_capLowercase)) { + _caps[_capLowercase] = value; + SYNC(ARG(capability), ARG(value)) + emit capAdded(_capLowercase); + } +} + +void Network::acknowledgeCap(const QString &capability) +{ + // IRCv3 specs all use lowercase capability names + QString _capLowercase = capability.toLower(); + if (!_capsEnabled.contains(_capLowercase)) { + _capsEnabled.append(_capLowercase); + SYNC(ARG(capability)) + emit capAcknowledged(_capLowercase); + } +} + +void Network::removeCap(const QString &capability) +{ + // IRCv3 specs all use lowercase capability names + QString _capLowercase = capability.toLower(); + if (_caps.contains(_capLowercase)) { + // Remove from the list of available capabilities. + _caps.remove(_capLowercase); + // Remove it from the acknowledged list if it was previously acknowledged. The SYNC call + // ensures this propogates to the other side. + // Use removeOne() for speed; no more than one due to contains() check in acknowledgeCap(). + _capsEnabled.removeOne(_capLowercase); + SYNC(ARG(capability)) + emit capRemoved(_capLowercase); + } +} + +void Network::clearCaps() +{ + // IRCv3 specs all use lowercase capability names + // To ease core-side configuration, loop through the list and emit capRemoved for each entry. + // If performance issues arise, this can be converted to a more-efficient setup without breaking + // protocol (in theory). + QString _capLowercase; + foreach (const QString &capability, _caps) { + _capLowercase = capability.toLower(); + emit capRemoved(_capLowercase); + } + // Clear capabilities from the stored list + _caps.clear(); + _capsEnabled.clear(); + + SYNC(NO_ARG) +} + +QVariantMap Network::initCaps() const +{ + QVariantMap caps; + QHashIterator iter(_caps); + while (iter.hasNext()) { + iter.next(); + caps[iter.key()] = iter.value(); + } + return caps; +} + // There's potentially a lot of users and channels, so it makes sense to optimize the format of this. // Rather than sending a thousand maps with identical keys, we convert this into one map containing lists @@ -820,6 +887,16 @@ void Network::initSetSupports(const QVariantMap &supports) } +void Network::initSetCaps(const QVariantMap &caps) +{ + QMapIterator iter(caps); + while (iter.hasNext()) { + iter.next(); + addCap(iter.key(), iter.value().toString()); + } +} + + IrcUser *Network::updateNickFromMask(const QString &mask) { QString nick(nickFromMask(mask).toLower()); diff --git a/src/common/network.h b/src/common/network.h index f6e7de1f..09310fd7 100644 --- a/src/common/network.h +++ b/src/common/network.h @@ -164,6 +164,18 @@ public : inline IdentityId identity() const { return _identity; } QStringList nicks() const; inline QStringList channels() const { return _ircChannels.keys(); } + /** + * Gets the list of available capabilities. + * + * @returns QStringList of available capabilities + */ + inline const QStringList caps() const { return QStringList(_caps.keys()); } + /** + * Gets the list of enabled (acknowledged) capabilities. + * + * @returns QStringList of enabled (acknowledged) capabilities + */ + inline const QStringList capsEnabled() const { return _capsEnabled; } inline const ServerList &serverList() const { return _serverList; } inline bool useRandomServer() const { return _useRandomServer; } inline const QStringList &perform() const { return _perform; } @@ -189,6 +201,26 @@ public : bool supports(const QString ¶m) const { return _supports.contains(param); } QString support(const QString ¶m) const; + /** + * Checks if a given capability is acknowledged and active. + * + * @param[in] capability Name of capability + * @returns True if acknowledged (active), otherwise false + */ + inline bool capEnabled(const QString &capability) const { return _capsEnabled.contains(capability.toLower()); } + // IRCv3 specs all use lowercase capability names + + /** + * Gets the value of an available capability, e.g. for SASL, "EXTERNAL,PLAIN". + * + * @param[in] capability Name of capability + * @returns Value of capability if one was specified, otherwise empty string + */ + QString capValue(const QString &capability) const { return _caps.value(capability.toLower()); } + // IRCv3 specs all use lowercase capability names + // QHash returns the default constructed value if not found, in this case, empty string + // See: https://doc.qt.io/qt-4.8/qhash.html#value + IrcUser *newIrcUser(const QString &hostmask, const QVariantMap &initData = QVariantMap()); inline IrcUser *newIrcUser(const QByteArray &hostmask) { return newIrcUser(decodeServerString(hostmask)); } IrcUser *ircUser(QString nickname) const; @@ -256,16 +288,81 @@ public slots: void addSupport(const QString ¶m, const QString &value = QString()); void removeSupport(const QString ¶m); + // IRCv3 capability negotiation (can be connected to signals) + + /** + * Add an available capability, optionally providing a value. + * + * This may happen during first connect, or at any time later if a new capability becomes + * available (e.g. SASL service starting). + * + * @param[in] capability Name of the capability + * @param[in] value + * @parblock + * Optional value of the capability, e.g. sasl=plain. + * @endparblock + */ + void addCap(const QString &capability, const QString &value = QString()); + + /** + * Marks a capability as acknowledged (enabled by the IRC server). + * + * @param[in] capability Name of the capability + */ + void acknowledgeCap(const QString &capability); + + /** + * Removes a capability from the list of available capabilities. + * + * This may happen during first connect, or at any time later if an existing capability becomes + * unavailable (e.g. SASL service stopping). This also removes the capability from the list + * of acknowledged capabilities. + * + * @param[in] capability Name of the capability + */ + void removeCap(const QString &capability); + + /** + * Clears all capabilities from the list of available capabilities. + * + * This also removes the capability from the list of acknowledged capabilities. + */ + void clearCaps(); + inline void addIrcUser(const QString &hostmask) { newIrcUser(hostmask); } inline void addIrcChannel(const QString &channel) { newIrcChannel(channel); } //init geters QVariantMap initSupports() const; + /** + * Get the initial list of available capabilities. + * + * @return QVariantMap of indicating available capabilities and values + */ + QVariantMap initCaps() const; + /** + * Get the initial list of enabled (acknowledged) capabilities. + * + * @return QVariantList of QString indicating enabled (acknowledged) capabilities and values + */ + QVariantList initCapsEnabled() const { return toVariantList(capsEnabled()); } inline QVariantList initServerList() const { return toVariantList(serverList()); } virtual QVariantMap initIrcUsersAndChannels() const; //init seters void initSetSupports(const QVariantMap &supports); + /** + * Initialize the list of available capabilities. + * + * @param[in] caps QVariantMap of indicating available capabilities and values + */ + void initSetCaps(const QVariantMap &caps); + /** + * Initialize the list of enabled (acknowledged) capabilities. + * + * @param[in] caps QVariantList of QString indicating enabled (acknowledged) capabilities and values + */ + inline void initSetCapsEnabled(const QVariantList &capsEnabled) { _capsEnabled = fromVariantList(capsEnabled); } inline void initSetServerList(const QVariantList &serverList) { _serverList = fromVariantList(serverList); } virtual void initSetIrcUsersAndChannels(const QVariantMap &usersAndChannels); @@ -325,6 +422,34 @@ signals: // void supportAdded(const QString ¶m, const QString &value); // void supportRemoved(const QString ¶m); + // IRCv3 capability negotiation (can drive other slots) + /** + * Indicates a capability is now available, with optional value in Network::capValue(). + * + * @see Network::addCap() + * + * @param[in] capability Name of the capability + */ + void capAdded (const QString &capability); + + /** + * Indicates a capability was acknowledged (enabled by the IRC server). + * + * @see Network::acknowledgeCap() + * + * @param[in] capability Name of the capability + */ + void capAcknowledged(const QString &capability); + + /** + * Indicates a capability was removed from the list of available capabilities. + * + * @see Network::removeCap() + * + * @param[in] capability Name of the capability + */ + void capRemoved(const QString &capability); + // void ircUserAdded(const QString &hostmask); void ircUserAdded(IrcUser *); // void ircChannelAdded(const QString &channelname); @@ -358,6 +483,13 @@ private: QHash _ircChannels; // stores all known channels QHash _supports; // stores results from RPL_ISUPPORT + QHash _caps; /// Capabilities supported by the IRC server + // By synchronizing the supported capabilities, the client could suggest certain behaviors, e.g. + // in the Network settings dialog, recommending SASL instead of using NickServ, or warning if + // SASL EXTERNAL isn't available. + QStringList _capsEnabled; /// Enabled capabilities that received 'CAP ACK' + // _capsEnabled uses the same values from the = pairs stored in _caps + ServerList _serverList; bool _useRandomServer; QStringList _perform; diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index b8a5ab8d..f049fa07 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), @@ -80,6 +83,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 +167,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()) { @@ -867,68 +877,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 +963,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 +1022,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 +1069,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) { @@ -1025,12 +1088,12 @@ void CoreNetwork::sendAutoWho() } 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(); } diff --git a/src/core/corenetwork.h b/src/core/corenetwork.h index f0f56257..74540be6 100644 --- a/src/core/corenetwork.h +++ b/src/core/corenetwork.h @@ -25,6 +25,9 @@ #include "coreircchannel.h" #include "coreircuser.h" +// IRCv3 capabilities +#include "irccap.h" + #include #ifdef HAVE_SSL @@ -102,14 +105,6 @@ public: // IRCv3 capability negotiation - /** - * Checks if a given capability is enabled. - * - * @param[in] capability Name of capability - * @returns True if enabled, otherwise false - */ - inline bool capEnabled(const QString &capability) const { return _capsSupported.contains(capability); } - /** * Checks if capability negotiation is currently ongoing. * @@ -118,75 +113,34 @@ public: inline bool capNegotiationInProgress() const { return !_capsQueued.empty(); } /** - * Gets the value of an enabled or pending capability, e.g. sasl=plain. + * Queues a capability to be requested. * - * @param[in] capability Name of capability - * @returns Value of capability if one was specified, otherwise empty string - */ - QString capValue(const QString &capability) const; - - /** - * Gets the next capability to request, removing it from the queue. + * Adds to the list of capabilities being requested. If non-empty, CAP REQ messages are sent + * to the IRC server. This may happen at login or if capabilities are announced via CAP NEW. * - * @returns Name of capability to request - */ - QString takeQueuedCap(); - - // Specific capabilities for easy reference - - /** - * Gets the status of the sasl authentication capability. - * - * http://ircv3.net/specs/extensions/sasl-3.2.html - * - * @returns True if SASL authentication is enabled, otherwise false - */ - inline bool useCapSASL() const { return capEnabled("sasl"); } - - /** - * Gets the status of the away-notify capability. - * - * http://ircv3.net/specs/extensions/away-notify-3.1.html - * - * @returns True if away-notify is enabled, otherwise false - */ - inline bool useCapAwayNotify() const { return capEnabled("away-notify"); } - - /** - * Gets the status of the account-notify capability. - * - * http://ircv3.net/specs/extensions/account-notify-3.1.html - * - * @returns True if account-notify is enabled, otherwise false - */ - inline bool useCapAccountNotify() const { return capEnabled("account-notify"); } - - /** - * Gets the status of the extended-join capability. - * - * http://ircv3.net/specs/extensions/extended-join-3.1.html - * - * @returns True if extended-join is enabled, otherwise false + * @param[in] capability Name of the capability */ - inline bool useCapExtendedJoin() const { return capEnabled("extended-join"); } + void queueCap(const QString &capability); /** - * Gets the status of the userhost-in-names capability. - * - * http://ircv3.net/specs/extensions/userhost-in-names-3.2.html + * Begins capability negotiation if capabilities are queued, otherwise returns. * - * @returns True if userhost-in-names is enabled, otherwise false + * If any capabilities are queued, this will begin the cycle of taking each capability and + * requesting it. When no capabilities remain, capability negotiation is suitably ended. */ - inline bool useCapUserhostInNames() const { return capEnabled("userhost-in-names"); } + void beginCapNegotiation(); /** - * Gets the status of the multi-prefix capability. + * List of capabilities requiring further core<->server messages to configure. * - * http://ircv3.net/specs/extensions/multi-prefix-3.1.html + * For example, SASL requires the back-and-forth of AUTHENTICATE, so the next capability cannot + * be immediately sent. * - * @returns True if multi-prefix is enabled, otherwise false + * See: http://ircv3.net/specs/extensions/sasl-3.2.html */ - inline bool useCapMultiPrefix() const { return capEnabled("multi-prefix"); } + const QStringList capsRequiringConfiguration = QStringList { + IrcCap::SASL + }; public slots: virtual void setMyNick(const QString &mynick); @@ -225,40 +179,39 @@ public slots: // IRCv3 capability negotiation (can be connected to signals) /** - * Marks a capability as accepted, providing an optional value. + * Indicates a capability is now available, with optional value in Network::capValue(). * - * Removes it from queue of pending capabilities and triggers any capability-specific - * activation. + * @see Network::addCap() * * @param[in] capability Name of the capability - * @param[in] value - * @parblock - * Optional value of the capability, e.g. sasl=plain. If left empty, will be copied from the - * pending capability. - * @endparblock */ - void addCap(const QString &capability, const QString &value = QString()); + void serverCapAdded(const QString &capability); /** - * Marks a capability as denied. + * Indicates a capability was acknowledged (enabled by the IRC server). * - * Removes it from the queue of pending capabilities and triggers any capability-specific - * deactivation. + * @see Network::acknowledgeCap() * * @param[in] capability Name of the capability */ - void removeCap(const QString &capability); + void serverCapAcknowledged(const QString &capability); /** - * Queues a capability as available but not yet accepted or denied. + * Indicates a capability was removed from the list of available capabilities. * - * Capabilities should be queued when registration pauses for CAP LS for capabilities are only - * requested during login. + * @see Network::removeCap() * * @param[in] capability Name of the capability - * @param[in] value Optional value of the capability, e.g. sasl=plain */ - void queuePendingCap(const QString &capability, const QString &value = QString()); + void serverCapRemoved(const QString &capability); + + /** + * Sends the next capability from the queue. + * + * During nick registration if any capabilities remain queued, this will take the next and + * request it. When no capabilities remain, capability negotiation is ended. + */ + void sendNextCap(); void setAutoWhoEnabled(bool enabled); void setAutoWhoInterval(int interval); @@ -376,11 +329,20 @@ private: QHash _autoWhoPending; QTimer _autoWhoTimer, _autoWhoCycleTimer; - // CAPs may have parameter values + // Maintain a list of CAPs that are being checked; if empty, negotiation finished // See http://ircv3.net/specs/core/capability-negotiation-3.2.html - QStringList _capsQueued; /// Capabilities to be checked - QHash _capsPending; /// Capabilities pending 'CAP ACK' from server - QHash _capsSupported; /// Enabled capabilities that received 'CAP ACK' + QStringList _capsQueued; /// Capabilities to be checked + bool _capNegotiationActive; /// Whether or not full capability negotiation was started + // Avoid displaying repeat "negotiation finished" messages + bool _capInitialNegotiationEnded; /// Whether or not initial capability negotiation finished + // Avoid sending repeat "CAP END" replies when registration is already ended + + /** + * Gets the next capability to request, removing it from the queue. + * + * @returns Name of capability to request + */ + QString takeQueuedCap(); QTimer _tokenBucketTimer; int _messageDelay; // token refill speed in ms diff --git a/src/core/coresessioneventprocessor.cpp b/src/core/coresessioneventprocessor.cpp index 71619ef1..ade8c3bd 100644 --- a/src/core/coresessioneventprocessor.cpp +++ b/src/core/coresessioneventprocessor.cpp @@ -37,6 +37,9 @@ # include "keyevent.h" #endif +// IRCv3 capabilities +#include "irccap.h" + CoreSessionEventProcessor::CoreSessionEventProcessor(CoreSession *session) : BasicHandler("handleCtcp", session), _coreSession(session) @@ -94,14 +97,21 @@ void CoreSessionEventProcessor::processIrcEventNumeric(IrcEventNumeric *e) switch (e->number()) { // SASL authentication replies // See: http://ircv3.net/specs/extensions/sasl-3.1.html + + //case 900: // RPL_LOGGEDIN + //case 901: // RPL_LOGGEDOUT + // Don't use 900 or 901 for updating the local hostmask. Unreal 3.2 gives it as the IP address + // even when cloaked. + // Every other reply should result in moving on // TODO Handle errors to stop connection if appropriate + case 902: // ERR_NICKLOCKED case 903: // RPL_SASLSUCCESS case 904: // ERR_SASLFAIL case 905: // ERR_SASLTOOLONG case 906: // ERR_SASLABORTED case 907: // ERR_SASLALREADY // Move on to the next capability - sendNextCap(e->network()); + coreNetwork(e)->sendNextCap(); break; default: @@ -140,24 +150,6 @@ void CoreSessionEventProcessor::processIrcEventAuthenticate(IrcEvent *e) #endif } -void CoreSessionEventProcessor::sendNextCap(Network *net) -{ - CoreNetwork *coreNet = qobject_cast(net); - if (coreNet->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 - coreNet->putRawLine(coreNet->serverEncode(QString("CAP REQ :%1").arg(coreNet->takeQueuedCap()))); - } else { - // If SASL requested but not available, print a warning - if (coreNet->networkInfo().useSasl && !coreNet->useCapSASL()) - emit newEvent(new MessageEvent(Message::Error, net, tr("SASL authentication not supported by server, continuing without"), QString(), QString(), Message::None, QDateTime::currentDateTimeUtc())); - - // No pending desired capabilities, end negotiation - coreNet->putRawLine(coreNet->serverEncode(QString("CAP END"))); - emit newEvent(new MessageEvent(Message::Server, net, tr("Capability negotiation finished"), QString(), QString(), Message::None, QDateTime::currentDateTimeUtc())); - } -} - void CoreSessionEventProcessor::processIrcEventCap(IrcEvent *e) { // Handle capability negotiation @@ -165,7 +157,9 @@ void CoreSessionEventProcessor::processIrcEventCap(IrcEvent *e) // And: http://ircv3.net/specs/core/capability-negotiation-3.1.html if (e->params().count() >= 3) { CoreNetwork *coreNet = coreNetwork(e); - if (e->params().at(1).compare("LS", Qt::CaseInsensitive) == 0) { + QString capCommand = e->params().at(1).trimmed().toUpper(); + if (capCommand == "LS" || capCommand == "NEW") { + // Either we've gotten a list of capabilities, or new capabilities we may want // Server: CAP * LS * :multi-prefix extended-join account-notify batch invite-notify tls // Server: CAP * LS * :cap-notify server-time example.org/dummy-cap=dummyvalue example.org/second-dummy-cap // Server: CAP * LS :userhost-in-names sasl=EXTERNAL,DH-AES,DH-BLOWFISH,ECDSA-NIST256P-CHALLENGE,PLAIN @@ -180,78 +174,60 @@ void CoreSessionEventProcessor::processIrcEventCap(IrcEvent *e) capListFinished = true; availableCaps = e->params().at(2).split(' '); } - // We know what capabilities are available, request what we want. + // Store what capabilities are available QStringList availableCapPair; - bool queueCurrentCap; for (int i = 0; i < availableCaps.count(); ++i) { // Capability may include values, e.g. CAP * LS :multi-prefix sasl=EXTERNAL availableCapPair = availableCaps[i].trimmed().split('='); - queueCurrentCap = false; - if (availableCapPair.at(0).startsWith("sasl")) { - // Only request SASL if it's enabled - if (coreNet->networkInfo().useSasl) - queueCurrentCap = true; - } else if (availableCapPair.at(0).startsWith("away-notify") || - availableCapPair.at(0).startsWith("account-notify") || - availableCapPair.at(0).startsWith("extended-join") || - availableCapPair.at(0).startsWith("userhost-in-names") || - availableCapPair.at(0).startsWith("multi-prefix")) { - // Always request these capabilities if available - queueCurrentCap = true; - } - if (queueCurrentCap) { - if(availableCapPair.count() >= 2) - coreNet->queuePendingCap(availableCapPair.at(0).trimmed(), availableCapPair.at(1).trimmed()); - else - coreNet->queuePendingCap(availableCapPair.at(0).trimmed()); + if(availableCapPair.count() >= 2) { + coreNet->addCap(availableCapPair.at(0).trimmed().toLower(), availableCapPair.at(1).trimmed()); + } else { + coreNet->addCap(availableCapPair.at(0).trimmed().toLower()); } } + // Begin capability requests when capability listing complete - if (capListFinished) { - emit newEvent(new MessageEvent(Message::Server, e->network(), tr("Negotiating capabilities..."), QString(), QString(), Message::None, e->timestamp())); - sendNextCap(coreNet); - } - } else if (e->params().at(1).compare("ACK", Qt::CaseInsensitive) == 0) { + if (capListFinished) + coreNet->beginCapNegotiation(); + } else if (capCommand == "ACK") { // Server: CAP * ACK :multi-prefix sasl - // Got the capability we want, enable, handle as needed. + // Got the capability we want, handle as needed. // As only one capability is requested at a time, no need to split - // Lower-case the list to make later comparisons easier - // Capability may include values, e.g. CAP * LS :multi-prefix sasl=EXTERNAL - QStringList acceptedCap = e->params().at(2).trimmed().split('='); + QString acceptedCap = e->params().at(2).trimmed().toLower(); // Mark this cap as accepted - if(acceptedCap.count() >= 2) - coreNet->addCap(acceptedCap.at(0), acceptedCap.at(1)); - else - coreNet->addCap(acceptedCap.at(0)); - - // Handle special cases - if (acceptedCap.at(0).startsWith("sasl")) { - // Freenode (at least) sends "sasl " with a trailing space for some reason! - // if the current identity has a cert set, use SASL EXTERNAL - // FIXME use event - // TODO If value of sasl capability is not empty, limit to accepted -#ifdef HAVE_SSL - if (!coreNet->identityPtr()->sslCert().isNull()) { - coreNet->putRawLine(coreNet->serverEncode("AUTHENTICATE EXTERNAL")); - } else { -#endif - // Only working with PLAIN atm, blowfish later - coreNet->putRawLine(coreNet->serverEncode("AUTHENTICATE PLAIN")); -#ifdef HAVE_SSL - } -#endif - } else { - // Special handling not needed, move on to next cap - sendNextCap(coreNet); + coreNet->acknowledgeCap(acceptedCap); + + if (!coreNet->capsRequiringConfiguration.contains(acceptedCap)) { + // Some capabilities (e.g. SASL) require further messages to finish. If so, do NOT + // send the next capability; it will be handled elsewhere in CoreNetwork. + // Otherwise, move on to the next capability + coreNet->sendNextCap(); + } + } else if (capCommand == "NAK" || capCommand == "DEL") { + // Either something went wrong with this capability, or it is no longer supported + // > For CAP NAK + // Server: CAP * NAK :multi-prefix sasl + // > For CAP DEL + // Server: :irc.example.com CAP modernclient DEL :multi-prefix sasl + // CAP NAK and CAP DEL replies are always single-line + + QStringList removedCaps; + removedCaps = e->params().at(2).split(' '); + + // Store what capability was denied or removed + QString removedCap; + for (int i = 0; i < removedCaps.count(); ++i) { + removedCap = removedCaps[i].trimmed().toLower(); + // Mark this cap as removed + coreNet->removeCap(removedCap); + } + + if (capCommand == "NAK") { + // Continue negotiation when capability listing complete only if this is the result + // of a denied cap, not a removed cap + coreNet->sendNextCap(); } - } else if (e->params().at(1).compare("NAK", Qt::CaseInsensitive) == 0) { - // Something went wrong with this capability, disable, go to next cap - // As only one capability is requested at a time, no need to split - // Lower-case the list to make later comparisons easier - QString deniedCap = e->params().at(2).trimmed(); - coreNet->removeCap(deniedCap); - sendNextCap(coreNet); } } } @@ -321,7 +297,7 @@ void CoreSessionEventProcessor::processIrcEventJoin(IrcEvent *e) QString channel = e->params()[0]; IrcUser *ircuser = net->updateNickFromMask(e->prefix()); - if (net->useCapExtendedJoin()) { + if (net->capEnabled(IrcCap::EXTENDED_JOIN)) { if (!checkParamCount(e, 3)) return; // If logged in, :nick!user@host JOIN #channelname accountname :Real Name @@ -341,8 +317,8 @@ void CoreSessionEventProcessor::processIrcEventJoin(IrcEvent *e) // If using away-notify, check new users. Works around buggy IRC servers // forgetting to send :away messages for users who join channels when away. - if (coreNetwork(e)->useCapAwayNotify()) { - coreNetwork(e)->queueAutoWhoOneshot(ircuser->nick()); + if (net->capEnabled(IrcCap::AWAY_NOTIFY)) { + net->queueAutoWhoOneshot(ircuser->nick()); } if (!handledByNetsplit) @@ -924,7 +900,7 @@ void CoreSessionEventProcessor::processIrcEvent352(IrcEvent *e) ircuser->setServer(e->params()[3]); ircuser->setRealName(e->params().last().section(" ", 1)); - if (coreNetwork(e)->useCapMultiPrefix()) { + if (coreNetwork(e)->capEnabled(IrcCap::MULTI_PREFIX)) { // If multi-prefix is enabled, all modes will be sent in WHO replies. // :kenny.chatspike.net 352 guest #test grawity broken.symlink *.chatspike.net grawity H@%+ :0 Mantas M. // See: http://ircv3.net/specs/extensions/multi-prefix-3.1.html @@ -990,7 +966,7 @@ void CoreSessionEventProcessor::processIrcEvent353(IrcEvent *e) QStringList modes; // Cache result of multi-prefix to avoid unneeded casts and lookups with each iteration. - bool _useCapMultiPrefix = coreNetwork(e)->useCapMultiPrefix(); + bool _useCapMultiPrefix = coreNetwork(e)->capEnabled(IrcCap::MULTI_PREFIX); foreach(QString nick, e->params()[2].split(' ', QString::SkipEmptyParts)) { QString mode; diff --git a/src/core/coresessioneventprocessor.h b/src/core/coresessioneventprocessor.h index 27371e35..ff167299 100644 --- a/src/core/coresessioneventprocessor.h +++ b/src/core/coresessioneventprocessor.h @@ -154,17 +154,6 @@ private: // key: quit message // value: the corresponding netsplit object QHash > _netsplits; - - // IRCv3 capability negotiation - /** - * Sends the next capability from the queue. - * - * During nick registration if any capabilities remain queued, this will take the next and - * request it. When no capabilities remain, capability negotiation is ended. - * - * @param[in,out] A network currently undergoing capability negotiation - */ - void sendNextCap(Network *net); }; diff --git a/src/core/ircparser.cpp b/src/core/ircparser.cpp index 2e95fafb..44b4b523 100644 --- a/src/core/ircparser.cpp +++ b/src/core/ircparser.cpp @@ -280,10 +280,13 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent *e) case EventManager::IrcEventAway: { + // Update hostmask info first. This will create the nick if it doesn't exist, e.g. + // away-notify data being sent before JOIN messages. + net->updateNickFromMask(prefix); + // Separate nick in order to separate server and user decoding QString nick = nickFromMask(prefix); decParams << nick; decParams << (params.count() >= 1 ? net->userDecode(nick, params.at(0)) : QString()); - net->updateNickFromMask(prefix); } break;