diff options
author | Maurice Kalinowski <maurice.kalinowski@qt.io> | 2018-07-26 12:01:54 +0200 |
---|---|---|
committer | Maurice Kalinowski <maurice.kalinowski@qt.io> | 2018-08-02 09:15:55 +0000 |
commit | 4ffc9efc7ba30e0459a6653aac626c01a1f57173 (patch) | |
tree | 48fb58be9e8c7c24c772cd35eb64dc7622543d8f | |
parent | bcc2a4e7b5a38e7314e0444138c8271ecbced9d0 (diff) |
MQTT5: Introduce AUTH support
Task-number: QTPM-1455
Change-Id: I8eef500fe5eb0e8a36f449534c70e35487c04a7b
Reviewed-by: hjk <hjk@qt.io>
-rw-r--r-- | src/mqtt/mqtt.pro | 2 | ||||
-rw-r--r-- | src/mqtt/qmqttauthenticationproperties.cpp | 99 | ||||
-rw-r--r-- | src/mqtt/qmqttauthenticationproperties.h | 73 | ||||
-rw-r--r-- | src/mqtt/qmqttclient.cpp | 14 | ||||
-rw-r--r-- | src/mqtt/qmqttclient.h | 5 | ||||
-rw-r--r-- | src/mqtt/qmqttconnection.cpp | 150 | ||||
-rw-r--r-- | src/mqtt/qmqttconnection_p.h | 4 | ||||
-rw-r--r-- | src/mqtt/qmqttcontrolpacket_p.h | 1 | ||||
-rw-r--r-- | tests/auto/qmqttclient/tst_qmqttclient.cpp | 30 |
9 files changed, 378 insertions, 0 deletions
diff --git a/src/mqtt/mqtt.pro b/src/mqtt/mqtt.pro index 13efe74..adbfc54 100644 --- a/src/mqtt/mqtt.pro +++ b/src/mqtt/mqtt.pro @@ -8,6 +8,7 @@ QT += core-private QMAKE_DOCS = $$PWD/doc/qtmqtt.qdocconf PUBLIC_HEADERS += \ + qmqttauthenticationproperties.h \ qmqttglobal.h \ qmqttclient.h \ qmqttconnectionproperties.h \ @@ -28,6 +29,7 @@ PRIVATE_HEADERS += \ qmqttsubscription_p.h SOURCES += \ + qmqttauthenticationproperties.cpp \ qmqttclient.cpp \ qmqttconnection.cpp \ qmqttconnectionproperties.cpp \ diff --git a/src/mqtt/qmqttauthenticationproperties.cpp b/src/mqtt/qmqttauthenticationproperties.cpp new file mode 100644 index 0000000..778aa6b --- /dev/null +++ b/src/mqtt/qmqttauthenticationproperties.cpp @@ -0,0 +1,99 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qmqttauthenticationproperties.h" + +QT_BEGIN_NAMESPACE + +class QMqttAuthenticationPropertiesData : public QSharedData +{ +public: + QString authenticationMethod; + QByteArray authenticationData; + QString reason; + QMqttUserProperties userProperties; +}; + +QMqttAuthenticationProperties::QMqttAuthenticationProperties() : data(new QMqttAuthenticationPropertiesData) +{ + +} + +QMqttAuthenticationProperties::QMqttAuthenticationProperties(const QMqttAuthenticationProperties &) = default; + +QMqttAuthenticationProperties &QMqttAuthenticationProperties::operator=(const QMqttAuthenticationProperties &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +QMqttAuthenticationProperties::~QMqttAuthenticationProperties() = default; + +QString QMqttAuthenticationProperties::authenticationMethod() const +{ + return data->authenticationMethod; +} + +void QMqttAuthenticationProperties::setAuthenticationMethod(const QString &method) +{ + data->authenticationMethod = method; +} + +QByteArray QMqttAuthenticationProperties::authenticationData() const +{ + return data->authenticationData; +} + +void QMqttAuthenticationProperties::setAuthenticationData(const QByteArray &adata) +{ + data->authenticationData = adata; +} + +QString QMqttAuthenticationProperties::reason() const +{ + return data->reason; +} + +void QMqttAuthenticationProperties::setReason(const QString &r) +{ + data->reason = r; +} + +QMqttUserProperties QMqttAuthenticationProperties::userProperties() const +{ + return data->userProperties; +} + +void QMqttAuthenticationProperties::setUserProperties(const QMqttUserProperties &user) +{ + data->userProperties = user; +} + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqttauthenticationproperties.h b/src/mqtt/qmqttauthenticationproperties.h new file mode 100644 index 0000000..b587338 --- /dev/null +++ b/src/mqtt/qmqttauthenticationproperties.h @@ -0,0 +1,73 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QMQTTAUTHENTICATIONPROPERTIES_H +#define QMQTTAUTHENTICATIONPROPERTIES_H + +#include <QtMqtt/qmqttglobal.h> +#include <QtMqtt/qmqtttype.h> + +#include <QtCore/QHash> +#include <QtCore/QObject> +#include <QtCore/QSharedDataPointer> +#include <QtCore/QString> + +QT_BEGIN_NAMESPACE + +class QMqttAuthenticationPropertiesData; + +class Q_MQTT_EXPORT QMqttAuthenticationProperties +{ + Q_GADGET + +public: + QMqttAuthenticationProperties(); + QMqttAuthenticationProperties(const QMqttAuthenticationProperties &); + QMqttAuthenticationProperties &operator=(const QMqttAuthenticationProperties &); + ~QMqttAuthenticationProperties(); + + QString authenticationMethod() const; + void setAuthenticationMethod(const QString &method); + + QByteArray authenticationData() const; + void setAuthenticationData(const QByteArray &adata); + + QString reason() const; + void setReason(const QString &r); + + QMqttUserProperties userProperties() const; + void setUserProperties(const QMqttUserProperties &user); + +private: + QSharedDataPointer<QMqttAuthenticationPropertiesData> data; +}; + +QT_END_NAMESPACE + +#endif // QMQTTAUTHENTICATIONPROPERTIES_H diff --git a/src/mqtt/qmqttclient.cpp b/src/mqtt/qmqttclient.cpp index 5bf862e..948ea9a 100644 --- a/src/mqtt/qmqttclient.cpp +++ b/src/mqtt/qmqttclient.cpp @@ -553,6 +553,20 @@ QMqttServerConnectionProperties QMqttClient::serverConnectionProperties() const return d->m_serverConnectionProperties; } +void QMqttClient::authenticate(const QMqttAuthenticationProperties &prop) +{ + Q_D(QMqttClient); + if (protocolVersion() != QMqttClient::MQTT_5_0) { + qCDebug(lcMqttClient) << "Authentication is only supported on protocol level 5."; + return; + } + if (state() == QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Cannot send authentication request while disconnected."; + return; + } + d->m_connection.sendControlAuthenticate(prop); +} + QMqttClient::ClientError QMqttClient::error() const { Q_D(const QMqttClient); diff --git a/src/mqtt/qmqttclient.h b/src/mqtt/qmqttclient.h index d0ec8a1..d462e35 100644 --- a/src/mqtt/qmqttclient.h +++ b/src/mqtt/qmqttclient.h @@ -31,6 +31,7 @@ #define QTMQTTCLIENT_H #include <QtMqtt/qmqttglobal.h> +#include <QtMqtt/qmqttauthenticationproperties.h> #include <QtMqtt/qmqttconnectionproperties.h> #include <QtMqtt/qmqttpublishproperties.h> #include <QtMqtt/qmqttsubscription.h> @@ -148,6 +149,8 @@ public: QMqttLastWillProperties lastWillProperties() const; QMqttServerConnectionProperties serverConnectionProperties() const; + + void authenticate(const QMqttAuthenticationProperties &prop); Q_SIGNALS: void connected(); void disconnected(); @@ -172,6 +175,8 @@ Q_SIGNALS: void willMessageChanged(QByteArray willMessage); void willRetainChanged(bool willRetain); + void authenticationRequested(const QMqttAuthenticationProperties &p); + void authenticationFinished(const QMqttAuthenticationProperties &p); public Q_SLOTS: void setHostname(const QString &hostname); void setPort(quint16 port); diff --git a/src/mqtt/qmqttconnection.cpp b/src/mqtt/qmqttconnection.cpp index 1337911..9d58959 100644 --- a/src/mqtt/qmqttconnection.cpp +++ b/src/mqtt/qmqttconnection.cpp @@ -81,6 +81,15 @@ QString QMqttConnection::readBufferTyped(qint64 *dataSize) return QString::fromUtf8(reinterpret_cast<const char *>(readBuffer(size).constData()), size); } +template<> +QByteArray QMqttConnection::readBufferTyped(qint64 *dataSize) +{ + const quint16 size = readBufferTyped<quint16>(dataSize); + if (dataSize) + *dataSize -= size; + return QByteArray(reinterpret_cast<const char *>(readBuffer(size).constData()), size); +} + QMqttConnection::QMqttConnection(QObject *parent) : QObject(parent) { m_pingTimer.setSingleShot(false); @@ -299,6 +308,36 @@ bool QMqttConnection::sendControlConnect() return true; } +bool QMqttConnection::sendControlAuthenticate(const QMqttAuthenticationProperties &properties) +{ + qCDebug(lcMqttConnection) << Q_FUNC_INFO; + + QMqttControlPacket packet(QMqttControlPacket::AUTH); + + switch (m_clientPrivate->m_state) { + case QMqttClient::Disconnected: + qCDebug(lcMqttConnection) << "Using AUTH while disconnected."; + return false; + case QMqttClient::Connecting: + qCDebug(lcMqttConnection) << "AUTH while connecting, set continuation flag."; + packet.append(char(0x18)); + break; + case QMqttClient::Connected: + qCDebug(lcMqttConnection) << "AUTH while connected, initiate re-authentication."; + packet.append(char(0x19)); + break; + } + + packet.appendRaw(writeAuthenticationProperties(properties)); + + if (!writePacketToTransport(packet)) { + qCDebug(lcMqttConnection) << "Could not write AUTH frame to transport."; + return false; + } + + return true; +} + qint32 QMqttConnection::sendControlPublish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos, @@ -647,6 +686,47 @@ QByteArray QMqttConnection::readBuffer(quint64 size) return res; } +void QMqttConnection::readAuthProperties(QMqttAuthenticationProperties &properties) +{ + qint64 propertyLength = readVariableByteInteger(); + m_missingData = 0; + + QMqttUserProperties userProperties; + while (propertyLength > 0) { + quint8 propertyId = readBufferTyped<quint8>(); + propertyLength--; + + switch (propertyId) { + case 0x15: { //3.15.2.2.2 Authentication Method + const QString method = readBufferTyped<QString>(&propertyLength); + properties.setAuthenticationMethod(method); + break; + } + case 0x16: { // 3.15.2.2.3 Authentication Data + const QByteArray data = readBufferTyped<QByteArray>(&propertyLength); + properties.setAuthenticationData(data); + break; + } + case 0x1F: { // 3.15.2.2.4 Reason String + const QString reasonString = readBufferTyped<QString>(&propertyLength); + properties.setReason(reasonString); + break; + } + case 0x26: { // 3.15.2.2.5 User property + const QString propertyName = readBufferTyped<QString>(&propertyLength); + const QString propertyValue = readBufferTyped<QString>(&propertyLength); + + userProperties.append(QMqttStringPair(propertyName, propertyValue)); + break; + } + default: + qCDebug(lcMqttConnection) << "Unknown property id in AUTH:" << propertyId; + break; + } + } + properties.setUserProperties(userProperties); +} + void QMqttConnection::readConnackProperties() { qint64 propertyLength = readVariableByteInteger(); @@ -1156,6 +1236,73 @@ QByteArray QMqttConnection::writeUnsubscriptionProperties(const QMqttUnsubscript return packet.serializePayload(); } +QByteArray QMqttConnection::writeAuthenticationProperties(const QMqttAuthenticationProperties &properties) +{ + QMqttControlPacket packet; + + // 3.15.2.2.2 + if (!properties.authenticationMethod().isEmpty()) { + packet.append(char(0x15)); + packet.append(properties.authenticationMethod().toUtf8()); + } + // 3.15.2.2.3 + if (!properties.authenticationData().isEmpty()) { + packet.append(char(0x16)); + packet.append(properties.authenticationData()); + } + + // 3.15.2.2.4 + if (!properties.reason().isEmpty()) { + packet.append(char(0x1F)); + packet.append(properties.reason().toUtf8()); + } + + // 3.15.2.2.5 + auto userProperties = properties.userProperties(); + if (!userProperties.isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Unsubscription Properties: specify user properties"; + for (const auto &prop : userProperties) { + packet.append(char(0x26)); + packet.append(prop.name().toUtf8()); + packet.append(prop.value().toUtf8()); + } + } + + return packet.serializePayload(); +} + +void QMqttConnection::finalize_auth() +{ + qCDebug(lcMqttConnectionVerbose) << "Finalize AUTH"; + + const quint8 authReason = readBufferTyped<quint8>(); + m_missingData--; + QMqttAuthenticationProperties authProperties; + // 3.15.2.1 - The Reason Code and Property Length can be omitted if the Reason Code + // is 0x00 (Success) and there are no Properties. In this case the AUTH has a + // Remaining Length of 0. + if (m_missingData == 0 && authReason != 0) { + qCDebug(lcMqttConnection) << "Received non success AUTH without properties."; + closeConnection(QMqttClient::ProtocolViolation); + return; + } else if (m_missingData > 0) + readAuthProperties(authProperties); + + switch (authReason) { + case 0x00: // Success + emit m_clientPrivate->m_client->authenticationFinished(authProperties); + break; + case 0x18: // Continue Authentication + case 0x19: // Re-authenticate + emit m_clientPrivate->m_client->authenticationRequested(authProperties); + break; + default: + qCDebug(lcMqttConnection) << "Received illegal AUTH reason code:" << authReason; + closeConnection(QMqttClient::ProtocolViolation); + break; + } +} + void QMqttConnection::finalize_connack() { qCDebug(lcMqttConnectionVerbose) << "Finalize CONNACK"; @@ -1413,6 +1560,9 @@ bool QMqttConnection::processDataHelper() return false; switch (m_currentPacket & 0xF0) { + case QMqttControlPacket::AUTH: + finalize_auth(); + break; case QMqttControlPacket::CONNACK: finalize_connack(); break; diff --git a/src/mqtt/qmqttconnection_p.h b/src/mqtt/qmqttconnection_p.h index 6eee164..b8da93a 100644 --- a/src/mqtt/qmqttconnection_p.h +++ b/src/mqtt/qmqttconnection_p.h @@ -77,6 +77,7 @@ public: bool ensureTransportOpen(const QString &sslPeerName = QString()); bool sendControlConnect(); + bool sendControlAuthenticate(const QMqttAuthenticationProperties &properties); qint32 sendControlPublish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos = 0, bool retain = false, const QMqttPublishProperties &properties = QMqttPublishProperties()); bool sendControlPublishAcknowledge(quint16 id); @@ -107,6 +108,7 @@ public: QMqttClientPrivate *m_clientPrivate{nullptr}; private: Q_DISABLE_COPY(QMqttConnection) + void finalize_auth(); void finalize_connack(); void finalize_suback(); void finalize_unsuback(); @@ -118,6 +120,7 @@ private: bool processDataHelper(); void readBuffer(char *data, quint64 size); qint32 readVariableByteInteger(qint32 *byteCount = nullptr); + void readAuthProperties(QMqttAuthenticationProperties &properties); void readConnackProperties(); void readPublishProperties(QMqttPublishProperties &properties); void readSubscriptionProperties(QMqttSubscription *sub); @@ -126,6 +129,7 @@ private: QByteArray writePublishProperties(const QMqttPublishProperties &properties); QByteArray writeSubscriptionProperties(const QMqttSubscriptionProperties &properties); QByteArray writeUnsubscriptionProperties(const QMqttUnsubscriptionProperties &properties); + QByteArray writeAuthenticationProperties(const QMqttAuthenticationProperties &properties); void closeConnection(QMqttClient::ClientError error); QByteArray readBuffer(quint64 size); template<typename T> T readBufferTyped(qint64 *dataSize = nullptr); diff --git a/src/mqtt/qmqttcontrolpacket_p.h b/src/mqtt/qmqttcontrolpacket_p.h index a5f17e7..740b1c4 100644 --- a/src/mqtt/qmqttcontrolpacket_p.h +++ b/src/mqtt/qmqttcontrolpacket_p.h @@ -66,6 +66,7 @@ public: PINGREQ = 0xC0, PINGRESP = 0xD0, DISCONNECT = 0xE0, + AUTH = 0xF0, }; QMqttControlPacket(); diff --git a/tests/auto/qmqttclient/tst_qmqttclient.cpp b/tests/auto/qmqttclient/tst_qmqttclient.cpp index 6d81e27..f2e0888 100644 --- a/tests/auto/qmqttclient/tst_qmqttclient.cpp +++ b/tests/auto/qmqttclient/tst_qmqttclient.cpp @@ -68,6 +68,7 @@ private Q_SLOTS: void openIODevice_QTBUG66955(); void staticProperties_QTBUG_67176_data(); void staticProperties_QTBUG_67176(); + void authentication(); private: QProcess m_brokerProcess; QString m_testBroker; @@ -595,6 +596,35 @@ void Tst_QMqttClient::staticProperties_QTBUG_67176() QCOMPARE(client.cleanSession(), clean); } +void Tst_QMqttClient::authentication() +{ + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + + QMqttConnectionProperties connectionProperties; + connectionProperties.setAuthenticationMethod(QLatin1String("SCRAM-SHA-1")); + client.setConnectionProperties(connectionProperties); + + connect(&client, &QMqttClient::authenticationRequested, [](const QMqttAuthenticationProperties &prop) + { + qDebug() << "Authentication requested:" << prop.authenticationMethod(); + }); + + connect(&client, &QMqttClient::authenticationFinished, [](const QMqttAuthenticationProperties &prop) + { + qDebug() << "Authentication finished:" << prop.authenticationMethod(); + }); + + // ### FIXME : There is no public test broker yet able to handle authentication methods + // Theoretically the broker should send an AUTH request, followed by AUTH call including + // authentication data. See 4.12 of MQTT v5 specs. + QSKIP("No broker available with enhanced authentication."); + client.connectToHost(); + QTRY_COMPARE(client.state(), QMqttClient::Connected); +} + QTEST_MAIN(Tst_QMqttClient) #include "tst_qmqttclient.moc" |