diff options
56 files changed, 6368 insertions, 296 deletions
diff --git a/.qmake.conf b/.qmake.conf index 56c0f31..81caa27 100644 --- a/.qmake.conf +++ b/.qmake.conf @@ -1,4 +1,4 @@ load(qt_build_config) CONFIG += warning_clean -MODULE_VERSION = 5.11.2 +MODULE_VERSION = 5.12.0 diff --git a/examples/mqtt/mqtt.pro b/examples/mqtt/mqtt.pro index 767ed5e..c734248 100644 --- a/examples/mqtt/mqtt.pro +++ b/examples/mqtt/mqtt.pro @@ -4,4 +4,5 @@ SUBDIRS += \ subscriptions qtHaveModule(quick): SUBDIRS += quicksubscription +qtHaveModule(quick): SUBDIRS += quickpublication qtHaveModule(websockets): SUBDIRS += websocketsubscription diff --git a/examples/mqtt/quickpublication/main.cpp b/examples/mqtt/quickpublication/main.cpp new file mode 100644 index 0000000..a1bd557 --- /dev/null +++ b/examples/mqtt/quickpublication/main.cpp @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qmlmqttclient.h" + +#include <QGuiApplication> +#include <QQmlApplicationEngine> +#include <QLoggingCategory> + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + + qmlRegisterType<QmlMqttClient>("MqttClient", 1, 0, "MqttClient"); + + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + if (engine.rootObjects().isEmpty()) + return -1; + + return app.exec(); +} diff --git a/examples/mqtt/quickpublication/main.qml b/examples/mqtt/quickpublication/main.qml new file mode 100644 index 0000000..b018e68 --- /dev/null +++ b/examples/mqtt/quickpublication/main.qml @@ -0,0 +1,221 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.8 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import MqttClient 1.0 + +Window { + visible: true + width: 640 + height: 480 + title: qsTr("Qt Quick MQTT Publish Example") + id: root + + MqttClient { + id: client + hostname: hostnameField.text + port: portField.text + } + + ListModel { + id: messageModel + } + + function addMessage(payload) + { + messageModel.insert(0, {"payload" : payload}) + + if (messageModel.count >= 100) + messageModel.remove(99) + } + + GridLayout { + anchors.fill: parent + columns: 2 + + Label { + text: "Hostname:" + enabled: client.state === MqttClient.Disconnected + } + + TextField { + id: hostnameField + Layout.fillWidth: true + text: "test.mosquitto.org" + placeholderText: "<Enter host running MQTT broker>" + enabled: client.state === MqttClient.Disconnected + } + + Label { + text: "Port:" + enabled: client.state === MqttClient.Disconnected + } + + TextField { + id: portField + Layout.fillWidth: true + text: "1883" + placeholderText: "<Port>" + inputMethodHints: Qt.ImhDigitsOnly + enabled: client.state === MqttClient.Disconnected + } + + Button { + id: connectButton + Layout.columnSpan: 2 + text: client.state === MqttClient.Connected ? "Disconnect" : "Connect" + onClicked: { + if (client.state === MqttClient.Connected) + client.disconnectFromHost() + else + client.connectToHost() + } + } + + RowLayout { + enabled: client.state === MqttClient.Connected + Layout.columnSpan: 2 + Layout.fillWidth: true + + Label { + text: "Topic:" + } + + TextField { + id: pubField + placeholderText: "<Publication topic>" + } + + Label { + text: "Message:" + } + + TextField { + id: msgField + placeholderText: "<Publication message>" + } + } + + RowLayout { + enabled: client.state === MqttClient.Connected + Layout.columnSpan: 2 + Layout.fillWidth: true + + Label { + text: "QoS:" + } + + ComboBox { + id: qosItems + editable: false + model: [0, 1, 2] + } + + CheckBox { + id: retain + checked: false + text: "Retain" + } + + Button { + id: pubButton + text: "Publish" + onClicked: { + if (pubField.text.length === 0) { + console.log("No payload to send. Skipping publish...") + return + } + client.publish(pubField.text, msgField.text, qosItems.currentText, retain.checked) + addMessage(msgField.text) + } + } + } + + ListView { + id: messageView + model: messageModel + height: 300 + width: 200 + Layout.fillHeight: true + delegate: Rectangle { + width: 150 + height: 30 + color: index % 2 ? "#DDDDDD" : "#888888" + radius: 5 + Text { + text: payload + anchors.centerIn: parent + } + } + } + + Label { + function stateToString(value) { + if (value === 0) + return "Disconnected" + else if (value === 1) + return "Connecting" + else if (value === 2) + return "Connected" + else + return "Unknown" + } + + Layout.columnSpan: 2 + Layout.fillWidth: true + color: "#333333" + text: "Status:" + stateToString(client.state) + "(" + client.state + ")" + enabled: client.state === MqttClient.Connected + } + } +} diff --git a/examples/mqtt/quickpublication/qml.qrc b/examples/mqtt/quickpublication/qml.qrc new file mode 100644 index 0000000..5f6483a --- /dev/null +++ b/examples/mqtt/quickpublication/qml.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file>main.qml</file> + </qresource> +</RCC> diff --git a/examples/mqtt/quickpublication/qmlmqttclient.cpp b/examples/mqtt/quickpublication/qmlmqttclient.cpp new file mode 100644 index 0000000..28f69fc --- /dev/null +++ b/examples/mqtt/quickpublication/qmlmqttclient.cpp @@ -0,0 +1,64 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qmlmqttclient.h" +#include <QDebug> +#include <QMqttTopicName> + +QmlMqttClient::QmlMqttClient(QObject *parent) + : QMqttClient(parent) +{ +} + +int QmlMqttClient::publish(const QString &topic, const QString &message, int qos, bool retain) +{ + auto result = QMqttClient::publish(QMqttTopicName(topic), message.toUtf8(), qos, retain); + return result; +} diff --git a/examples/mqtt/quickpublication/qmlmqttclient.h b/examples/mqtt/quickpublication/qmlmqttclient.h new file mode 100644 index 0000000..85b6532 --- /dev/null +++ b/examples/mqtt/quickpublication/qmlmqttclient.h @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QMLMQTTCLIENT_H +#define QMLMQTTCLIENT_H + +#include <QtCore/QMap> +#include <QtMqtt/QMqttClient> + +class QmlMqttClient : public QMqttClient +{ + Q_OBJECT +public: + QmlMqttClient(QObject *parent = nullptr); + + Q_INVOKABLE int publish(const QString &topic, const QString &message, int qos = 0, bool retain = false); +private: + Q_DISABLE_COPY(QmlMqttClient) +}; + +#endif // QMLMQTTCLIENT_H diff --git a/examples/mqtt/quickpublication/quickpublication.pro b/examples/mqtt/quickpublication/quickpublication.pro new file mode 100644 index 0000000..20bdcc7 --- /dev/null +++ b/examples/mqtt/quickpublication/quickpublication.pro @@ -0,0 +1,32 @@ +TEMPLATE = app + +QT += qml quick mqtt +CONFIG += c++11 + +SOURCES += main.cpp \ + qmlmqttclient.cpp + +HEADERS += \ + qmlmqttclient.h + +RESOURCES += qml.qrc + +# Additional import path used to resolve QML modules in Qt Creator's code model +QML_IMPORT_PATH = + +# Additional import path used to resolve QML modules just for Qt Quick Designer +QML_DESIGNER_IMPORT_PATH = + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which as been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +target.path = $$[QT_INSTALL_EXAMPLES]/mqtt/quickpublication +INSTALLS += target diff --git a/examples/mqtt/quicksubscription/main.qml b/examples/mqtt/quicksubscription/main.qml index d42eddd..7b930e2 100644 --- a/examples/mqtt/quicksubscription/main.qml +++ b/examples/mqtt/quicksubscription/main.qml @@ -58,7 +58,7 @@ Window { visible: true width: 640 height: 480 - title: qsTr("Hello World") + title: qsTr("Qt Quick MQTT Subscription Example") id: root property var tempSubscription: 0 @@ -93,7 +93,6 @@ Window { TextField { id: hostnameField Layout.fillWidth: true -// text: "broker.hivemq.com" text: "test.mosquitto.org" placeholderText: "<Enter host running MQTT broker>" enabled: client.state === MqttClient.Disconnected @@ -143,8 +142,10 @@ Window { id: subButton text: "Subscribe" onClicked: { - if (subField.text.length === 0) + if (subField.text.length === 0) { + console.log("No topic specified to subscribe to.") return + } tempSubscription = client.subscribe(subField.text) tempSubscription.messageReceived.connect(addMessage) } diff --git a/examples/mqtt/websocketsubscription/main.cpp b/examples/mqtt/websocketsubscription/main.cpp index 03ca84e..a91fc89 100644 --- a/examples/mqtt/websocketsubscription/main.cpp +++ b/examples/mqtt/websocketsubscription/main.cpp @@ -99,7 +99,7 @@ int main(int argc, char *argv[]) } else if (versionString == "3") { clientsub.setVersion(3); } else { - qWarning() << "Unknown MQTT version"; + qInfo() << "Unknown MQTT version"; return -2; } diff --git a/src/mqtt/doc/src/index.qdoc b/src/mqtt/doc/src/index.qdoc index fd50391..92aacbb 100644 --- a/src/mqtt/doc/src/index.qdoc +++ b/src/mqtt/doc/src/index.qdoc @@ -41,8 +41,8 @@ The Qt MQTT module provides a standard compliant implementation of the MQTT protocol specification. It enables applications to act as telemetry displays - and devices to publish telemetry data. The supported versions are MQTT 3.1 - and MQTT 3.1.1. + and devices to publish telemetry data. The supported versions are MQTT 3.1, + MQTT 3.1.1, and MQTT 5.0. \note Qt MQTT is part of the Qt for Automation offering and not Qt. For further details please see \l {Qt for Automation}. diff --git a/src/mqtt/doc/src/overview.qdoc b/src/mqtt/doc/src/overview.qdoc index 64f5fab..5ba73c1 100644 --- a/src/mqtt/doc/src/overview.qdoc +++ b/src/mqtt/doc/src/overview.qdoc @@ -90,6 +90,61 @@ receive messages on the temperature of all rooms in all apartments in a house. + \section1 Shared Subscriptions + + \e {Shared subscriptions} describe a pool of subscribers to one topic + filter. Instead of all subscribers receiving a message, only one subscriber + receives it. This enables load balancing on multiple clients. + The format of a shared subscription is: + + \badcode + $share/{sharename}/{topicfilter} + \endcode + + For example, if \e {Client 1} and \e {Client 2} should share a subscription + to the topic \e {sensors/house/temperature}, the topic filter to subscribe + to is: + + \badcode + $share/poolAB/sensors/house/temperature + \endcode + + It is not defined in which order messages are distributed by the server. + This is a server specific option. + + To identify whether a server supports shared subscriptions, see also + QMqttServerConnectionProperties::sharedSubscriptionSupported(). + + \section1 Topic Aliases + + Structuring topics in a tree helps to separate data channels and provide a + logical order of information. However, this can lead to very long topic + names being used during the publication of messages, hence increasing + the size of each message. + + The MQTT 5.0 protocol version introduced \e {topic aliases} to circumvent + this. Instead of the topic string, an integer value is sent. To create an + initial mapping between the client and the server, both the topic string and + the alias need to be part of a message. Thereafter, only the ID with an + empty topic is used. + + This mapping can be changed at any time by using a topic alias with another + topic string. Note that this mapping does not necessarily apply to other + connections, such as connections from the server to other clients. Each + connection needs to create this mapping manually. + + Qt MQTT provides an automated mechanism to help reduce data rates. After + QMqttClient creates a connection, information about topic aliases supported + by the server is stored. Subsequently, topic aliases are used in the + order the messages are published, until all available aliases are in use. A + user is always able to modify this mapping by using + QMqttPublishProperties::setTopicAlias() during publication. + + When QMqttClient subscribes to a topic, the server can use topic aliases + as well, depending on the QMqttConnectionProperties::maximumTopicAlias() + value set by the client. The client automatically maps topic aliases and + transparently forwards messages to the user including the full topic string. + \section1 Security The connections between the clients and the broker are secured by an @@ -145,4 +200,5 @@ any previously retained message for its topic at the broker \e{must} be discarded. The broker \e{should} store the last message, but \e{may} also discard it. This depends on the implementation of the broker. + */ diff --git a/src/mqtt/mqtt.pro b/src/mqtt/mqtt.pro index c7866ba..aa6c99c 100644 --- a/src/mqtt/mqtt.pro +++ b/src/mqtt/mqtt.pro @@ -8,27 +8,40 @@ QT += core-private QMAKE_DOCS = $$PWD/doc/qtmqtt.qdocconf PUBLIC_HEADERS += \ + qmqttauthenticationproperties.h \ qmqttglobal.h \ qmqttclient.h \ + qmqttconnectionproperties.h \ qmqttmessage.h \ + qmqttpublishproperties.h \ qmqttsubscription.h \ + qmqttsubscriptionproperties.h \ qmqtttopicfilter.h \ - qmqtttopicname.h + qmqtttopicname.h \ + qmqtttype.h PRIVATE_HEADERS += \ qmqttclient_p.h \ qmqttconnection_p.h \ + qmqttconnectionproperties_p.h \ qmqttcontrolpacket_p.h \ + qmqttmessage_p.h \ + qmqttpublishproperties_p.h \ qmqttsubscription_p.h SOURCES += \ + qmqttauthenticationproperties.cpp \ qmqttclient.cpp \ qmqttconnection.cpp \ + qmqttconnectionproperties.cpp \ qmqttcontrolpacket.cpp \ qmqttmessage.cpp \ + qmqttpublishproperties.cpp \ qmqttsubscription.cpp \ + qmqttsubscriptionproperties.cpp \ qmqtttopicfilter.cpp \ - qmqtttopicname.cpp + qmqtttopicname.cpp \ + qmqtttype.cpp HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS diff --git a/src/mqtt/qmqttauthenticationproperties.cpp b/src/mqtt/qmqttauthenticationproperties.cpp new file mode 100644 index 0000000..0425939 --- /dev/null +++ b/src/mqtt/qmqttauthenticationproperties.cpp @@ -0,0 +1,149 @@ +/****************************************************************************** +** +** 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 QMqttAuthenticationProperties + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttAuthenticationProperties class represents configuration + options during the authentication process. + + \note Authentication properties are part of the MQTT 5.0 specification and + cannot be used when connecting with a lower protocol level. See + QMqttClient::ProtocolVersion for more information. +*/ + +class QMqttAuthenticationPropertiesData : public QSharedData +{ +public: + QString authenticationMethod; + QByteArray authenticationData; + QString reason; + QMqttUserProperties userProperties; +}; + +/*! + \internal +*/ +QMqttAuthenticationProperties::QMqttAuthenticationProperties() : data(new QMqttAuthenticationPropertiesData) +{ + +} + +/*! + \internal +*/ +QMqttAuthenticationProperties::QMqttAuthenticationProperties(const QMqttAuthenticationProperties &) = default; + +QMqttAuthenticationProperties &QMqttAuthenticationProperties::operator=(const QMqttAuthenticationProperties &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +QMqttAuthenticationProperties::~QMqttAuthenticationProperties() = default; + +/*! + Returns the authentication method. +*/ +QString QMqttAuthenticationProperties::authenticationMethod() const +{ + return data->authenticationMethod; +} + +/*! + Sets the authentication method to \a method. +*/ +void QMqttAuthenticationProperties::setAuthenticationMethod(const QString &method) +{ + data->authenticationMethod = method; +} + +/*! + Returns the authentication data +*/ +QByteArray QMqttAuthenticationProperties::authenticationData() const +{ + return data->authenticationData; +} + +/*! + Sets the authentication data to \a adata. + + Authentication data can only be used if an authentication method has + been specified. + + \sa authenticationMethod() +*/ +void QMqttAuthenticationProperties::setAuthenticationData(const QByteArray &adata) +{ + data->authenticationData = adata; +} + +/*! + Returns the reason string. The reason string specifies the reason for + a disconnect. +*/ +QString QMqttAuthenticationProperties::reason() const +{ + return data->reason; +} + +/*! + Sets the reason string to \a r. +*/ +void QMqttAuthenticationProperties::setReason(const QString &r) +{ + data->reason = r; +} + +/*! + Returns the user properties. +*/ +QMqttUserProperties QMqttAuthenticationProperties::userProperties() const +{ + return data->userProperties; +} + +/*! + Sets the user properties to \a user. +*/ +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 5150663..fd6ca5d 100644 --- a/src/mqtt/qmqttclient.cpp +++ b/src/mqtt/qmqttclient.cpp @@ -30,11 +30,14 @@ #include "qmqttclient.h" #include "qmqttclient_p.h" +#include <QtCore/QLoggingCategory> #include <QtCore/QUuid> #include <QtCore/QtEndian> QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcMqttClient, "qt.mqtt.client") + /*! \class QMqttClient @@ -207,6 +210,9 @@ QT_BEGIN_NAMESPACE connection. \value UnknownError An unknown error occurred. + \value Mqtt5SpecificError + The error is related to MQTT protocol level 5. A reason code might + provide more details. */ /*! @@ -219,6 +225,8 @@ QT_BEGIN_NAMESPACE MQTT Standard 3.1 \value MQTT_3_1_1 MQTT Standard 3.1.1, publicly referred to as version 4 + \value MQTT_5_0 + MQTT Standard 5.0 */ /*! @@ -243,6 +251,15 @@ QT_BEGIN_NAMESPACE */ /*! + \fn QMqttClient::messageStatusChanged(qint32 id, QMqtt::MessageStatus s, const QMqttMessageStatusProperties &properties); + \since 5.12 + + This signal is emitted when the status for the message identified by \a id + changes. \a s specifies the new status of the message, and + \a properties specify additional properties provided by the server. +*/ + +/*! \fn QMqttClient::messageSent(qint32 id) Indicates that a message that was sent via the publish() function has been @@ -269,6 +286,37 @@ QT_BEGIN_NAMESPACE */ /*! + \since 5.12 + \fn QMqttClient::authenticationRequested(const QMqttAuthenticationProperties &p) + + This signal is emitted after a client invoked QMqttClient::connectToHost or + QMqttClient::connectToHostEncrypted and before the connection is + established. In extended authentication, a broker might request additional + details which need to be provided by invoking QMqttClient::authenticate. + \a p specifies properties provided by the broker. + + \note Extended authentication is part of the MQTT 5.0 standard and can + only be used when the client specifies MQTT_5_0 as ProtocolVersion. + + \sa authenticationFinished(), authenticate() +*/ + +/*! + \since 5.12 + \fn QMqttClient::authenticationFinished(const QMqttAuthenticationProperties &p) + + This signal is emitted after extended authentication has finished. \a p + specifies available details on the authentication process. + + After successful authentication QMqttClient::connected is emitted. + + \note Extended authentication is part of the MQTT 5.0 standard and can + only be used when the client specifies MQTT_5_0 as ProtocolVersion. + + \sa authenticationRequested(), authenticate() +*/ + +/* Creates a new MQTT client instance with the specified \a parent. */ QMqttClient::QMqttClient(QObject *parent) : QObject(*(new QMqttClientPrivate(this)), parent) @@ -293,7 +341,7 @@ void QMqttClient::setTransport(QIODevice *device, QMqttClient::TransportType tra Q_D(QMqttClient); if (d->m_state != Disconnected) { - qWarning("Changing transport layer while connected is not possible"); + qCDebug(lcMqttClient) << "Changing transport layer while connected is not possible."; return; } d->m_connection.setTransport(device, transport); @@ -319,12 +367,33 @@ QIODevice *QMqttClient::transport() const */ QMqttSubscription *QMqttClient::subscribe(const QMqttTopicFilter &topic, quint8 qos) { + return subscribe(topic, QMqttSubscriptionProperties(), qos); +} + +/*! + \since 5.12 + + Adds a new subscription to receive notifications on \a topic. The parameter + \a properties specifies additional subscription properties to be validated + by the broker. The parameter \a qos specifies the level at which security + messages are received. For more information about the available QoS levels, + see \l {Quality of Service}. + + This function returns a pointer to a \l QMqttSubscription. If the same topic + is subscribed twice, the return value points to the same subscription + instance. The MQTT client is the owner of the subscription. + + \note \a properties will only be passed to the broker when the client + specifies MQTT_5_0 as ProtocolVersion. +*/ +QMqttSubscription *QMqttClient::subscribe(const QMqttTopicFilter &topic, const QMqttSubscriptionProperties &properties, quint8 qos) +{ Q_D(QMqttClient); if (d->m_state != QMqttClient::Connected) return nullptr; - return d->m_connection.sendControlSubscribe(topic, qos); + return d->m_connection.sendControlSubscribe(topic, qos, properties); } /*! @@ -336,8 +405,26 @@ QMqttSubscription *QMqttClient::subscribe(const QMqttTopicFilter &topic, quint8 */ void QMqttClient::unsubscribe(const QMqttTopicFilter &topic) { + unsubscribe(topic, QMqttUnsubscriptionProperties()); +} + +/*! + \since 5.12 + + Unsubscribes from \a topic. No notifications will be sent to any of the + subscriptions made by calling subscribe(). \a properties specifies + additional user properties to be passed to the broker. + + \note If a client disconnects from a broker without unsubscribing, the + broker will store all messages and publish them on the next reconnect. + + \note \a properties will only be passed to the broker when the client + specifies MQTT_5_0 as ProtocolVersion. +*/ +void QMqttClient::unsubscribe(const QMqttTopicFilter &topic, const QMqttUnsubscriptionProperties &properties) +{ Q_D(QMqttClient); - d->m_connection.sendControlUnsubscribe(topic); + d->m_connection.sendControlUnsubscribe(topic, properties); } /*! @@ -348,9 +435,30 @@ void QMqttClient::unsubscribe(const QMqttTopicFilter &topic) other clients to connect and receive the message. Returns an ID that is used internally to identify the message. - */ +*/ qint32 QMqttClient::publish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos, bool retain) { + return publish(topic, QMqttPublishProperties(), message, qos, retain); +} + +/*! + \since 5.12 + + Publishes a \a message to the broker with the specified \a properties and + \a topic. \a qos specifies the level of security required for transferring + the message. + + If \a retain is set to \c true, the message will stay on the broker for + other clients to connect and receive the message. + + Returns an ID that is used internally to identify the message. + + \note \a properties will only be passed to the broker when the client + specifies MQTT_5_0 as ProtocolVersion. +*/ +qint32 QMqttClient::publish(const QMqttTopicName &topic, const QMqttPublishProperties &properties, + const QByteArray &message, quint8 qos, bool retain) +{ Q_D(QMqttClient); if (qos > 2) return -1; @@ -358,7 +466,7 @@ qint32 QMqttClient::publish(const QMqttTopicName &topic, const QByteArray &messa if (d->m_state != QMqttClient::Connected) return -1; - return d->m_connection.sendControlPublish(topic, message, qos, retain); + return d->m_connection.sendControlPublish(topic, message, qos, retain, properties); } /*! @@ -414,12 +522,12 @@ void QMqttClient::connectToHost(bool encrypted, const QString &sslPeerName) Q_D(QMqttClient); if (state() == QMqttClient::Connected) { - qWarning("Already connected to a broker. Rejecting connection request."); + qCDebug(lcMqttClient) << "Already connected to a broker. Rejecting connection request."; return; } if (!d->m_connection.ensureTransport(encrypted)) { - qWarning("Could not ensure connection"); + qCDebug(lcMqttClient) << "Could not ensure connection."; d->setStateAndError(Disconnected, TransportInvalid); return; } @@ -430,7 +538,7 @@ void QMqttClient::connectToHost(bool encrypted, const QString &sslPeerName) d->m_connection.cleanSubscriptions(); if (!d->m_connection.ensureTransportOpen(sslPeerName)) { - qWarning("Could not ensure that connection is open"); + qCDebug(lcMqttClient) << "Could not ensure that connection is open."; d->setStateAndError(Disconnected, TransportInvalid); return; } @@ -450,11 +558,11 @@ void QMqttClient::disconnectFromHost() switch (d->m_connection.internalState()) { case QMqttConnection::BrokerConnected: d->m_connection.sendControlDisconnect(); + break; case QMqttConnection::BrokerDisconnected: - return; + break; case QMqttConnection::BrokerConnecting: case QMqttConnection::BrokerWaitForConnectAck: - default: d->m_connection.m_transport->close(); break; } @@ -508,6 +616,122 @@ bool QMqttClient::willRetain() const return d->m_willRetain; } +/*! + \since 5.12 + + Sets the connection properties to \a prop. \l QMqttConnectionProperties + can be used to ask the server to use a specific feature set. After a + connection request the server response can be obtained by calling + \l QMqttClient::serverConnectionProperties. + + \note The connection properties can only be set if the MQTT client is in the + \l Disconnected state. + + \note QMqttConnectionProperties can only be used when the client specifies + MQTT_5_0 as ProtocolVersion. +*/ +void QMqttClient::setConnectionProperties(const QMqttConnectionProperties &prop) +{ + Q_D(QMqttClient); + d->m_connectionProperties = prop; +} + +/*! + \since 5.12 + + Returns the connection properties the client requests to the broker. + + \note QMqttConnectionProperties can only be used when the client specifies + MQTT_5_0 as ProtocolVersion. +*/ +QMqttConnectionProperties QMqttClient::connectionProperties() const +{ + Q_D(const QMqttClient); + return d->m_connectionProperties; +} + +/*! + \since 5.12 + + Sets the last will properties to \a prop. QMqttLastWillProperties allows + to set additional features for the last will message stored at the broker. + + \note The connection properties can only be set if the MQTT client is in the + \l Disconnected state. + + \note QMqttLastWillProperties can only be used when the client specifies + MQTT_5_0 as ProtocolVersion. +*/ +void QMqttClient::setLastWillProperties(const QMqttLastWillProperties &prop) +{ + Q_D(QMqttClient); + d->m_lastWillProperties = prop; +} + +/*! + \since 5.12 + + Returns the last will properties. + + \note QMqttLastWillProperties can only be used when the client specifies + MQTT_5_0 as ProtocolVersion. +*/ +QMqttLastWillProperties QMqttClient::lastWillProperties() const +{ + Q_D(const QMqttClient); + return d->m_lastWillProperties; +} + +/*! + \since 5.12 + + Returns the QMqttServerConnectionProperties the broker returned after a + connection attempt. + + This can be used to verify that client side connection properties set by + QMqttClient::setConnectionProperties have been accepted by the broker. Also, + in case of a failed connection attempt, it can be used for connection + diagnostics. + + \note QMqttServerConnectionProperties can only be used when the client + specifies MQTT_5_0 as ProtocolVersion. + + \sa connectionProperties() +*/ +QMqttServerConnectionProperties QMqttClient::serverConnectionProperties() const +{ + Q_D(const QMqttClient); + return d->m_serverConnectionProperties; +} + +/*! + \since 5.12 + + Sends an authentication request to the broker. \a prop specifies + the required information to fulfill the authentication request. + + This function should only be called after a + QMqttClient::authenticationRequested signal has been emitted. + + \note Extended authentication is part of the MQTT 5.0 standard and can + only be used when the client specifies MQTT_5_0 as ProtocolVersion. + + \sa authenticationRequested(), authenticationFinished() +*/ +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); @@ -535,6 +759,12 @@ quint16 QMqttClient::keepAlive() const void QMqttClient::setHostname(const QString &hostname) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing hostname while connected is not possible."; + return; + } + if (d->m_hostname == hostname) return; @@ -545,6 +775,12 @@ void QMqttClient::setHostname(const QString &hostname) void QMqttClient::setPort(quint16 port) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing port while connected is not possible."; + return; + } + if (d->m_port == port) return; @@ -555,11 +791,12 @@ void QMqttClient::setPort(quint16 port) void QMqttClient::setClientId(const QString &clientId) { Q_D(QMqttClient); - if (d->m_clientId == clientId) - return; - d->m_clientId = clientId; - emit clientIdChanged(clientId); + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing client ID while connected is not possible."; + return; + } + d->setClientId(clientId); } void QMqttClient::setKeepAlive(quint16 keepAlive) @@ -569,7 +806,7 @@ void QMqttClient::setKeepAlive(quint16 keepAlive) return; if (state() != QMqttClient::Disconnected) { - qWarning("Trying to modify keepAlive while connected."); + qCDebug(lcMqttClient) << "Changing keepAlive while connected is not possible."; return; } @@ -580,11 +817,16 @@ void QMqttClient::setKeepAlive(quint16 keepAlive) void QMqttClient::setProtocolVersion(ProtocolVersion protocolVersion) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing protocol version while connected is not possible."; + return; + } + if (d->m_protocolVersion == protocolVersion) return; - // Only MQTT 3 and 4 are supported - if (protocolVersion < 3 || protocolVersion > 4) + if (protocolVersion < 3 || protocolVersion > 5) return; d->m_protocolVersion = protocolVersion; @@ -608,6 +850,12 @@ void QMqttClient::setState(ClientState state) void QMqttClient::setUsername(const QString &username) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing username while connected is not possible."; + return; + } + if (d->m_username == username) return; @@ -618,6 +866,12 @@ void QMqttClient::setUsername(const QString &username) void QMqttClient::setPassword(const QString &password) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing password while connected is not possible."; + return; + } + if (d->m_password == password) return; @@ -628,6 +882,12 @@ void QMqttClient::setPassword(const QString &password) void QMqttClient::setCleanSession(bool cleanSession) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing clean session while connected is not possible."; + return; + } + if (d->m_cleanSession == cleanSession) return; @@ -638,6 +898,12 @@ void QMqttClient::setCleanSession(bool cleanSession) void QMqttClient::setWillTopic(const QString &willTopic) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing will topic while connected is not possible."; + return; + } + if (d->m_willTopic == willTopic) return; @@ -648,6 +914,12 @@ void QMqttClient::setWillTopic(const QString &willTopic) void QMqttClient::setWillQoS(quint8 willQoS) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing will qos while connected is not possible."; + return; + } + if (d->m_willQoS == willQoS) return; @@ -658,6 +930,12 @@ void QMqttClient::setWillQoS(quint8 willQoS) void QMqttClient::setWillMessage(const QByteArray &willMessage) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing will message while connected is not possible."; + return; + } + if (d->m_willMessage == willMessage) return; @@ -668,6 +946,12 @@ void QMqttClient::setWillMessage(const QByteArray &willMessage) void QMqttClient::setWillRetain(bool willRetain) { Q_D(QMqttClient); + + if (state() != QMqttClient::Disconnected) { + qCDebug(lcMqttClient) << "Changing will retain while connected is not possible."; + return; + } + if (d->m_willRetain == willRetain) return; @@ -694,6 +978,15 @@ QMqttClientPrivate::QMqttClientPrivate(QMqttClient *c) m_clientId.remove(QLatin1Char('}')); m_clientId.remove(QLatin1Char('-')); m_clientId.resize(23); +#ifdef QT_BUILD_INTERNAL + // Some test servers require a username token + if (qEnvironmentVariableIsSet("QT_MQTT_TEST_USERNAME")) + m_username = qEnvironmentVariable("QT_MQTT_TEST_USERNAME"); + if (qEnvironmentVariableIsSet("QT_MQTT_TEST_PASSWORD")) + m_password = qEnvironmentVariable("QT_MQTT_TEST_PASSWORD"); + if (qEnvironmentVariableIsSet("QT_MQTT_TEST_CLIENTID")) + m_clientId = qEnvironmentVariable("QT_MQTT_TEST_CLIENTID"); +#endif } QMqttClientPrivate::~QMqttClientPrivate() @@ -710,4 +1003,15 @@ void QMqttClientPrivate::setStateAndError(QMqttClient::ClientState s, QMqttClien q->setError(e); } +void QMqttClientPrivate::setClientId(const QString &id) +{ + Q_Q(QMqttClient); + + if (m_clientId == id) + return; + + m_clientId = id; + emit q->clientIdChanged(id); +} + QT_END_NAMESPACE diff --git a/src/mqtt/qmqttclient.h b/src/mqtt/qmqttclient.h index 9b06ae7..92e114b 100644 --- a/src/mqtt/qmqttclient.h +++ b/src/mqtt/qmqttclient.h @@ -31,7 +31,11 @@ #define QTMQTTCLIENT_H #include <QtMqtt/qmqttglobal.h> +#include <QtMqtt/qmqttauthenticationproperties.h> +#include <QtMqtt/qmqttconnectionproperties.h> +#include <QtMqtt/qmqttpublishproperties.h> #include <QtMqtt/qmqttsubscription.h> +#include <QtMqtt/qmqttsubscriptionproperties.h> #include <QtMqtt/qmqtttopicfilter.h> #include <QtCore/QIODevice> @@ -67,11 +71,13 @@ public: // Qt states TransportInvalid = 256, ProtocolViolation, - UnknownError + UnknownError, + Mqtt5SpecificError }; enum ProtocolVersion { MQTT_3_1 = 3, - MQTT_3_1_1 = 4 + MQTT_3_1_1 = 4, + MQTT_5_0 = 5 }; private: @@ -99,10 +105,18 @@ public: QIODevice *transport() const; QMqttSubscription *subscribe(const QMqttTopicFilter &topic, quint8 qos = 0); + QMqttSubscription *subscribe(const QMqttTopicFilter &topic, + const QMqttSubscriptionProperties &properties, quint8 qos = 0); void unsubscribe(const QMqttTopicFilter &topic); + void unsubscribe(const QMqttTopicFilter &topic, const QMqttUnsubscriptionProperties &properties); Q_INVOKABLE qint32 publish(const QMqttTopicName &topic, const QByteArray &message = QByteArray(), quint8 qos = 0, bool retain = false); + Q_INVOKABLE qint32 publish(const QMqttTopicName &topic, const QMqttPublishProperties &properties, + const QByteArray &message = QByteArray(), + quint8 qos = 0, + bool retain = false); + bool requestPing(); QString hostname() const; @@ -129,10 +143,20 @@ public: QByteArray willMessage() const; bool willRetain() const; + void setConnectionProperties(const QMqttConnectionProperties &prop); + QMqttConnectionProperties connectionProperties() const; + + void setLastWillProperties(const QMqttLastWillProperties &prop); + QMqttLastWillProperties lastWillProperties() const; + + QMqttServerConnectionProperties serverConnectionProperties() const; + + void authenticate(const QMqttAuthenticationProperties &prop); Q_SIGNALS: void connected(); void disconnected(); void messageReceived(const QByteArray &message, const QMqttTopicName &topic = QMqttTopicName()); + void messageStatusChanged(qint32 id, QMqtt::MessageStatus s, const QMqttMessageStatusProperties &properties); void messageSent(qint32 id); void pingResponseReceived(); void brokerSessionRestored(); @@ -153,6 +177,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/qmqttclient_p.h b/src/mqtt/qmqttclient_p.h index 2bec37e..2ce9f48 100644 --- a/src/mqtt/qmqttclient_p.h +++ b/src/mqtt/qmqttclient_p.h @@ -57,14 +57,13 @@ public: QMqttClientPrivate(QMqttClient *c); ~QMqttClientPrivate() override; void setStateAndError(QMqttClient::ClientState s, QMqttClient::ClientError e = QMqttClient::NoError); + void setClientId(const QString &id); QMqttClient *m_client{nullptr}; QString m_hostname; quint16 m_port{0}; QMqttConnection m_connection; QString m_clientId; // auto-generated quint16 m_keepAlive{60}; - // 3 == MQTT Standard 3.1 - // 4 == MQTT Standard 3.1.1 QMqttClient::ProtocolVersion m_protocolVersion{QMqttClient::MQTT_3_1_1}; QMqttClient::ClientState m_state{QMqttClient::Disconnected}; QMqttClient::ClientError m_error{QMqttClient::NoError}; @@ -75,6 +74,9 @@ public: QString m_username; QString m_password; bool m_cleanSession{true}; + QMqttConnectionProperties m_connectionProperties; + QMqttLastWillProperties m_lastWillProperties; + QMqttServerConnectionProperties m_serverConnectionProperties; }; QT_END_NAMESPACE diff --git a/src/mqtt/qmqttconnection.cpp b/src/mqtt/qmqttconnection.cpp index f010cad..fc0ac13 100644 --- a/src/mqtt/qmqttconnection.cpp +++ b/src/mqtt/qmqttconnection.cpp @@ -28,7 +28,10 @@ ******************************************************************************/ #include "qmqttconnection_p.h" +#include "qmqttconnectionproperties_p.h" #include "qmqttcontrolpacket_p.h" +#include "qmqttmessage_p.h" +#include "qmqttpublishproperties_p.h" #include "qmqttsubscription_p.h" #include "qmqttclient_p.h" @@ -44,6 +47,50 @@ QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcMqttConnection, "qt.mqtt.connection") Q_LOGGING_CATEGORY(lcMqttConnectionVerbose, "qt.mqtt.connection.verbose"); +template<> +quint32 QMqttConnection::readBufferTyped(qint64 *dataSize) +{ + if (dataSize) + *dataSize -= sizeof(quint32); + return qFromBigEndian<quint32>(reinterpret_cast<const quint32 *>(readBuffer(4).constData())); +} + +template<> +quint16 QMqttConnection::readBufferTyped(qint64 *dataSize) +{ + if (dataSize) + *dataSize -= sizeof(quint16); + return qFromBigEndian<quint16>(reinterpret_cast<const quint16 *>(readBuffer(2).constData())); +} + +template<> +quint8 QMqttConnection::readBufferTyped(qint64 *dataSize) +{ + quint8 result; + readBuffer(reinterpret_cast<char *>(&result), 1); + if (dataSize) + *dataSize -= sizeof(quint8); + return result; +} + +template<> +QString QMqttConnection::readBufferTyped(qint64 *dataSize) +{ + const quint16 size = readBufferTyped<quint16>(dataSize); + if (dataSize) + *dataSize -= size; + 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); @@ -97,7 +144,7 @@ bool QMqttConnection::ensureTransport(bool createSecureIfNeeded) // We are asked to create a transport layer if (m_clientPrivate->m_hostname.isEmpty() || m_clientPrivate->m_port == 0) { - qWarning("Trying to create a transport layer, but no hostname is specified"); + qCDebug(lcMqttConnection) << "No hostname specified, not able to create a transport layer."; return false; } auto socket = @@ -129,12 +176,14 @@ bool QMqttConnection::ensureTransportOpen(const QString &sslPeerName) return sendControlConnect(); if (!m_transport->open(QIODevice::ReadWrite)) { - qWarning("Could not open Transport IO device"); + qCDebug(lcMqttConnection) << "Could not open Transport IO device."; m_internalState = BrokerDisconnected; return false; } return sendControlConnect(); - } else if (m_transportType == QMqttClient::AbstractSocket) { + } + + if (m_transportType == QMqttClient::AbstractSocket) { auto socket = dynamic_cast<QTcpSocket*>(m_transport); Q_ASSERT(socket); if (socket->state() == QAbstractSocket::ConnectedState) @@ -154,12 +203,12 @@ bool QMqttConnection::ensureTransportOpen(const QString &sslPeerName) socket->connectToHostEncrypted(m_clientPrivate->m_hostname, m_clientPrivate->m_port, sslPeerName); if (!socket->waitForConnected()) { - qWarning("Could not establish socket connection for transport"); + qCDebug(lcMqttConnection) << "Could not establish socket connection for transport."; return false; } if (!socket->waitForEncrypted()) { - qWarning("Could not initiate encryption."); + qCDebug(lcMqttConnection) << "Could not initiate encryption."; return false; } } @@ -188,10 +237,10 @@ bool QMqttConnection::sendControlConnect() packet.append("MQTT"); packet.append(char(4)); // Version 3.1.1 break; - default: - qCWarning(lcMqttConnection) << "Illegal MQTT Version"; - m_clientPrivate->setStateAndError(QMqttClient::Disconnected, QMqttClient::InvalidProtocolVersion); - return false; + case QMqttClient::MQTT_5_0: + packet.append("MQTT"); + packet.append(char(5)); // Version 5.0 + break; } // 3.1.2.3 Connect Flags @@ -203,7 +252,7 @@ bool QMqttConnection::sendControlConnect() if (!m_clientPrivate->m_willMessage.isEmpty()) { flags |= 1 << 2; if (m_clientPrivate->m_willQoS > 2) { - qWarning("Will QoS does not have a valid value"); + qCDebug(lcMqttConnection) << "Invalid Will QoS specified."; return false; } if (m_clientPrivate->m_willQoS == 1) @@ -224,6 +273,9 @@ bool QMqttConnection::sendControlConnect() // 3.1.2.10 Keep Alive packet.append(m_clientPrivate->m_keepAlive); + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) + packet.appendRaw(writeConnectProperties()); + // 3.1.3 Payload // 3.1.3.1 Client Identifier const QByteArray clientStringArray = m_clientPrivate->m_clientId.toUtf8(); @@ -235,6 +287,9 @@ bool QMqttConnection::sendControlConnect() } if (!m_clientPrivate->m_willMessage.isEmpty()) { + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) + packet.appendRaw(writeLastWillProperties()); + packet.append(m_clientPrivate->m_willTopic.toUtf8()); packet.append(m_clientPrivate->m_willMessage); } @@ -246,7 +301,7 @@ bool QMqttConnection::sendControlConnect() packet.append(m_clientPrivate->m_password.toUtf8()); if (!writePacketToTransport(packet)) { - qWarning("Could not write CONNECT frame to transport"); + qCDebug(lcMqttConnection) << "Could not write CONNECT frame to transport."; return false; } @@ -255,7 +310,41 @@ bool QMqttConnection::sendControlConnect() return true; } -qint32 QMqttConnection::sendControlPublish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos, bool retain) +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, + bool retain, + const QMqttPublishProperties &properties) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << topic << " Size:" << message.size() << " bytes." << "QoS:" << qos << " Retain:" << retain; @@ -273,13 +362,55 @@ qint32 QMqttConnection::sendControlPublish(const QMqttTopicName &topic, const QB header |= 0x01; QSharedPointer<QMqttControlPacket> packet(new QMqttControlPacket(header)); - packet->append(topic.name().toUtf8()); + // topic alias + QMqttPublishProperties publishProperties(properties); + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) { + const quint16 topicAlias = publishProperties.topicAlias(); + if (topicAlias > 0) { // User specified topic Alias + if (topicAlias > m_clientPrivate->m_serverConnectionProperties.maximumTopicAlias()) { + qCDebug(lcMqttConnection) << "TopicAlias publish: overflow."; + return -1; + } + if (m_publishAliases.at(topicAlias - 1) != topic) { + qCDebug(lcMqttConnection) << "TopicAlias publish: Assign:" << topicAlias << ":" << topic; + m_publishAliases[topicAlias - 1] = topic; + packet->append(topic.name().toUtf8()); + } else { + qCDebug(lcMqttConnectionVerbose) << "TopicAlias publish: Reuse:" << topicAlias; + packet->append(quint16(0)); + } + } else if (m_publishAliases.size() > 0) { // Automatic module alias assignment + int autoAlias = m_publishAliases.indexOf(topic); + if (autoAlias != -1) { + qCDebug(lcMqttConnectionVerbose) << "TopicAlias publish: Use auto alias:" << autoAlias; + packet->append(quint16(0)); + publishProperties.setTopicAlias(quint16(autoAlias + 1)); + } else { + autoAlias = m_publishAliases.indexOf(QMqttTopicName()); + if (autoAlias != -1) { + qCDebug(lcMqttConnectionVerbose) << "TopicAlias publish: auto alias assignment:" << autoAlias; + m_publishAliases[autoAlias] = topic; + publishProperties.setTopicAlias(quint16(autoAlias) + 1); + } else + qCDebug(lcMqttConnectionVerbose) << "TopicAlias publish: alias storage full, using full topic"; + packet->append(topic.name().toUtf8()); + } + } else { + packet->append(topic.name().toUtf8()); + } + } else { // ! MQTT_5_0 + packet->append(topic.name().toUtf8()); + } quint16 identifier = 0; if (qos > 0) { identifier = unusedPacketIdentifier(); packet->append(identifier); m_pendingMessages.insert(identifier, packet); } + + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) + packet->appendRaw(writePublishProperties(publishProperties)); + packet->appendRaw(message); const bool written = writePacketToTransport(*packet.data()); @@ -324,11 +455,20 @@ bool QMqttConnection::sendControlPublishComp(quint16 id) return writePacketToTransport(packet); } -QMqttSubscription *QMqttConnection::sendControlSubscribe(const QMqttTopicFilter &topic, quint8 qos) +QMqttSubscription *QMqttConnection::sendControlSubscribe(const QMqttTopicFilter &topic, + quint8 qos, + const QMqttSubscriptionProperties &properties) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << " Topic:" << topic << " qos:" << qos; - if (m_activeSubscriptions.contains(topic)) + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) { + if (!topic.shareName().isEmpty()) { + const QMqttTopicFilter filter(topic.filter().section(QLatin1Char('/'), 2)); + if (m_activeSubscriptions.contains(filter) && m_activeSubscriptions.value(filter)->shareName() == topic.shareName()) + return m_activeSubscriptions[filter]; + } else if (m_activeSubscriptions.contains(topic) && !m_activeSubscriptions.value(topic)->isShared()) + return m_activeSubscriptions[topic]; + } else if (m_activeSubscriptions.contains(topic)) return m_activeSubscriptions[topic]; // has to have 0010 as bits 3-0, maybe update SUBSCRIBE instead? @@ -341,9 +481,12 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QMqttTopicFilter packet.append(identifier); + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) + packet.appendRaw(writeSubscriptionProperties(properties)); + // Overflow protection if (!topic.isValid()) { - qWarning("Subscribed topic filter is not valid."); + qCDebug(lcMqttConnection) << "Invalid subscription topic filter."; return nullptr; } @@ -361,6 +504,11 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QMqttTopicFilter result->setClient(m_clientPrivate->m_client); result->setQos(qos); result->setState(QMqttSubscription::SubscriptionPending); + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0 && !topic.shareName().isEmpty()) { + result->setShareName(topic.shareName()); + result->setShared(true); + result->setTopic(topic.filter().section(QLatin1Char('/'), 2)); + } if (!writePacketToTransport(packet)) { delete result; @@ -373,7 +521,7 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QMqttTopicFilter return result; } -bool QMqttConnection::sendControlUnsubscribe(const QMqttTopicFilter &topic) +bool QMqttConnection::sendControlUnsubscribe(const QMqttTopicFilter &topic, const QMqttUnsubscriptionProperties &properties) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << " Topic:" << topic; @@ -399,6 +547,10 @@ bool QMqttConnection::sendControlUnsubscribe(const QMqttTopicFilter &topic) packet.append(identifier); + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) { + packet.appendRaw(writeUnsubscriptionProperties(properties)); + } + packet.append(topic.filter().toUtf8()); auto sub = m_activeSubscriptions[topic]; sub->setState(QMqttSubscription::UnsubscriptionPending); @@ -422,7 +574,7 @@ bool QMqttConnection::sendControlPingRequest() const QMqttControlPacket packet(QMqttControlPacket::PINGREQ); if (!writePacketToTransport(packet)) { - qWarning("Could not write DISCONNECT frame to transport"); + qCDebug(lcMqttConnection) << "Failed to write PINGREQ to transport."; return false; } return true; @@ -436,9 +588,12 @@ bool QMqttConnection::sendControlDisconnect() m_activeSubscriptions.clear(); + m_receiveAliases.clear(); + m_publishAliases.clear(); + const QMqttControlPacket packet(QMqttControlPacket::DISCONNECT); if (!writePacketToTransport(packet)) { - qWarning("Could not write DISCONNECT frame to transport"); + qCDebug(lcMqttConnection) << "Failed to write DISCONNECT to transport."; return false; } m_internalState = BrokerDisconnected; @@ -471,7 +626,7 @@ quint16 QMqttConnection::unusedPacketIdentifier() const packetIdentifierCounter++; if (lastIdentifier == packetIdentifierCounter) { - qWarning("Can't generate unique packet identifier."); + qCDebug(lcMqttConnection) << "Could not generate unique packet identifier."; break; } } while (m_pendingSubscriptionAck.contains(packetIdentifierCounter) @@ -504,7 +659,7 @@ void QMqttConnection::transportConnectionEstablished() } if (!sendControlConnect()) { - qWarning("Could not send CONNECT to broker"); + qCDebug(lcMqttConnection) << "Failed to write CONNECT to transport."; // ### Who disconnects now? Connection or client? m_clientPrivate->setStateAndError(QMqttClient::Disconnected, QMqttClient::TransportInvalid); } @@ -513,6 +668,7 @@ void QMqttConnection::transportConnectionEstablished() void QMqttConnection::transportConnectionClosed() { m_readBuffer.clear(); + m_readPosition = 0; m_pingTimer.stop(); m_clientPrivate->setStateAndError(QMqttClient::Disconnected, QMqttClient::TransportInvalid); } @@ -524,15 +680,40 @@ void QMqttConnection::transportReadReady() processData(); } -void QMqttConnection::readBuffer(char *data, qint64 size) +void QMqttConnection::readBuffer(char *data, quint64 size) +{ + memcpy(data, m_readBuffer.constData() + m_readPosition, size); + m_readPosition += size; +} + +qint32 QMqttConnection::readVariableByteInteger(qint32 *byteCount) { - memcpy(data, m_readBuffer.constData(), size); - m_readBuffer = m_readBuffer.mid(size); + quint32 multiplier = 1; + qint32 msgLength = 0; + quint8 b = 0; + quint8 iteration = 0; + if (byteCount) + *byteCount = 0; + do { + b = readBufferTyped<quint8>(); + msgLength += (b & 127) * multiplier; + multiplier *= 128; + iteration++; + if (iteration > 4) { + qCDebug(lcMqttConnection) << "Overflow trying to read variable integer."; + closeConnection(QMqttClient::ProtocolViolation); + return -1; + } + } while ((b & 128) != 0); + if (byteCount) + *byteCount += iteration; + return msgLength; } void QMqttConnection::closeConnection(QMqttClient::ClientError error) { m_readBuffer.clear(); + m_readPosition = 0; m_pingTimer.stop(); m_activeSubscriptions.clear(); m_internalState = BrokerDisconnected; @@ -541,20 +722,665 @@ void QMqttConnection::closeConnection(QMqttClient::ClientError error) m_clientPrivate->setStateAndError(QMqttClient::Disconnected, error); } -QByteArray QMqttConnection::readBuffer(qint64 size) +QByteArray QMqttConnection::readBuffer(quint64 size) { - QByteArray res = m_readBuffer.left(size); - m_readBuffer = m_readBuffer.mid(size); + QByteArray res(m_readBuffer.constData() + m_readPosition, int(size)); + m_readPosition += 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(QMqttServerConnectionProperties &properties) +{ + qint64 propertyLength = readVariableByteInteger(); + m_missingData = 0; + + properties.serverData->valid = true; + + while (propertyLength > 0) { + quint8 propertyId = readBufferTyped<quint8>(&propertyLength); + switch (propertyId) { + case 0x11: { // 3.2.2.3.2 Session Expiry Interval + const quint32 expiryInterval = readBufferTyped<quint32>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::SessionExpiryInterval; + properties.setSessionExpiryInterval(expiryInterval); + break; + } + case 0x21: { // 3.2.2.3.3 Receive Maximum + const quint16 receiveMaximum = readBufferTyped<quint16>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::MaximumReceive; + properties.setMaximumReceive(receiveMaximum); + break; + } + case 0x24: { // 3.2.2.3.4 Maximum QoS Level + const quint8 maxQoS = readBufferTyped<quint8>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::MaximumQoS; + properties.serverData->maximumQoS = maxQoS; + break; + } + case 0x25: { // 3.2.2.3.5 Retain available + const quint8 retainAvailable = readBufferTyped<quint8>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::RetainAvailable; + properties.serverData->retainAvailable = retainAvailable == 1; + break; + } + case 0x27: { // 3.2.2.3.6 Maximum packet size + const quint32 maxPacketSize = readBufferTyped<quint32>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::MaximumPacketSize; + properties.setMaximumPacketSize(maxPacketSize); + break; + } + case 0x12: { // 3.2.2.3.7 Assigned clientId + const QString assignedClientId = readBufferTyped<QString>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::AssignedClientId; + m_clientPrivate->setClientId(assignedClientId); + break; + } + case 0x22: { // 3.2.2.3.8 Topic Alias Maximum + const quint16 topicAliasMaximum = readBufferTyped<quint16>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::MaximumTopicAlias; + properties.setMaximumTopicAlias(topicAliasMaximum); + break; + } + case 0x1F: { // 3.2.2.3.9 Reason String + const QString reasonString = readBufferTyped<QString>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::ReasonString; + properties.serverData->reasonString = reasonString; + break; + } + case 0x26: { // 3.2.2.3.10 User property + const QString propertyName = readBufferTyped<QString>(&propertyLength); + const QString propertyValue = readBufferTyped<QString>(&propertyLength); + + properties.serverData->details |= QMqttServerConnectionProperties::UserProperty; + properties.data->userProperties.append(QMqttStringPair(propertyName, propertyValue)); + break; + } + case 0x28: { // 3.2.2.3.11 Wildcard subscriptions available + const quint8 available = readBufferTyped<quint8>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::WildCardSupported; + properties.serverData->wildcardSupported = available == 1; + break; + } + case 0x29: { // 3.2.2.3.12 Subscription identifiers available + const quint8 available = readBufferTyped<quint8>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::SubscriptionIdentifierSupport; + properties.serverData->subscriptionIdentifierSupported = available == 1; + break; + } + case 0x2A: { // 3.2.2.3.13 Shared subscriptions available + const quint8 available = readBufferTyped<quint8>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::SharedSubscriptionSupport; + properties.serverData->sharedSubscriptionSupported = available == 1; + break; + } + case 0x13: { // 3.2.2.3.14 Server Keep Alive + const quint16 serverKeepAlive = readBufferTyped<quint16>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::ServerKeepAlive; + m_clientPrivate->m_client->setKeepAlive(serverKeepAlive); + break; + } + case 0x1A: { // 3.2.2.3.15 Response information + const QString responseInfo = readBufferTyped<QString>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::ResponseInformation; + properties.serverData->responseInformation = responseInfo; + break; + } + case 0x1C: { // 3.2.2.3.16 Server reference + const QString serverReference = readBufferTyped<QString>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::ServerReference; + properties.serverData->serverReference = serverReference; + break; + } + case 0x15: { // 3.2.2.3.17 Authentication method + const QString method = readBufferTyped<QString>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::AuthenticationMethod; + properties.data->authenticationMethod = method; + break; + } + case 0x16: { // 3.2.2.3.18 Authentication data + const QByteArray data = readBufferTyped<QByteArray>(&propertyLength); + properties.serverData->details |= QMqttServerConnectionProperties::AuthenticationData; + properties.data->authenticationData = data; + break; + } + default: + qCDebug(lcMqttConnection) << "Unknown property id in CONNACK:" << int(propertyId); + break; + } + } +} + +void QMqttConnection::readMessageStatusProperties(QMqttMessageStatusProperties &properties) +{ + qint64 propertyLength = readVariableByteInteger(); + + m_missingData -= propertyLength; + while (propertyLength > 0) { + const quint8 propertyId = readBufferTyped<quint8>(&propertyLength); + switch (propertyId) { + case 0x1f: { // 3.4.2.2.2 Reason String + const QString content = readBufferTyped<QString>(&propertyLength); + properties.data->reasonString = content; + break; + } + case 0x26: { // 3.4.2.2.3 User Properites + const QString propertyName = readBufferTyped<QString>(&propertyLength); + const QString propertyValue = readBufferTyped<QString>(&propertyLength); + properties.data->userProperties.append(QMqttStringPair(propertyName, propertyValue)); + break; + } + default: + qCDebug(lcMqttConnection) << "Unknown subscription property received."; + break; + } + } +} + +void QMqttConnection::readPublishProperties(QMqttPublishProperties &properties) +{ + qint32 propertySize = 0; + qint64 propertyLength = readVariableByteInteger(&propertySize); + m_missingData -= propertySize; + m_missingData -= propertyLength; + + QMqttUserProperties userProperties; + QList<quint32> subscriptionIds; + + while (propertyLength > 0) { + const quint8 propertyId = readBufferTyped<quint8>(&propertyLength); + switch (propertyId) { + case 0x01: { // 3.3.2.3.2 Payload Format Indicator + const quint8 format = readBufferTyped<quint8>(&propertyLength); + if (format == 1) + properties.setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator::UTF8Encoded); + break; + } + case 0x02: { // 3.3.2.3.3 Message Expiry Interval + const quint32 interval = readBufferTyped<quint32>(&propertyLength); + properties.setMessageExpiryInterval(interval); + break; + } + case 0x23: { // 3.3.2.3.4 Topic alias + const quint16 alias = readBufferTyped<quint16>(&propertyLength); + properties.setTopicAlias(alias); + break; + } + case 0x08: { // 3.3.2.3.5 Response Topic + const QString responseTopic = readBufferTyped<QString>(&propertyLength); + properties.setResponseTopic(responseTopic); + break; + } + case 0x09: { // 3.3.2.3.6 Correlation Data + const QByteArray data = readBufferTyped<QByteArray>(&propertyLength); + properties.setCorrelationData(data); + break; + } + case 0x26: { // 3.3.2.3.7 User property + const QString propertyName = readBufferTyped<QString>(&propertyLength); + const QString propertyValue = readBufferTyped<QString>(&propertyLength); + userProperties.append(QMqttStringPair(propertyName, propertyValue)); + break; + } + case 0x0b: { // 3.3.2.3.8 Subscription Identifier + qint32 idSize = 0; + qint32 id = readVariableByteInteger(&idSize); + if (id < 0) + return; // readVariableByteInteger closes connection + propertyLength -= idSize; + subscriptionIds.append(quint32(id)); + break; + } + case 0x03: { // 3.3.2.3.9 Content Type + const QString content = readBufferTyped<QString>(&propertyLength); + properties.setContentType(content); + break; + } + default: + qCDebug(lcMqttConnection) << "Unknown publish property received."; + break; + } + } + if (!userProperties.isEmpty()) + properties.setUserProperties(userProperties); + + if (!subscriptionIds.isEmpty()) + properties.setSubscriptionIdentifiers(subscriptionIds); +} + +void QMqttConnection::readSubscriptionProperties(QMqttSubscription *sub) +{ + qint32 bytes = 0; + qint64 propertyLength = readVariableByteInteger(&bytes); + + m_missingData -= bytes; + while (propertyLength > 0) { + const quint8 propertyId = readBufferTyped<quint8>(&propertyLength); + switch (propertyId) { + case 0x1f: { // 3.9.2.1.2 Reason String + const QString content = readBufferTyped<QString>(&propertyLength); + sub->d_func()->m_reasonString = content; + break; + } + case 0x26: { // 3.9.2.1.3 + const QString propertyName = readBufferTyped<QString>(&propertyLength); + const QString propertyValue = readBufferTyped<QString>(&propertyLength); + + sub->d_func()->m_userProperties.append(QMqttStringPair(propertyName, propertyValue)); + break; + } + default: + qCDebug(lcMqttConnection) << "Unknown subscription property received."; + break; + } + } +} + +QByteArray QMqttConnection::writeConnectProperties() +{ + QMqttControlPacket properties; + + // According to MQTT5 3.1.2.11 default values do not need to be included in the + // connect statement. + + // 3.1.2.11.2 + if (m_clientPrivate->m_connectionProperties.sessionExpiryInterval() != 0) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: specify sessionExpiryInterval"; + properties.append(char(0x11)); + properties.append(m_clientPrivate->m_connectionProperties.sessionExpiryInterval()); + } + + // 3.1.2.11.3 + if (m_clientPrivate->m_connectionProperties.maximumReceive() != 65535) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: specify maximumReceive"; + properties.append(char(0x21)); + properties.append(m_clientPrivate->m_connectionProperties.maximumReceive()); + } + + // 3.1.2.11.4 + if (m_clientPrivate->m_connectionProperties.maximumPacketSize() != std::numeric_limits<quint32>::max()) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: specify maximumPacketSize"; + properties.append(char(0x27)); + properties.append(m_clientPrivate->m_connectionProperties.maximumPacketSize()); + } + + // 3.1.2.11.5 + if (m_clientPrivate->m_connectionProperties.maximumTopicAlias() != 0) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: specify maximumTopicAlias"; + properties.append(char(0x22)); + properties.append(m_clientPrivate->m_connectionProperties.maximumTopicAlias()); + } + + // 3.1.2.11.6 + if (m_clientPrivate->m_connectionProperties.requestResponseInformation()) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: specify requestResponseInformation"; + properties.append(char(0x19)); + properties.append(char(1)); + } + + // 3.1.2.11.7 + if (!m_clientPrivate->m_connectionProperties.requestProblemInformation()) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: specify requestProblemInformation"; + properties.append(char(0x17)); + properties.append(char(0)); + } + + // 3.1.2.11.8 Add User properties + auto userProperties = m_clientPrivate->m_connectionProperties.userProperties(); + if (!userProperties.isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: specify user properties"; + for (const auto &prop : userProperties) { + properties.append(char(0x26)); + properties.append(prop.name().toUtf8()); + properties.append(prop.value().toUtf8()); + } + } + + // 3.1.2.11.9 Add Authentication + const QString authenticationMethod = m_clientPrivate->m_connectionProperties.authenticationMethod(); + if (!authenticationMethod.isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: specify AuthenticationMethod:"; + qCDebug(lcMqttConnectionVerbose) << " " << authenticationMethod; + properties.append(char(0x15)); + properties.append(authenticationMethod.toUtf8()); + // 3.1.2.11.10 + const QByteArray authenticationData = m_clientPrivate->m_connectionProperties.authenticationData(); + if (!authenticationData.isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Connection Properties: Authentication Data:"; + qCDebug(lcMqttConnectionVerbose) << " " << authenticationData; + properties.append(char(0x16)); + properties.append(authenticationData); + } + } + + return properties.serializePayload(); +} + +QByteArray QMqttConnection::writeLastWillProperties() const +{ + QMqttControlPacket properties; + const QMqttLastWillProperties &lastWillProperties = m_clientPrivate->m_lastWillProperties; + // Will Delay interval 3.1.3.2.2 + if (lastWillProperties.willDelayInterval() > 0) { + const quint32 delay = lastWillProperties.willDelayInterval(); + qCDebug(lcMqttConnectionVerbose) << "Last Will Properties: specify will delay interval:" + << delay; + properties.append(char(0x18)); + properties.append(delay); + } + + // Payload Format Indicator 3.1.3.2.3 + if (lastWillProperties.payloadFormatIndicator() != QMqtt::PayloadFormatIndicator::Unspecified) { + qCDebug(lcMqttConnectionVerbose) << "Last Will Properties: payload format indicator specified"; + properties.append(char(0x01)); + properties.append(char(0x01)); // UTF8 + } + + // Message Expiry Interval 3.1.3.2.4 + if (lastWillProperties.messageExpiryInterval() > 0) { + const quint32 interval = lastWillProperties.messageExpiryInterval(); + qCDebug(lcMqttConnectionVerbose) << "Last Will Properties: Message Expiry interval:" + << interval; + properties.append(char(0x02)); + properties.append(interval); + } + + // Content Type 3.1.3.2.5 + if (!lastWillProperties.contentType().isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Last Will Properties: Content Type:" + << lastWillProperties.contentType(); + properties.append(char(0x03)); + properties.append(lastWillProperties.contentType().toUtf8()); + } + + // Response Topic 3.1.3.2.6 + if (!lastWillProperties.responseTopic().isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Last Will Properties: Response Topic:" + << lastWillProperties.responseTopic(); + properties.append(char(0x08)); + properties.append(lastWillProperties.responseTopic().toUtf8()); + } + + // Correlation Data 3.1.3.2.7 + if (!lastWillProperties.correlationData().isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Last Will Properties: Correlation Data:" + << lastWillProperties.correlationData(); + properties.append(char(0x09)); + properties.append(lastWillProperties.correlationData()); + } + + // User Properties 3.1.3.2.8 + if (!lastWillProperties.userProperties().isEmpty()) { + auto userProperties = lastWillProperties.userProperties(); + qCDebug(lcMqttConnectionVerbose) << "Last Will Properties: specify user properties"; + for (const auto &prop : userProperties) { + properties.append(char(0x26)); + properties.append(prop.name().toUtf8()); + properties.append(prop.value().toUtf8()); + } + } + + return properties.serializePayload(); +} + +QByteArray QMqttConnection::writePublishProperties(const QMqttPublishProperties &properties) +{ + QMqttControlPacket packet; + + // 3.3.2.3.2 Payload Indicator + if (properties.availableProperties() & QMqttPublishProperties::PayloadFormatIndicator && + properties.payloadFormatIndicator() != QMqtt::PayloadFormatIndicator::Unspecified) { + qCDebug(lcMqttConnectionVerbose) << "Publish Properties: Payload Indicator:" + << (properties.payloadFormatIndicator() == QMqtt::PayloadFormatIndicator::UTF8Encoded ? 1 : 0); + packet.append(char(0x01)); + switch (properties.payloadFormatIndicator()) { + case QMqtt::PayloadFormatIndicator::UTF8Encoded: + packet.append(char(0x01)); + break; + default: + qCDebug(lcMqttConnection) << "Unknown payload indicator."; + break; + } + } + + // 3.3.2.3.3 Message Expiry + if (properties.availableProperties() & QMqttPublishProperties::MessageExpiryInterval && + properties.messageExpiryInterval() > 0) { + qCDebug(lcMqttConnectionVerbose) << "Publish Properties: Message Expiry :" + << properties.messageExpiryInterval(); + packet.append(char(0x02)); + packet.append(properties.messageExpiryInterval()); + } + + // 3.3.2.3.4 Topic alias + if (properties.availableProperties() & QMqttPublishProperties::TopicAlias && + properties.topicAlias() > 0) { + qCDebug(lcMqttConnectionVerbose) << "Publish Properties: Topic Alias :" + << properties.topicAlias(); + if (m_clientPrivate->m_serverConnectionProperties.availableProperties() & QMqttServerConnectionProperties::MaximumTopicAlias + && properties.topicAlias() > m_clientPrivate->m_serverConnectionProperties.maximumTopicAlias()) { + qCDebug(lcMqttConnection) << "Invalid topic alias specified: " << properties.topicAlias() + << " Maximum by server is:" + << m_clientPrivate->m_serverConnectionProperties.maximumTopicAlias(); + + } else { + packet.append(char(0x23)); + packet.append(properties.topicAlias()); + } + } + + // 3.3.2.3.5 Response Topic + if (properties.availableProperties() & QMqttPublishProperties::ResponseTopic && + !properties.responseTopic().isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Publish Properties: Response Topic :" + << properties.responseTopic(); + packet.append(char(0x08)); + packet.append(properties.responseTopic().toUtf8()); + } + + // 3.3.2.3.6 Correlation Data + if (properties.availableProperties() & QMqttPublishProperties::CorrelationData && + !properties.correlationData().isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Publish Properties: Correlation Data :" + << properties.correlationData(); + packet.append(char(0x09)); + packet.append(properties.correlationData()); + } + + // 3.3.2.3.7 User Property + if (properties.availableProperties() & QMqttPublishProperties::UserProperty) { + auto userProperties = properties.userProperties(); + if (!userProperties.isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Publish Properties: specify user properties"; + for (const auto &prop : userProperties) { + packet.append(char(0x26)); + packet.append(prop.name().toUtf8()); + packet.append(prop.value().toUtf8()); + } + } + } + + // 3.3.2.3.8 Subscription Identifier + if (properties.availableProperties() & QMqttPublishProperties::SubscriptionIdentifier) { + for (auto id : properties.subscriptionIdentifiers()) { + qCDebug(lcMqttConnectionVerbose) << "Publish Properties: Subscription ID:" << id; + packet.append(char(0x0b)); + packet.appendRawVariableInteger(id); + } + } + + // 3.3.2.3.9 Content Type + if (properties.availableProperties() & QMqttPublishProperties::ContentType && + !properties.contentType().isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Publish Properties: Content Type :" + << properties.contentType(); + packet.append(char(0x03)); + packet.append(properties.contentType().toUtf8()); + } + + return packet.serializePayload(); +} + +QByteArray QMqttConnection::writeSubscriptionProperties(const QMqttSubscriptionProperties &properties) +{ + QMqttControlPacket packet; + + // 3.8.2.1.2 Subscription Identifier + if (properties.subscriptionIdentifier() > 0) { + qCDebug(lcMqttConnectionVerbose) << "Subscription Properties: Subscription Identifier"; + packet.append(char(0x0b)); + packet.appendRawVariableInteger(properties.subscriptionIdentifier()); + } + + // 3.8.2.1.3 User Property + auto userProperties = properties.userProperties(); + if (!userProperties.isEmpty()) { + qCDebug(lcMqttConnectionVerbose) << "Subscription 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(); +} + +QByteArray QMqttConnection::writeUnsubscriptionProperties(const QMqttUnsubscriptionProperties &properties) +{ + QMqttControlPacket packet; + + // 3.10.2.1.2 + 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(); +} + +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"; - quint8 ackFlags; - readBuffer((char*)&ackFlags, 1); + + const quint8 ackFlags = readBufferTyped<quint8>(&m_missingData); + if (ackFlags > 1) { // MQTT-3.2.2.1 - qWarning("Unexpected CONNACK Flags set"); + qCDebug(lcMqttConnection) << "Unexpected CONNACK Flags specified:" << QString::number(ackFlags); + readBuffer(quint64(m_missingData)); + m_missingData = 0; closeConnection(QMqttClient::ProtocolViolation); return; } @@ -564,25 +1390,34 @@ void QMqttConnection::finalize_connack() if (sessionPresent) { emit m_clientPrivate->m_client->brokerSessionRestored(); if (m_clientPrivate->m_cleanSession) - qWarning("Connected with a clean session, ack contains session present"); + qCDebug(lcMqttConnection) << "Connected with a clean session, ack contains session present."; } else { // MQTT-4.1.0.-1 MQTT-4.1.0-2 Session not stored on broker side // regardless whether cleanSession is false cleanSubscriptions(); } - quint8 connectResultValue; - readBuffer((char*)&connectResultValue, 1); - if (connectResultValue != 0) { - qWarning("Connection has been rejected"); - // MQTT-3.2.2-5 - m_readBuffer.clear(); - m_transport->close(); - m_internalState = BrokerDisconnected; - // Table 3.1, values 1-5 - m_clientPrivate->setStateAndError(QMqttClient::Disconnected, static_cast<QMqttClient::ClientError>(connectResultValue)); + quint8 connectResultValue = readBufferTyped<quint8>(&m_missingData); + QMqttServerConnectionProperties serverProp; + serverProp.serverData->reasonCode = QMqtt::ReasonCode(connectResultValue); + if (connectResultValue != 0 && m_clientPrivate->m_protocolVersion != QMqttClient::MQTT_5_0) { + qCDebug(lcMqttConnection) << "Connection has been rejected."; + closeConnection(static_cast<QMqttClient::ClientError>(connectResultValue)); return; } + + // MQTT 5.0 has variable part != 2 in the header + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) { + readConnackProperties(serverProp); + m_clientPrivate->m_serverConnectionProperties = serverProp; + m_receiveAliases.resize(m_clientPrivate->m_serverConnectionProperties.maximumTopicAlias()); + m_publishAliases.resize(m_clientPrivate->m_connectionProperties.maximumTopicAlias()); + if (connectResultValue != 0) { + closeConnection(QMqttClient::Mqtt5SpecificError); + return; + } + } + m_internalState = BrokerConnected; m_clientPrivate->setStateAndError(QMqttClient::Connected); @@ -592,42 +1427,43 @@ void QMqttConnection::finalize_connack() void QMqttConnection::finalize_suback() { - quint16 id; - readBuffer((char*)&id, 2); - id = qFromBigEndian<quint16>(id); + const quint16 id = readBufferTyped<quint16>(&m_missingData); if (!m_pendingSubscriptionAck.contains(id)) { - qWarning("Received SUBACK for unknown subscription request"); + qCDebug(lcMqttConnection) << "Received SUBACK for unknown subscription request."; return; } - quint8 result; - readBuffer((char*)&result, 1); + auto sub = m_pendingSubscriptionAck.take(id); - qCDebug(lcMqttConnectionVerbose) << "Finalize SUBACK: id:" << id << "qos:" << result; - if (result <= 2) { - // The broker might have a different support level for QoS than what - // the client requested - if (result != sub->qos()) { - sub->setQos(result); - emit sub->qosChanged(result); - } - sub->setState(QMqttSubscription::Subscribed); - } else if (result == 0x80) { - qWarning() << "Subscription for id " << id << " failed."; - sub->setState(QMqttSubscription::Error); - } else { - qWarning("Received invalid SUBACK result value"); - sub->setState(QMqttSubscription::Error); + + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) + readSubscriptionProperties(sub); + + while (m_missingData > 0) { + quint8 reason = readBufferTyped<quint8>(&m_missingData); + + sub->d_func()->m_reasonCode = QMqtt::ReasonCode(reason); + qCDebug(lcMqttConnectionVerbose) << "Finalize SUBACK: id:" << id << "qos:" << reason; + if (reason <= 2) { + // The broker might have a different support level for QoS than what + // the client requested + if (reason != sub->qos()) { + sub->setQos(reason); + emit sub->qosChanged(reason); + } + sub->setState(QMqttSubscription::Subscribed); + } else { + qCDebug(lcMqttConnection) << "Subscription for id " << id << " failed. Reason Code:" << reason; + sub->setState(QMqttSubscription::Error); + } } } void QMqttConnection::finalize_unsuback() { - quint16 id; - readBuffer((char*)&id, 2); - id = qFromBigEndian<quint16>(id); + const quint16 id = readBufferTyped<quint16>(&m_missingData); qCDebug(lcMqttConnectionVerbose) << "Finalize UNSUBACK: " << id; if (!m_pendingUnsubscriptions.contains(id)) { - qWarning("Received UNSUBACK for unknown request"); + qCDebug(lcMqttConnection) << "Received UNSUBACK for unknown request."; return; } auto sub = m_pendingUnsubscriptions.take(id); @@ -638,17 +1474,39 @@ void QMqttConnection::finalize_unsuback() void QMqttConnection::finalize_publish() { // String topic - const quint16 topicLength = qFromBigEndian<quint16>(reinterpret_cast<const quint16 *>(readBuffer(2).constData())); - const QMqttTopicName topic = QString::fromUtf8(reinterpret_cast<const char *>(readBuffer(topicLength).constData())); + QMqttTopicName topic = readBufferTyped<QString>(&m_missingData); + const int topicLength = topic.name().length(); quint16 id = 0; - if (m_currentPublish.qos > 0) { - id = qFromBigEndian<quint16>(reinterpret_cast<const quint16 *>(readBuffer(2).constData())); + if (m_currentPublish.qos > 0) + id = readBufferTyped<quint16>(&m_missingData); + + QMqttPublishProperties publishProperties; + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) + readPublishProperties(publishProperties); + + const quint16 topicAlias = publishProperties.topicAlias(); + if (topicAlias > 0) { + if (topicAlias > m_clientPrivate->m_serverConnectionProperties.maximumTopicAlias()) { + qCDebug(lcMqttConnection) << "TopicAlias receive: overflow."; + closeConnection(QMqttClient::ProtocolViolation); + } else if (topicLength == 0) { // New message on existing topic alias + if (m_receiveAliases.at(topicAlias - 1).name().isEmpty()) { + qCDebug(lcMqttConnection) << "TopicAlias receive: alias for unknown topic."; + closeConnection(QMqttClient::ProtocolViolation); + } + qCDebug(lcMqttConnectionVerbose) << "TopicAlias receive: Using " << topicAlias; + topic = m_receiveAliases.at(topicAlias - 1); + } else { // Resetting a topic alias + qCDebug(lcMqttConnection) << "TopicAlias receive: Resetting:" << topic.name() << " : " << topicAlias; + m_receiveAliases[topicAlias - 1] = topic; + } } // message - qint64 payloadLength = m_missingData - (topicLength + 2) - (m_currentPublish.qos > 0 ? 2 : 0); + const quint64 payloadLength = quint64(m_missingData); const QByteArray message = readBuffer(payloadLength); + m_missingData -= payloadLength; qCDebug(lcMqttConnectionVerbose) << "Finalize PUBLISH: topic:" << topic << " payloadLength:" << payloadLength;; @@ -657,6 +1515,13 @@ void QMqttConnection::finalize_publish() QMqttMessage qmsg(topic, message, id, m_currentPublish.qos, m_currentPublish.dup, m_currentPublish.retain); + qmsg.d->m_publishProperties = publishProperties; + + if (id != 0) { + QMqttMessageStatusProperties statusProp; + statusProp.data->userProperties = publishProperties.userProperties(); + emit m_clientPrivate->m_client->messageStatusChanged(id, QMqtt::MessageStatus::Published, statusProp); + } for (auto sub = m_activeSubscriptions.constBegin(); sub != m_activeSubscriptions.constEnd(); sub++) { if (sub.key().match(topic)) @@ -672,42 +1537,59 @@ void QMqttConnection::finalize_publish() void QMqttConnection::finalize_pubAckRecComp() { qCDebug(lcMqttConnectionVerbose) << "Finalize PUBACK/REC/COMP"; - quint16 id; - readBuffer((char*)&id, 2); - id = qFromBigEndian<quint16>(id); - + const quint16 id = readBufferTyped<quint16>(&m_missingData); + + QMqttMessageStatusProperties properties; + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0 && m_missingData > 0) { + // Reason Code (1byte) + const quint8 reasonCode = readBufferTyped<quint8>(&m_missingData); + properties.data->reasonCode = QMqtt::ReasonCode(reasonCode); + if (m_missingData > 0) + readMessageStatusProperties(properties); + } if ((m_currentPacket & 0xF0) == QMqttControlPacket::PUBCOMP) { qCDebug(lcMqttConnectionVerbose) << " PUBCOMP:" << id; auto pendingRelease = m_pendingReleaseMessages.take(id); if (!pendingRelease) - qWarning("Received PUBCOMP for unknown released message"); + qCDebug(lcMqttConnection) << "Received PUBCOMP for unknown released message."; + emit m_clientPrivate->m_client->messageStatusChanged(id, QMqtt::MessageStatus::Completed, properties); emit m_clientPrivate->m_client->messageSent(id); return; } auto pendingMsg = m_pendingMessages.take(id); if (!pendingMsg) { - qWarning() << QLatin1String("Received PUBACK for unknown message: ") << id; + qCDebug(lcMqttConnection) << "Received PUBACK for unknown message: " << id; return; } if ((m_currentPacket & 0xF0) == QMqttControlPacket::PUBREC) { qCDebug(lcMqttConnectionVerbose) << " PUBREC:" << id; m_pendingReleaseMessages.insert(id, pendingMsg); + emit m_clientPrivate->m_client->messageStatusChanged(id, QMqtt::MessageStatus::Received, properties); sendControlPublishRelease(id); } else { qCDebug(lcMqttConnectionVerbose) << " PUBACK:" << id; + emit m_clientPrivate->m_client->messageStatusChanged(id, QMqtt::MessageStatus::Acknowledged, properties); emit m_clientPrivate->m_client->messageSent(id); } } void QMqttConnection::finalize_pubrel() { - quint16 id; - readBuffer((char*)&id, 2); - id = qFromBigEndian<quint16>(id); + const quint16 id = readBufferTyped<quint16>(&m_missingData); qCDebug(lcMqttConnectionVerbose) << "Finalize PUBREL:" << id; + QMqttMessageStatusProperties properties; + if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0 && m_missingData > 0) { + const quint8 reasonCode = readBufferTyped<quint8>(&m_missingData); + properties.data->reasonCode = QMqtt::ReasonCode(reasonCode); + if (m_missingData > 0) + readMessageStatusProperties(properties); + } + + emit m_clientPrivate->m_client->messageStatusChanged(id, QMqtt::MessageStatus::Released, properties); + // ### TODO: send to our app now or not??? // See standard Figure 4.3 Method A or B ??? sendControlPublishComp(id); @@ -716,23 +1598,26 @@ void QMqttConnection::finalize_pubrel() void QMqttConnection::finalize_pingresp() { qCDebug(lcMqttConnectionVerbose) << "Finalize PINGRESP"; - quint8 v; - readBuffer((char*)&v, 1); + const quint8 v = readBufferTyped<quint8>(&m_missingData); + if (v != 0) { - qWarning("Received a PINGRESP with payload!"); + qCDebug(lcMqttConnection) << "Received a PINGRESP including payload."; closeConnection(QMqttClient::ProtocolViolation); return; } emit m_clientPrivate->m_client->pingResponseReceived(); } -void QMqttConnection::processData() +bool QMqttConnection::processDataHelper() { if (m_missingData > 0) { - if (m_readBuffer.size() < m_missingData) - return; + if ((m_readBuffer.size() - m_readPosition) < m_missingData) + return false; switch (m_currentPacket & 0xF0) { + case QMqttControlPacket::AUTH: + finalize_auth(); + break; case QMqttControlPacket::CONNACK: finalize_connack(); break; @@ -757,11 +1642,15 @@ void QMqttConnection::processData() finalize_pubrel(); break; default: - qWarning("Unknown packet to finalize"); + qCDebug(lcMqttConnection) << "Unknown packet to finalize."; closeConnection(QMqttClient::ProtocolViolation); - return; + return false; } - m_missingData = 0; + + Q_ASSERT(m_missingData == 0); + + m_readBuffer = m_readBuffer.mid(m_readPosition); + m_readPosition = 0; } // MQTT-2.2 A fixed header of a control packet must be at least 2 bytes. If the payload is @@ -769,48 +1658,48 @@ void QMqttConnection::processData() switch (m_readBuffer.size()) { case 0: case 1: - return; + return false; case 2: if ((m_readBuffer.at(1) & 128) != 0) - return; + return false; break; case 3: if ((m_readBuffer.at(1) & 128) != 0 && (m_readBuffer.at(2) & 128) != 0) - return; + return false; break; case 4: if ((m_readBuffer.at(1) & 128) != 0 && (m_readBuffer.at(2) & 128) != 0 && (m_readBuffer.at(3) & 128) != 0) - return; + return false; break; default: break; } - readBuffer((char*)&m_currentPacket, 1); + readBuffer(reinterpret_cast<char *>(&m_currentPacket), 1); switch (m_currentPacket & 0xF0) { case QMqttControlPacket::CONNACK: { qCDebug(lcMqttConnectionVerbose) << "Received CONNACK"; if (m_internalState != BrokerWaitForConnectAck) { - qWarning("Received CONNACK at unexpected time!"); + qCDebug(lcMqttConnection) << "Received CONNACK at unexpected time."; closeConnection(QMqttClient::ProtocolViolation); - return; + return false; } - quint8 payloadSize; - readBuffer((char*)&payloadSize, 1); - if (payloadSize != 2) { - qWarning("Unexpected FRAME size in CONNACK"); - closeConnection(QMqttClient::ProtocolViolation); - return; + qint32 payloadSize = readVariableByteInteger(); + if (m_clientPrivate->m_protocolVersion != QMqttClient::MQTT_5_0) { + if (payloadSize != 2) { + qCDebug(lcMqttConnection) << "Unexpected FRAME size in CONNACK."; + closeConnection(QMqttClient::ProtocolViolation); + return false; + } } - m_missingData = 2; + m_missingData = payloadSize; break; } case QMqttControlPacket::SUBACK: { qCDebug(lcMqttConnectionVerbose) << "Received SUBACK"; - quint8 remaining; - readBuffer((char*)&remaining, 1); + const quint8 remaining = readBufferTyped<quint8>(); m_missingData = remaining; break; } @@ -822,26 +1711,12 @@ void QMqttConnection::processData() if ((m_currentPublish.qos == 0 && m_currentPublish.dup != 0) || m_currentPublish.qos > 2) { closeConnection(QMqttClient::ProtocolViolation); - return; + return false; } - // remaining length - quint32 multiplier = 1; - quint32 msgLength = 0; - quint8 b = 0; - quint8 iteration = 0; - do { - readBuffer((char*)&b, 1); - msgLength += (b & 127) * multiplier; - multiplier *= 128; - iteration++; - if (iteration > 4) { - qWarning("Publish message is too big to handle"); - closeConnection(QMqttClient::ProtocolViolation); - return; - } - } while ((b & 128) != 0); - m_missingData = msgLength; + m_missingData = readVariableByteInteger(); + if (m_missingData == -1) + return false; // Connection closed inside readVariableByteInteger break; } case QMqttControlPacket::PINGRESP: @@ -852,17 +1727,16 @@ void QMqttConnection::processData() case QMqttControlPacket::PUBREL: { qCDebug(lcMqttConnectionVerbose) << "Received PUBREL"; - char remaining; - readBuffer(&remaining, 1); + const quint8 remaining = readBufferTyped<quint8>(); if (remaining != 0x02) { - qWarning("Received 2 byte message with invalid remaining length"); + qCDebug(lcMqttConnection) << "Received 2 byte message with invalid remaining length."; closeConnection(QMqttClient::ProtocolViolation); - return; + return false; } if ((m_currentPacket & 0x0F) != 0x02) { - qWarning("Malformed fixed header for PUBREL"); + qCDebug(lcMqttConnection) << "Malformed fixed header for PUBREL."; closeConnection(QMqttClient::ProtocolViolation); - return; + return false; } m_missingData = 2; break; @@ -874,39 +1748,44 @@ void QMqttConnection::processData() case QMqttControlPacket::PUBCOMP: { qCDebug(lcMqttConnectionVerbose) << "Received UNSUBACK/PUBACK/PUBREC/PUBCOMP"; if ((m_currentPacket & 0x0F) != 0) { - qWarning("Malformed fixed header"); + qCDebug(lcMqttConnection) << "Malformed fixed header for UNSUBACK/PUBACK/PUBREC/PUBCOMP."; closeConnection(QMqttClient::ProtocolViolation); - return; + return false; } - char remaining; - readBuffer(&remaining, 1); - if (remaining != 0x02) { - qWarning("Received 2 byte message with invalid remaining length"); + const quint8 remaining = readBufferTyped<quint8>(); + if (m_clientPrivate->m_protocolVersion != QMqttClient::MQTT_5_0 && remaining != 0x02) { + qCDebug(lcMqttConnection) << "Received 2 byte message with invalid remaining length."; closeConnection(QMqttClient::ProtocolViolation); - return; + return false; } - m_missingData = 2; + m_missingData = remaining; break; } default: - qWarning("Received unknown command"); + qCDebug(lcMqttConnection) << "Received unknown command."; closeConnection(QMqttClient::ProtocolViolation); - return; + return false; } /* set current command CONNACK - PINGRESP */ /* read command size */ /* calculate missing_data */ - processData(); // implicitly finishes and enqueues - return; + return true; // reiterate. implicitly finishes and enqueues +} + +void QMqttConnection::processData() +{ + while (processDataHelper()) + ; } bool QMqttConnection::writePacketToTransport(const QMqttControlPacket &p) { const QByteArray writeData = p.serialize(); + qCDebug(lcMqttConnectionVerbose) << Q_FUNC_INFO << " DataSize:" << writeData.size(); const qint64 res = m_transport->write(writeData.constData(), writeData.size()); if (Q_UNLIKELY(res == -1)) { - qWarning("Could not write frame to transport"); + qCDebug(lcMqttConnection) << "Could not write frame to transport."; return false; } return true; diff --git a/src/mqtt/qmqttconnection_p.h b/src/mqtt/qmqttconnection_p.h index cf9c912..cfe1641 100644 --- a/src/mqtt/qmqttconnection_p.h +++ b/src/mqtt/qmqttconnection_p.h @@ -67,7 +67,7 @@ public: BrokerConnected }; - explicit QMqttConnection(QObject *parent = 0); + explicit QMqttConnection(QObject *parent = nullptr); ~QMqttConnection() override; void setTransport(QIODevice *device, QMqttClient::TransportType transport); @@ -77,13 +77,15 @@ public: bool ensureTransportOpen(const QString &sslPeerName = QString()); bool sendControlConnect(); - qint32 sendControlPublish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos = 0, bool retain = false); + 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); bool sendControlPublishRelease(quint16 id); bool sendControlPublishReceive(quint16 id); bool sendControlPublishComp(quint16 id); - QMqttSubscription *sendControlSubscribe(const QMqttTopicFilter &topic, quint8 qos = 0); - bool sendControlUnsubscribe(const QMqttTopicFilter &topic); + QMqttSubscription *sendControlSubscribe(const QMqttTopicFilter &topic, quint8 qos, const QMqttSubscriptionProperties &properties); + bool sendControlUnsubscribe(const QMqttTopicFilter &topic, const QMqttUnsubscriptionProperties &properties); bool sendControlPingRequest(); bool sendControlDisconnect(); @@ -106,7 +108,7 @@ public: QMqttClientPrivate *m_clientPrivate{nullptr}; private: Q_DISABLE_COPY(QMqttConnection) - void someFuncToBeRemoved(); + void finalize_auth(); void finalize_connack(); void finalize_suback(); void finalize_unsuback(); @@ -115,17 +117,32 @@ private: void finalize_pubrel(); void finalize_pingresp(); void processData(); - void readBuffer(char *data, qint64 size); + bool processDataHelper(); + void readBuffer(char *data, quint64 size); + qint32 readVariableByteInteger(qint32 *byteCount = nullptr); + void readAuthProperties(QMqttAuthenticationProperties &properties); + void readConnackProperties(QMqttServerConnectionProperties &properties); + void readMessageStatusProperties(QMqttMessageStatusProperties &properties); + void readPublishProperties(QMqttPublishProperties &properties); + void readSubscriptionProperties(QMqttSubscription *sub); + QByteArray writeConnectProperties(); + QByteArray writeLastWillProperties() const; + 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(qint64 size); + QByteArray readBuffer(quint64 size); + template<typename T> T readBufferTyped(qint64 *dataSize = nullptr); QByteArray m_readBuffer; + int m_readPosition{0}; qint64 m_missingData{0}; struct PublishData { quint8 qos; bool dup; bool retain; }; - PublishData m_currentPublish; + PublishData m_currentPublish{0, false, false}; QMqttControlPacket::PacketType m_currentPacket{QMqttControlPacket::UNKNOWN}; bool writePacketToTransport(const QMqttControlPacket &p); @@ -136,6 +153,9 @@ private: QMap<quint16, QSharedPointer<QMqttControlPacket>> m_pendingReleaseMessages; InternalConnectionState m_internalState{BrokerDisconnected}; QTimer m_pingTimer; + + QVector<QMqttTopicName> m_receiveAliases; + QVector<QMqttTopicName> m_publishAliases; }; QT_END_NAMESPACE diff --git a/src/mqtt/qmqttconnectionproperties.cpp b/src/mqtt/qmqttconnectionproperties.cpp new file mode 100644 index 0000000..aae5b58 --- /dev/null +++ b/src/mqtt/qmqttconnectionproperties.cpp @@ -0,0 +1,651 @@ +/****************************************************************************** +** +** 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 "qmqttconnectionproperties.h" + +#include "qmqttconnectionproperties_p.h" + +#include <QtCore/QLoggingCategory> + +#include <limits> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcMqttConnection) + +/*! + \class QMqttConnectionProperties + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttConnectionProperties class represents configuration + options a QMqttClient can pass to the server when invoking + QMqttClient::connectToHost(). + + \note Connection properties are part of the MQTT 5.0 specification and + cannot be used when connecting with a lower protocol level. See + QMqttClient::ProtocolVersion for more information. +*/ + +/*! + \class QMqttServerConnectionProperties + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttServerConnectionProperties class represents configuration + options of a server a QMqttClient is connected to. + + When a connection has been established the server might send additional + details about the connection properties. Use availableProperties() to + identify properties set by the server. If a property is not set by the + server, default values are assumed and can be obtained by invoking access + functions of this instance. + + \note Connection properties are part of the MQTT 5.0 specification and + cannot be used when connecting with a lower protocol level. See + QMqttClient::ProtocolVersion for more information. +*/ + +/*! + \class QMqttLastWillProperties + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttLastWillProperties class represents configuration + options a QMqttClient can pass to the server when specifying the last will + during connecting to a server. + + \note Last Will properties are part of the MQTT 5.0 specification and + cannot be used when connecting with a lower protocol level. See + QMqttClient::ProtocolVersion for more information. +*/ + +/*! + \enum QMqttServerConnectionProperties::ServerPropertyDetail + + This enum type specifies the available properties set by the + server or the client after establishing a connection. + + \value None + No property has been specified. + \value SessionExpiryInterval + The number of seconds the server keeps the session after + a disconnect. + \value MaximumReceive + The maximum number of QoS 1 and 2 message the server is + capable of managing concurrently. + \value MaximumQoS + The maximum QoS level the server can understand. + \value RetainAvailable + Specifies whether retained messages are supported. + \value MaximumPacketSize + Specifies the maximum packet size including the message header + and properties. + \value AssignedClientId + Specifies whether the server assigned a client identifier. + \value MaximumTopicAlias + Specifies the maximum amount of topic aliases. + \value ReasonString + Specifies a string providing more details on connection state. + \value UserProperty + Specifies additional user properties. + \value WildCardSupported + Specifies whether the server supports wildcard subscriptions. + \value SubscriptionIdentifierSupport + Specifies whether the server supports subscription identifiers. + \value SharedSubscriptionSupport + Specifies whether the server supports shared subscriptions. + \value ServerKeepAlive + Specifies the number of seconds the server expects a keep alive + packet from the client. + \value ResponseInformation + Specifies the response information. + \value ServerReference + Specifies an alternative server address for the client to + connect to. + \value AuthenticationMethod + Specifies the authentication method. + \value AuthenticationData + Specifies the authentication data. +*/ + +/*! + \internal +*/ +QMqttLastWillProperties::QMqttLastWillProperties() : data(new QMqttLastWillPropertiesData) +{ +} + +/*! + \internal +*/ +QMqttLastWillProperties::QMqttLastWillProperties(const QMqttLastWillProperties &) = default; + +QMqttLastWillProperties &QMqttLastWillProperties::operator=(const QMqttLastWillProperties &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +QMqttLastWillProperties::~QMqttLastWillProperties() = default; + +/*! + Returns the delay in seconds a last will message will be sent after + disconnecting from the server. +*/ +quint32 QMqttLastWillProperties::willDelayInterval() const +{ + return data->willDelayInterval; +} + +/*! + Returns the payload format indicator. +*/ +QMqtt::PayloadFormatIndicator QMqttLastWillProperties::payloadFormatIndicator() const +{ + return data->formatIndicator; +} + +/*! + Returns the lifetime of the last will message in seconds, starting from + the will delay interval. +*/ +quint32 QMqttLastWillProperties::messageExpiryInterval() const +{ + return data->messageExpiryInterval; +} + +/*! + Returns the content type of the last will message. +*/ +QString QMqttLastWillProperties::contentType() const +{ + return data->contentType; +} + +/*! + Returns the topic that subscribers to the last will message should respond + to. +*/ +QString QMqttLastWillProperties::responseTopic() const +{ + return data->responseTopic; +} + +/*! + Returns the correlation data to identify the request. +*/ +QByteArray QMqttLastWillProperties::correlationData() const +{ + return data->correlationData; +} + +/*! + Returns the user properties. +*/ +QMqttUserProperties QMqttLastWillProperties::userProperties() const +{ + return data->userProperties; +} + +/*! + Sets the will delay interval to \a delay. +*/ +void QMqttLastWillProperties::setWillDelayInterval(quint32 delay) +{ + data->willDelayInterval = delay; +} + +/*! + Sets the payload format indicator to \a p. +*/ +void QMqttLastWillProperties::setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator p) +{ + data->formatIndicator = p; +} + +/*! + Sets the message expiry interval to \a expiry. +*/ +void QMqttLastWillProperties::setMessageExpiryInterval(quint32 expiry) +{ + data->messageExpiryInterval = expiry; +} + +/*! + Sets the content type to \a content. +*/ +void QMqttLastWillProperties::setContentType(const QString &content) +{ + data->contentType = content; +} + +/*! + Sets the response topic to \a response. +*/ +void QMqttLastWillProperties::setResponseTopic(const QString &response) +{ + data->responseTopic = response; +} + +/*! + Sets the correlation data to \a correlation. +*/ +void QMqttLastWillProperties::setCorrelationData(const QByteArray &correlation) +{ + data->correlationData = correlation; +} + +/*! + Sets the user properties to \a properties. +*/ +void QMqttLastWillProperties::setUserProperties(const QMqttUserProperties &properties) +{ + data->userProperties = properties; +} + +/*! + \internal +*/ +QMqttConnectionProperties::QMqttConnectionProperties() : data(new QMqttConnectionPropertiesData) +{ + +} + +/*! + \internal +*/ +QMqttConnectionProperties::QMqttConnectionProperties(const QMqttConnectionProperties &) = default; + +QMqttConnectionProperties &QMqttConnectionProperties::operator=(const QMqttConnectionProperties &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +QMqttConnectionProperties::~QMqttConnectionProperties() = default; + +/*! + Sets the session expiry interval to \a expiry. The session expiry interval + specifies the number of seconds a server holds information on the client + state after a connection has been closed. + + The default value is 0, which specifies that the session is closed when + the network connection ends. If the value is specified as maximum of + quint32, then the session does not expire. +*/ +void QMqttConnectionProperties::setSessionExpiryInterval(quint32 expiry) +{ + data->sessionExpiryInterval = expiry; +} + +/*! + Sets the maximum QoS level the client is allowed to receive to \a qos. + + A maximum receive value of 0 is not allowed. +*/ +void QMqttConnectionProperties::setMaximumReceive(quint16 qos) +{ + if (qos == 0) { + qCDebug(lcMqttConnection) << "Maximum Receive is not allowed to be 0."; + return; + } + data->maximumReceive = qos; +} + +/*! + Sets the maximum packet size to \a packetSize. The maximum packet size + specifies the maximum size one packet can contain. This includes the + packet header and its properties. + + If no maximum packet size is specified, no limit is imposed beyond the + limitations of the protocol itself. +*/ +void QMqttConnectionProperties::setMaximumPacketSize(quint32 packetSize) +{ + if (packetSize == 0) { + qCDebug(lcMqttConnection) << "Packet size is not allowed to be 0."; + return; + } + data->maximumPacketSize = packetSize; +} + +/*! + Sets the maximum topic alias to \a alias. The maximum topic alias specifies + the highest value that the client will accept from the server. The client + uses this value to limit the number of topic aliases it is willing to hold + for the connection. + + The default value is 0. 0 indicates that the client does not accept any + topic aliases on this connection. +*/ +void QMqttConnectionProperties::setMaximumTopicAlias(quint16 alias) +{ + data->maximumTopicAlias = alias; +} + +/*! + Sets the request response information to \a response. A client uses this + to request the server to return response information after the connection + request has been handled. + + The default value is \c false, which indicates that the client must not + return any response information. If the value is \c true, the server + may return response information, but is not enforced to do so. +*/ +void QMqttConnectionProperties::setRequestResponseInformation(bool response) +{ + data->requestResponseInformation = response; +} + +/*! + Sets the request problem information to \a problem. A client uses this + to request the server to return additional information in case of failure. + Types of failure include connection and message management on the server + side. + + The default value is \c false, which indicates that the client must not + receive any problem information for anything but connection management. + The server still may send problem information for connection handling. + If the value is \c true, the server may return problem information. + + Problem information is available in user properties or reason strings + of the property classes. +*/ +void QMqttConnectionProperties::setRequestProblemInformation(bool problem) +{ + data->requestProblemInformation = problem; +} + +/*! + Sets the user properties of the connection to \a properties. + + The default value is to not send any user information. +*/ +void QMqttConnectionProperties::setUserProperties(const QMqttUserProperties &properties) +{ + data->userProperties = properties; +} + +/*! + Sets the authentication method to \a authMethod. + + \sa authenticationData() +*/ +void QMqttConnectionProperties::setAuthenticationMethod(const QString &authMethod) +{ + data->authenticationMethod = authMethod; +} + +/*! + Sets the authentication data to \a authData. + + Authentication data can only be used if an authentication method has + been specified. + + \sa authenticationMethod() +*/ +void QMqttConnectionProperties::setAuthenticationData(const QByteArray &authData) +{ + data->authenticationData = authData; +} + +/*! + Returns the session expiry interval. +*/ +quint32 QMqttConnectionProperties::sessionExpiryInterval() const +{ + return data->sessionExpiryInterval; +} + +/*! + Returns the maximum QoS level the client can receive. +*/ +quint16 QMqttConnectionProperties::maximumReceive() const +{ + return data->maximumReceive; +} + +/*! + Returns the maximum packet size the client can receive. +*/ +quint32 QMqttConnectionProperties::maximumPacketSize() const +{ + return data->maximumPacketSize; +} + +/*! + Returns the maximum topic alias ID the client can use. +*/ +quint16 QMqttConnectionProperties::maximumTopicAlias() const +{ + return data->maximumTopicAlias; +} + +/*! + Returns whether the client should receive response information. +*/ +bool QMqttConnectionProperties::requestResponseInformation() const +{ + return data->requestResponseInformation; +} + +/*! + Returns whether the client should receive problem information. +*/ +bool QMqttConnectionProperties::requestProblemInformation() const +{ + return data->requestProblemInformation; +} + +/*! + Returns the user properties for the connection. +*/ +QMqttUserProperties QMqttConnectionProperties::userProperties() const +{ + return data->userProperties; +} + +/*! + Returns the authentication method. +*/ +QString QMqttConnectionProperties::authenticationMethod() const +{ + return data->authenticationMethod; +} + +/*! + Returns the authentication data. +*/ +QByteArray QMqttConnectionProperties::authenticationData() const +{ + return data->authenticationData; +} + +/*! + \internal +*/ +QMqttServerConnectionProperties::QMqttServerConnectionProperties() + : QMqttConnectionProperties() + , serverData(new QMqttServerConnectionPropertiesData) +{ + +} + +/*! + \internal +*/ +QMqttServerConnectionProperties::QMqttServerConnectionProperties(const QMqttServerConnectionProperties &rhs) + : QMqttConnectionProperties(rhs) + , serverData(rhs.serverData) +{ + +} + +QMqttServerConnectionProperties &QMqttServerConnectionProperties::operator=(const QMqttServerConnectionProperties &rhs) +{ + if (this != &rhs) { + serverData.operator=(rhs.serverData); + QMqttConnectionProperties::operator=(rhs); + } + return *this; +} + +QMqttServerConnectionProperties::~QMqttServerConnectionProperties() = default; + +/*! + Returns the available properties specified by the server. +*/ +QMqttServerConnectionProperties::ServerPropertyDetails QMqttServerConnectionProperties::availableProperties() const +{ + return serverData->details; +} + +/*! + Returns \c true if the server provided properties as part of the connection + aknowledgment. Returns \c false if no properties have been provided. +*/ +bool QMqttServerConnectionProperties::isValid() const +{ + return serverData->valid; +} + +/*! + Returns the maximum QoS level the server is able to understand. + The default value is 2. +*/ +quint8 QMqttServerConnectionProperties::maximumQoS() const +{ + return serverData->maximumQoS; +} + +/*! + Returns \c true if the server accepts retained messages. + The default value is \c true. +*/ +bool QMqttServerConnectionProperties::retainAvailable() const +{ + return serverData->retainAvailable; +} + +/*! + Returns \c true if the server assigned a new client identifier to + the client. + + \sa QMqttClient::clientId() +*/ +bool QMqttServerConnectionProperties::clientIdAssigned() const +{ + return serverData->details & QMqttServerConnectionProperties::AssignedClientId; +} + +/*! + Returns the reason string associated with this response. +*/ +QString QMqttServerConnectionProperties::reason() const +{ + return serverData->reasonString; +} + +/*! + Returns the reason code associated with this response. +*/ +QMqtt::ReasonCode QMqttServerConnectionProperties::reasonCode() const +{ + return serverData->reasonCode; +} + +/*! + Returns \c true if the server accepts subscriptions including wildcards. + The default value is \c true. +*/ +bool QMqttServerConnectionProperties::wildcardSupported() const +{ + return serverData->wildcardSupported; +} + +/*! + Returns \c true if the server accepts subscription identifiers. + Subscription identifiers can be passed to the server when creating + a new subscription. + + The default value is \c true. + + \sa QMqttSubscriptionProperties::setSubscriptionIdentifier() +*/ +bool QMqttServerConnectionProperties::subscriptionIdentifierSupported() const +{ + return serverData->subscriptionIdentifierSupported; +} + +/*! + Returns \c true if the server accepts shared subscriptions. + The default value is \c true. +*/ +bool QMqttServerConnectionProperties::sharedSubscriptionSupported() const +{ + return serverData->sharedSubscriptionSupported; +} + + +/*! + Returns the number of seconds the server requested as keep alive. This + overwrites the keep alive being set from the client side. + + \sa QMqttClient::setKeepAlive() +*/ +quint16 QMqttServerConnectionProperties::serverKeepAlive() const +{ + return serverData->serverKeepAlive; +} + +/*! + Returns the response information. +*/ +QString QMqttServerConnectionProperties::responseInformation() const +{ + return serverData->responseInformation; +} + +/*! + Returns a server address which can be used by the client alternatively + to connect to. Typically, this is used together with the reason + code \c 0x9c (Use another server) or \c 0x9c (Server moved). +*/ +QString QMqttServerConnectionProperties::serverReference() const +{ + return serverData->serverReference; +} + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqttconnectionproperties.h b/src/mqtt/qmqttconnectionproperties.h new file mode 100644 index 0000000..8741f76 --- /dev/null +++ b/src/mqtt/qmqttconnectionproperties.h @@ -0,0 +1,166 @@ +/****************************************************************************** +** +** 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 QMQTTCONNECTIONPROPERTIES_H +#define QMQTTCONNECTIONPROPERTIES_H + +#include <QtMqtt/qmqttglobal.h> +#include <QtMqtt/qmqtttype.h> + +#include <QtCore/QObject> +#include <QtCore/QSharedDataPointer> + +QT_BEGIN_NAMESPACE + +class QMqttConnectionPropertiesData; +class QMqttLastWillPropertiesData; +class QMqttServerConnectionPropertiesData; + +class Q_MQTT_EXPORT QMqttLastWillProperties +{ + Q_GADGET +public: + QMqttLastWillProperties(); + QMqttLastWillProperties(const QMqttLastWillProperties &); + QMqttLastWillProperties &operator=(const QMqttLastWillProperties &); + ~QMqttLastWillProperties(); + + quint32 willDelayInterval() const; + QMqtt::PayloadFormatIndicator payloadFormatIndicator() const; + quint32 messageExpiryInterval() const; + QString contentType() const; + QString responseTopic() const; + QByteArray correlationData() const; + QMqttUserProperties userProperties() const; + + void setWillDelayInterval(quint32 delay); + void setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator p); + void setMessageExpiryInterval(quint32 expiry); + void setContentType(const QString &content); + void setResponseTopic(const QString &response); + void setCorrelationData(const QByteArray &correlation); + void setUserProperties(const QMqttUserProperties &properties); + +protected: + QSharedDataPointer<QMqttLastWillPropertiesData> data; +}; + +class Q_MQTT_EXPORT QMqttConnectionProperties +{ + Q_GADGET +public: + QMqttConnectionProperties(); + QMqttConnectionProperties(const QMqttConnectionProperties &); + QMqttConnectionProperties &operator=(const QMqttConnectionProperties &); + ~QMqttConnectionProperties(); + + quint32 sessionExpiryInterval() const; + quint16 maximumReceive() const; + quint32 maximumPacketSize() const; + quint16 maximumTopicAlias() const; + bool requestResponseInformation() const; + bool requestProblemInformation() const; + QMqttUserProperties userProperties() const; + QString authenticationMethod() const; + QByteArray authenticationData() const; + + void setSessionExpiryInterval(quint32 expiry); + void setMaximumReceive(quint16 qos); + void setMaximumPacketSize(quint32 packetSize); + void setMaximumTopicAlias(quint16 alias); + void setRequestResponseInformation(bool response); + void setRequestProblemInformation(bool problem); + void setUserProperties(const QMqttUserProperties &properties); + void setAuthenticationMethod(const QString &authMethod); + void setAuthenticationData(const QByteArray &authData); + +protected: + QSharedDataPointer<QMqttConnectionPropertiesData> data; +}; + +class Q_MQTT_EXPORT QMqttServerConnectionProperties + : public QMqttConnectionProperties +{ + Q_GADGET +public: + enum ServerPropertyDetail : quint32 { + None = 0x00000000, + SessionExpiryInterval = 0x00000001, + MaximumReceive = 0x00000002, + MaximumQoS = 0x00000004, + RetainAvailable = 0x00000010, + MaximumPacketSize = 0x00000020, + AssignedClientId = 0x00000040, + MaximumTopicAlias = 0x00000080, + ReasonString = 0x00000100, + UserProperty = 0x00000200, + WildCardSupported = 0x00000400, + SubscriptionIdentifierSupport = 0x00000800, + SharedSubscriptionSupport = 0x00001000, + ServerKeepAlive = 0x00002000, + ResponseInformation = 0x00004000, + ServerReference = 0x00008000, + AuthenticationMethod = 0x00010000, + AuthenticationData = 0x00020000 + }; + Q_ENUM(ServerPropertyDetail) + Q_DECLARE_FLAGS(ServerPropertyDetails, ServerPropertyDetail) + + QMqttServerConnectionProperties(); + QMqttServerConnectionProperties(const QMqttServerConnectionProperties &); + QMqttServerConnectionProperties &operator=(const QMqttServerConnectionProperties &); + ~QMqttServerConnectionProperties(); + + ServerPropertyDetails availableProperties() const; + + bool isValid() const; + + quint8 maximumQoS() const; + bool retainAvailable() const; + bool clientIdAssigned() const; + QString reason() const; + QMqtt::ReasonCode reasonCode() const; + bool wildcardSupported() const; + bool subscriptionIdentifierSupported() const; + bool sharedSubscriptionSupported() const; + quint16 serverKeepAlive() const; + QString responseInformation() const; + QString serverReference() const; + + +private: + friend class QMqttConnection; + QSharedDataPointer<QMqttServerConnectionPropertiesData> serverData; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QMqttServerConnectionProperties::ServerPropertyDetails) + +QT_END_NAMESPACE + +#endif // QMQTTCONNECTIONPROPERTIES_H diff --git a/src/mqtt/qmqttconnectionproperties_p.h b/src/mqtt/qmqttconnectionproperties_p.h new file mode 100644 index 0000000..561fdb3 --- /dev/null +++ b/src/mqtt/qmqttconnectionproperties_p.h @@ -0,0 +1,93 @@ +/****************************************************************************** +** +** 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 QMQTTCONNECTIONPROPERTIES_P_H +#define QMQTTCONNECTIONPROPERTIES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qmqttconnectionproperties.h" + +QT_BEGIN_NAMESPACE + +class QMqttLastWillPropertiesData : public QSharedData +{ +public: + QString contentType; + QString responseTopic; + QByteArray correlationData; + QMqttUserProperties userProperties; + quint32 willDelayInterval{0}; + quint32 messageExpiryInterval{0}; + QMqtt::PayloadFormatIndicator formatIndicator{QMqtt::PayloadFormatIndicator::Unspecified}; +}; + +class QMqttConnectionPropertiesData : public QSharedData +{ +public: + QMqttUserProperties userProperties; + QString authenticationMethod; + QByteArray authenticationData; + quint32 sessionExpiryInterval{0}; + quint32 maximumPacketSize{std::numeric_limits<quint32>::max()}; + quint16 maximumReceive{65535}; + quint16 maximumTopicAlias{0}; + bool requestResponseInformation{false}; + bool requestProblemInformation{true}; +}; + +class QMqttServerConnectionPropertiesData : public QSharedData +{ +public: + QMqttServerConnectionProperties::ServerPropertyDetails details{QMqttServerConnectionProperties::None}; + QString reasonString; + QString responseInformation; + QString serverReference; + quint16 serverKeepAlive{0}; + quint8 maximumQoS{2}; + QMqtt::ReasonCode reasonCode{QMqtt::ReasonCode::Success}; + bool valid{false}; // Only set to true after CONNACK + bool retainAvailable{true}; + bool wildcardSupported{true}; + bool subscriptionIdentifierSupported{true}; + bool sharedSubscriptionSupported{true}; +}; + +QT_END_NAMESPACE + +#endif // QMQTTCONNECTIONPROPERTIES_P_H diff --git a/src/mqtt/qmqttcontrolpacket.cpp b/src/mqtt/qmqttcontrolpacket.cpp index 3d0db8d..4eb3663 100644 --- a/src/mqtt/qmqttcontrolpacket.cpp +++ b/src/mqtt/qmqttcontrolpacket.cpp @@ -28,10 +28,14 @@ ******************************************************************************/ #include "qmqttcontrolpacket_p.h" + #include <QtCore/QtEndian> +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcMqttClient) + QMqttControlPacket::QMqttControlPacket() { @@ -73,8 +77,14 @@ void QMqttControlPacket::append(quint16 value) { const quint16 msb = qToBigEndian<quint16>(value); const char * msb_c = reinterpret_cast<const char*>(&msb); - m_payload.append(msb_c[0]); - m_payload.append(msb_c[1]); + m_payload.append(msb_c, 2); +} + +void QMqttControlPacket::append(quint32 value) +{ + const quint32 msb = qToBigEndian<quint32>(value); + const char * msb_c = reinterpret_cast<const char*>(&msb); + m_payload.append(msb_c, 4); } void QMqttControlPacket::append(const QByteArray &data) @@ -88,16 +98,40 @@ void QMqttControlPacket::appendRaw(const QByteArray &data) m_payload.append(data); } +void QMqttControlPacket::appendRawVariableInteger(quint32 value) +{ + QByteArray data; + // Add length + if (value > 268435455) + qCDebug(lcMqttClient) << "Attempting to write variable integer overflow."; + do { + quint8 b = value % 128; + value /= 128; + if (value > 0) + b |= 0x80; + data.append(char(b)); + } while (value > 0); + appendRaw(data); +} + QByteArray QMqttControlPacket::serialize() const { // Create ByteArray QByteArray data; // Add Header data.append(char(m_header)); + data.append(serializePayload()); + + return data; +} + +QByteArray QMqttControlPacket::serializePayload() const +{ + QByteArray data; // Add length - quint32 msgSize = m_payload.size(); - if (msgSize > 268435455) // 0xFFFFFF7F - qWarning("Publishing a message bigger than maximum size!"); + quint32 msgSize = quint32(m_payload.size()); + if (msgSize > 268435455) + qCDebug(lcMqttClient) << "Publishing a message bigger than maximum size."; do { quint8 b = msgSize % 128; msgSize /= 128; @@ -107,7 +141,6 @@ QByteArray QMqttControlPacket::serialize() const } while (msgSize > 0); // Add payload data.append(m_payload); - return data; } diff --git a/src/mqtt/qmqttcontrolpacket_p.h b/src/mqtt/qmqttcontrolpacket_p.h index 48bf4cd..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(); @@ -79,10 +80,13 @@ public: void append(char value); void append(quint16 value); + void append(quint32 value); void append(const QByteArray &data); void appendRaw(const QByteArray &data); + void appendRawVariableInteger(quint32 value); QByteArray serialize() const; + QByteArray serializePayload() const; inline QByteArray payload() const { return m_payload; } private: quint8 m_header{UNKNOWN}; diff --git a/src/mqtt/qmqttglobal.h b/src/mqtt/qmqttglobal.h index 720531b..8386a7c 100644 --- a/src/mqtt/qmqttglobal.h +++ b/src/mqtt/qmqttglobal.h @@ -44,6 +44,57 @@ QT_BEGIN_NAMESPACE # define Q_MQTT_EXPORT #endif +namespace QMqtt +{ +enum class PayloadFormatIndicator : quint8 { + Unspecified = 0, + UTF8Encoded = 1 +}; + +enum class MessageStatus : quint8 { + Unknown = 0, + Published, + Acknowledged, + Received, + Released, + Completed +}; + +enum class ReasonCode : quint16 { + Success = 0, + SubscriptionQoSLevel0 = 0, + SubscriptionQoSLevel1 = 0x01, + SubscriptionQoSLevel2 = 0x02, + NoMatchingSubscriber = 0x10, + UnspecifiedError = 0x80, + MalformedPacket = 0x81, + ProtocolError = 0x82, + ImplementationSpecificError = 0x83, + UnsupportedProtocolVersion = 0x84, + InvalidClientId = 0x85, + InvalidUserNameOrPassword = 0x86, + NotAuthorized = 0x87, + ServerNotAvailable = 0x88, + ServerBusy = 0x89, + ClientBanned = 0x8A, + InvalidAuthenticationMethod = 0x8C, + InvalidTopicFilter = 0x8F, + InvalidTopicName = 0x90, + MessageIdInUse = 0x91, + MessageIdNotFound = 0x92, + PacketTooLarge = 0x95, + QuotaExceeded = 0x97, + InvalidPayloadFormat = 0x99, + RetainNotSupported = 0x9A, + QoSNotSupported = 0x9B, + UseAnotherServer = 0x9C, + ServerMoved = 0x9D, + SharedSubscriptionsNotSupported = 0x9E, + ExceededConnectionRate = 0x9F, + SubscriptionIdsNotSupported = 0xA1, + WildCardSubscriptionsNotSupported = 0xA2 +}; +} QT_END_NAMESPACE #endif //QTQMQTTGLOBAL_H diff --git a/src/mqtt/qmqttmessage.cpp b/src/mqtt/qmqttmessage.cpp index 25ed768..1f3b49e 100644 --- a/src/mqtt/qmqttmessage.cpp +++ b/src/mqtt/qmqttmessage.cpp @@ -28,6 +28,7 @@ ******************************************************************************/ #include "qmqttmessage.h" +#include "qmqttmessage_p.h" QT_BEGIN_NAMESPACE @@ -86,25 +87,6 @@ QT_BEGIN_NAMESPACE live update. A broker can store only one retained message per topic. */ -class QMqttMessagePrivate : public QSharedData -{ -public: - bool operator==(const QMqttMessagePrivate &other) { - return m_topic == other.m_topic && - m_payload == other.m_payload && - m_id == other.m_id && - m_qos == other.m_qos && - m_duplicate == other.m_duplicate && - m_retain == other.m_retain; - } - QMqttTopicName m_topic; - QByteArray m_payload; - quint16 m_id{0}; - quint8 m_qos{0}; - bool m_duplicate{false}; - bool m_retain{false}; -}; - /*! Creates a new MQTT message. */ @@ -181,6 +163,24 @@ bool QMqttMessage::retain() const } /*! + \since 5.12 + + Returns the publish properties received as part of the message. + + \note This function only specifies the properties when a + publish message is received. Messages with a QoS value of + 1 or 2 can contain additional properties when a message is released. + Those can be obtained by the QMqttClient::messageStatusChanged signal. + + \note This function will only provide valid data when the client + specifies QMqttClient::MQTT_5_0 as QMqttClient::ProtocolVersion. +*/ +QMqttPublishProperties QMqttMessage::publishProperties() const +{ + return d->m_publishProperties; +} + +/*! \internal Constructs a new MQTT message with \a content on the topic \a topic. Furthermore the properties \a id, \a qos, \a dup, \a retain must be specified. diff --git a/src/mqtt/qmqttmessage.h b/src/mqtt/qmqttmessage.h index c011cbe..af36918 100644 --- a/src/mqtt/qmqttmessage.h +++ b/src/mqtt/qmqttmessage.h @@ -31,6 +31,7 @@ #define QMQTTMESSAGE_H #include <QtMqtt/qmqttglobal.h> +#include <QtMqtt/qmqttpublishproperties.h> #include <QtMqtt/qmqtttopicname.h> #include <QtCore/QObject> @@ -66,6 +67,7 @@ public: bool duplicate() const; bool retain() const; + QMqttPublishProperties publishProperties() const; private: friend class QMqttConnection; QMqttMessage(const QMqttTopicName &topic, const QByteArray &payload, diff --git a/src/mqtt/qmqttmessage_p.h b/src/mqtt/qmqttmessage_p.h new file mode 100644 index 0000000..ee27e9e --- /dev/null +++ b/src/mqtt/qmqttmessage_p.h @@ -0,0 +1,74 @@ +/****************************************************************************** +** +** 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 QMQTTMESSAGE_P_H +#define QMQTTMESSAGE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qmqttglobal.h" +#include "qmqtttopicname.h" +#include "qmqttpublishproperties.h" + +#include <QtCore/QSharedData> + +QT_BEGIN_NAMESPACE + +class QMqttMessagePrivate : public QSharedData +{ +public: + bool operator==(const QMqttMessagePrivate &other) const { + return m_topic == other.m_topic + && m_payload == other.m_payload + && m_id == other.m_id + && m_qos == other.m_qos + && m_duplicate == other.m_duplicate + && m_retain == other.m_retain; + } + QMqttTopicName m_topic; + QByteArray m_payload; + quint16 m_id{0}; + quint8 m_qos{0}; + bool m_duplicate{false}; + bool m_retain{false}; + QMqttPublishProperties m_publishProperties; +}; + +QT_END_NAMESPACE + +#endif // QMQTTMESSAGE_P_H diff --git a/src/mqtt/qmqttpublishproperties.cpp b/src/mqtt/qmqttpublishproperties.cpp new file mode 100644 index 0000000..77e2118 --- /dev/null +++ b/src/mqtt/qmqttpublishproperties.cpp @@ -0,0 +1,302 @@ +/****************************************************************************** +** +** 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 "qmqttpublishproperties.h" +#include "qmqttpublishproperties_p.h" +#include "qmqtttype.h" + +#include <QtCore/QLoggingCategory> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcMqttClient) + +/*! + \class QMqttPublishProperties + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttPublishProperties class represents configuration + options for sending or receiving a message. + + Invoking QMqttClient::publish() to send a message to a broker can + include QMqttPublishProperties to provide additional arguments on + how the message should be treated on the broker. + + Furthermore receiving a message by an instantiated subscription + might contain publish properties which have been forwarded or + adapted by the server. + + \note Publish properties are part of the MQTT 5.0 specification and + cannot be used when connecting with a lower protocol level. See + QMqttClient::ProtocolVersion for more information. +*/ + +/*! + \enum QMqttPublishProperties::PublishPropertyDetail + + This enum type specifies the available properties set by the + server or the client when creating a message. + + \value None + No property has been specified. + \value PayloadFormatIndicator + The type of content of the message. + \value MessageExpiryInterval + The duration a message is valid. + \value TopicAlias + The topic alias for this message. + \value ResponseTopic + The topic the receipient should respond to. + \value CorrelationData + An identifier of the response message. + \value UserProperty + Additional properties set by the user. + \value SubscriptionIdentifier + An identifier of subscriptions matching the publication. + \value ContentType + A description of the content of the message. +*/ + +QMqttPublishProperties::QMqttPublishProperties() : data(new QMqttPublishPropertiesData) +{ +} + +/*! + \internal +*/ +QMqttPublishProperties::QMqttPublishProperties(const QMqttPublishProperties &) = default; + +/*! + \internal +*/ +QMqttPublishProperties &QMqttPublishProperties::operator=(const QMqttPublishProperties &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +QMqttPublishProperties::~QMqttPublishProperties() = default; + +/*! + Returns the available properties specified in this instance. When a message + is created, it does not need to include all properties. This function + serves as an indicator of those properties which have been explicitly + set. +*/ +QMqttPublishProperties::PublishPropertyDetails QMqttPublishProperties::availableProperties() const +{ + return data->details; +} + +/*! + Returns the payload format indicator. +*/ +QMqtt::PayloadFormatIndicator QMqttPublishProperties::payloadFormatIndicator() const +{ + return data->payloadIndicator; +} + +/*! + Sets the payload format indicator to \a indicator. +*/ +void QMqttPublishProperties::setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator indicator) +{ + data->details |= QMqttPublishProperties::PayloadFormatIndicator; + data->payloadIndicator = indicator; +} + +/*! + Returns the message expiry interval. This value specifies the number + of seconds a server is allowed to forward the message. If the interval + expires, the server must delete the message and abort publishing it. +*/ +quint32 QMqttPublishProperties::messageExpiryInterval() const +{ + return data->messageExpiry; +} + +/*! + Sets the message expiry interval to \a interval. +*/ +void QMqttPublishProperties::setMessageExpiryInterval(quint32 interval) +{ + data->details |= QMqttPublishProperties::MessageExpiryInterval; + data->messageExpiry = interval; +} + +/*! + Returns the topic alias used for publishing a message. +*/ +quint16 QMqttPublishProperties::topicAlias() const +{ + return data->topicAlias; +} + +/*! + Sets the topic alias for publishing a message to \a alias. A topic alias + value must be greater than zero and less than the maximum topic alias + specified by the server. + + \sa QMqttServerConnectionProperties::maximumTopicAlias() +*/ +void QMqttPublishProperties::setTopicAlias(quint16 alias) +{ + if (alias == 0) { + qCDebug(lcMqttClient) << "A topic alias with value 0 is not allowed."; + return; + } + data->details |= QMqttPublishProperties::TopicAlias; + data->topicAlias = alias; +} + +/*! + Returns the response topic a user should use as a follow up to + a request. +*/ +QString QMqttPublishProperties::responseTopic() const +{ + return data->responseTopic; +} + +/*! + Sets the response topic to \a topic. +*/ +void QMqttPublishProperties::setResponseTopic(const QString &topic) +{ + data->details |= QMqttPublishProperties::ResponseTopic; + data->responseTopic = topic; +} + +/*! + Returns the correlation data. +*/ +QByteArray QMqttPublishProperties::correlationData() const +{ + return data->correlationData; +} + +/*! + Sets the correlation data to \a correlation. +*/ +void QMqttPublishProperties::setCorrelationData(const QByteArray &correlation) +{ + data->details |= QMqttPublishProperties::CorrelationData; + data->correlationData = correlation; +} + +/*! + Returns the user properties of a message. +*/ +QMqttUserProperties QMqttPublishProperties::userProperties() const +{ + return data->userProperties; +} + +/*! + Sets the user properties of a message to \a properties. +*/ +void QMqttPublishProperties::setUserProperties(const QMqttUserProperties &properties) +{ + data->details |= QMqttPublishProperties::UserProperty; + data->userProperties = properties; +} + +/*! + Returns the subscription identifiers of subscriptions matching + the topic filter of the message. +*/ +QList<quint32> QMqttPublishProperties::subscriptionIdentifiers() const +{ + return data->subscriptionIdentifier; +} + +/*! + Sets the subscription identifiers to \a ids. +*/ +void QMqttPublishProperties::setSubscriptionIdentifiers(const QList<quint32> &ids) +{ + if (ids.contains(0)) { + qCDebug(lcMqttClient) << "A subscription identifier with value 0 is not allowed."; + return; + } + data->details |= QMqttPublishProperties::SubscriptionIdentifier; + data->subscriptionIdentifier = ids; +} + +/*! + Returns the content type of the message. +*/ +QString QMqttPublishProperties::contentType() const +{ + return data->contentType; +} + +/*! + Sets the content type of the message to \a type. +*/ +void QMqttPublishProperties::setContentType(const QString &type) +{ + data->details |= QMqttPublishProperties::ContentType; + data->contentType = type; +} + +QMqttMessageStatusProperties::QMqttMessageStatusProperties() : data(new QMqttMessageStatusPropertiesData) +{ + +} + +QMqttMessageStatusProperties &QMqttMessageStatusProperties::operator=(const QMqttMessageStatusProperties &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +QMqtt::ReasonCode QMqttMessageStatusProperties::reasonCode() const +{ + return data->reasonCode; +} + +QString QMqttMessageStatusProperties::reason() const +{ + return data->reasonString; +} + +QMqttUserProperties QMqttMessageStatusProperties::userProperties() const +{ + return data->userProperties; +} + +QMqttMessageStatusProperties::~QMqttMessageStatusProperties() = default; +QMqttMessageStatusProperties::QMqttMessageStatusProperties(const QMqttMessageStatusProperties &) = default; + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqttpublishproperties.h b/src/mqtt/qmqttpublishproperties.h new file mode 100644 index 0000000..a239b4d --- /dev/null +++ b/src/mqtt/qmqttpublishproperties.h @@ -0,0 +1,117 @@ +/****************************************************************************** +** +** 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 QMQTTPUBLISHPROPERTIES_H +#define QMQTTPUBLISHPROPERTIES_H + +#include <QtMqtt/qmqttglobal.h> +#include <QtMqtt/qmqtttype.h> + +#include <QtCore/QObject> +#include <QtCore/QSharedDataPointer> + +QT_BEGIN_NAMESPACE + +class QMqttPublishPropertiesData; +class QMqttMessageStatusPropertiesData; + +class Q_MQTT_EXPORT QMqttPublishProperties +{ + Q_GADGET +public: + enum PublishPropertyDetail : quint32 { + None = 0x00000000, + PayloadFormatIndicator = 0x00000001, + MessageExpiryInterval = 0x00000002, + TopicAlias = 0x00000004, + ResponseTopic = 0x00000008, + CorrelationData = 0x00000010, + UserProperty = 0x00000020, + SubscriptionIdentifier = 0x00000040, + ContentType = 0x00000080 + }; + Q_ENUM(PublishPropertyDetail) + Q_DECLARE_FLAGS(PublishPropertyDetails, PublishPropertyDetail) + + QMqttPublishProperties(); + QMqttPublishProperties(const QMqttPublishProperties &); + QMqttPublishProperties &operator=(const QMqttPublishProperties &); + ~QMqttPublishProperties(); + + PublishPropertyDetails availableProperties() const; + + QMqtt::PayloadFormatIndicator payloadFormatIndicator() const; + void setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator indicator); + + quint32 messageExpiryInterval() const; + void setMessageExpiryInterval(quint32 interval); + + quint16 topicAlias() const; + void setTopicAlias(quint16 alias); + + QString responseTopic() const; + void setResponseTopic(const QString &topic); + + QByteArray correlationData() const; + void setCorrelationData(const QByteArray &correlation); + + QMqttUserProperties userProperties() const; + void setUserProperties(const QMqttUserProperties &properties); + + QList<quint32> subscriptionIdentifiers() const; + void setSubscriptionIdentifiers(const QList<quint32> &ids); + + QString contentType() const; + void setContentType(const QString &type); +private: + QSharedDataPointer<QMqttPublishPropertiesData> data; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QMqttPublishProperties::PublishPropertyDetails) + +class Q_MQTT_EXPORT QMqttMessageStatusProperties +{ + Q_GADGET +public: + QMqttMessageStatusProperties(); + QMqttMessageStatusProperties(const QMqttMessageStatusProperties &); + QMqttMessageStatusProperties &operator=(const QMqttMessageStatusProperties &); + ~QMqttMessageStatusProperties(); + + QMqtt::ReasonCode reasonCode() const; + QString reason() const; + QMqttUserProperties userProperties() const; + +private: + friend class QMqttConnection; + QSharedDataPointer<QMqttMessageStatusPropertiesData> data; +}; + +QT_END_NAMESPACE + +#endif // QMQTTPUBLISHPROPERTIES_H diff --git a/src/mqtt/qmqttpublishproperties_p.h b/src/mqtt/qmqttpublishproperties_p.h new file mode 100644 index 0000000..2fc9d21 --- /dev/null +++ b/src/mqtt/qmqttpublishproperties_p.h @@ -0,0 +1,75 @@ +/****************************************************************************** +** +** 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 QMQTTPUBLISHPROPERTIES_P_H +#define QMQTTPUBLISHPROPERTIES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qmqttglobal.h" +#include "qmqttpublishproperties.h" + +#include <QtCore/QSharedData> + +QT_BEGIN_NAMESPACE + +class QMqttPublishPropertiesData : public QSharedData +{ +public: + QString responseTopic; + QString contentType; + QByteArray correlationData; + quint32 messageExpiry{0}; + QList<quint32> subscriptionIdentifier; + QMqttPublishProperties::PublishPropertyDetails details{QMqttPublishProperties::None}; + quint16 topicAlias{0}; + QMqtt::PayloadFormatIndicator payloadIndicator{QMqtt::PayloadFormatIndicator::Unspecified}; + QMqttUserProperties userProperties; +}; + +class QMqttMessageStatusPropertiesData : public QSharedData +{ +public: + QMqttUserProperties userProperties; + QString reasonString; + QMqtt::ReasonCode reasonCode{QMqtt::ReasonCode::Success}; +}; + +QT_END_NAMESPACE + +#endif // QMQTTPUBLISHPROPERTIES_P_H diff --git a/src/mqtt/qmqttsubscription.cpp b/src/mqtt/qmqttsubscription.cpp index a492796..35f3e52 100644 --- a/src/mqtt/qmqttsubscription.cpp +++ b/src/mqtt/qmqttsubscription.cpp @@ -86,6 +86,36 @@ QT_BEGIN_NAMESPACE This signal is emitted when the new message \a msg has been received. */ +/*! + \property QMqttSubscription::reason + \since 5.12 + \brief This property holds the reason string for the subscription. + + A reason string is used by the server to provide additional information + about the subscription. It is optional for the server to send it. +*/ + +/*! + \property QMqttSubscription::reasonCode + \since 5.12 + \brief This property holds the reason code for the subscription. + + The reason code specifies the error type if a subscription has failed, + or the level of QoS for success. +*/ + +/*! + \property QMqttSubscription::shared + \since 5.12 + \brief This property holds whether the subscription is shared. +*/ + +/*! + \property QMqttSubscription::shareName + \since 5.12 + \brief This property holds the name of the shared subscription. +*/ + QMqttSubscription::QMqttSubscription(QObject *parent) : QObject(*(new QMqttSubscriptionPrivate), parent) { @@ -120,6 +150,45 @@ quint8 QMqttSubscription::qos() const return d->m_qos; } +QString QMqttSubscription::reason() const +{ + Q_D(const QMqttSubscription); + return d->m_reasonString; +} + +QMqtt::ReasonCode QMqttSubscription::reasonCode() const +{ + Q_D(const QMqttSubscription); + return d->m_reasonCode; +} + +/*! + \since 5.12 + + Returns the user properties received from the broker when the subscription + has been accepted. + + \note This function will only provide valid data when the client + specifies QMqttClient::MQTT_5_0 as QMqttClient::ProtocolVersion. +*/ +QMqttUserProperties QMqttSubscription::userProperties() const +{ + Q_D(const QMqttSubscription); + return d->m_userProperties; +} + +bool QMqttSubscription::isShared() const +{ + Q_D(const QMqttSubscription); + return d->m_shared; +} + +QString QMqttSubscription::shareName() const +{ + Q_D(const QMqttSubscription); + return d->m_shareName; +} + void QMqttSubscription::setState(QMqttSubscription::SubscriptionState state) { Q_D(QMqttSubscription); @@ -161,6 +230,18 @@ void QMqttSubscription::setQos(quint8 qos) d->m_qos = qos; } +void QMqttSubscription::setShared(bool s) +{ + Q_D(QMqttSubscription); + d->m_shared = s; +} + +void QMqttSubscription::setShareName(const QString &name) +{ + Q_D(QMqttSubscription); + d->m_shareName = name; +} + QMqttSubscriptionPrivate::QMqttSubscriptionPrivate() : QObjectPrivate() { diff --git a/src/mqtt/qmqttsubscription.h b/src/mqtt/qmqttsubscription.h index b2f6891..ad10d37 100644 --- a/src/mqtt/qmqttsubscription.h +++ b/src/mqtt/qmqttsubscription.h @@ -48,6 +48,10 @@ class Q_MQTT_EXPORT QMqttSubscription : public QObject Q_PROPERTY(SubscriptionState state READ state NOTIFY stateChanged) Q_PROPERTY(quint8 qos READ qos NOTIFY qosChanged) Q_PROPERTY(QMqttTopicFilter topic READ topic) + Q_PROPERTY(QString reason READ reason) + Q_PROPERTY(QMqtt::ReasonCode reasonCode READ reasonCode) + Q_PROPERTY(bool shared READ isShared) + Q_PROPERTY(QString shareName READ shareName) public: ~QMqttSubscription() override; enum SubscriptionState { @@ -61,6 +65,12 @@ public: SubscriptionState state() const; QMqttTopicFilter topic() const; quint8 qos() const; + QString reason() const; + QMqtt::ReasonCode reasonCode() const; + QMqttUserProperties userProperties() const; + + bool isShared() const; + QString shareName() const; Q_SIGNALS: void stateChanged(SubscriptionState state); @@ -77,6 +87,8 @@ private: void setTopic(const QMqttTopicFilter &topic); void setClient(QMqttClient *client); void setQos(quint8 qos); + void setShared(bool s); + void setShareName(const QString &name); friend class QMqttConnection; friend class QMqttClient; explicit QMqttSubscription(QObject *parent = nullptr); diff --git a/src/mqtt/qmqttsubscription_p.h b/src/mqtt/qmqttsubscription_p.h index 89ffdf0..f56c404 100644 --- a/src/mqtt/qmqttsubscription_p.h +++ b/src/mqtt/qmqttsubscription_p.h @@ -53,9 +53,14 @@ public: QMqttSubscriptionPrivate(); ~QMqttSubscriptionPrivate() override = default; QMqttClient *m_client{nullptr}; - QMqttSubscription::SubscriptionState m_state{QMqttSubscription::Unsubscribed}; QMqttTopicFilter m_topic; + QString m_reasonString; + QMqttUserProperties m_userProperties; + QString m_shareName; + QMqttSubscription::SubscriptionState m_state{QMqttSubscription::Unsubscribed}; + QMqtt::ReasonCode m_reasonCode{QMqtt::ReasonCode::Success}; quint8 m_qos{0}; + bool m_shared{false}; }; QT_END_NAMESPACE diff --git a/src/mqtt/qmqttsubscriptionproperties.cpp b/src/mqtt/qmqttsubscriptionproperties.cpp new file mode 100644 index 0000000..08977a9 --- /dev/null +++ b/src/mqtt/qmqttsubscriptionproperties.cpp @@ -0,0 +1,168 @@ +/****************************************************************************** +** +** 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 "qmqttsubscriptionproperties.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QMqttSubscriptionProperties + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttSubscriptionProperties class represents configuration + options a QMqttClient can pass to the server when subscribing to a + topic filter. + + \note Subscription properties are part of the MQTT 5.0 specification and + cannot be used when connecting with a lower protocol level. See + QMqttClient::ProtocolVersion for more information. +*/ + +/*! + \class QMqttUnsubscriptionProperties + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttUnsubscriptionProperties class represents configuration + options a QMqttClient can pass to the server when unsubscribing from a + topic filter. + + \note Unsubscription properties are part of the MQTT 5.0 specification and + cannot be used when connecting with a lower protocol level. See + QMqttClient::ProtocolVersion for more information. +*/ + +class QMqttSubscriptionPropertiesData : public QSharedData +{ +public: + quint32 subscriptionIdentifier{0}; + QMqttUserProperties userProperties; +}; + +class QMqttUnsubscriptionPropertiesData : public QSharedData +{ +public: + QMqttUserProperties userProperties; +}; + +/*! + \internal +*/ +QMqttSubscriptionProperties::QMqttSubscriptionProperties() : data(new QMqttSubscriptionPropertiesData) +{ + +} + +/*! + \internal +*/ +QMqttSubscriptionProperties::QMqttSubscriptionProperties(const QMqttSubscriptionProperties &) = default; + +QMqttSubscriptionProperties &QMqttSubscriptionProperties::operator=(const QMqttSubscriptionProperties &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +QMqttSubscriptionProperties::~QMqttSubscriptionProperties() = default; + +/*! + Returns the user specified properties. +*/ +QMqttUserProperties QMqttSubscriptionProperties::userProperties() const +{ + return data->userProperties; +} + +/*! + Sets the user properties to \a user. +*/ +void QMqttSubscriptionProperties::setUserProperties(const QMqttUserProperties &user) +{ + data->userProperties = user; +} + +/*! + Returns the subscription identifier used to describe this subscription. +*/ +quint32 QMqttSubscriptionProperties::subscriptionIdentifier() const +{ + return data->subscriptionIdentifier; +} + +/*! + Sets the subscription identifier to \a id. +*/ +void QMqttSubscriptionProperties::setSubscriptionIdentifier(quint32 id) +{ + data->subscriptionIdentifier = id; +} + +/*! + \internal +*/ +QMqttUnsubscriptionProperties::QMqttUnsubscriptionProperties() : data(new QMqttUnsubscriptionPropertiesData) +{ +} + +/*! + \internal +*/ +QMqttUnsubscriptionProperties &QMqttUnsubscriptionProperties::operator=(const QMqttUnsubscriptionProperties &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +/*! + Returns the user specified properties. +*/ +QMqttUserProperties QMqttUnsubscriptionProperties::userProperties() const +{ + return data->userProperties; +} + +/*! + Sets the user properties to \a user. +*/ +void QMqttUnsubscriptionProperties::setUserProperties(const QMqttUserProperties &user) +{ + data->userProperties = user; +} + +QMqttUnsubscriptionProperties::~QMqttUnsubscriptionProperties() = default; + +QMqttUnsubscriptionProperties::QMqttUnsubscriptionProperties(const QMqttUnsubscriptionProperties &) = default; + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqttsubscriptionproperties.h b/src/mqtt/qmqttsubscriptionproperties.h new file mode 100644 index 0000000..3794609 --- /dev/null +++ b/src/mqtt/qmqttsubscriptionproperties.h @@ -0,0 +1,84 @@ +/****************************************************************************** +** +** 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 QMQTTSUBSCRIPTIONPROPERTIES_H +#define QMQTTSUBSCRIPTIONPROPERTIES_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 QMqttSubscriptionPropertiesData; +class QMqttUnsubscriptionPropertiesData; + +class Q_MQTT_EXPORT QMqttSubscriptionProperties +{ + Q_GADGET + +public: + QMqttSubscriptionProperties(); + QMqttSubscriptionProperties(const QMqttSubscriptionProperties &); + QMqttSubscriptionProperties &operator=(const QMqttSubscriptionProperties &); + ~QMqttSubscriptionProperties(); + + QMqttUserProperties userProperties() const; + void setUserProperties(const QMqttUserProperties &user); + + quint32 subscriptionIdentifier() const; + void setSubscriptionIdentifier(quint32 id); +private: + QSharedDataPointer<QMqttSubscriptionPropertiesData> data; +}; + +class Q_MQTT_EXPORT QMqttUnsubscriptionProperties +{ + Q_GADGET + +public: + QMqttUnsubscriptionProperties(); + QMqttUnsubscriptionProperties(const QMqttUnsubscriptionProperties &); + QMqttUnsubscriptionProperties &operator=(const QMqttUnsubscriptionProperties &rhs); + ~QMqttUnsubscriptionProperties(); + + QMqttUserProperties userProperties() const; + void setUserProperties(const QMqttUserProperties &user); + +private: + QSharedDataPointer<QMqttUnsubscriptionPropertiesData> data; +}; + +QT_END_NAMESPACE + +#endif // QMQTTSUBSCRIPTIONPROPERTIES_H diff --git a/src/mqtt/qmqtttopicfilter.cpp b/src/mqtt/qmqtttopicfilter.cpp index d2071c7..1e6bd9c 100644 --- a/src/mqtt/qmqtttopicfilter.cpp +++ b/src/mqtt/qmqtttopicfilter.cpp @@ -154,6 +154,24 @@ void QMqttTopicFilter::setFilter(const QString &filter) } /*! + \since 5.12 + + Returns the name of a share if the topic filter has been specified as + a shared subscription. The format of shared subscriptions is defined + as \c $share/sharename/topicfilter. +*/ +QString QMqttTopicFilter::shareName() const +{ + QString result; + if (d->filter.startsWith(QLatin1String("$share/"))) { + // Has to have at least two / + // $share/<sharename>/topicfilter + result = d->filter.section(QLatin1Char('/'), 1, 1); + } + return result; +} + +/*! Returns \c true if the topic filter is valid according to the MQTT standard section 4.7, or \c false otherwise. */ @@ -184,6 +202,13 @@ bool QMqttTopicFilter::isValid() const singleLevelPosition = d->filter.indexOf(QLatin1Char('#'), singleLevelPosition + 1); } + // Shared Subscription syntax + // $share/shareName/TopicFilter -- must have at least 2 '/' + if (d->filter.startsWith(QLatin1String("$share/"))) { + const int index = d->filter.indexOf(QLatin1Char('/'), 7); + if (index == -1 || index == 7) + return false; + } return true; } diff --git a/src/mqtt/qmqtttopicfilter.h b/src/mqtt/qmqtttopicfilter.h index 626cc6a..e926cf4 100644 --- a/src/mqtt/qmqtttopicfilter.h +++ b/src/mqtt/qmqtttopicfilter.h @@ -69,6 +69,8 @@ public: QString filter() const; void setFilter(const QString &filter); + QString shareName() const; + Q_REQUIRED_RESULT bool isValid() const; Q_REQUIRED_RESULT bool match(const QMqttTopicName &name, MatchOptions matchOptions = NoMatchOption) const; diff --git a/src/mqtt/qmqtttype.cpp b/src/mqtt/qmqtttype.cpp new file mode 100644 index 0000000..373707b --- /dev/null +++ b/src/mqtt/qmqtttype.cpp @@ -0,0 +1,465 @@ +/****************************************************************************** +** +** 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 "qmqtttype.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QMqttStringPair + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttStringPair class represents the string pair data type + of the MQTT 5.0 standard. + + This data type is used to hold a name-value pair. +*/ + +/*! + \class QMqttUserProperties + + \inmodule QtMqtt + \since 5.12 + + \brief The QMqttUserProperties class represents a list of QMqttStringPair + values which can be passed to the server. + + User properties allow a client to pass additional data to the server or + other subscribers, which do not belong to the message payload. + + On the other hand it also provides a server the flexibility to provide + further information to connected clients. + + The server might add additional user properties to the ones provided. + However, the order of the provided properties is not changed during transfer. + + \note User properties are part of the MQTT 5.0 specification and + cannot be used when using QtMqtt with an older protocol level. See + QMqttClient::ProtocolVersion for more information. + + \sa QMqttAuthenticationProperties, QMqttConnectionProperties, + QMqttLastWillProperties, QMqttPublishProperties, QMqttSubscriptionProperties, + QMqttUnsubscriptionProperties, QMqttSubscription +*/ + +/*! + \namespace QMqtt + \inmodule QtMqtt + \since 5.12 + + \brief Contains miscellaneous identifiers used throughout the Qt MQTT module. +*/ + +/*! + \enum QMqtt::PayloadFormatIndicator + \since 5.12 + + The payload format provides information on the content of a message. + This can help other clients to handle the message faster. + + \value Unspecified + The format is not specified. + \value UTF8Encoded + The payload of the message is formatted as UTF-8 Encoded + Character Data. +*/ + +/*! + \enum QMqtt::MessageStatus + \since 5.12 + + This enum type specifies the available states of a message. Depending + on the QoS and role of the client, different message statuses are + expected. + + \value Unknown + The message status is unknown. + \value Published + The client received a message for one of its subscriptions. This + applies to all QoS levels. + \value Acknowledged + A message has been acknowledged. This applies to QoS 1 and states + that the message handling has been finished from the client side. + \value Received + A message has been received. This applies to QoS 2. + \value Released + A message has been released. This applies to QoS 2. For a publisher + the message handling has been finished. + \value Completed + A message has been completed. This applies to QoS 2 and states + that the message handling has been finished from the client side. +*/ + +/*! + \enum QMqtt::ReasonCode + \since 5.12 + + This enum type specifies the available error codes. + + \value Success + The specified action has succeeded. + \value SubscriptionQoSLevel0 + A subscription with QoS level 0 has been created. + \value SubscriptionQoSLevel1 + A subscription with QoS level 1 has been created. + \value SubscriptionQoSLevel2 + A subscription with QoS level 2 has been created. + \value NoMatchingSubscriber + The message has been accepted by the server, but there are no + subscribers to receive this message. A broker may send this + reason code instead of \l Success. + \value UnspecifiedError + An unspecified error occurred. + \value MalformedPacket + The packet sent to the server is invalid. + \value ProtocolError + A protocol error has occurred. In most cases, this will cause + the server to disconnect the client. + \value ImplementationSpecificError + The packet is valid, but the recipient rejects it. + \value UnsupportedProtocolVersion + The requested protocol version is not supported by the server. + \value InvalidClientId + The client ID is invalid. + \value InvalidUserNameOrPassword + The username or password specified is invalid. + \value NotAuthorized + The client is not authorized for the specified action. + \value ServerNotAvailable + The server to connect to is not available. + \value ServerBusy + The server to connect to is not available. The client is asked to + try at a later time. + \value ClientBanned + The client has been banned from the server. + \value InvalidAuthenticationMethod + The authentication method specified is invalid. + \value InvalidTopicFilter + The topic filter specified is invalid. + \value InvalidTopicName + The topic name specified is invalid. + \value MessageIdInUse + The message ID used in the previous packet is already in use. + \value MessageIdNotFound + The message ID used in the previous packet has not been found. + \value PacketTooLarge + The packet received is too large. See also + \l QMqttServerConnectionProperties::maximumPacketSize(). + \value QuotaExceeded + An administratively imposed limit has been exceeded. + \value InvalidPayloadFormat + The payload format is invalid. + \value RetainNotSupported + The server does not support retained messages. + \value QoSNotSupported + The QoS level requested is not supported. + \value UseAnotherServer + The server the client tries to connect to is not available. See also + \l QMqttServerConnectionProperties::serverReference(). + \value ServerMoved + The server the client tries to connect to has moved to a new address. + See also \l QMqttServerConnectionProperties::serverReference(). + \value SharedSubscriptionsNotSupported + Shared subscriptions are not supported. + \value ExceededConnectionRate + The connection rate limit has been exceeded. + \value SubscriptionIdsNotSupported + Subscription IDs are not supported. + \value WildCardSubscriptionsNotSupported + Subscriptions using wildcards are not supported by the server. + + Not all values are available in every use case. Especially, some servers + will reject a reason code not suited for a specific command. See below + table to highlight expected reason codes for specific actions. + + \table + \header + \li Reason Code + \li Connect Properties + \li Subscription Properties + \li Message Properties + \row + \li Success + \li X + \li X + \li X + \row + \li SubscriptionQoSLevel0 + \li + \li X + \li + \row + \li SubscriptionQoSLevel1 + \li + \li X + \li + \row + \li SubscriptionQoSLevel2 + \li + \li X + \li + \row + \li NoMatchingSubscriber + \li + \li + \li X + \row + \li UnspecifiedError + \li X + \li X + \li X + \row + \li MalformedPacket + \li X + \li + \li + \row + \li ProtocolError + \li X + \li + \li + \row + \li ImplementationSpecificError + \li X + \li X + \li X + \row + \li UnsupportedProtocolVersion + \li X + \li + \li + \row + \li InvalidClientId + \li X + \li + \li + \row + \li InvalidUserNameOrPassword + \li X + \li + \li + \row + \li NotAuthorized + \li X + \li X + \li X + \row + \li ServerNotAvailable + \li X + \li + \li + \row + \li ServerBusy + \li X + \li + \li + \row + \li ClientBanned + \li X + \li + \li + \row + \li InvalidAuthenticationMethod + \li X + \li + \li + \row + \li InvalidTopicFilter + \li + \li X + \li + \row + \li InvalidTopicName + \li X + \li + \li X + \row + \li MessageIdInUse + \li + \li X + \li X + \row + \li MessageIdNotFound + \li + \li + \li X + \row + \li PacketTooLarge + \li X + \li + \li + \row + \li QuotaExceeded + \li X + \li X + \li X + \row + \li InvalidPayloadFormat + \li X + \li + \li X + \row + \li RetainNotSupported + \li X + \li + \li + \row + \li QoSNotSupported + \li X + \li + \li + \row + \li UseAnotherServer + \li X + \li + \li + \row + \li ServerMoved + \li X + \li + \li + \row + \li SharedSubscriptionsNotSupported + \li + \li X + \li + \row + \li ExceededConnectionRate + \li X + \li + \li + \row + \li SubscriptionIdsNotSupported + \li + \li X + \li + \row + \li WildCardSubscriptionsNotSupported + \li + \li X + \li + \endtable +*/ + +class QMqttStringPairData : public QSharedData +{ +public: + QMqttStringPairData() = default; + QMqttStringPairData(const QString &name, const QString &value); + + bool operator==(const QMqttStringPairData &rhs) const; + QString m_name; + QString m_value; +}; + +QMqttStringPairData::QMqttStringPairData(const QString &name, const QString &value) + : m_name(name) + , m_value(value) +{ +} + +bool QMqttStringPairData::operator==(const QMqttStringPairData &rhs) const +{ + return m_name == rhs.m_name && m_value == rhs.m_value; +} + +QMqttStringPair::QMqttStringPair() + : data(new QMqttStringPairData) +{ + +} + +QMqttStringPair::QMqttStringPair(const QString &name, const QString &value) + : data(new QMqttStringPairData(name, value)) +{ +} + +QMqttStringPair::QMqttStringPair(const QMqttStringPair &) = default; + +QMqttStringPair::~QMqttStringPair() = default; + +/*! + Returns the name of the string pair. +*/ +QString QMqttStringPair::name() const +{ + return data->m_name; +} + +/*! + Sets the name to \a n. +*/ +void QMqttStringPair::setName(const QString &n) +{ + data->m_name = n; +} + +/*! + Returns the value of the string pair. +*/ +QString QMqttStringPair::value() const +{ + return data->m_value; +} + +/*! + Sets the value to \a v. +*/ +void QMqttStringPair::setValue(const QString &v) +{ + data->m_value = v; +} + +/*! + Returns \c true if this instance matches \a other. +*/ +bool QMqttStringPair::operator==(const QMqttStringPair &other) const +{ + return *data.constData() == *other.data.constData(); +} + +QMqttStringPair &QMqttStringPair::operator=(const QMqttStringPair &rhs) +{ + if (this != &rhs) + data.operator=(rhs.data); + return *this; +} + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QMqttStringPair &s) +{ + QDebugStateSaver saver(d); + d.nospace() << "QMqttStringPair(" << s.name() << " : " << s.value() << ')'; + return d; +} +#endif + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqtttype.h b/src/mqtt/qmqtttype.h new file mode 100644 index 0000000..d791d63 --- /dev/null +++ b/src/mqtt/qmqtttype.h @@ -0,0 +1,75 @@ +/****************************************************************************** +** +** 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 QMQTTTYPE_H +#define QMQTTTYPE_H + +#include <QtMqtt/qmqttglobal.h> + +#include <QtCore/QDebug> +#include <QtCore/QPair> +#include <QtCore/QSharedDataPointer> +#include <QtCore/QString> +#include <QtCore/QVector> + +QT_BEGIN_NAMESPACE + +class QMqttStringPairData; +class Q_MQTT_EXPORT QMqttStringPair +{ +public: + QMqttStringPair(); + QMqttStringPair(const QString &name, const QString &value); + QMqttStringPair(const QMqttStringPair &); + ~QMqttStringPair(); + + QString name() const; + void setName(const QString &n); + + QString value() const; + void setValue(const QString &v); + + bool operator==(const QMqttStringPair &other) const; + QMqttStringPair &operator=(const QMqttStringPair &); +private: + QSharedDataPointer<QMqttStringPairData> data; +}; + +#ifndef QT_NO_DEBUG_STREAM +Q_MQTT_EXPORT QDebug operator<<(QDebug d, const QMqttStringPair &s); +#endif + +class Q_MQTT_EXPORT QMqttUserProperties : public QVector<QMqttStringPair> +{ +public: +}; + +QT_END_NAMESPACE + +#endif // QMQTTTYPE_H diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index ecd1207..068e54a 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -2,8 +2,12 @@ TEMPLATE = subdirs win32|if(linux:!cross_compile): SUBDIRS += cmake \ conformance \ + qmqttconnectionproperties \ qmqttcontrolpacket \ qmqttclient \ + qmqttlastwillproperties \ + qmqttpublishproperties \ qmqttsubscription \ + qmqttsubscriptionproperties \ qmqtttopicname \ qmqtttopicfilter diff --git a/tests/auto/conformance/tst_conformance.cpp b/tests/auto/conformance/tst_conformance.cpp index ca3cc61..d093637 100644 --- a/tests/auto/conformance/tst_conformance.cpp +++ b/tests/auto/conformance/tst_conformance.cpp @@ -50,15 +50,19 @@ private Q_SLOTS: void initTestCase(); void cleanupTestCase(); + void basic_test_data(); void basic_test(); void retained_message_test_data(); void retained_message_test(); + void will_message_test_data(); void will_message_test(); void zero_length_clientid_test_data(); void zero_length_clientid_test(); + void offline_message_queueing_test_data(); void offline_message_queueing_test(); // overlapping_subscriptions_test // Skipped at the module emits multiple messages for each sub // keepalive_test // The module handles sending ping requests + void subscribe_failure_test_data(); void subscribe_failure_test(); private: QProcess m_brokerProcess; @@ -83,9 +87,13 @@ void Tst_MqttConformance::cleanupTestCase() { } +DefaultVersionTestData(Tst_MqttConformance::basic_test_data) + void Tst_MqttConformance::basic_test() { - QMqttClient client; + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + + VersionClient(mqttVersion, client); client.setHostname(m_testBroker); client.setPort(m_port); @@ -96,20 +104,32 @@ void Tst_MqttConformance::basic_test() client.disconnectFromHost(); QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Could not disconnect from broker"); + // The MQTT 5 broker might provide topic alias by default. Hence, disable it. + if (mqttVersion == QMqttClient::MQTT_5_0) { + QMqttConnectionProperties p; + p.setMaximumTopicAlias(0); + client.setConnectionProperties(p); + } + client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker"); const QString topic(QLatin1String("Qt/conformance")); - auto sub = client.subscribe(topic, 2); + auto sub = client.subscribe(topic, 1); QTRY_VERIFY2(sub->state() == QMqttSubscription::Subscribed, "Could not subscribe"); int msgCount = 0; - connect(sub, &QMqttSubscription::messageReceived, this, [&msgCount](QMqttMessage msg) { - qDebug() << "Message received:" << msg.payload(); + connect(sub, &QMqttSubscription::messageReceived, this, [&msgCount](QMqttMessage) { msgCount++; }); + connect(&client, &QMqttClient::messageReceived, this, [](const QByteArray &message, const QMqttTopicName &topic) + { + Q_UNUSED(message) + Q_UNUSED(topic) + }); + client.publish(topic, "qos 0", 0); client.publish(topic, "qos 1", 1); client.publish(topic, "qos 2", 2); @@ -126,22 +146,28 @@ void Tst_MqttConformance::basic_test() void Tst_MqttConformance::retained_message_test_data() { + QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion"); QTest::addColumn<QStringList>("messages"); QTest::addColumn<int>("expectedMsgCount"); - const QStringList topics1{"qos 0", "qos 1", "qos 2"}; - const QStringList topics2{"", "", ""}; + QList<QMqttClient::ProtocolVersion> versions{QMqttClient::MQTT_3_1_1, QMqttClient::MQTT_5_0}; + + for (int i = 0; i < 2; ++i) { + const QStringList topics1{"qos 0", "qos 1", "qos 2"}; + const QStringList topics2{"", "", ""}; - QTest::newRow("receiveRetain") << topics1 << 3; - QTest::newRow("clearRetain") << topics2 << 0; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":receiveRetain")) << versions[i] << topics1 << 3; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":clearRetain")) << versions[i] << topics2 << 0; + } } void Tst_MqttConformance::retained_message_test() { + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); QFETCH(QStringList, messages); QFETCH(int, expectedMsgCount); - QMqttClient client; + VersionClient(mqttVersion, client); client.setHostname(m_testBroker); client.setPort(m_port); @@ -149,8 +175,8 @@ void Tst_MqttConformance::retained_message_test() client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker"); - const QStringList topics{"Qt/tests/retain1", "Qt/tests/retain2", "Qt/tests2/retain1"}; - const QString subTop{"Qt/#"}; // ### TODO: The test suite uses {"Qt/+/+"}; but we do not support ++ yet. + const QStringList topics{"Qt/conformance/tests/retain1", "Qt/conformance/tests/retain2", "Qt/conformance/tests2/retain1"}; + const QString subTop{"Qt/conformance/#"}; // ### TODO: The test suite uses {"Qt/+/+"}; but we do not support ++ yet. client.publish(topics[0], messages[0].toLocal8Bit(), 0, true); client.publish(topics[1], messages[1].toLocal8Bit(), 1, true); @@ -181,14 +207,18 @@ void Tst_MqttConformance::retained_message_test() QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Could not disconnect"); } +DefaultVersionTestData(Tst_MqttConformance::will_message_test_data) + void Tst_MqttConformance::will_message_test() { - QMqttClient client; + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + + VersionClient(mqttVersion, client); client.setHostname(m_testBroker); client.setPort(m_port); - const QString wTopic{"Qt/willtest"}; + const QString wTopic{"Qt/conformance/willtest"}; const QByteArray wMessage{"client got lost"}; client.setWillMessage(wMessage); @@ -199,7 +229,7 @@ void Tst_MqttConformance::will_message_test() QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker"); - QMqttClient recipient; + VersionClient(mqttVersion, recipient); recipient.setHostname(m_testBroker); recipient.setPort(m_port); recipient.connectToHost(); @@ -221,17 +251,23 @@ void Tst_MqttConformance::will_message_test() void Tst_MqttConformance::zero_length_clientid_test_data() { + QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion"); QTest::addColumn<bool>("session"); - QTest::newRow("noncleanSession") << false; - QTest::newRow("cleanSession") << true; + QList<QMqttClient::ProtocolVersion> versions{QMqttClient::MQTT_3_1_1, QMqttClient::MQTT_5_0}; + + for (int i = 0; i < 2; ++i) { + QTest::newRow(qPrintable(QString::number(versions[i]) + ":noncleanSession")) << versions[i] << false; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":cleanSession")) << versions[i] << true; + } } void Tst_MqttConformance::zero_length_clientid_test() { + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); QFETCH(bool, session); - QMqttClient client; + VersionClient(mqttVersion, client); client.setHostname(m_testBroker); client.setPort(m_port); @@ -242,7 +278,13 @@ void Tst_MqttConformance::zero_length_clientid_test() QVERIFY2(client.state() == QMqttClient::Connecting, "Could not set state to connecting."); if (!session) { - QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Sessions with empty client should not be allowed."); + if (client.protocolVersion() == QMqttClient::MQTT_5_0) { + // For MQTT 5 the broker creates an ID and returns it in CONNACK + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + QVERIFY(!client.clientId().isEmpty()); + } else { + QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Sessions with empty client should not be allowed."); + } } else { QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); client.disconnectFromHost(); @@ -250,9 +292,12 @@ void Tst_MqttConformance::zero_length_clientid_test() } } +DefaultVersionTestData(Tst_MqttConformance::offline_message_queueing_test_data) + void Tst_MqttConformance::offline_message_queueing_test() { - QMqttClient client; + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + VersionClient(mqttVersion, client); client.setHostname(m_testBroker); client.setPort(m_port); @@ -260,23 +305,23 @@ void Tst_MqttConformance::offline_message_queueing_test() client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); - const QString subTopic{"Qt/offline/#"}; + const QString subTopic{"Qt/conformance/offline/#"}; auto sub = client.subscribe(subTopic, 2); Q_UNUSED(sub); client.disconnectFromHost(); QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Could not disconnect."); - QMqttClient publisher; + VersionClient(mqttVersion, publisher); publisher.setHostname(m_testBroker); publisher.setPort(m_port); publisher.connectToHost(); QTRY_VERIFY2(publisher.state() == QMqttClient::Connected, "Could not connect to broker."); QSignalSpy pubCounter(&publisher, SIGNAL(messageSent(qint32))); - publisher.publish(QLatin1String("Qt/offline/foo/bar"), "msg1", 1); - publisher.publish(QLatin1String("Qt/offline/foo/bar2"), "msg2", 1); - publisher.publish(QLatin1String("Qt/offline/foo2/bar"), "msg3", 1); + publisher.publish(QLatin1String("Qt/conformance/offline/foo/bar"), "msg1", 1); + publisher.publish(QLatin1String("Qt/conformance/offline/foo/bar2"), "msg2", 1); + publisher.publish(QLatin1String("Qt/conformance/offline/foo2/bar"), "msg3", 1); QTRY_VERIFY2(pubCounter.size() == 3, "Could not publish all messages."); publisher.disconnectFromHost(); @@ -287,17 +332,24 @@ void Tst_MqttConformance::offline_message_queueing_test() client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + // ### TODO: MQTT5 Investigate / Fixme + if (client.protocolVersion() == QMqttClient::MQTT_5_0) + QEXPECT_FAIL("", "Offline messages seem not supported with MQTT5", Continue); QTRY_VERIFY2(receiveCounter.size() == 3, "Did not receive all offline messages."); client.disconnectFromHost(); QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Could not disconnect."); } +DefaultVersionTestData(Tst_MqttConformance::subscribe_failure_test_data) + void Tst_MqttConformance::subscribe_failure_test() { - QMqttClient client; + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + + VersionClient(mqttVersion, client); - const QByteArray forbiddenTopic{"nosubscribe"}; + const QByteArray forbiddenTopic{"Qt/conformance/nosubscribe"}; // We do not have a test broker with forbidden topics. QSKIP("Missing infrastructure to set forbidden topics"); diff --git a/tests/auto/qmqttclient/tst_qmqttclient.cpp b/tests/auto/qmqttclient/tst_qmqttclient.cpp index b94bfd4..8aec3d5 100644 --- a/tests/auto/qmqttclient/tst_qmqttclient.cpp +++ b/tests/auto/qmqttclient/tst_qmqttclient.cpp @@ -46,18 +46,34 @@ public: private Q_SLOTS: void initTestCase(); void cleanupTestCase(); + void getSetCheck_data(); void getSetCheck(); void sendReceive_data(); void sendReceive(); + void retainMessage_data(); void retainMessage(); + void willMessage_data(); void willMessage(); void compliantTopic_data(); void compliantTopic(); + void subscribeLongTopic_data(); void subscribeLongTopic(); + void dataIncludingZero_data(); void dataIncludingZero(); + void publishLongTopic_data(); void publishLongTopic(); + void reconnect_QTBUG65726_data(); void reconnect_QTBUG65726(); + void openIODevice_QTBUG66955_data(); void openIODevice_QTBUG66955(); + void staticProperties_QTBUG_67176_data(); + void staticProperties_QTBUG_67176(); + void authentication(); + void messageStatus_data(); + void messageStatus(); + void messageStatusReceive_data(); + void messageStatusReceive(); + void subscriptionIdsOverlap(); private: QProcess m_brokerProcess; QString m_testBroker; @@ -79,9 +95,12 @@ void Tst_QMqttClient::cleanupTestCase() { } +DefaultVersionTestData(Tst_QMqttClient::getSetCheck_data) + void Tst_QMqttClient::getSetCheck() { - QMqttClient client; + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + VersionClient(mqttVersion, client); QVERIFY(client.clientId().size() > 0); const QString clientId = QLatin1String("testclient123"); @@ -101,15 +120,27 @@ void Tst_QMqttClient::getSetCheck() client.setKeepAlive(10); QCOMPARE(client.keepAlive(), quint16(10)); - QCOMPARE(client.protocolVersion(), QMqttClient::MQTT_3_1_1); - client.setProtocolVersion(QMqttClient::ProtocolVersion(0)); - QCOMPARE(client.protocolVersion(), QMqttClient::MQTT_3_1_1); - client.setProtocolVersion(QMqttClient::ProtocolVersion(5)); - QCOMPARE(client.protocolVersion(), QMqttClient::MQTT_3_1_1); - client.setProtocolVersion(QMqttClient::MQTT_3_1); - QCOMPARE(client.protocolVersion(), QMqttClient::MQTT_3_1); - + // Available protocol versions + QMqttClient client2; + QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_3_1_1); + client2.setProtocolVersion(QMqttClient::ProtocolVersion(0)); + QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_3_1_1); + client2.setProtocolVersion(QMqttClient::ProtocolVersion(6)); + QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_3_1_1); + client2.setProtocolVersion(QMqttClient::MQTT_3_1); + QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_3_1); + client2.setProtocolVersion(QMqttClient::MQTT_5_0); + QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_5_0); + +#ifdef QT_BUILD_INTERNAL + if (qEnvironmentVariableIsSet("QT_MQTT_TEST_USERNAME")) + QEXPECT_FAIL("", "Default username has been overwritten.", Continue); +#endif QCOMPARE(client.username(), QString()); +#ifdef QT_BUILD_INTERNAL + if (qEnvironmentVariableIsSet("QT_MQTT_TEST_PASSWORD")) + QEXPECT_FAIL("", "Default username has been overwritten.", Continue); +#endif QCOMPARE(client.password(), QString()); QCOMPARE(client.cleanSession(), true); QCOMPARE(client.willTopic(), QString()); @@ -120,22 +151,29 @@ void Tst_QMqttClient::getSetCheck() void Tst_QMqttClient::sendReceive_data() { + QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion"); QTest::addColumn<QByteArray>("data"); - QTest::newRow("empty") << QByteArray(); - QTest::newRow("simple") << QByteArray("This is a test message"); - QByteArray d; - d.fill('A', 500); - QTest::newRow("big") << d; - d.fill('B', (128 * 128 * 128) + 4); - QTest::newRow("huge") << d; + + QList<QMqttClient::ProtocolVersion> versions{QMqttClient::MQTT_3_1_1, QMqttClient::MQTT_5_0}; + + for (int i = 0; i < 2; ++i) { + QTest::newRow(qPrintable(QString::number(versions[i]) + ":empty")) << versions[i] << QByteArray(); + QTest::newRow(qPrintable(QString::number(versions[i]) + ":simple")) << versions[i] << QByteArray("This is a test message"); + QByteArray d; + d.fill('A', 500); + QTest::newRow(qPrintable(QString::number(versions[i]) + ":big")) << versions[i] << d; + d.fill('B', (128 * 128 * 128) + 4); + QTest::newRow(qPrintable(QString::number(versions[i]) + ":huge")) << versions[i] << d; + } } void Tst_QMqttClient::sendReceive() { + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); QFETCH(QByteArray, data); - const QString testTopic = QLatin1String("Topic"); + const QString testTopic = QLatin1String("Qt/QMqttClient/Topic"); - QMqttClient publisher; + VersionClient(mqttVersion, publisher); publisher.setClientId(QLatin1String("publisher")); publisher.setHostname(m_testBroker); publisher.setPort(m_port); @@ -143,7 +181,7 @@ void Tst_QMqttClient::sendReceive() publisher.connectToHost(); QTRY_COMPARE(publisher.state(), QMqttClient::Connected); - QMqttClient subscriber; + VersionClient(mqttVersion, subscriber); subscriber.setClientId(QLatin1String("subscriber")); subscriber.setHostname(m_testBroker); subscriber.setPort(m_port); @@ -162,19 +200,26 @@ void Tst_QMqttClient::sendReceive() QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed); + if (subscriber.protocolVersion() == QMqttClient::MQTT_5_0 && + (quint32)data.size() > subscriber.serverConnectionProperties().maximumPacketSize()) + QSKIP("The MQTT 5 test broker does not support huge packages.", SkipOnce); publisher.publish(testTopic, data, 1); QTRY_VERIFY2(received, "Subscriber did not receive message"); QVERIFY2(verified, "Subscriber received different message"); } +DefaultVersionTestData(Tst_QMqttClient::retainMessage_data) + void Tst_QMqttClient::retainMessage() { - const QString testTopic = QLatin1String("Topic2"); + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + + const QString testTopic = QLatin1String("Qt/QMqttClient/Topic2"); const QByteArray testMessage("retainedMessage"); // Publisher - QMqttClient publisher; + VersionClient(mqttVersion, publisher); publisher.setClientId(QLatin1String("publisher")); publisher.setHostname(m_testBroker); publisher.setPort(m_port); @@ -197,7 +242,7 @@ void Tst_QMqttClient::retainMessage() publisher.publish(testTopic, testMessage, 1, i == 1 ? true : false); QTRY_COMPARE(publishSpy.count(), 1); - QMqttClient sub; + VersionClient(mqttVersion, sub); sub.setClientId(QLatin1String("SubA")); sub.setHostname(m_testBroker); sub.setPort(m_port); @@ -213,19 +258,22 @@ void Tst_QMqttClient::retainMessage() auto subscription = sub.subscribe(testTopic); QTRY_COMPARE(subscription->state(), QMqttSubscription::Subscribed); - QTest::qWait(5000); - QVERIFY(msgCount == i); + QTRY_VERIFY(msgCount == i); } publisher.disconnect(); } +DefaultVersionTestData(Tst_QMqttClient::willMessage_data) + void Tst_QMqttClient::willMessage() { - const QString willTopic = QLatin1String("will/topic"); + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + + const QString willTopic = QLatin1String("Qt/QMqttClient/will/topic"); const QByteArray willMessage("The client died...."); // Client A connects - QMqttClient client1; + VersionClient(mqttVersion, client1); client1.setHostname(m_testBroker); client1.setPort(m_port); client1.connectToHost(); @@ -247,7 +295,7 @@ void Tst_QMqttClient::willMessage() QVERIFY(sock.waitForConnected()); for (int i = 1; i > 0; --i) { - QMqttClient willClient; + VersionClient(mqttVersion, willClient); if (i == 1) willClient.setTransport(&sock, QMqttClient::AbstractSocket); else { @@ -277,13 +325,21 @@ void Tst_QMqttClient::willMessage() void Tst_QMqttClient::compliantTopic_data() { + QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion"); QTest::addColumn<QString>("topic"); - QTest::newRow("simple") << QString::fromLatin1("topic"); - QTest::newRow("subPath") << QString::fromLatin1("topic/subtopic"); - QString l; - l.fill(QLatin1Char('T'), std::numeric_limits<std::uint16_t>::max()); - QTest::newRow("maxSize") << l; + QList<QMqttClient::ProtocolVersion> versions{QMqttClient::MQTT_3_1_1, QMqttClient::MQTT_5_0}; + + for (int i = 0; i < 2; ++i) { + QTest::newRow(qPrintable(QString::number(versions[i]) + ":simple")) << versions[i] << QString::fromLatin1("Qt/QMqttClient/topic"); + QTest::newRow(qPrintable(QString::number(versions[i]) + ":subPath")) << versions[i] << QString::fromLatin1("Qt/QMqttClient/topic/subtopic"); + + if (versions[i] != QMqttClient::MQTT_5_0) { + QString l; + l.fill(QLatin1Char('T'), std::numeric_limits<std::uint16_t>::max()); + QTest::newRow(qPrintable(QString::number(versions[i]) + ":maxSize")) << versions[i] << l; + } + } } void Tst_QMqttClient::compliantTopic() @@ -324,9 +380,13 @@ void Tst_QMqttClient::compliantTopic() QVERIFY2(verified, "Subscriber received different message"); } +DefaultVersionTestData(Tst_QMqttClient::subscribeLongTopic_data) + void Tst_QMqttClient::subscribeLongTopic() { - QMqttClient subscriber; + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + + VersionClient(mqttVersion, subscriber); subscriber.setClientId(QLatin1String("subscriber")); subscriber.setHostname(m_testBroker); subscriber.setPort(m_port); @@ -340,14 +400,18 @@ void Tst_QMqttClient::subscribeLongTopic() QCOMPARE(sub, nullptr); } +DefaultVersionTestData(Tst_QMqttClient::dataIncludingZero_data) + void Tst_QMqttClient::dataIncludingZero() { + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + QByteArray data; const int dataSize = 200; data.fill('A', dataSize); data[100] = '\0'; - QMqttClient client; + VersionClient(mqttVersion, client); client.setHostname(m_testBroker); client.setPort(m_port); @@ -357,7 +421,7 @@ void Tst_QMqttClient::dataIncludingZero() bool received = false; bool verified = false; bool correctSize = false; - const QString testTopic(QLatin1String("some/topic")); + const QString testTopic(QLatin1String("Qt/QMqttClient/some/topic")); auto sub = client.subscribe(testTopic, 1); QVERIFY(sub); connect(sub, &QMqttSubscription::messageReceived, [&](QMqttMessage msg) { @@ -375,9 +439,13 @@ void Tst_QMqttClient::dataIncludingZero() QVERIFY2(correctSize, "Subscriber received message of different size"); } +DefaultVersionTestData(Tst_QMqttClient::publishLongTopic_data) + void Tst_QMqttClient::publishLongTopic() { - QMqttClient publisher; + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + + VersionClient(mqttVersion, publisher); publisher.setClientId(QLatin1String("publisher")); publisher.setHostname(m_testBroker); publisher.setPort(m_port); @@ -428,11 +496,15 @@ public: bool connectionSuccess{false}; }; +DefaultVersionTestData(Tst_QMqttClient::reconnect_QTBUG65726_data) + void Tst_QMqttClient::reconnect_QTBUG65726() { + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + FakeServer server; - QMqttClient client; + VersionClient(mqttVersion, client); client.setClientId(QLatin1String("bugclient")); client.setHostname(QLatin1String("localhost")); client.setPort(5726); @@ -465,24 +537,280 @@ public: if (data[0] == 0x10) written = 1; else - qWarning() << "Received unknown/invalid data"; + qDebug() << "Received unknown/invalid data"; return len; } QAtomicInt written{0}; }; +DefaultVersionTestData(Tst_QMqttClient::openIODevice_QTBUG66955_data) + void Tst_QMqttClient::openIODevice_QTBUG66955() { + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + IOTransport trans; trans.open(QIODevice::ReadWrite); - QMqttClient client; + VersionClient(mqttVersion, client); client.setTransport(&trans, QMqttClient::IODevice); client.connectToHost(); QTRY_COMPARE(trans.written, 1); } +DefaultVersionTestData(Tst_QMqttClient::staticProperties_QTBUG_67176_data) + +void Tst_QMqttClient::staticProperties_QTBUG_67176() +{ + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + + VersionClient(mqttVersion, client); + client.setHostname(m_testBroker); + client.setPort(m_port); + + const QString clientId = client.clientId(); + const quint16 keepAlive = client.keepAlive(); + const bool clean = client.cleanSession(); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + client.setClientId(QLatin1String("someclient")); + QCOMPARE(client.clientId(), clientId); + + client.setHostname(QLatin1String("some.domain.foo")); + QCOMPARE(client.hostname(), m_testBroker); + + client.setPort(1234); + QCOMPARE(client.port(), m_port); + + client.setKeepAlive(keepAlive + 10); + QCOMPARE(client.keepAlive(), keepAlive); + + client.setProtocolVersion(QMqttClient::MQTT_3_1); + QCOMPARE(client.protocolVersion(), mqttVersion); + + client.setUsername(QLatin1String("someUser")); + QCOMPARE(client.username(), QLatin1String()); + + client.setPassword(QLatin1String("somePassword")); + QCOMPARE(client.password(), QLatin1String()); + + client.setCleanSession(!clean); + 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); +} + +Q_DECLARE_METATYPE(QMqtt::MessageStatus) + +void Tst_QMqttClient::messageStatus_data() +{ + QTest::addColumn<int>("qos"); + QTest::addColumn<QList<QMqtt::MessageStatus>>("expectedStatus"); + + QTest::newRow("QoS1") << 1 << (QList<QMqtt::MessageStatus>() << QMqtt::MessageStatus::Acknowledged); + QTest::newRow("QoS2") << 2 << (QList<QMqtt::MessageStatus>() << QMqtt::MessageStatus::Received + << QMqtt::MessageStatus::Completed); +} + +void Tst_QMqttClient::messageStatus() +{ + QFETCH(int, qos); + QFETCH(QList<QMqtt::MessageStatus>, expectedStatus); + + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + const QString topic = QLatin1String("Qt/client/statusCheck"); + + connect(&client, &QMqttClient::messageStatusChanged, [&expectedStatus](qint32, + QMqtt::MessageStatus s, + const QMqttMessageStatusProperties &) + { + QCOMPARE(s, expectedStatus.first()); + expectedStatus.takeFirst(); + }); + + QSignalSpy publishSpy(&client, &QMqttClient::messageSent); + client.publish(topic, QByteArray("someContent"), quint8(qos)); + QTRY_VERIFY2(publishSpy.count() == 1, "Could not publish message"); + QTRY_VERIFY2(expectedStatus.isEmpty(), "Did not receive all status updates."); +} + +void Tst_QMqttClient::messageStatusReceive_data() +{ + QTest::addColumn<int>("qos"); + QTest::addColumn<QList<QMqtt::MessageStatus>>("expectedStatus"); + + QTest::newRow("QoS1") << 1 << (QList<QMqtt::MessageStatus>() << QMqtt::MessageStatus::Published); + QTest::newRow("QoS2") << 2 << (QList<QMqtt::MessageStatus>() << QMqtt::MessageStatus::Published + << QMqtt::MessageStatus::Released); +} + +void Tst_QMqttClient::messageStatusReceive() +{ + QFETCH(int, qos); + QFETCH(QList<QMqtt::MessageStatus>, expectedStatus); + + QMqttClient publisher; + publisher.setProtocolVersion(QMqttClient::MQTT_5_0); + publisher.setHostname(m_testBroker); + publisher.setPort(m_port); + + publisher.connectToHost(); + QTRY_VERIFY2(publisher.state() == QMqttClient::Connected, "Could not connect to broker."); + + QMqttClient subscriber; + subscriber.setProtocolVersion(QMqttClient::MQTT_5_0); + subscriber.setHostname(m_testBroker); + subscriber.setPort(m_port); + + subscriber.connectToHost(); + QTRY_VERIFY2(subscriber.state() == QMqttClient::Connected, "Could not connect to broker."); + + const QString topic = QLatin1String("Qt/client/statusCheckReceive"); + + auto subscription = subscriber.subscribe(topic, quint8(qos)); + QTRY_VERIFY2(subscription->state() == QMqttSubscription::Subscribed, "Could not subscribe to topic"); + QVERIFY(subscription->qos() >= qos); + + connect(&subscriber, &QMqttClient::messageStatusChanged, [&expectedStatus](qint32, + QMqtt::MessageStatus s, + const QMqttMessageStatusProperties &) + { + QCOMPARE(s, expectedStatus.first()); + expectedStatus.takeFirst(); + }); + + QSignalSpy publishSpy(&publisher, &QMqttClient::messageSent); + QSignalSpy receiveSpy(&subscriber, &QMqttClient::messageReceived); + + publisher.publish(topic, QByteArray("someContent"), quint8(qos)); + QTRY_VERIFY2(publishSpy.count() == 1, "Could not publish message"); + QTRY_VERIFY2(receiveSpy.count() == 1, "Did not receive message"); + + QTRY_VERIFY2(expectedStatus.isEmpty(), "Did not receive all status updates."); +} + +void Tst_QMqttClient::subscriptionIdsOverlap() +{ + + // If the Server sends a single copy of the message it MUST include in the + // PUBLISH packet the Subscription Identifiers for all matching + // subscriptions which have a Subscription Identifiers, their order is not + // significant [MQTT-3.3.4-4]. + // If the Server sends multiple PUBLISH packets it MUST send, in each of + // them, the Subscription Identifier of the matching subscription if it has + // a Subscription Identifier [MQTT-3.3.4-5]. + + const QString topic = QLatin1String("Qt/client/idcheck"); + // Connect publisher + QMqttClient pub; + pub.setProtocolVersion(QMqttClient::MQTT_5_0); + pub.setHostname(m_testBroker); + pub.setPort(m_port); + + pub.connectToHost(); + QTRY_VERIFY2(pub.state() == QMqttClient::Connected, "Could not connect publisher."); + + // Connect subA + QMqttClient subClientA; + subClientA.setProtocolVersion(QMqttClient::MQTT_5_0); + subClientA.setHostname(m_testBroker); + subClientA.setPort(m_port); + + subClientA.connectToHost(); + QTRY_VERIFY2(subClientA.state() == QMqttClient::Connected, "Could not connect subscriber A."); + + QMqttSubscriptionProperties subAProp; + subAProp.setSubscriptionIdentifier(8); + auto subA = subClientA.subscribe(topic, subAProp, 1); + QTRY_VERIFY2(subA->state() == QMqttSubscription::Subscribed, "Could not subscibe A."); + + int receiveACounter = 0; + connect(subA, &QMqttSubscription::messageReceived, [&receiveACounter](QMqttMessage msg) { + qDebug() << "Sub A received:" << msg.publishProperties().subscriptionIdentifiers(); + // ### TODO: Wait for fix at https://github.com/eclipse/paho.mqtt.testing/issues/56 + //QVERIFY(msg.publishProperties().subscriptionIdentifiers().size() == 1); + //QVERIFY(msg.publishProperties().subscriptionIdentifiers().at(0) == 8); // Use sub->id(); + receiveACounter++; + }); + + // Connect subB + QMqttClient subClientB; + subClientB.setProtocolVersion(QMqttClient::MQTT_5_0); + subClientB.setHostname(m_testBroker); + subClientB.setPort(m_port); + + subClientB.connectToHost(); + QTRY_VERIFY2(subClientB.state() == QMqttClient::Connected, "Could not connect subscriber A."); + + QMqttSubscriptionProperties subBProp; + subBProp.setSubscriptionIdentifier(9); + auto subB = subClientB.subscribe(topic, subBProp, 1); + QTRY_VERIFY2(subB->state() == QMqttSubscription::Subscribed, "Could not subscibe A."); + + int receiveBCounter = 2; + connect(subB, &QMqttSubscription::messageReceived, [&receiveBCounter](QMqttMessage msg) { + qDebug() << "Sub B received:" << msg.publishProperties().subscriptionIdentifiers(); + QVERIFY(msg.publishProperties().subscriptionIdentifiers().size() > 0); + receiveBCounter -= msg.publishProperties().subscriptionIdentifiers().size(); + }); + + QMqttSubscriptionProperties subB2Prop; + subB2Prop.setSubscriptionIdentifier(14); + auto subB2 = subClientB.subscribe(topic + "/#", subB2Prop, 1); + QTRY_VERIFY2(subB2->state() == QMqttSubscription::Subscribed, "Could not subscibe A."); + + int receiveB2Counter = 2; + connect(subB2, &QMqttSubscription::messageReceived, [&receiveB2Counter](QMqttMessage msg) { + qDebug() << "Sub B2 received:" << msg.publishProperties().subscriptionIdentifiers(); + QVERIFY(msg.publishProperties().subscriptionIdentifiers().size() > 0); + receiveB2Counter -= msg.publishProperties().subscriptionIdentifiers().size(); + }); + + QSignalSpy publishSpy(&pub, &QMqttClient::messageSent); + pub.publish(topic, "SomeData", 1); + QTRY_VERIFY2(publishSpy.count() == 1, "Could not finalize publication."); + QTRY_VERIFY2(receiveBCounter == 0, "Did not receive both messages."); + QTRY_VERIFY2(receiveB2Counter == 0, "Did not receive both messages."); + QTRY_VERIFY2(receiveACounter == 1, "Did not receive non-overlapping message."); +} + QTEST_MAIN(Tst_QMqttClient) #include "tst_qmqttclient.moc" diff --git a/tests/auto/qmqttconnectionproperties/qmqttconnectionproperties.pro b/tests/auto/qmqttconnectionproperties/qmqttconnectionproperties.pro new file mode 100644 index 0000000..4cd41d5 --- /dev/null +++ b/tests/auto/qmqttconnectionproperties/qmqttconnectionproperties.pro @@ -0,0 +1,17 @@ +CONFIG += testcase +QT += network testlib mqtt +QT -= gui +QT_PRIVATE += mqtt-private + +TARGET = tst_qmqttconnectionproperties + +SOURCES += \ + tst_qmqttconnectionproperties.cpp + +HEADERS += \ + $$PWD/../../common/broker_connection.h + +INCLUDEPATH += \ + $$PWD/../../common + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/qmqttconnectionproperties/tst_qmqttconnectionproperties.cpp b/tests/auto/qmqttconnectionproperties/tst_qmqttconnectionproperties.cpp new file mode 100644 index 0000000..f08adcd --- /dev/null +++ b/tests/auto/qmqttconnectionproperties/tst_qmqttconnectionproperties.cpp @@ -0,0 +1,411 @@ +/****************************************************************************** +** +** 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-EXCEPT$ +** 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 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** 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 "broker_connection.h" + +#include <QtCore/QLoggingCategory> +#include <QtCore/QString> +#include <QtTest/QtTest> +#include <QtMqtt/QMqttClient> +#include <QtMqtt/QMqttConnectionProperties> +#include <QtMqtt/QMqttSubscription> +#include <QtMqtt/private/qmqttconnectionproperties_p.h> + +class tst_QMqttConnectionProperties : public QObject +{ + Q_OBJECT + +public: + tst_QMqttConnectionProperties(); + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void getSet(); + void receiveServerProperties(); + void maximumPacketSize(); + void maximumTopicAlias(); + void maximumTopicAliasReceive(); + void assignedClientId(); + void userProperties(); +private: + QProcess m_brokerProcess; + QString m_testBroker; + quint16 m_port{1883}; +}; + +tst_QMqttConnectionProperties::tst_QMqttConnectionProperties() +{ +} + +void tst_QMqttConnectionProperties::initTestCase() +{ + m_testBroker = invokeOrInitializeBroker(&m_brokerProcess); + if (m_testBroker.isEmpty()) + qFatal("No MQTT broker present to test against."); +} + +void tst_QMqttConnectionProperties::cleanupTestCase() +{ +} + +void tst_QMqttConnectionProperties::getSet() +{ + QMqttConnectionProperties p; + + QVERIFY(p.userProperties().isEmpty()); + QMqttUserProperties properties; + properties.append(QMqttStringPair(QLatin1String("someKey"), QLatin1String("someValue"))); + p.setUserProperties(properties); + QCOMPARE(p.userProperties(), properties); + + QVERIFY(p.authenticationMethod().isEmpty()); + const QLatin1String authMethod("SomeAuthentication"); + p.setAuthenticationMethod(authMethod); + QCOMPARE(p.authenticationMethod(), authMethod); + + QVERIFY(p.authenticationData().isEmpty()); + const QByteArray authData("AuthData123"); + p.setAuthenticationData(authData); + QCOMPARE(p.authenticationData(), authData); + + QCOMPARE(p.sessionExpiryInterval(), 0u); + p.setSessionExpiryInterval(1000); + QCOMPARE(p.sessionExpiryInterval(), 1000u); + + QCOMPARE(p.maximumPacketSize(), std::numeric_limits<quint32>::max()); + p.setMaximumPacketSize(0); + QVERIFY(p.maximumPacketSize() != 0u); + p.setMaximumPacketSize(500); + QCOMPARE(p.maximumPacketSize(), 500u); + + QCOMPARE(p.maximumReceive(), 65535); + p.setMaximumReceive(0); + QVERIFY(p.maximumReceive() != 0u); + p.setMaximumReceive(30); + QCOMPARE(p.maximumReceive(), 30u); + + QCOMPARE(p.maximumTopicAlias(), 0u); + p.setMaximumTopicAlias(5); + QCOMPARE(p.maximumTopicAlias(), 5u); + + QCOMPARE(p.requestResponseInformation(), false); + p.setRequestResponseInformation(true); + QCOMPARE(p.requestResponseInformation(), true); + + QCOMPARE(p.requestProblemInformation(), true); + p.setRequestProblemInformation(false); + QCOMPARE(p.requestProblemInformation(), false); +} + +void tst_QMqttConnectionProperties::receiveServerProperties() +{ + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + QMqttServerConnectionProperties server = client.serverConnectionProperties(); + + QCOMPARE(server.isValid(), false); + + client.setHostname(m_testBroker); + client.setPort(m_port); + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + server = client.serverConnectionProperties(); + + QCOMPARE(server.isValid(), true); + + QMqttServerConnectionProperties::ServerPropertyDetails properties = server.availableProperties(); + qDebug() << "Specified properties:" << properties; + + if (properties & QMqttServerConnectionProperties::SessionExpiryInterval) + qDebug() << " SessionExpiryInterval:" << server.sessionExpiryInterval(); + if (properties & QMqttServerConnectionProperties::MaximumReceive) + qDebug() << " MaximumReceive:" << server.maximumReceive(); + if (properties & QMqttServerConnectionProperties::MaximumQoS) + qDebug() << " MaximumQoS:" << server.maximumQoS(); + if (properties & QMqttServerConnectionProperties::RetainAvailable) + qDebug() << " RetainAvailable:" << server.retainAvailable(); + if (properties & QMqttServerConnectionProperties::MaximumPacketSize) + qDebug() << " MaximumPacketSize:" << server.maximumPacketSize(); + if (properties & QMqttServerConnectionProperties::AssignedClientId) + qDebug() << " AssignedClientId:" << server.clientIdAssigned(); + if (properties & QMqttServerConnectionProperties::MaximumTopicAlias) + qDebug() << " MaximumTopicAlias:" << server.maximumTopicAlias(); + if (properties & QMqttServerConnectionProperties::ReasonString) + qDebug() << " ReasonString:" << server.reason(); + if (properties & QMqttServerConnectionProperties::UserProperty) + qDebug() << " UserProperty:" << server.userProperties(); + if (properties & QMqttServerConnectionProperties::WildCardSupported) + qDebug() << " WildCard Support:" << server.wildcardSupported(); + if (properties & QMqttServerConnectionProperties::SubscriptionIdentifierSupport) + qDebug() << " Subscription Identifier Support:" << server.subscriptionIdentifierSupported(); + if (properties & QMqttServerConnectionProperties::SharedSubscriptionSupport) + qDebug() << " Shared Subscription Support:" << server.sharedSubscriptionSupported(); + if (properties & QMqttServerConnectionProperties::ServerKeepAlive) + qDebug() << " Server KeepAlive:" << server.serverKeepAlive(); + if (properties & QMqttServerConnectionProperties::ResponseInformation) + qDebug() << " ResponseInformation:" << server.responseInformation(); + if (properties & QMqttServerConnectionProperties::ServerReference) + qDebug() << " Server Reference:" << server.serverReference(); + if (properties & QMqttServerConnectionProperties::AuthenticationMethod) + qDebug() << " AuthenticationMethod:" << server.authenticationMethod(); + if (properties & QMqttServerConnectionProperties::AuthenticationData) + qDebug() << " AuthenticationData:" << server.authenticationData(); +} + +void tst_QMqttConnectionProperties::maximumPacketSize() +{ + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + + QMqttConnectionProperties props; + props.setMaximumPacketSize(500); + client.setConnectionProperties(props); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + QMqttServerConnectionProperties serverProperties = client.serverConnectionProperties(); + if (serverProperties.availableProperties() & QMqttServerConnectionProperties::MaximumPacketSize) { + if (serverProperties.maximumPacketSize() < props.maximumPacketSize()) { + qDebug() << "Server accepts less data than required for this test."; + } + } else { + QSKIP("Server has no max packet size defined. Default is unlimited."); + } + + const QString topic = QLatin1String("Qt/ConnectionProperties/some/Topic/maxSize"); + + auto sub = client.subscribe(topic, 1); + QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed); + + QSignalSpy subscribeSpy(sub, SIGNAL(messageReceived(QMqttMessage))); + + QByteArray shortData(100, 'd'); + QByteArray overFlowData(1000, 'o'); + + QSignalSpy publishSpy(&client, SIGNAL(messageSent(qint32))); + client.publish(topic, shortData, 1); + QTRY_COMPARE(publishSpy.count(), 1); + + QTRY_COMPARE(subscribeSpy.count(), 1); + + client.publish(topic, overFlowData, 1); + QTRY_COMPARE(publishSpy.count(), 1); + + // We defined maximum size to receive is 500, hence the message should not be sent back + // to us. Wait for some time and verify no message got sent to subscriber + QTest::qWait(3000); + QTRY_COMPARE(subscribeSpy.count(), 1); +} + +void tst_QMqttConnectionProperties::maximumTopicAlias() +{ + const QByteArray msgContent("SomeContent"); + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + + const int userMaxTopicAlias = 9; + QMqttConnectionProperties userProperties; + userProperties.setMaximumTopicAlias(userMaxTopicAlias); + client.setConnectionProperties(userProperties); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + int publishTransportSize = 0; + auto transport = client.transport(); + QSignalSpy transportSpy(transport, SIGNAL(bytesWritten(qint64))); //&QIODevice::bytesWritten); + + const auto serverProperties = client.serverConnectionProperties(); + const quint16 serverMaxAlias = serverProperties.maximumTopicAlias(); + if (serverMaxAlias == 0) + QSKIP("Need to skip this test due to topic aliases not supported on server"); + + //qDebug() << "Server Max Alias:" << serverMaxAlias; + //QLoggingCategory::setFilterRules("qt.mqtt.connection*=true"); + + // Fill up the internal publish vector + const QLatin1String topicBase("Qt/connprop/alias/top"); + for (quint16 i = 0; i < serverMaxAlias; ++i) { + QSignalSpy publishSpy(&client, SIGNAL(messageSent(qint32))); + + QMqttTopicName topic(topicBase + QString::number(i)); + client.publish(topic, msgContent, 1); + QTRY_VERIFY(publishSpy.count() == 1); + + QVERIFY(transportSpy.count() == 1); + const int dataSize = transportSpy.at(0).at(0).toInt(); + if (publishTransportSize == 0) { + publishTransportSize = dataSize; + } else { + QCOMPARE(dataSize, publishTransportSize); + } + transportSpy.clear(); + } + + // Verify non auto assignable defaults to old behavior + QSignalSpy fullSpy(&client, SIGNAL(messageSent(qint32))); + transportSpy.clear(); + client.publish(QLatin1String("Qt/connprop/alias/full/with/long/topic/to/verify/bigger/size"), msgContent, 1); + QTRY_VERIFY(fullSpy.count() == 1); + QVERIFY(transportSpy.count() == 1); + QVERIFY(transportSpy.at(0).at(0).toInt() > publishTransportSize); + + // Verify alias is used at sending second time + transportSpy.clear(); + fullSpy.clear(); + + client.publish(topicBase + QLatin1String("0"), msgContent, 1); + QTRY_VERIFY(fullSpy.count() == 1); + QVERIFY(transportSpy.count() == 1); + int usageSize = transportSpy.at(0).at(0).toInt(); + QVERIFY(usageSize < publishTransportSize); + + // Manually overwrite topic alias 1 + const QMqttTopicName overwrite(QLatin1String("Qt/connprop/alias/overwrite/with/long/topic/to/verify/reset")); + QMqttPublishProperties overProp; + overProp.setTopicAlias(2); + fullSpy.clear(); + transportSpy.clear(); + client.publish(overwrite, overProp, msgContent, 1); + QTRY_VERIFY(fullSpy.count() == 1); + QVERIFY(transportSpy.count() == 1); + const int overwriteSize = transportSpy.at(0).at(0).toInt(); + QVERIFY(overwriteSize > publishTransportSize); + // After resend new alias should be used and msg size reduced + fullSpy.clear(); + transportSpy.clear(); + client.publish(overwrite, msgContent, 1); + QTRY_VERIFY(fullSpy.count() == 1); + QVERIFY(transportSpy.count() == 1); + usageSize = transportSpy.at(0).at(0).toInt(); + QVERIFY(usageSize < overwriteSize); +} + +void createTopicAliasClient(QMqttClient &client, const QString &hostname, quint16 port) +{ + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(hostname); + client.setPort(port); + + const int userMaxTopicAlias = 9; + QMqttConnectionProperties userProperties; + userProperties.setMaximumTopicAlias(userMaxTopicAlias); + client.setConnectionProperties(userProperties); +} + +void tst_QMqttConnectionProperties::maximumTopicAliasReceive() +{ + const QByteArray msgContent("SomeContent"); + const QString topic("Qt/connprop/receive/alias"); + + QMqttClient client; + createTopicAliasClient(client, m_testBroker, m_port); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + QMqttClient subscriber; + createTopicAliasClient(subscriber, m_testBroker, m_port); + + subscriber.connectToHost(); + QTRY_VERIFY2(subscriber.state() == QMqttClient::Connected, "Could not connect to broker."); + + auto sub = subscriber.subscribe(topic + "/#", 1); + + int receiveCounter = 0; + connect(sub, &QMqttSubscription::messageReceived, [&receiveCounter](QMqttMessage msg) { + qDebug() << "Received message with alias:" << msg.publishProperties().topicAlias(); + receiveCounter++; + }); + + QTRY_VERIFY2(sub->state() == QMqttSubscription::Subscribed, "Could not subscribe"); + + QSignalSpy publishSpy(&client, &QMqttClient::messageSent); + //QLoggingCategory::setFilterRules("qt.mqtt.connection*=true"); + client.publish(topic, msgContent, 1); + QTRY_VERIFY2(publishSpy.count() == 1, "Could not publish"); + QTRY_VERIFY2(receiveCounter == 1, "Did not receive initial message"); + + publishSpy.clear(); + client.publish(topic, msgContent, 1); + QTRY_VERIFY2(publishSpy.count() == 1, "Could not publish"); + QTRY_VERIFY2(receiveCounter == 2, "Did not receive second aliases message"); +} + +void tst_QMqttConnectionProperties::assignedClientId() +{ + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + client.setClientId(QLatin1String("")); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + QMqttServerConnectionProperties serverProperties = client.serverConnectionProperties(); + QVERIFY2(serverProperties.availableProperties() & QMqttServerConnectionProperties::AssignedClientId, "Must contain client ID"); + + QVERIFY2(!client.clientId().isEmpty(), "Client ID must not be empty"); +} + +void tst_QMqttConnectionProperties::userProperties() +{ + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + QMqttConnectionProperties p; + QMqttUserProperties userProperties; + userProperties.append(QMqttStringPair(QLatin1String("TestKey"), QLatin1String("TestValue"))); + p.setUserProperties(userProperties); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + // ### TODO: We should a method to verify transmission of the user data + // Probably in conjunction with authentication + + //QMqttServerConnectionProperties serverProperties = client.serverConnectionProperties(); + //if (serverProperties.availableProperties() & QMqttServerConnectionProperties::UserProperty) { + // auto serverUserProps = serverProperties.userProperties(); + // qDebug() << serverUserProps; + //} else + // qDebug() << "No user props available"; +} + +QTEST_MAIN(tst_QMqttConnectionProperties) + +#include "tst_qmqttconnectionproperties.moc" diff --git a/tests/auto/qmqttlastwillproperties/qmqttlastwillproperties.pro b/tests/auto/qmqttlastwillproperties/qmqttlastwillproperties.pro new file mode 100644 index 0000000..e61752e --- /dev/null +++ b/tests/auto/qmqttlastwillproperties/qmqttlastwillproperties.pro @@ -0,0 +1,17 @@ +CONFIG += testcase +QT += network testlib mqtt +QT -= gui +QT_PRIVATE += mqtt-private + +TARGET = tst_qmqttlastwillproperties + +SOURCES += \ + tst_qmqttlastwillproperties.cpp + +HEADERS += \ + $$PWD/../../common/broker_connection.h + +INCLUDEPATH += \ + $$PWD/../../common + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/qmqttlastwillproperties/tst_qmqttlastwillproperties.cpp b/tests/auto/qmqttlastwillproperties/tst_qmqttlastwillproperties.cpp new file mode 100644 index 0000000..77a46b5 --- /dev/null +++ b/tests/auto/qmqttlastwillproperties/tst_qmqttlastwillproperties.cpp @@ -0,0 +1,203 @@ +/****************************************************************************** +** +** 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-EXCEPT$ +** 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 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** 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 "broker_connection.h" + +#include <QtCore/QString> +#include <QtTest/QtTest> +#include <QtMqtt/QMqttClient> +#include <QtMqtt/QMqttConnectionProperties> + +class tst_QMqttLastWillProperties : public QObject +{ + Q_OBJECT + +public: + tst_QMqttLastWillProperties(); + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void getSet(); + void payloadFormat(); + void willDelay_data(); + void willDelay(); + +private: + QProcess m_brokerProcess; + QString m_testBroker; + quint16 m_port{1883}; +}; + +tst_QMqttLastWillProperties::tst_QMqttLastWillProperties() +{ +} + +void tst_QMqttLastWillProperties::initTestCase() +{ + m_testBroker = invokeOrInitializeBroker(&m_brokerProcess); + if (m_testBroker.isEmpty()) + qFatal("No MQTT broker present to test against."); +} + +void tst_QMqttLastWillProperties::cleanupTestCase() +{ +} + +void tst_QMqttLastWillProperties::getSet() +{ + QMqttLastWillProperties properties; + + QCOMPARE(properties.willDelayInterval(), 0u); + properties.setWillDelayInterval(50); + QCOMPARE(properties.willDelayInterval(), 50u); + + QCOMPARE(properties.payloadFormatIndicator(), QMqtt::PayloadFormatIndicator::Unspecified); + properties.setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator::UTF8Encoded); + QCOMPARE(properties.payloadFormatIndicator(), QMqtt::PayloadFormatIndicator::UTF8Encoded); + + QCOMPARE(properties.messageExpiryInterval(), 0u); + properties.setMessageExpiryInterval(50); + QCOMPARE(properties.messageExpiryInterval(), 50u); + + const QString content = QLatin1String("contentType"); + const QString response = QLatin1String("Qt/some/responseTopic"); + const QByteArray correlation(500, char('a')); + + QCOMPARE(properties.contentType(), QString()); + properties.setContentType(content); + QCOMPARE(properties.contentType(), content); + + QCOMPARE(properties.responseTopic(), QString()); + properties.setResponseTopic(response); + QCOMPARE(properties.responseTopic(), response); + + QCOMPARE(properties.correlationData(), QByteArray()); + properties.setCorrelationData(correlation); + QCOMPARE(properties.correlationData(), correlation); + + QVERIFY(properties.userProperties().isEmpty()); + QMqttUserProperties user; + user.append(QMqttStringPair(QLatin1String("SomeName"), QLatin1String("SomeValue"))); + user.append(QMqttStringPair(QLatin1String("SomeName2"), QLatin1String("SomeValue2"))); + properties.setUserProperties(user); + QCOMPARE(properties.userProperties(), user); +} + +void tst_QMqttLastWillProperties::payloadFormat() +{ + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + + client.setWillMessage(QString::fromLatin1("willmessage to utf8").toUtf8()); + client.setWillTopic(QLatin1String("Qt/LastWillProperties/willtopic")); + client.setWillQoS(1); + + QMqttLastWillProperties lastWill; + lastWill.setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator::UTF8Encoded); + + // The broker MAY verify the content is utf8 + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); +} + +void tst_QMqttLastWillProperties::willDelay_data() +{ + QTest::addColumn<int>("delay"); + QTest::addColumn<int>("expiry"); + QTest::newRow("delay == expiry") << 5 << 5; + QTest::newRow("delay < expiry") << 3 << 10; // will delay is send first + QTest::newRow("delay > expiry") << 10 << 3; // will is send at expiry + QTest::newRow("delay > expiry(0)") << 5 << 0; // No expiry, hence will is send immediately +} + +void tst_QMqttLastWillProperties::willDelay() +{ + QFETCH(int, delay); + QFETCH(int, expiry); + + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + + const QString wTopic{"Qt/lastwillproperties/willdelay"}; + const QByteArray wMessage{"client got lost"}; + + client.setWillMessage(wMessage); + client.setWillQoS(2); + client.setWillTopic(wTopic); + + if (expiry > 0) { + // If a session expires first, then last will is send immediately + QMqttConnectionProperties connectionProperties; + connectionProperties.setSessionExpiryInterval(quint32(expiry)); + client.setConnectionProperties(connectionProperties); + } + + QMqttLastWillProperties lastWillProperties; + lastWillProperties.setWillDelayInterval(quint32(delay)); + client.setLastWillProperties(lastWillProperties); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker"); + + QMqttClient recipient; + recipient.setProtocolVersion(QMqttClient::MQTT_5_0); + recipient.setHostname(m_testBroker); + recipient.setPort(m_port); + recipient.connectToHost(); + QTRY_VERIFY2(recipient.state() == QMqttClient::Connected, "Could not connect to broker"); + + QTime delayTime; + bool receivedWill = false; + auto sub = recipient.subscribe(wTopic, 1); + connect(sub, &QMqttSubscription::messageReceived, this, [wMessage, &receivedWill](QMqttMessage m) { + if (m.payload() == wMessage) + receivedWill = true; + }); + QTRY_VERIFY2(sub->state() == QMqttSubscription::Subscribed, "Could not subscribe."); + + auto transport = client.transport(); + transport->close(); // closing transport does not send DISCONNECT + delayTime = QTime::currentTime(); + delayTime.start(); + + const int minimalWait = qMin(delay, expiry) * 1000; + const int maximumWait = 2 * (minimalWait == 0 ? 1000 : minimalWait); + QTRY_VERIFY2_WITH_TIMEOUT(receivedWill, "Did not receive a will message", delay * 1000 * 3); + const int elapsed = delayTime.elapsed(); + QVERIFY(elapsed > minimalWait); + QVERIFY(elapsed < maximumWait); +} + +QTEST_MAIN(tst_QMqttLastWillProperties) + +#include "tst_qmqttlastwillproperties.moc" diff --git a/tests/auto/qmqttpublishproperties/qmqttpublishproperties.pro b/tests/auto/qmqttpublishproperties/qmqttpublishproperties.pro new file mode 100644 index 0000000..c28480b --- /dev/null +++ b/tests/auto/qmqttpublishproperties/qmqttpublishproperties.pro @@ -0,0 +1,17 @@ +CONFIG += testcase +QT += network testlib mqtt +QT -= gui +QT_PRIVATE += mqtt-private + +TARGET = tst_qmqttpublishproperties + +SOURCES += \ + tst_qmqttpublishproperties.cpp + +HEADERS += \ + $$PWD/../../common/broker_connection.h + +INCLUDEPATH += \ + $$PWD/../../common + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/qmqttpublishproperties/tst_qmqttpublishproperties.cpp b/tests/auto/qmqttpublishproperties/tst_qmqttpublishproperties.cpp new file mode 100644 index 0000000..669fe5d --- /dev/null +++ b/tests/auto/qmqttpublishproperties/tst_qmqttpublishproperties.cpp @@ -0,0 +1,218 @@ +/****************************************************************************** +** +** 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-EXCEPT$ +** 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 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** 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 "broker_connection.h" + +#include <QtCore/QString> +#include <QtTest/QtTest> +#include <QtMqtt/QMqttClient> +#include <QtMqtt/QMqttPublishProperties> +#include <QtMqtt/QMqttSubscription> + +class tst_QMqttPublishProperties : public QObject +{ + Q_OBJECT + +public: + tst_QMqttPublishProperties(); + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void getSet(); + void propertyConsistency(); + void topicAlias(); + +private: + QProcess m_brokerProcess; + QString m_testBroker; + quint16 m_port{1883}; +}; + +tst_QMqttPublishProperties::tst_QMqttPublishProperties() +{ +} + +void tst_QMqttPublishProperties::initTestCase() +{ + m_testBroker = invokeOrInitializeBroker(&m_brokerProcess); + if (m_testBroker.isEmpty()) + qFatal("No MQTT broker present to test against."); +} + +void tst_QMqttPublishProperties::cleanupTestCase() +{ +} + +void tst_QMqttPublishProperties::getSet() +{ + QMqttPublishProperties p; + + QCOMPARE(p.availableProperties(), 0); + + QVERIFY(!(p.availableProperties() & QMqttPublishProperties::PayloadFormatIndicator)); + QCOMPARE(p.payloadFormatIndicator(), QMqtt::PayloadFormatIndicator::Unspecified); + p.setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator::UTF8Encoded); + QCOMPARE(p.payloadFormatIndicator(), QMqtt::PayloadFormatIndicator::UTF8Encoded); + QVERIFY(p.availableProperties() & QMqttPublishProperties::PayloadFormatIndicator); + + QVERIFY(!(p.availableProperties() & QMqttPublishProperties::MessageExpiryInterval)); + p.setMessageExpiryInterval(200); + QVERIFY(p.availableProperties() & QMqttPublishProperties::MessageExpiryInterval); + QCOMPARE(p.messageExpiryInterval(), 200u); + + QVERIFY(!(p.availableProperties() & QMqttPublishProperties::TopicAlias)); + p.setTopicAlias(1); + QCOMPARE(p.topicAlias(), 1); + QVERIFY(p.availableProperties() & QMqttPublishProperties::TopicAlias); + p.setTopicAlias(0); // Zero is not allowed + QCOMPARE(p.topicAlias(), 1); + + const QString responseTopic = QLatin1String("reply/to/this"); + QVERIFY(!(p.availableProperties() & QMqttPublishProperties::ResponseTopic)); + QCOMPARE(p.responseTopic(), QString()); + p.setResponseTopic(responseTopic); + QVERIFY(p.availableProperties() & QMqttPublishProperties::ResponseTopic); + QCOMPARE(p.responseTopic(), responseTopic); + + const QByteArray data = QByteArray(1, char('c')); + QVERIFY(!(p.availableProperties() & QMqttPublishProperties::CorrelationData)); + QCOMPARE(p.correlationData(), QByteArray()); + p.setCorrelationData(data); + QVERIFY(p.availableProperties() & QMqttPublishProperties::CorrelationData); + QCOMPARE(p.correlationData(), data); + + const QString userKey = QLatin1String("UserName"); + const QString userValue = QLatin1String("SomeValue"); + QMqttUserProperties userProperty; + QVERIFY(!(p.availableProperties() & QMqttPublishProperties::UserProperty)); + QCOMPARE(p.userProperties(), userProperty); + userProperty.append(QMqttStringPair(userKey, userValue)); + p.setUserProperties(userProperty); + QVERIFY(p.availableProperties() & QMqttPublishProperties::UserProperty); + QCOMPARE(p.userProperties(), userProperty); + + QVERIFY(!(p.availableProperties() & QMqttPublishProperties::SubscriptionIdentifier)); + const QList<quint32> one{1}; + p.setSubscriptionIdentifiers(one); + QCOMPARE(p.subscriptionIdentifiers(), one); + + const QList<quint32> invalidZero{1, 0}; + p.setSubscriptionIdentifiers(invalidZero); + QVERIFY(p.availableProperties() & QMqttPublishProperties::SubscriptionIdentifier); + QCOMPARE(p.subscriptionIdentifiers(), one); + + const QString contentType = QLatin1String("MultimediaContent123"); + QVERIFY(!(p.availableProperties() & QMqttPublishProperties::ContentType)); + QCOMPARE(p.contentType(), QString()); + p.setContentType(contentType); + QVERIFY(p.availableProperties() & QMqttPublishProperties::ContentType); + QCOMPARE(p.contentType(), contentType); +} + +void tst_QMqttPublishProperties::propertyConsistency() +{ + + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + QMqttClient client2; + client2.setProtocolVersion(QMqttClient::MQTT_5_0); + client2.setHostname(m_testBroker); + client2.setPort(m_port); + + client2.connectToHost(); + QTRY_VERIFY2(client2.state() == QMqttClient::Connected, "Could not connect to broker."); + + + const QByteArray correlation(10, 'c'); + const QString responseTopic = QLatin1String("topic/to/reply"); + const QString userKey1 = QLatin1String("UserName1"); + const QString userValue1 = QLatin1String("SomeValue"); + const QString userKey2 = QLatin1String("UserName2"); + const QString userValue2 = QLatin1String("OtherValue"); + QMqttUserProperties userProperty; + userProperty.append(QMqttStringPair(userKey1, userValue1)); + userProperty.append(QMqttStringPair(userKey2, userValue2)); + const QString content = QLatin1String("ContentType"); + + const QString testTopic = QLatin1String("Qt/PublishProperties/publish/consistent"); + + auto sub = client2.subscribe(testTopic, 1); + QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed); + + QMqttPublishProperties pubProp; + pubProp.setPayloadFormatIndicator(QMqtt::PayloadFormatIndicator::UTF8Encoded); + pubProp.setMessageExpiryInterval(60); + //pubProp.setTopicAlias(1); + pubProp.setResponseTopic(responseTopic); + pubProp.setCorrelationData(correlation); + pubProp.setUserProperties(userProperty); + pubProp.setContentType(content); + + QSignalSpy publishSpy(&client, SIGNAL(messageSent(qint32))); + QSignalSpy subscribeSpy(sub, SIGNAL(messageReceived(QMqttMessage))); + client.publish(testTopic, pubProp, QByteArray("Some Content"), 1); + + QTRY_COMPARE(publishSpy.count(), 1); + QTRY_COMPARE(subscribeSpy.count(), 1); + + auto msg = subscribeSpy.at(0).at(0).value<QMqttMessage>(); + + QCOMPARE(msg.payload(), QByteArray("Some Content")); + QMqttPublishProperties receivalProp = msg.publishProperties(); + QCOMPARE(pubProp.payloadFormatIndicator(), receivalProp.payloadFormatIndicator()); + QVERIFY(pubProp.messageExpiryInterval() >= receivalProp.messageExpiryInterval()); // The broker MIGHT reduce + QCOMPARE(pubProp.responseTopic(), receivalProp.responseTopic()); + QCOMPARE(pubProp.correlationData(), receivalProp.correlationData()); + + // Paho test server sends identical userProperties, Flespi adds additional properties (ie timestamp) + const auto userSend = pubProp.userProperties(); + const auto userReceive = receivalProp.userProperties(); + QVERIFY(userSend.size() <= userReceive.size()); + for (auto it = userSend.constBegin(); it != userSend.constEnd(); ++it) + QVERIFY(userReceive.contains(*it)); + + QCOMPARE(pubProp.contentType(), receivalProp.contentType()); +} + +void tst_QMqttPublishProperties::topicAlias() +{ + // Get serverproperties + // Send data with higher alias than available + // Connection gets killed +} + +QTEST_MAIN(tst_QMqttPublishProperties) + +#include "tst_qmqttpublishproperties.moc" diff --git a/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp b/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp index fda8ef6..78bb05c 100644 --- a/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp +++ b/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp @@ -47,8 +47,13 @@ private Q_SLOTS: void getSetCheck(); void wildCards_data(); void wildCards(); + void reconnect_data(); void reconnect(); + void sharedConnection(); + void sharedNonShared_data(); + void sharedNonShared(); private: + void createAndSubscribe(QMqttClient *c, QMqttSubscription **sub, const QString &topic); QProcess m_brokerProcess; QString m_testBroker; quint16 m_port{1883}; @@ -75,35 +80,41 @@ void Tst_QMqttSubscription::getSetCheck() void Tst_QMqttSubscription::wildCards_data() { + QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion"); QTest::addColumn<QString>("subscription"); QTest::addColumn<int>("expectedReceival"); - QTest::newRow("#") << "Qt/#" << 6; - QTest::newRow("Qt/a/b/c/d/e/f") << "Qt/a/b/c/d/e/f" << 1; - QTest::newRow("Qt/+/b/c/d/e/f") << "Qt/+/b/c/d/e/f" << 1; - QTest::newRow("Qt/a/+/c/d/e/f") << "Qt/a/+/c/d/e/f" << 1; - QTest::newRow("Qt/a/b/+/d/e/f") << "Qt/a/b/+/d/e/f" << 1; - QTest::newRow("Qt/a/b/c/+/e/f") << "Qt/a/b/c/+/e/f" << 1; - QTest::newRow("Qt/a/b/c/d/+/f") << "Qt/a/b/c/d/+/f" << 1; - QTest::newRow("Qt/a/b/c/d/e/+") << "Qt/a/b/c/d/e/+" << 1; - QTest::newRow("Qt/+/b/+/d/e/+") << "Qt/+/b/+/d/e/+" << 1; - QTest::newRow("Qt/a/+") << "Qt/a/+" << 1; - QTest::newRow("Qt/a/+/c") << "Qt/a/+/c" << 1; + QList<QMqttClient::ProtocolVersion> versions{QMqttClient::MQTT_3_1_1, QMqttClient::MQTT_5_0}; + + for (int i = 0; i < 2; ++i) { + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/#")) << versions[i] << "Qt/subscription/#" << 6; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/a/b/c/d/e/f")) << versions[i] << "Qt/subscription/a/b/c/d/e/f" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/+/b/c/d/e/f")) << versions[i] << "Qt/subscription/+/b/c/d/e/f" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/a/+/c/d/e/f")) << versions[i] << "Qt/subscription/a/+/c/d/e/f" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/a/b/+/d/e/f")) << versions[i] << "Qt/subscription/a/b/+/d/e/f" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/a/b/c/+/e/f")) << versions[i] << "Qt/subscription/a/b/c/+/e/f" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/a/b/c/d/+/f")) << versions[i] << "Qt/subscription/a/b/c/d/+/f" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/a/b/c/d/e/+")) << versions[i] << "Qt/subscription/a/b/c/d/e/+" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/+/b/+/d/e/+")) << versions[i] << "Qt/subscription/+/b/+/d/e/+" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/a/+")) << versions[i] << "Qt/subscription/a/+" << 1; + QTest::newRow(qPrintable(QString::number(versions[i]) + ":Qt/subscription/a/+/c")) << versions[i] << "Qt/subscription/a/+/c" << 1; + } } void Tst_QMqttSubscription::wildCards() { + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); QFETCH(QString, subscription); QFETCH(int, expectedReceival); - QMqttClient client; + VersionClient(mqttVersion, client); client.setHostname(m_testBroker); client.setPort(m_port); client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); - QMqttClient publisher; + VersionClient(mqttVersion, publisher); publisher.setHostname(m_testBroker); publisher.setPort(m_port); publisher.connectToHost(); @@ -115,12 +126,12 @@ void Tst_QMqttSubscription::wildCards() QSignalSpy receivalSpy(sub, SIGNAL(messageReceived(QMqttMessage))); QStringList topics; - topics << "Qt/a" - << "Qt/a/b" - << "Qt/a/b/c" - << "Qt/a/b/c/d" - << "Qt/a/b/c/d/e" - << "Qt/a/b/c/d/e/f"; + topics << "Qt/subscription/a" + << "Qt/subscription/a/b" + << "Qt/subscription/a/b/c" + << "Qt/subscription/a/b/c/d" + << "Qt/subscription/a/b/c/d/e" + << "Qt/subscription/a/b/c/d/e/f"; for (auto t : topics) { QSignalSpy spy(&publisher, SIGNAL(messageSent(qint32))); @@ -142,11 +153,15 @@ void Tst_QMqttSubscription::wildCards() QTRY_VERIFY2(publisher.state() == QMqttClient::Disconnected, "Could not disconnect."); } +DefaultVersionTestData(Tst_QMqttSubscription::reconnect_data) + void Tst_QMqttSubscription::reconnect() { + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + // QTBUG-64042 // - Connect with clean session - QMqttClient client; + VersionClient(mqttVersion, client); client.setHostname(m_testBroker); client.setPort(m_port); @@ -155,7 +170,7 @@ void Tst_QMqttSubscription::reconnect() QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); // - Subscribe to topic A - const QString subscription("topics/resub"); + const QString subscription("Qt/subscription/topics/resub"); auto sub = client.subscribe(subscription, 1); QTRY_VERIFY2(sub->state() == QMqttSubscription::Subscribed, "Could not subscribe to topic."); @@ -217,6 +232,151 @@ void Tst_QMqttSubscription::reconnect() QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Could not disconnect."); } +void Tst_QMqttSubscription::createAndSubscribe(QMqttClient *c, QMqttSubscription **sub, const QString &topic) +{ + c->setProtocolVersion(QMqttClient::MQTT_5_0); + c->setHostname(m_testBroker); + c->setPort(m_port); + + c->connectToHost(); + QTRY_VERIFY2(c->state() == QMqttClient::Connected, "Could not connect to broker."); + + *sub = c->subscribe(topic, 1); + QTRY_VERIFY2((*sub)->state() == QMqttSubscription::Subscribed, "Could not subscribe."); +} + +void Tst_QMqttSubscription::sharedConnection() +{ + // Create / Connect publisher + QMqttClient sender; + sender.setProtocolVersion(QMqttClient::MQTT_5_0); + sender.setHostname(m_testBroker); + sender.setPort(m_port); + sender.connectToHost(); + QTRY_VERIFY2(sender.state() == QMqttClient::Connected, "Could not connect to broker."); + + // Create GroupA + const int groupSizeA = 2; + const QString groupTopicA{QLatin1String("$share/groupA/shared/sub")}; + QMqttClient listenersA[groupSizeA]; + QMqttSubscription *subsA[groupSizeA]; + int messageCounterA[groupSizeA] = {0}; + int messageSumA = 0; + // listenerAx: $share/groupA/Qt/Subscription/shared_check/# + for (int i = 0; i < groupSizeA; ++i) { + createAndSubscribe(&listenersA[i], &subsA[i], groupTopicA); + QCOMPARE(subsA[i]->isShared(), true); + QCOMPARE(subsA[i]->shareName(), QLatin1String("groupA")); + connect(subsA[i], &QMqttSubscription::messageReceived, [i, &messageCounterA, &messageSumA]() { + messageCounterA[i]++; + messageSumA++; + //qDebug() << "A Got message:" << i << ":" << messageCounterA[i]; + }); + } + + // Create GroupB + const int groupSizeB = 5; + const QString groupTopicB{QLatin1String("$share/groupB/shared/#")}; + QMqttClient listenersB[groupSizeB]; + QMqttSubscription *subsB[groupSizeB]; + int messageCounterB[groupSizeB] = {0}; + int messageSumB = 0; + // listenerBx: $share/groupB/Qt/Subscription/shared_check/# + for (int i = 0; i < groupSizeB; ++i) { + createAndSubscribe(&listenersB[i], &subsB[i], groupTopicB); + QCOMPARE(subsB[i]->isShared(), true); + QCOMPARE(subsB[i]->shareName(), QLatin1String("groupB")); + connect(subsB[i], &QMqttSubscription::messageReceived, [i, &messageCounterB, &messageSumB]() { + messageCounterB[i]++; + messageSumB++; + //qDebug() << "B Got message:" << i << ":" << messageCounterB[i]; + }); + } + + const int publishedMessages = 10; + for (int i = 0; i < publishedMessages; ++i) { + QSignalSpy publishSpy(&sender, SIGNAL(messageSent(qint32))); + sender.publish(QLatin1String("shared/sub"), QByteArray("Foobidoo"), 1); + QTRY_VERIFY2(publishSpy.size() == 1, "Could not publish message."); + } + + QTRY_VERIFY2(messageSumA == publishedMessages, "Group A did not receive enough messages."); + QTRY_VERIFY2(messageSumB == publishedMessages, "Group A did not receive enough messages."); + + QString formatString; + for (int i = 0; i < groupSizeA; ++i) { + formatString.append(QString::number(messageCounterA[i])); + formatString.append(QLatin1Char(' ')); + } + qDebug() << "Statistics GroupA : " << formatString; + formatString.clear(); + for (int i = 0; i < groupSizeB; ++i) { + formatString.append(QString::number(messageCounterB[i])); + formatString.append(QLatin1Char(' ')); + } + qDebug() << "Statistics GroupB : " << formatString; +} + +void Tst_QMqttSubscription::sharedNonShared_data() +{ + const QString topic(QLatin1String("Qt/Subscription/SharedNonShared")); + const QString groupTopic = QString::fromLatin1("$share/somegroup/") + topic; + + QTest::addColumn<QString>("topic1"); + QTest::addColumn<bool>("shared1"); + QTest::addColumn<QString>("topic2"); + QTest::addColumn<bool>("shared2"); + QTest::addColumn<bool>("expected"); + + QTest::newRow("non - non") << topic << false << topic << false << true; + QTest::newRow("non - yes") << topic << false << groupTopic << true << false; + QTest::newRow("yes - non") << groupTopic << true << topic << false << false; + QTest::newRow("yes - yes") << groupTopic << true << groupTopic << true << true; +} + +void Tst_QMqttSubscription::sharedNonShared() +{ + QFETCH(QString, topic1); + QFETCH(bool, shared1); + QFETCH(QString, topic2); + QFETCH(bool, shared2); + QFETCH(bool, expected); + + // Create / Connect publisher + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + QMqttSubscription *sub1 = client.subscribe(topic1, 1); + QCOMPARE(sub1->isShared(), shared1); + QVERIFY(sub1->shareName().isEmpty() == !shared1); + + QMqttSubscription *sub2 = client.subscribe(topic2, 1); + QCOMPARE(sub2->isShared(), shared2); + QVERIFY(sub2->shareName().isEmpty() == !shared2); + + // Verify that a subscription is reused / not reused + QCOMPARE(sub1 == sub2, expected); + + // Depending on the broker, it may decide to not send a message twice due to overlapping + // subscriptions on the same client (eg. paho). + // Using two different clients would make the receival work, but is not part of this test. + +// QSignalSpy receivalSpy1(sub1, SIGNAL(messageReceived(QMqttMessage))); +// QSignalSpy receivalSpy2(sub2, SIGNAL(messageReceived(QMqttMessage))); +// QSignalSpy publishSpy(&client, SIGNAL(messageSent(qint32))); + +// client.publish(QLatin1String("Qt/Subscription/SharedNonShared"), QByteArray(), 1); +// QTRY_VERIFY(publishSpy.count() == 1); + +// // Verify both subscriptions receive the message +// QTRY_VERIFY(receivalSpy1.count() == 1); +// QTRY_VERIFY(receivalSpy2.count() == 1); +} + QTEST_MAIN(Tst_QMqttSubscription) #include "tst_qmqttsubscription.moc" diff --git a/tests/auto/qmqttsubscriptionproperties/qmqttsubscriptionproperties.pro b/tests/auto/qmqttsubscriptionproperties/qmqttsubscriptionproperties.pro new file mode 100644 index 0000000..857a7c2 --- /dev/null +++ b/tests/auto/qmqttsubscriptionproperties/qmqttsubscriptionproperties.pro @@ -0,0 +1,17 @@ +CONFIG += testcase +QT += network testlib mqtt +QT -= gui +QT_PRIVATE += mqtt-private + +TARGET = tst_qmqttsubscriptionproperties + +SOURCES += \ + tst_qmqttsubscriptionproperties.cpp + +HEADERS += \ + $$PWD/../../common/broker_connection.h + +INCLUDEPATH += \ + $$PWD/../../common + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/qmqttsubscriptionproperties/tst_qmqttsubscriptionproperties.cpp b/tests/auto/qmqttsubscriptionproperties/tst_qmqttsubscriptionproperties.cpp new file mode 100644 index 0000000..00c7a3d --- /dev/null +++ b/tests/auto/qmqttsubscriptionproperties/tst_qmqttsubscriptionproperties.cpp @@ -0,0 +1,128 @@ +/****************************************************************************** +** +** 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-EXCEPT$ +** 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 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** 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 "broker_connection.h" + +#include <QtCore/QString> +#include <QtTest/QtTest> +#include <QtMqtt/QMqttClient> +#include <QtMqtt/QMqttConnectionProperties> +#include <QtMqtt/QMqttSubscription> +#include <QtMqtt/QMqttSubscriptionProperties> + +class tst_QMqttSubscriptionProperties : public QObject +{ + Q_OBJECT + +public: + tst_QMqttSubscriptionProperties(); + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void getSet(); + void subscribe(); + +private: + QProcess m_brokerProcess; + QString m_testBroker; + quint16 m_port{1883}; +}; + +tst_QMqttSubscriptionProperties::tst_QMqttSubscriptionProperties() +{ +} + +void tst_QMqttSubscriptionProperties::initTestCase() +{ + m_testBroker = invokeOrInitializeBroker(&m_brokerProcess); + if (m_testBroker.isEmpty()) + qFatal("No MQTT broker present to test against."); +} + +void tst_QMqttSubscriptionProperties::cleanupTestCase() +{ +} + +void tst_QMqttSubscriptionProperties::getSet() +{ + QMqttSubscriptionProperties properties; + + const quint32 id = 123; + properties.setSubscriptionIdentifier(id); + QCOMPARE(properties.subscriptionIdentifier(), id); + + const QString userKey1 = QLatin1String("UserName1"); + const QString userValue1 = QLatin1String("SomeValue"); + const QString userKey2 = QLatin1String("UserName2"); + const QString userValue2 = QLatin1String("OtherValue"); + QMqttUserProperties userProperty; + QCOMPARE(properties.userProperties(), userProperty); + userProperty.append(QMqttStringPair(userKey1, userValue1)); + userProperty.append(QMqttStringPair(userKey2, userValue2)); + properties.setUserProperties(userProperty); + QCOMPARE(properties.userProperties(), userProperty); +} + +void tst_QMqttSubscriptionProperties::subscribe() +{ + QMqttClient client; + client.setProtocolVersion(QMqttClient::MQTT_5_0); + client.setHostname(m_testBroker); + client.setPort(m_port); + + QMqttConnectionProperties conProperties; + conProperties.setRequestResponseInformation(true); + conProperties.setRequestProblemInformation(true); + client.setConnectionProperties(conProperties); + + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + const QString topic = QLatin1String("sub/props"); + + QMqttSubscriptionProperties properties; + properties.setSubscriptionIdentifier(10); + const QString userKey1 = QLatin1String("UserName1"); + const QString userValue1 = QLatin1String("SomeValue"); + const QString userKey2 = QLatin1String("UserName2"); + const QString userValue2 = QLatin1String("OtherValue"); + QMqttUserProperties userProperty; + userProperty.append(QMqttStringPair(userKey1, userValue1)); + userProperty.append(QMqttStringPair(userKey2, userValue2)); + + auto sub = client.subscribe(topic, properties, 1); + QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed); + + // ### TODO: Try to create a subscription which generates a reason code + // and/or user properties. +} + +QTEST_MAIN(tst_QMqttSubscriptionProperties) + +#include "tst_qmqttsubscriptionproperties.moc" diff --git a/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp b/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp index f83999c..ce1cd67 100644 --- a/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp +++ b/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp @@ -75,6 +75,14 @@ void Tst_QMqttTopicFilter::checkValidity() QVERIFY(!QMqttTopicFilter("++").isValid()); QVERIFY(!QMqttTopicFilter(QString(3, QChar(QChar::Null))).isValid()); + + QVERIFY(QMqttTopicFilter("$share/group/topic").isValid()); + QVERIFY(QMqttTopicFilter("$share/group/topic/subtopic").isValid()); + QVERIFY(QMqttTopicFilter("$share/group/topic/+/someother").isValid()); + QVERIFY(QMqttTopicFilter("$share/group/topic/#").isValid()); + QVERIFY(!QMqttTopicFilter("$share/groupnotopic").isValid()); + QVERIFY(!QMqttTopicFilter("$share/").isValid()); + QVERIFY(!QMqttTopicFilter("$share//foo").isValid()); } void Tst_QMqttTopicFilter::matches() diff --git a/tests/benchmarks/qmqttclient/tst_qmqttclient.cpp b/tests/benchmarks/qmqttclient/tst_qmqttclient.cpp index f0856cc..27001a1 100644 --- a/tests/benchmarks/qmqttclient/tst_qmqttclient.cpp +++ b/tests/benchmarks/qmqttclient/tst_qmqttclient.cpp @@ -90,7 +90,7 @@ void Tst_QMqttClient::stressTest() quint64 messageCount = 0; QMqttSubscription *subscription; - const QString testTopic = QLatin1String("test/topic"); + const QString testTopic = QLatin1String("Qt/benchmark/test/topic"); connect(&subscriber, &QMqttClient::connected, [&](){ subscription = subscriber.subscribe(testTopic); @@ -148,7 +148,7 @@ void Tst_QMqttClient::stressTest2() publisher.connectToHost(); QTRY_COMPARE(spy.count(), 1); - const QString topic = QLatin1String("SomeTopic/Sub"); + const QString topic = QLatin1String("Qt/benchmark/SomeTopic/Sub"); const QByteArray message("messageContent"); for (qint32 i = 0; i < msgCount; ++i) { diff --git a/tests/common/broker_connection.h b/tests/common/broker_connection.h index 7a04468..372f860 100644 --- a/tests/common/broker_connection.h +++ b/tests/common/broker_connection.h @@ -29,6 +29,7 @@ #include <QtCore/QFile> #include <QtCore/QProcess> #include <QtCore/QString> +#include <QtMqtt/QMqttClient> #include <QtNetwork/QTcpSocket> #include <QtTest/QTest> @@ -66,7 +67,18 @@ QString invokeOrInitializeBroker(QProcess *gBrokerProcess) #endif } - const QStringList arguments = QStringList() << brokerLocation; + QStringList arguments = {brokerLocation}; + + // MQTT5 tests use the same configuration as mosquitto + const QString configuration = QLatin1String("localhost_testing.conf"); + const QDir brokerDir = QFileInfo(brokerLocation).absoluteDir(); + if (brokerDir.exists(configuration)) { + arguments << QLatin1String("-c") << QDir::toNativeSeparators(brokerDir.absoluteFilePath(configuration)); + // Configuration files use relative paths, hence the working directory of the broker + // process needs to be set correctly + gBrokerProcess->setWorkingDirectory(brokerDir.absolutePath()); + } + qDebug() << "Launching broker:" << python << arguments; gBrokerProcess->start(python, arguments); if (!gBrokerProcess->waitForStarted()) @@ -84,6 +96,17 @@ QString invokeOrInitializeBroker(QProcess *gBrokerProcess) QTest::qWait(5000); } - qWarning("Could not launch MQTT test broker."); + qWarning() << "Could not launch MQTT test broker."; return QString(); } + +Q_DECLARE_METATYPE(QMqttClient::ProtocolVersion) +#define VersionClient(MQTTVERSION, CLIENTNAME) QMqttClient CLIENTNAME; CLIENTNAME.setProtocolVersion(MQTTVERSION) + +#define DefaultVersionTestData(FUNCTION) \ +void FUNCTION() \ +{ \ + QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion"); \ + QTest::newRow("V3.1.1") << QMqttClient::MQTT_3_1_1; \ + QTest::newRow("V5.0.0") << QMqttClient::MQTT_5_0; \ +} |