From: Shane Synan Date: Wed, 17 Feb 2016 20:00:43 +0000 (-0600) Subject: Implement IRCv3.2 capability negotiation X-Git-Tag: travis-deploy-test~513^2~6 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=adc18a7284e7124639fa4b354251d6b102dcf6b7 Implement IRCv3.2 capability negotiation Always send CAP LS, similar to ZNC and HexChat. IrcEventCap checks returned capabilities, queuing up desired ones until all replies finish, then requests each capability one-by-one. This prevents an error with one capability from blocking all of them. See http://ircv3.net/specs/core/capability-negotiation-3.1.html And http://ircv3.net/specs/core/capability-negotiation-3.2.html Modify SASL authentication to only happen if present in capability list. If it's not supported, log an error in the status buffer and continue connecting anyways. See http://ircv3.net/specs/extensions/sasl-3.1.html And http://ircv3.net/specs/extensions/sasl-3.2.html --- diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index a6dc7855..ecf2a2d9 100644 --- a/src/core/corenetwork.cpp +++ b/src/core/corenetwork.cpp @@ -158,6 +158,11 @@ void CoreNetwork::connectToIrc(bool reconnecting) // cleaning up old quit reason _quitReason.clear(); + // reset capability negotiation in case server changes during a reconnect + _capsQueued.clear(); + _capsPending.clear(); + _capsSupported.clear(); + // use a random server? if (useRandomServer()) { _lastUsedServerIndex = qrand() % serverList().size(); @@ -482,9 +487,11 @@ void CoreNetwork::socketInitialized() _tokenBucket = _burstSize; // init with a full bucket _tokenBucketTimer.start(_messageDelay); - if (networkInfo().useSasl) { - putRawLine(serverEncode(QString("CAP REQ :sasl"))); - } + // Request capabilities as per IRCv3.2 specifications + // Older servers should ignore this; newer servers won't downgrade to RFC1459 + displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Requesting capability list...")); + putRawLine(serverEncode(QString("CAP LS 302"))); + if (!server.password.isEmpty()) { putRawLine(serverEncode(QString("PASS %1").arg(server.password))); } @@ -858,6 +865,73 @@ void CoreNetwork::setPingInterval(int interval) _pingTimer.setInterval(interval * 1000); } +/******** IRCv3 Capability Negotiation ********/ + +void CoreNetwork::addCap(const QString &capability, const QString &value) +{ + // 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(); + } + } + if (_capsPending.contains(capability)) + _capsPending.remove(capability); + + // Handle special cases here + // TODO Use events if it makes sense +} + +void CoreNetwork::removeCap(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); + + // Handle special cases here + // TODO Use events if it makes sense +} + +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) +{ + if (!_capsQueued.contains(capability)) { + _capsQueued.append(capability); + // Some capabilities may have values attached, preserve them as pending + _capsPending[capability] = value; + } +} + +QString CoreNetwork::takeQueuedCap() +{ + if (!_capsQueued.empty()) { + return _capsQueued.takeFirst(); + } else { + return QString(); + } +} /******** AutoWHO ********/ diff --git a/src/core/corenetwork.h b/src/core/corenetwork.h index 359d032e..cc64d711 100644 --- a/src/core/corenetwork.h +++ b/src/core/corenetwork.h @@ -100,6 +100,47 @@ public: QList> splitMessage(const QString &cmd, const QString &message, std::function(QString &)> cmdGenerator); + // IRCv3 capability negotiation + + /** + * Checks if a given capability is enabled. + * + * @returns True if enabled, otherwise false + */ + inline bool capEnabled(const QString &capability) const { return _capsSupported.contains(capability); } + + /** + * Checks if capability negotiation is currently ongoing. + * + * @returns True if in progress, otherwise false + */ + inline bool capNegotiationInProgress() const { return !_capsQueued.empty(); } + + /** + * Gets the value of an enabled or pending capability, e.g. sasl=plain. + * + * @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. + * + * @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"); } + public slots: virtual void setMyNick(const QString &mynick); @@ -134,6 +175,44 @@ public slots: bool cipherUsesCBC(const QString &target); #endif + // IRCv3 capability negotiation (can be connected to signals) + + /** + * Marks a capability as accepted, providing an optional value. + * + * Removes it from queue of pending capabilities and triggers any capability-specific + * activation. + * + * @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()); + + /** + * Marks a capability as denied. + * + * Removes it from the queue of pending capabilities and triggers any capability-specific + * deactivation. + * + * @param[in] capability Name of the capability + */ + void removeCap(const QString &capability); + + /** + * Queues a capability as available but not yet accepted or denied. + * + * Capabilities should be queued when registration pauses for CAP LS for capabilities are only + * requested during login. + * + * @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 setAutoWhoEnabled(bool enabled); void setAutoWhoInterval(int interval); void setAutoWhoDelay(int delay); @@ -240,6 +319,12 @@ private: QHash _autoWhoPending; QTimer _autoWhoTimer, _autoWhoCycleTimer; + // CAPs may have parameter values + // 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' + QTimer _tokenBucketTimer; int _messageDelay; // token refill speed in ms int _burstSize; // size of the token bucket diff --git a/src/core/coresessioneventprocessor.cpp b/src/core/coresessioneventprocessor.cpp index eef0dd2e..a47b2c05 100644 --- a/src/core/coresessioneventprocessor.cpp +++ b/src/core/coresessioneventprocessor.cpp @@ -92,13 +92,16 @@ void CoreSessionEventProcessor::tryNextNick(NetworkEvent *e, const QString &errn void CoreSessionEventProcessor::processIrcEventNumeric(IrcEventNumeric *e) { switch (e->number()) { - // CAP stuff - case 903: - case 904: - case 905: - case 906: - case 907: - qobject_cast(e->network())->putRawLine("CAP END"); + // SASL authentication replies + // See: http://ircv3.net/specs/extensions/sasl-3.1.html + // TODO Handle errors to stop connection if appropriate + 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()); break; default: @@ -137,26 +140,111 @@ 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) { - // for SASL, there will only be a single param of 'sasl', however you can check here for - // additional CAP messages (ls, multi-prefix, et cetera). - - if (e->params().count() == 3) { - if (e->params().at(2).startsWith("sasl")) { // Freenode (at least) sends "sasl " with a trailing space for some reason! - // FIXME use event - // if the current identity has a cert set, use SASL EXTERNAL -#ifdef HAVE_SSL - if (!coreNetwork(e)->identityPtr()->sslCert().isNull()) { - coreNetwork(e)->putRawLine(coreNetwork(e)->serverEncode("AUTHENTICATE EXTERNAL")); + // Handle capability negotiation + // See: http://ircv3.net/specs/core/capability-negotiation-3.2.html + // 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) { + // 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 + bool capListFinished; + QStringList availableCaps; + if (e->params().count() == 4) { + // Middle of multi-line reply, ignore the asterisk + capListFinished = false; + availableCaps = e->params().at(3).split(' '); } else { + // Single line reply + capListFinished = true; + availableCaps = e->params().at(2).split(' '); + } + // We know what capabilities are available, request what we want. + 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; + } + if (queueCurrentCap) { + if(availableCapPair.count() >= 2) + coreNet->queuePendingCap(availableCapPair.at(0).trimmed(), availableCapPair.at(1).trimmed()); + else + coreNet->queuePendingCap(availableCapPair.at(0).trimmed()); + } + } + // 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) { + // Server: CAP * ACK :multi-prefix sasl + // Got the capability we want, enable, 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('='); + + // 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 - coreNetwork(e)->putRawLine(coreNetwork(e)->serverEncode("AUTHENTICATE PLAIN")); + // 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); + } + } 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); } } } diff --git a/src/core/coresessioneventprocessor.h b/src/core/coresessioneventprocessor.h index 43e6754c..18d853a4 100644 --- a/src/core/coresessioneventprocessor.h +++ b/src/core/coresessioneventprocessor.h @@ -46,8 +46,8 @@ public: Q_INVOKABLE void processIrcEventNumeric(IrcEventNumeric *event); - Q_INVOKABLE void processIrcEventAuthenticate(IrcEvent *event); // SASL auth - Q_INVOKABLE void processIrcEventCap(IrcEvent *event); // CAP framework + Q_INVOKABLE void processIrcEventAuthenticate(IrcEvent *event); /// SASL authentication + Q_INVOKABLE void processIrcEventCap(IrcEvent *event); /// CAP framework negotiation Q_INVOKABLE void processIrcEventInvite(IrcEvent *event); Q_INVOKABLE void processIrcEventJoin(IrcEvent *event); Q_INVOKABLE void lateProcessIrcEventKick(IrcEvent *event); @@ -152,6 +152,17 @@ 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 f5363554..9e8461f3 100644 --- a/src/core/ircparser.cpp +++ b/src/core/ircparser.cpp @@ -298,6 +298,17 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent *e) decParams << net->channelDecode(channel, params.at(2)); } break; + case 451: /* You have not registered... */ + if (target.compare("CAP", Qt::CaseInsensitive) == 0) { + // :irc.server.com 451 CAP :You have not registered + // If server doesn't support capabilities, it will report this message. Turn it + // into a nicer message since it's not a real error. + defaultHandling = false; + events << new MessageEvent(Message::Server, e->network(), + tr("Capability negotiation not supported"), + QString(), QString(), Message::None, e->timestamp()); + } + break; } default: