From: Manuel Nickschas Date: Wed, 15 Jun 2016 23:13:31 +0000 (+0200) Subject: Merge pull request #198 - IRCv3 improvements X-Git-Tag: travis-deploy-test~470 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=5b164bbc62960cea62a31287f679197b623ad7ac;hp=8714e651551428b0fed15b7a98d1be514921af7d Merge pull request #198 - IRCv3 improvements Closes GH-198. --- diff --git a/.gitignore b/.gitignore index a15aef74..ded1e431 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ build* *.pyc tags + +# Qt Creator configuration +CMakeLists.txt.user diff --git a/src/client/networkmodel.cpp b/src/client/networkmodel.cpp index 64349690..aea48bf8 100644 --- a/src/client/networkmodel.cpp +++ b/src/client/networkmodel.cpp @@ -569,12 +569,33 @@ QString QueryBufferItem::toolTip(int column) const NetworkItem::escapeHTML(_ircUser->suserHost()), !_ircUser->suserHost().isEmpty()); } + + // Keep track of whether or not the account information's been added. Don't show it twice. + bool accountAdded = false; + if(!_ircUser->account().isEmpty()) { + // IRCv3 account-notify is supported by the core and IRC server. + // Assume logged out (seems to be more common) + QString accountHTML = QString("

%1

").arg(tr("Not logged in")); + + // If account is logged in, replace with the escaped account name. + if (_ircUser->account() != "*") { + accountHTML = NetworkItem::escapeHTML(_ircUser->account()); + } + addRow(NetworkItem::escapeHTML(tr("Account"), true), + accountHTML, + true); + // Mark the row as added + accountAdded = true; + } // whoisServiceReply may return " is identified for this nick", which should be translated. // See https://www.alien.net.au/irc/irc2numerics.html if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) { addRow(NetworkItem::escapeHTML(tr("Account"), true), NetworkItem::escapeHTML(tr("Identified for this nick")), - true); + !accountAdded); + // Don't add the account row again if information's already added via account-notify + // Mark the row as added + accountAdded = true; } else { addRow(NetworkItem::escapeHTML(tr("Service Reply"), true), NetworkItem::escapeHTML(_ircUser->whoisServiceReply()), @@ -1100,12 +1121,33 @@ QString IrcUserItem::toolTip(int column) const NetworkItem::escapeHTML(_ircUser->suserHost()), !_ircUser->suserHost().isEmpty()); } + + // Keep track of whether or not the account information's been added. Don't show it twice. + bool accountAdded = false; + if(!_ircUser->account().isEmpty()) { + // IRCv3 account-notify is supported by the core and IRC server. + // Assume logged out (seems to be more common) + QString accountHTML = QString("

%1

").arg(tr("Not logged in")); + + // If account is logged in, replace with the escaped account name. + if (_ircUser->account() != "*") { + accountHTML = NetworkItem::escapeHTML(_ircUser->account()); + } + addRow(NetworkItem::escapeHTML(tr("Account"), true), + accountHTML, + true); + // Mark the row as added + accountAdded = true; + } // whoisServiceReply may return " is identified for this nick", which should be translated. // See https://www.alien.net.au/irc/irc2numerics.html if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) { addRow(NetworkItem::escapeHTML(tr("Account"), true), NetworkItem::escapeHTML(tr("Identified for this nick")), - true); + !accountAdded); + // Don't add the account row again if information's already added via account-notify + // Mark the row as added + accountAdded = true; } else { addRow(NetworkItem::escapeHTML(tr("Service Reply"), true), NetworkItem::escapeHTML(_ircUser->whoisServiceReply()), diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index bb3aef27..66416c37 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -43,6 +43,8 @@ set(SOURCES # needed for automoc coreinfo.h + irccap.h + protocol.h ) if (USE_QT5) diff --git a/src/common/eventmanager.h b/src/common/eventmanager.h index 37017430..2d88143a 100644 --- a/src/common/eventmanager.h +++ b/src/common/eventmanager.h @@ -91,6 +91,7 @@ public : IrcEventAccount, IrcEventAway, IrcEventCap, + IrcEventChghost, IrcEventInvite, IrcEventJoin, IrcEventKick, diff --git a/src/common/irccap.h b/src/common/irccap.h new file mode 100644 index 00000000..14c0ed7c --- /dev/null +++ b/src/common/irccap.h @@ -0,0 +1,153 @@ +/*************************************************************************** + * 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"; + + /** + * Magic number for WHOX, used to ignore user-requested WHOX replies from servers + * + * If a user initiates a WHOX, there's no easy way to tell what fields were requested. It's + * simpler to not attempt to parse data from user-requested WHOX replies. + */ + const uint ACCOUNT_NOTIFY_WHOX_NUM = 369; + + /** + * 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"; + + /** + * Hostname/user changed notification. + * + * http://ircv3.net/specs/extensions/chghost-3.2.html + */ + const QString CHGHOST = "chghost"; + + /** + * 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, + CHGHOST, + 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/ircuser.cpp b/src/common/ircuser.cpp index 9821cf46..463df85e 100644 --- a/src/common/ircuser.cpp +++ b/src/common/ircuser.cpp @@ -146,6 +146,15 @@ void IrcUser::setRealName(const QString &realName) } +void IrcUser::setAccount(const QString &account) +{ + if (_account != account) { + _account = account; + SYNC(ARG(account)) + } +} + + void IrcUser::setAway(const bool &away) { if (away != _away) { diff --git a/src/common/ircuser.h b/src/common/ircuser.h index d0a01493..87d9114a 100644 --- a/src/common/ircuser.h +++ b/src/common/ircuser.h @@ -43,6 +43,7 @@ class IrcUser : public SyncableObject Q_PROPERTY(QString host READ host WRITE setHost) Q_PROPERTY(QString nick READ nick WRITE setNick) Q_PROPERTY(QString realName READ realName WRITE setRealName) + Q_PROPERTY(QString account READ account WRITE setAccount) Q_PROPERTY(bool away READ isAway WRITE setAway) Q_PROPERTY(QString awayMessage READ awayMessage WRITE setAwayMessage) Q_PROPERTY(QDateTime idleTime READ idleTime WRITE setIdleTime) @@ -65,6 +66,12 @@ public : inline QString host() const { return _host; } inline QString nick() const { return _nick; } inline QString realName() const { return _realName; } + /** + * Account name, e.g. NickServ/SASL account + * + * @return Account name if logged in, * if logged out, or empty string if unknown + */ + inline QString account() const { return _account; } QString hostmask() const; inline bool isAway() const { return _away; } inline QString awayMessage() const { return _awayMessage; } @@ -104,6 +111,12 @@ public slots: void setHost(const QString &host); void setNick(const QString &nick); void setRealName(const QString &realName); + /** + * Set account name, e.g. NickServ/SASL account + * + * @param[in] account Account name if logged in, * if logged out, or empty string if unknown + */ + void setAccount(const QString &account); void setAway(const bool &away); void setAwayMessage(const QString &awayMessage); void setIdleTime(const QDateTime &idleTime); @@ -182,6 +195,7 @@ private: QString _user; QString _host; QString _realName; + QString _account; /// Account name, e.g. NickServ/SASL account QString _awayMessage; bool _away; QString _server; 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 3e58fdac..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,19 +288,90 @@ 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); + /** + * Update IrcUser hostmask and username from mask, creating an IrcUser if one does not exist. + * + * @param[in] mask Full nick!user@hostmask string + * @return IrcUser of the matching nick if exists, otherwise a new IrcUser + */ IrcUser *updateNickFromMask(const QString &mask); // these slots are to keep the hashlists of all users and the @@ -319,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); @@ -352,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/common/quassel.h b/src/common/quassel.h index ba883b0b..09a5d847 100644 --- a/src/common/quassel.h +++ b/src/common/quassel.h @@ -71,10 +71,11 @@ public: SaslExternal = 0x0004, HideInactiveNetworks = 0x0008, PasswordChange = 0x0010, + CapNegotiation = 0x0020, /// IRCv3 capability negotiation, account tracking - NumFeatures = 0x0010 + NumFeatures = 0x0020 }; - Q_DECLARE_FLAGS(Features, Feature); + Q_DECLARE_FLAGS(Features, Feature) //! The features the current version of Quassel supports (\sa Feature) /** \return An ORed list of all enum values in Feature diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index b8a5ab8d..ca935d96 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 -{ - // 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) +void CoreNetwork::queueCap(const QString &capability) { - 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) { @@ -1017,20 +1080,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(); } 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..8acae81c 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); } } } @@ -266,16 +242,15 @@ void CoreSessionEventProcessor::processIrcEventAccount(IrcEvent *e) IrcUser *ircuser = e->network()->updateNickFromMask(e->prefix()); if (ircuser) { - // FIXME Keep track of authed user account, requires adding support to ircuser.h/cpp - /* - if (e->params().at(0) != "*") { - // Account logged in - qDebug() << "account-notify:" << ircuser->nick() << "logged in to" << e->params().at(0); + QString newAccount = e->params().at(0); + // WHOX uses '0' to indicate logged-out, account-notify uses '*' + if (newAccount != "*") { + // Account logged in, set account name + ircuser->setAccount(newAccount); } else { - // Account logged out - qDebug() << "account-notify:" << ircuser->nick() << "logged out"; + // Account logged out, set account name to logged-out + ircuser->setAccount("*"); } - */ } else { qDebug() << "Received account-notify data for unknown user" << e->prefix(); } @@ -301,6 +276,23 @@ void CoreSessionEventProcessor::processIrcEventAway(IrcEvent *e) } } +/* IRCv3 chghost - ":nick!user@host CHGHOST newuser new.host.goes.here" */ +void CoreSessionEventProcessor::processIrcEventChghost(IrcEvent *e) +{ + if (!checkParamCount(e, 2)) + return; + + IrcUser *ircuser = e->network()->updateNickFromMask(e->prefix()); + if (ircuser) { + // Update with new user/hostname information. setUser/setHost handles checking what + // actually changed. + ircuser->setUser(e->params().at(0)); + ircuser->setHost(e->params().at(1)); + } else { + qDebug() << "Received chghost data for unknown user" << e->prefix(); + } +} + void CoreSessionEventProcessor::processIrcEventInvite(IrcEvent *e) { if (checkParamCount(e, 2)) { @@ -321,7 +313,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 +333,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) @@ -882,6 +874,19 @@ void CoreSessionEventProcessor::processIrcEvent324(IrcEvent *e) } +/* RPL_WHOISACCOUNT: " :is authed as */ +void CoreSessionEventProcessor::processIrcEvent330(IrcEvent *e) +{ + if (!checkParamCount(e, 3)) + return; + + IrcUser *ircuser = e->network()->ircUser(e->params().at(0)); + if (ircuser) { + ircuser->setAccount(e->params().at(1)); + } +} + + /* RPL_NOTOPIC */ void CoreSessionEventProcessor::processIrcEvent331(IrcEvent *e) { @@ -916,49 +921,8 @@ void CoreSessionEventProcessor::processIrcEvent352(IrcEvent *e) QString channel = e->params()[0]; IrcUser *ircuser = e->network()->ircUser(e->params()[4]); if (ircuser) { - ircuser->setUser(e->params()[1]); - ircuser->setHost(e->params()[2]); - - bool away = e->params()[5].contains("G", Qt::CaseInsensitive); - ircuser->setAway(away); - ircuser->setServer(e->params()[3]); - ircuser->setRealName(e->params().last().section(" ", 1)); - - if (coreNetwork(e)->useCapMultiPrefix()) { - // 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 - QString uncheckedModes = e->params()[5]; - QString validModes = QString(); - while (!uncheckedModes.isEmpty()) { - // Mode found in 1 left-most character, add it to the list - if (e->network()->prefixes().contains(uncheckedModes[0])) { - validModes.append(e->network()->prefixToMode(uncheckedModes[0])); - } - // Remove this mode from the list of unchecked modes - uncheckedModes = uncheckedModes.remove(0, 1); - } - - // Some IRC servers decide to not follow the spec, returning only -some- of the user - // modes in WHO despite listing them all in NAMES. For now, assume it can only add - // and not take away. *sigh* - if (!validModes.isEmpty()) { - if (channel != "*") { - // Channel-specific modes received, apply to given channel only - IrcChannel *ircChan = e->network()->ircChannel(channel); - if (ircChan) { - // Do one mode at a time - // TODO Better way of syncing this without breaking protocol? - for (int i = 0; i < validModes.count(); ++i) { - ircChan->addUserMode(ircuser, validModes.at(i)); - } - } - } else { - // Modes apply to the user everywhere - ircuser->addUserModes(validModes); - } - } - } + processWhoInformation(e->network(), channel, ircuser, e->params()[3], e->params()[1], + e->params()[2], e->params()[5], e->params().last().section(" ", 1)); } // Check if channel name has a who in progress. @@ -990,7 +954,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; @@ -1026,6 +990,124 @@ void CoreSessionEventProcessor::processIrcEvent353(IrcEvent *e) } +/* RPL_WHOSPCRPL: " 152 # ~ + ("H"/ "G") :" + is * if not specific to any channel + is * if not logged in +Follows HexChat's usage of 'whox' +See https://github.com/hexchat/hexchat/blob/c874a9525c9b66f1d5ddcf6c4107d046eba7e2c5/src/common/proto-irc.c#L750 +And http://faerion.sourceforge.net/doc/irc/whox.var*/ +void CoreSessionEventProcessor::processIrcEvent354(IrcEvent *e) +{ + // First only check if at least one parameter exists. Otherwise, it'll stop the result from + // being shown if the user chooses different parameters. + if (!checkParamCount(e, 1)) + return; + + if (e->params()[0].toUInt() != IrcCap::ACCOUNT_NOTIFY_WHOX_NUM) { + // Ignore WHOX replies without expected number for we have no idea what fields are specified + return; + } + + // Now we're fairly certain this is supposed to be an automated WHOX. Bail out if it doesn't + // match what we require - 9 parameters. + if (!checkParamCount(e, 9)) + return; + + QString channel = e->params()[1]; + IrcUser *ircuser = e->network()->ircUser(e->params()[5]); + if (ircuser) { + processWhoInformation(e->network(), channel, ircuser, e->params()[4], e->params()[2], + e->params()[3], e->params()[6], e->params().last()); + // Don't use .section(" ", 1) with WHOX replies, for there's no hopcount to trim out + + // As part of IRCv3 account-notify, check account name + // WHOX uses '0' to indicate logged-out, account-notify uses '*' + QString newAccount = e->params()[7]; + if (newAccount != "0") { + // Account logged in, set account name + ircuser->setAccount(newAccount); + } else { + // Account logged out, set account name to logged-out + ircuser->setAccount("*"); + } + } + + // Check if channel name has a who in progress. + // If not, then check if user nick exists and has a who in progress. + if (coreNetwork(e)->isAutoWhoInProgress(channel) || + (ircuser && coreNetwork(e)->isAutoWhoInProgress(ircuser->nick()))) { + e->setFlag(EventManager::Silent); + } +} + + +void CoreSessionEventProcessor::processWhoInformation (Network *net, const QString &targetChannel, IrcUser *ircUser, + const QString &server, const QString &user, const QString &host, + const QString &awayStateAndModes, const QString &realname) +{ + ircUser->setUser(user); + ircUser->setHost(host); + ircUser->setServer(server); + ircUser->setRealName(realname); + + bool away = awayStateAndModes.contains("G", Qt::CaseInsensitive); + ircUser->setAway(away); + + if (net->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 + QString uncheckedModes = awayStateAndModes; + QString validModes = QString(); + while (!uncheckedModes.isEmpty()) { + // Mode found in 1 left-most character, add it to the list + if (net->prefixes().contains(uncheckedModes[0])) { + validModes.append(net->prefixToMode(uncheckedModes[0])); + } + // Remove this mode from the list of unchecked modes + uncheckedModes = uncheckedModes.remove(0, 1); + } + + // Some IRC servers decide to not follow the spec, returning only -some- of the user + // modes in WHO despite listing them all in NAMES. For now, assume it can only add + // and not take away. *sigh* + if (!validModes.isEmpty()) { + if (targetChannel != "*") { + // Channel-specific modes received, apply to given channel only + IrcChannel *ircChan = net->ircChannel(targetChannel); + if (ircChan) { + // Do one mode at a time + // TODO Better way of syncing this without breaking protocol? + for (int i = 0; i < validModes.count(); ++i) { + ircChan->addUserMode(ircUser, validModes.at(i)); + } + } + } else { + // Modes apply to the user everywhere + ircUser->addUserModes(validModes); + } + } + } +} + + +/* ERR_NOSUCHCHANNEL - " :No such channel" */ +void CoreSessionEventProcessor::processIrcEvent403(IrcEventNumeric *e) +{ + // If this is the result of an AutoWho, hide it. It's confusing to show to the user. + if (!checkParamCount(e, 2)) + return; + + QString channelOrNick = e->params()[0]; + // Check if channel name has a who in progress. + // If not, then check if user nick exists and has a who in progress. + if (coreNetwork(e)->isAutoWhoInProgress(channelOrNick)) { + qDebug() << "Channel/nick" << channelOrNick << "no longer exists during AutoWho, ignoring"; + e->setFlag(EventManager::Silent); + } +} + /* ERR_ERRONEUSNICKNAME */ void CoreSessionEventProcessor::processIrcEvent432(IrcEventNumeric *e) { diff --git a/src/core/coresessioneventprocessor.h b/src/core/coresessioneventprocessor.h index 27371e35..3a1070d2 100644 --- a/src/core/coresessioneventprocessor.h +++ b/src/core/coresessioneventprocessor.h @@ -50,6 +50,7 @@ public: Q_INVOKABLE void processIrcEventCap(IrcEvent *event); /// CAP framework negotiation Q_INVOKABLE void processIrcEventAccount(IrcEvent *event); /// account-notify received Q_INVOKABLE void processIrcEventAway(IrcEvent *event); /// away-notify received + Q_INVOKABLE void processIrcEventChghost(IrcEvent *event); /// chghost received Q_INVOKABLE void processIrcEventInvite(IrcEvent *event); Q_INVOKABLE void processIrcEventJoin(IrcEvent *event); Q_INVOKABLE void lateProcessIrcEventKick(IrcEvent *event); @@ -84,10 +85,13 @@ public: Q_INVOKABLE void processIrcEvent322(IrcEvent *event); // RPL_LIST Q_INVOKABLE void processIrcEvent323(IrcEvent *event); // RPL_LISTEND Q_INVOKABLE void processIrcEvent324(IrcEvent *event); // RPL_CHANNELMODEIS + Q_INVOKABLE void processIrcEvent330(IrcEvent *event); // RPL_WHOISACCOUNT (quakenet/snircd/undernet) Q_INVOKABLE void processIrcEvent331(IrcEvent *event); // RPL_NOTOPIC Q_INVOKABLE void processIrcEvent332(IrcEvent *event); // RPL_TOPIC Q_INVOKABLE void processIrcEvent352(IrcEvent *event); // RPL_WHOREPLY Q_INVOKABLE void processIrcEvent353(IrcEvent *event); // RPL_NAMREPLY + Q_INVOKABLE void processIrcEvent354(IrcEvent *event); // RPL_WHOSPCRPL + Q_INVOKABLE void processIrcEvent403(IrcEventNumeric *event); // ERR_NOSUCHCHANNEL Q_INVOKABLE void processIrcEvent432(IrcEventNumeric *event); // ERR_ERRONEUSNICKNAME Q_INVOKABLE void processIrcEvent433(IrcEventNumeric *event); // ERR_NICKNAMEINUSE Q_INVOKABLE void processIrcEvent437(IrcEventNumeric *event); // ERR_UNAVAILRESOURCE @@ -155,16 +159,24 @@ private: // value: the corresponding netsplit object QHash > _netsplits; - // IRCv3 capability negotiation /** - * Sends the next capability from the queue. + * Process given WHO reply information, updating user data, channel modes, etc as needed * - * During nick registration if any capabilities remain queued, this will take the next and - * request it. When no capabilities remain, capability negotiation is ended. + * This takes information from WHO and WHOX replies, processing information that's common + * between them. * - * @param[in,out] A network currently undergoing capability negotiation + * @param[in] net Network object for the IRC server + * @param[in] targetChannel Target channel, or * if unspecified + * @param[in] ircUser IrcUser representing the desired nick + * @param[in] server Nick server name + * @param[in] user Nick username + * @param[in] host Nick hostname + * @param[in] awayStateAndModes Nick away-state and modes (e.g. G@) + * @param[in] realname Nick realname */ - void sendNextCap(Network *net); + void processWhoInformation (Network *net, const QString &targetChannel, IrcUser *ircUser, + const QString &server, const QString &user, const QString &host, + const QString &awayStateAndModes, const QString &realname); }; diff --git a/src/core/eventstringifier.cpp b/src/core/eventstringifier.cpp index 218eeea3..f18af109 100644 --- a/src/core/eventstringifier.cpp +++ b/src/core/eventstringifier.cpp @@ -205,15 +205,29 @@ void EventStringifier::processIrcEventNumeric(IrcEventNumeric *e) case 376: break; - // CAP stuff - case 900: - case 903: - case 904: - case 905: - case 906: - case 907: + // SASL authentication stuff + // See: http://ircv3.net/specs/extensions/sasl-3.1.html + case 900: // RPL_LOGGEDIN + case 901: // RPL_LOGGEDOUT { - displayMsg(e, Message::Info, "CAP: " + e->params().join("")); + // :server 900 !@ :You are now logged in as + // :server 901 !@ :You are now logged out + if (!checkParamCount(e, 3)) + return; + displayMsg(e, Message::Server, "SASL: " + e->params().at(2)); + break; + } + // Ignore SASL success, partially redundant with RPL_LOGGEDIN and RPL_LOGGEDOUT + case 903: // RPL_SASLSUCCESS :server 903 :SASL authentication successful + break; + case 902: // ERR_NICKLOCKED :server 902 :You must use a nick assigned to you + case 904: // ERR_SASLFAIL :server 904 :SASL authentication failed + case 905: // ERR_SASLTOOLONG :server 905 :SASL message too long + case 906: // ERR_SASLABORTED :server 906 :SASL authentication aborted + case 907: // ERR_SASLALREADY :server 907 :You have already authenticated using SASL + case 908: // RPL_SASLMECHS :server 908 :are available SASL mechanisms + { + displayMsg(e, Message::Server, "SASL: " + e->params().join("")); break; } @@ -643,6 +657,16 @@ void EventStringifier::processIrcEvent352(IrcEvent *e) } +/* RPL_WHOSPCRPL: " # ~ + ("H"/ "G") :" +Could be anything else, though. User-specified fields. +See http://faerion.sourceforge.net/doc/irc/whox.var */ +void EventStringifier::processIrcEvent354(IrcEvent *e) +{ + displayMsg(e, Message::Server, tr("[WhoX] %1").arg(e->params().join(" "))); +} + + /* RPL_ENDOFWHOWAS - " :End of WHOWAS" */ void EventStringifier::processIrcEvent369(IrcEvent *e) { diff --git a/src/core/eventstringifier.h b/src/core/eventstringifier.h index 66066b98..bef6ab86 100644 --- a/src/core/eventstringifier.h +++ b/src/core/eventstringifier.h @@ -88,6 +88,7 @@ public: Q_INVOKABLE void processIrcEvent333(IrcEvent *event); // RPL_??? (topic set by) Q_INVOKABLE void processIrcEvent341(IrcEvent *event); // RPL_INVITING Q_INVOKABLE void processIrcEvent352(IrcEvent *event); // RPL_WHOREPLY + Q_INVOKABLE void processIrcEvent354(IrcEvent *event); // RPL_WHOSPCRPL Q_INVOKABLE void processIrcEvent369(IrcEvent *event); // RPL_ENDOFWHOWAS Q_INVOKABLE void processIrcEvent432(IrcEvent *event); // ERR_ERRONEUSNICKNAME Q_INVOKABLE void processIrcEvent433(IrcEvent *event); // ERR_NICKNAMEINUSE 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;