diff options
| author | Linus Jahn <lnj@kaidan.im> | 2019-06-25 18:57:43 +0200 |
|---|---|---|
| committer | LNJ <lnj@kaidan.im> | 2020-02-04 13:07:23 +0100 |
| commit | 1580a6b1582171b7141b072b774faeb387671282 (patch) | |
| tree | 325eaf173de03f6f551aa1254270412240537dc4 /src/client | |
| parent | 4adbb82142d3830a7d9d71291895d918a7242e79 (diff) | |
| download | qxmpp-1580a6b1582171b7141b072b774faeb387671282.tar.gz | |
Add new QXmppRegistrationManager
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/QXmppClient.h | 8 | ||||
| -rw-r--r-- | src/client/QXmppRegistrationManager.cpp | 301 | ||||
| -rw-r--r-- | src/client/QXmppRegistrationManager.h | 361 |
3 files changed, 666 insertions, 4 deletions
diff --git a/src/client/QXmppClient.h b/src/client/QXmppClient.h index be411993..79eaf38b 100644 --- a/src/client/QXmppClient.h +++ b/src/client/QXmppClient.h @@ -88,10 +88,10 @@ public: /// An enumeration for type of error. /// Error could come due a TCP socket or XML stream or due to various stanzas. enum Error { - NoError, ///< No error. - SocketError, ///< Error due to TCP socket. - KeepAliveError, ///< Error due to no response to a keep alive. - XmppStreamError ///< Error due to XML stream. + NoError, ///< No error. + SocketError, ///< Error due to TCP socket. + KeepAliveError, ///< Error due to no response to a keep alive. + XmppStreamError, ///< Error due to XML stream. }; Q_ENUM(Error) diff --git a/src/client/QXmppRegistrationManager.cpp b/src/client/QXmppRegistrationManager.cpp new file mode 100644 index 00000000..48cd3d47 --- /dev/null +++ b/src/client/QXmppRegistrationManager.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2008-2020 The QXmpp developers + * + * Author: + * Melvin Keskin + * Linus Jahn + * + * Source: + * https://github.com/qxmpp-project/qxmpp + * + * This file is a part of QXmpp library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + */ + +#include "QXmppRegistrationManager.h" + +#include "QXmppClient.h" +#include "QXmppConstants_p.h" +#include "QXmppDiscoveryManager.h" +#include "QXmppRegisterIq.h" +#include "QXmppStreamFeatures.h" +#include "QXmppUtils.h" + +#include <QDomElement> + +class QXmppRegistrationManagerPrivate +{ +public: + QXmppRegistrationManagerPrivate(); + + // whether to block login and request the registration form on connect + bool registerOnConnectEnabled; + // whether the server supports registration (after login) + bool supportedByServer; + + // caching + QString changePasswordIqId; + QString newPassword; + + QString registrationIqId; + + QXmppRegisterIq registrationFormToSend; +}; + +QXmppRegistrationManagerPrivate::QXmppRegistrationManagerPrivate() + : registerOnConnectEnabled(false), + supportedByServer(false) +{ +} + +/// +/// Default constructor. +/// +QXmppRegistrationManager::QXmppRegistrationManager() + : d(new QXmppRegistrationManagerPrivate) +{ +} + +QXmppRegistrationManager::~QXmppRegistrationManager() = default; + +/// +/// This adds the \c jabber:iq:register namespace to the features. +/// +QStringList QXmppRegistrationManager::discoveryFeatures() const +{ + return QStringList { + ns_register + }; +} + +/// +/// Changes the password of the user's account. +/// +/// \note Be sure to only call this when any previous requests have finished. +/// +/// \param newPassword The new password to be set. This must not be empty. +/// +void QXmppRegistrationManager::changePassword(const QString &newPassword) +{ + auto iq = QXmppRegisterIq::createChangePasswordRequest(client()->configuration().user(), newPassword); + + d->changePasswordIqId = iq.id(); + d->newPassword = newPassword; + + client()->sendPacket(iq); +} + +/// +/// Cancels an existing registration on the server. +/// +/// \returns The ID of the sent IQ, if it was sent successfully. A null string +/// is returned otherwise. +/// +QString QXmppRegistrationManager::deleteAccount() +{ + auto iq = QXmppRegisterIq::createUnregistrationRequest(); + + if (client()->sendPacket(iq)) + return iq.id(); + return {}; +} + +bool QXmppRegistrationManager::supportedByServer() const +{ + return d->supportedByServer; +} + +/// +/// Requests the registration form for registering. +/// +/// \param service The service which the registration form should be requested +/// from. If left empty, this will default to the local server. +/// +void QXmppRegistrationManager::requestRegistrationForm(const QString &service) +{ + QXmppRegisterIq iq; + iq.setType(QXmppIq::Get); + iq.setTo(service); + client()->sendPacket(iq); +} + +/// +/// Sets a registration form to be sent on the next connect with the server. +/// \param iq The completed registration form. +/// +void QXmppRegistrationManager::setRegistrationFormToSend(const QXmppRegisterIq &iq) +{ + d->registrationFormToSend = iq; +} + +/// +/// Sets a registration form to be sent on the next connect with the server. +/// \param dataForm The completed data form for registration. +/// +void QXmppRegistrationManager::setRegistrationFormToSend(const QXmppDataForm &dataForm) +{ + d->registrationFormToSend = QXmppRegisterIq(); + d->registrationFormToSend.setForm(dataForm); +} + +/// +/// Sends a completed registration form that was previously set using +/// setRegistrationFormToSend(). +/// +/// You usually only need to set the form and the manager will automatically +/// send it when connected. More details can be found in the documentation of +/// the QXmppRegistrationManager. +/// +void QXmppRegistrationManager::sendCachedRegistrationForm() +{ + if (!d->registrationFormToSend.form().isNull()) + d->registrationFormToSend.form().setType(QXmppDataForm::Submit); + + d->registrationFormToSend.setType(QXmppIq::Set); + + client()->sendPacket(d->registrationFormToSend); + d->registrationIqId = d->registrationFormToSend.id(); + + // clear cache + d->registrationFormToSend = QXmppRegisterIq(); +} + +/// +/// Returns whether to only request the registration form and not to connect +/// with username/password. +/// +bool QXmppRegistrationManager::registerOnConnectEnabled() const +{ + return d->registerOnConnectEnabled; +} + +/// +/// Sets whether to only request the registration form and not to connect with +/// username/password. +/// +/// \param enabled true to register, false to connect normally. +/// +void QXmppRegistrationManager::setRegisterOnConnectEnabled(bool enabled) +{ + d->registerOnConnectEnabled = enabled; +} + +/// \cond +bool QXmppRegistrationManager::handleStanza(const QDomElement &stanza) +{ + if (d->registerOnConnectEnabled && QXmppStreamFeatures::isStreamFeatures(stanza)) { + QXmppStreamFeatures features; + features.parse(stanza); + + if (features.registerMode() == QXmppStreamFeatures::Disabled) { + warning(QStringLiteral("Could not request the registration form, because the server does not advertise the register stream feature.")); + client()->disconnectFromServer(); + emit registrationFailed({ QXmppStanza::Error::Cancel, + QXmppStanza::Error::FeatureNotImplemented, + QStringLiteral("The server does not advertise the register stream feature.") }); + return true; + } + + if (!d->registrationFormToSend.form().isNull() || !d->registrationFormToSend.username().isNull()) { + info(QStringLiteral("Sending completed form.")); + sendCachedRegistrationForm(); + return true; + } + + info(QStringLiteral("Requesting registration form from server.")); + requestRegistrationForm(); + return true; + } + + if (stanza.tagName() == "iq") { + const QString &id = stanza.attribute(QStringLiteral("id")); + + if (!id.isEmpty() && id == d->registrationIqId) { + QXmppIq iq; + iq.parse(stanza); + + switch (iq.type()) { + case QXmppIq::Result: + info(QStringLiteral("Successfully registered with the service.")); + emit registrationSucceeded(); + case QXmppIq::Error: + warning(QStringLiteral("Registering with the service failed: ").append(iq.error().text())); + emit registrationFailed(iq.error()); + default: + break; // should never occur + } + + d->registrationIqId.clear(); + return true; + } else if (!id.isEmpty() && id == d->changePasswordIqId) { + QXmppIq iq; + iq.parse(stanza); + + switch (iq.type()) { + case QXmppIq::Result: + info(QStringLiteral("Changed password successfully.")); + client()->configuration().setPassword(d->newPassword); + emit passwordChanged(d->newPassword); + break; + case QXmppIq::Error: + warning(QStringLiteral("Failed to change password: ").append(iq.error().text())); + emit passwordChangeFailed(iq.error()); + break; + default: + break; // should never occur + } + + d->changePasswordIqId.clear(); + d->newPassword.clear(); + return true; + } else if (QXmppRegisterIq::isRegisterIq(stanza)) { + QXmppRegisterIq iq; + iq.parse(stanza); + + emit registrationFormReceived(iq); + } + } + return false; +} +/// \endcond + +void QXmppRegistrationManager::setClient(QXmppClient *client) +{ + QXmppClientExtension::setClient(client); + // get service discovery manager + auto *disco = client->findExtension<QXmppDiscoveryManager>(); + if (disco) { + connect(disco, &QXmppDiscoveryManager::infoReceived, this, &QXmppRegistrationManager::handleDiscoInfo); + } + + connect(client, &QXmppClient::disconnected, [=]() { + setSupportedByServer(false); + }); +} + +void QXmppRegistrationManager::handleDiscoInfo(const QXmppDiscoveryIq &iq) +{ + // check features of own server + if (iq.from().isEmpty() || iq.from() == client()->configuration().domain()) { + if (iq.features().contains(ns_register)) + setSupportedByServer(true); + } +} + +void QXmppRegistrationManager::setSupportedByServer(bool registrationSupported) +{ + if (d->supportedByServer != registrationSupported) { + d->supportedByServer = registrationSupported; + emit supportedByServerChanged(); + } +} diff --git a/src/client/QXmppRegistrationManager.h b/src/client/QXmppRegistrationManager.h new file mode 100644 index 00000000..ece72f34 --- /dev/null +++ b/src/client/QXmppRegistrationManager.h @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2008-2020 The QXmpp developers + * + * Author: + * Melvin Keskin + * Linus Jahn + * + * Source: + * https://github.com/qxmpp-project/qxmpp + * + * This file is a part of QXmpp library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + */ + +#ifndef QXMPPREGISTRATIONMANAGER_H +#define QXMPPREGISTRATIONMANAGER_H + +#include "QXmppClientExtension.h" +#include "QXmppRegisterIq.h" + +#include <QScopedPointer> + +class QXmppRegistrationManagerPrivate; + +/// +/// \brief The QXmppRegistrationManager class manages in-band registration and +/// account management tasks like changing the password as defined in +/// \xep{0077}: In-Band Registration. +/// +/// <h3 id="activation">Activating the manager</h3> +/// +/// To make use of this manager, you need to instantiate it and load it into +/// the QXmppClient instance as follows: +/// +/// \code +/// auto *registrationManager = new QXmppRegistrationManager; +/// client->addExtension(registrationManager); +/// \endcode +/// +/// <h3>Setting up service discovery correctly for this manager</h3> +/// +/// This manager automatically recognizes whether the local server supports +/// \xep{0077} (see supportedByServer()). You just need to request the service +/// discovery information from the server on connect as below: +/// +/// \code +/// connect(client, &QXmppClient::connected, [=]() { +/// // The service discovery manager is added to the client by default. +/// auto *discoManager = client->findExtension<QXmppDiscoveryManager>(); +/// discoManager->requestInfo(client->configuration().server()); +/// }); +/// \endcode +/// +/// As soon as the result is retrieved, the supportedByServer() property should +/// be correct and could be used to display the user whether account management +/// tasks can be performed on this server. +/// +/// However, this is not relevant if you only want to +/// <a href="#register-account">register a new account on a server</a>. +/// +/// <h3>Changing the account's password</h3> +/// +/// To change the password of the current account changePassword() can be used. +/// Upon that either passwordChanged() or passwordChangeFailed() is emitted. +/// +/// If changing the password was successful, the new password is automatically +/// set in the QXmppClient::configuration(), so reconnecting works properly. +/// +/// Example: +/// \code +/// auto *registrationManager = client->findExtension<QXmppRegistrationManager>(); +/// connect(registrationManager, &QXmppRegistrationManager::passwordChanged, [=](const QString &newPassword) { +/// qDebug() << "Password changed to:" << newPassword; +/// }); +/// connect(registrationManager, &QXmppRegistrationManager::passwordChangeFailed, [=](const QXmppStanza::Error &error) { +/// qDebug() << "Couldn't change the password:" << error.text(); +/// }); +/// +/// registrationManager->changePassword(client->configuration().user(), "m1cr0$0ft"); +/// \endcode +/// +/// <h3>Unregistration with the server</h3> +/// +/// If you want to delete your account on the server, you can do that using +/// deleteAccount(). The result of the IQ request is not handled. If you want +/// to do that manually, you can use the returned ID. +/// +/// \code +/// auto *registrationManager = client->findExtension<QXmppRegistrationManager>(); +/// QString deleteId = registrationManager->deleteAccount(); +/// if (deleteId.isEmpty()) { +/// // stanza could not be sent +/// } else { +/// // stanza was sent, you can handle the result using deleteId +/// } +/// \endcode +/// +/// <h3 id="register-account">Registering with a server</h3> +/// +/// Registering with a server consists of multiple steps: +/// -# Requesting the registration form from the server +/// -# Filling out the registration form +/// -# Sending the completed form to the server +/// - On failure (e.g. because of a username conflict), the process +/// continues at step 1 again. +/// -# Connecting with the newly created account +/// +/// <h4>Requesting the registration form from the server</h4> +/// +/// First of all, you need to enable the registration process in the +/// registration manager, which of course needs to be <a href="#activation"> +/// activated</a> in the client. +/// +/// \code +/// auto *registrationManager = client->findExtension<QXmppRegistrationManager>(); +/// registrationManager->setRegisterOnConnectEnabled(true); +/// \endcode +/// +/// After that you can start to connect to the server you want to register +/// with. No JID is set in the QXmppConfiguration for the client and instead +/// only the server is set. +/// +/// \code +/// QXmppConfiguration config; +/// config.setDomain("example.org"); +/// +/// client->connectToServer(config); +/// \endcode +/// +/// Alternatively, you can also provide a domain-only JID and no password to +/// connectToServer(): +/// +/// \code +/// client->connectToServer("example.org", QString()); +/// \endcode +/// +/// Now as soon as (START)TLS was handled, the registration manager interrupts +/// the normal connection process. The manager checks whether the server +/// supports in-band registration and whether the server advertises this as a +/// stream feature. +/// +/// If the server does not support in-band registration, the manager will abort +/// the connection at this point and emit the registrationFailed() signal with +/// a fixed QXmppStanza::Error of type QXmppStanza::Error::Cancel and with a +/// condition of QXmppStanza::Error::FeatureNotImplemented. +/// +/// The manager will now request the registration form. This will either result +/// in an error (reported by registrationFailed()) or, if everything went well, +/// the registration form is reported by registrationFormReceived(). +/// +/// To handle everything correctly, you need to connect to both signals: +/// +/// \code +/// connect(registrationManager, &QXmppRegistrationManager::registrationFormReceived, [=](const QXmppRegisterIq &iq) { +/// qDebug() << "Form received:" << iq.instructions(). +/// // you now need to complete the form +/// }); +/// connect(registrationManager, &QXmppRegistrationManager::registrationFailed, [=](const QXmppStanza::Error &error) { +/// qDebug() << "Requesting the registration form failed:" << error.text(); +/// }); +/// \endcode +/// +/// <h4>Filling out the registration form</h4> +/// +/// Now you need to fill out the registration form. If this requires user +/// interaction, it is recommended that you disconnect from the server at this +/// point now. This is required, because some servers will kick inactive, not +/// authorized clients after a few seconds. +/// +/// If the returned IQ contains a data form, that can be displayed to a user or +/// can be filled out in another way. +/// +/// If the server does not support data forms, you can check the standard +/// fields of the QXmppRegisterIq. You need to search the fields for empty +/// (non-null) strings. All fields that contain an empty string are required +/// and can be filled out. You can just set values for those fields and send +/// the form as described in the next step. +/// +/// \note QXmpp currently has only implemented the most important default +/// fields in the QXmppRegisterIq. The other fields are not very widespread, +/// because data forms are usually used for such purposes. +/// +/// <h4>Sending the completed form to the server</h4> +/// +/// <b>Option A</b>: If filling out the form goes very quick, you can set the +/// filled out form directly using setRegistrationFormToSend() and then +/// directly trigger the form to be sent using sendCachedRegistrationForm(). +/// +/// \code +/// registrationManager->setRegistrationFormToSend(completedForm); +/// registrationManager->sendCachedRegistrationForm(); +/// \endcode +/// +/// <b>Option B</b>: If filling out the form takes longer, i.e. because user +/// interaction is required, you should disconnect now. As soon as you have +/// completed the form, you can set it using setRegistrationFormToSend(). After +/// that you can reconnect to the server and the registration manager will +/// automatically send the set form. +/// +/// \code +/// client->disconnectFromServer(); +/// // user fills out form ... +/// registrationManager->setRegistrationFormToSend(completedForm); +/// +/// // As before, you only need to provide a domain to connectToServer() +/// client->connectToServer(...); +/// // the registration manager sends the form automatically +/// \endcode +/// +/// The form is now sent to the server. As soon as the result is received, +/// either registrationSucceeded() or registrationFailed() is emitted. +/// +/// In case there was a conflict or another error, you should request a new +/// form and restart the process. This is especially important, if the form can +/// only be used once as with most CAPTCHA implementations. +/// +/// <h4>Connecting with the newly created account</h4> +/// +/// You need to disconnect now. The user can then enter their credentials and +/// connect as usually. +/// +/// It is also possible to extract username and password from the sent form, +/// but that does not work always. There might also be forms that have no clear +/// username or password fields. +/// +/// \ingroup Managers +/// +/// \since QXmpp 1.2 +/// +class QXMPP_EXPORT QXmppRegistrationManager : public QXmppClientExtension +{ + Q_OBJECT + + /// Whether support of \xep{0077}: In-band Registration has been discovered on the server. + Q_PROPERTY(bool supportedByServer READ supportedByServer NOTIFY supportedByServerChanged) + +public: + QXmppRegistrationManager(); + ~QXmppRegistrationManager(); + + QStringList discoveryFeatures() const override; + + void changePassword(const QString &newPassword); + QString deleteAccount(); + + // documentation needs to be here, see https://stackoverflow.com/questions/49192523/ + /// + /// Returns whether the server supports registration. + /// + /// By default this is set to false and only changes, if you request the + /// service discovery info of the connected server using + /// QXmppDiscoveryManager::requestInfo(). + /// + /// This is only relevant to actions that happen after authentication. + /// + /// \sa QXmppRegistrationManager::supportedByServerChanged() + /// + bool supportedByServer() const; + + void requestRegistrationForm(const QString &service = {}); + + void setRegistrationFormToSend(const QXmppRegisterIq &iq); + void setRegistrationFormToSend(const QXmppDataForm &dataForm); + void sendCachedRegistrationForm(); + + bool registerOnConnectEnabled() const; + void setRegisterOnConnectEnabled(bool enabled); + + /// \cond + bool handleStanza(const QDomElement &stanza) override; + /// \endcond + +signals: + /// + /// Emitted, when registrationSupported() changed. + /// + /// This can happen after the service discovery info of the server was + /// retrieved using QXmppDiscoveryManager::requestInfo() or on disconnect. + /// + void supportedByServerChanged(); + + /// + /// Emitted, when the password of the account was changed successfully. + /// + /// The new password is automatically set in QXmppClient::configuration(). + /// + /// \param newPassword The new password that was set on the server. + /// + void passwordChanged(const QString &newPassword); + + /// + /// Emitted, when changing the password did not succeed. + /// + /// \param error Error returned from the service. + /// + void passwordChangeFailed(const QXmppStanza::Error &error); + + /// + /// Emitted, when a registration form has been received. + /// + /// When registering an account on the server and user interaction is + /// required now to complete the form, it is recommended to disconnect and + /// sending the completed registration form on reconnect using + /// QXmppRegistrationManager::setRegistrationFormToSend(). Some servers + /// (i.e. ejabberd) kick their clients after a timeout when they are not + /// active. This can be avoided this way. + /// + /// \param iq The received form. If it does not contain a valid data form + /// (see QXmppRegisterIq::form()), the required fields should be marked by + /// empty (but not null) strings in the QXmppRegisterIq (i.e. + /// QXmppRegisterIq::password().isNull() => false). + /// + void registrationFormReceived(const QXmppRegisterIq &iq); + + /// + /// Emitted, when the registration with a service completed successfully. + /// + /// To connect with the account you still need to set the correct + /// credentials in QXmppClient::configuration() and reconnect. + /// + void registrationSucceeded(); + + /// + /// Emitted, when the registration failed. + /// + /// \param error The returned error from the service. The reported errors + /// might be different from server to server, but the common ones are the + /// following: + /// \li type=Cancel and condition=Conflict: The username already exists. + /// \li type=Cancel and condition=NotAllowed: The CAPTCHA verification + /// failed. + /// \li type=Modify and condition=NotAcceptable: Some required information + /// was missing or the selected password was too weak. + /// \li type=Modify and condition=JidMalformed: No username was provided or + /// the username has an illegal format. + /// + void registrationFailed(const QXmppStanza::Error &error); + +protected: + void setClient(QXmppClient *client) override; + +private slots: + void handleDiscoInfo(const QXmppDiscoveryIq &iq); + +private: + void setSupportedByServer(bool supportedByServer); + + QScopedPointer<QXmppRegistrationManagerPrivate> d; +}; + +#endif // QXMPPREGISTRATIONMANAGER_H |
