Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package qt6-mqtt for openSUSE:Factory checked in at 2026-10-18 19:22:44 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/qt6-mqtt (Old) and /work/SRC/openSUSE:Factory/.qt6-mqtt.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "qt6-mqtt" Sun Oct 18 19:22:44 2026 rev:44 rq:1384398 version:6.12.0 Changes: -------- --- /work/SRC/openSUSE:Factory/qt6-mqtt/qt6-mqtt.changes 2026-08-21 16:54:08.094691294 +0200 +++ /work/SRC/openSUSE:Factory/.qt6-mqtt.new.3698736/qt6-mqtt.changes 2026-10-18 19:23:04.426136644 +0200 @@ -1,0 +2,6 @@ +Wed Sep 30 16:10:54 UTC 2026 - Christophe Marin <[email protected]> + +- Update to 6.12.0 + https://www.qt.io/blog/qt-6.12-released + +------------------------------------------------------------------- Old: ---- qtmqtt-everywhere-src-6.11.2.tar.xz New: ---- qtmqtt-everywhere-src-6.12.0.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ qt6-mqtt.spec ++++++ --- /var/tmp/diff_new_pack.knkWd7/_old 2026-10-18 19:23:08.028285808 +0200 +++ /var/tmp/diff_new_pack.knkWd7/_new 2026-10-18 19:23:08.030285891 +0200 @@ -16,8 +16,8 @@ # -%define real_version 6.11.2 -%define short_version 6.11 +%define real_version 6.12.0 +%define short_version 6.12 %define tar_name qtmqtt-everywhere-src %define tar_suffix %{nil} # @@ -27,7 +27,7 @@ %endif # Name: qt6-mqtt%{?pkg_suffix} -Version: 6.11.2 +Version: 6.12.0 Release: 0 Summary: Qt 6 Module to implement MQTT protocol version 3.1 and 3.1.1 License: GPL-3.0-only WITH Qt-GPL-exception-1.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.knkWd7/_old 2026-10-18 19:23:08.071287589 +0200 +++ /var/tmp/diff_new_pack.knkWd7/_new 2026-10-18 19:23:08.073287672 +0200 @@ -1,9 +1,9 @@ <services> <service name="tar_scm" mode="disabled"> <param name="changesgenerate">disable</param> - <param name="version">6.11.2</param> + <param name="version">6.12.0</param> <param name="url">git://code.qt.io/qt/qtmqtt.git</param> - <param name="revision">v6.11.2</param> + <param name="revision">v6.12.0</param> <param name="scm">git</param> <param name="filename">qtmqtt-everywhere-src</param> </service> ++++++ qtmqtt-everywhere-src-6.11.2.tar.xz -> qtmqtt-everywhere-src-6.12.0.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtmqtt-everywhere-src-6.11.2/.cmake.conf new/qtmqtt-everywhere-src-6.12.0/.cmake.conf --- old/qtmqtt-everywhere-src-6.11.2/.cmake.conf 2026-08-12 05:39:57.000000000 +0200 +++ new/qtmqtt-everywhere-src-6.12.0/.cmake.conf 2026-09-27 12:11:48.000000000 +0200 @@ -1,4 +1,4 @@ -set(QT_REPO_MODULE_VERSION "6.11.2") +set(QT_REPO_MODULE_VERSION "6.12.0") set(QT_REPO_MODULE_PRERELEASE_VERSION_SEGMENT "alpha1") set(QT_EXTRA_INTERNAL_TARGET_DEFINES "QT_NO_QASCONST=1") list(APPEND QT_EXTRA_INTERNAL_TARGET_DEFINES "QT_NO_FOREACH=1") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtmqtt-everywhere-src-6.11.2/CONTRIBUTING.md new/qtmqtt-everywhere-src-6.12.0/CONTRIBUTING.md --- old/qtmqtt-everywhere-src-6.11.2/CONTRIBUTING.md 1970-01-01 01:00:00.000000000 +0100 +++ new/qtmqtt-everywhere-src-6.12.0/CONTRIBUTING.md 2026-09-27 12:11:48.000000000 +0200 @@ -0,0 +1,11 @@ +## Qt contribution guidelines + +We welcome contributions to Qt! + +Note that we cannot accept pull requests on GitHub. All contributions +to the Qt project are exclusively handled through the +[Gerrit code review system](https://codereview.qt-project.org). + +See +[contribute.qt-project.org](https://contribute.qt-project.org/) to learn +more. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtmqtt-everywhere-src-6.11.2/REUSE.toml new/qtmqtt-everywhere-src-6.12.0/REUSE.toml --- old/qtmqtt-everywhere-src-6.11.2/REUSE.toml 2026-08-12 05:39:57.000000000 +0200 +++ new/qtmqtt-everywhere-src-6.12.0/REUSE.toml 2026-09-27 12:11:48.000000000 +0200 @@ -22,7 +22,8 @@ SPDX-License-Identifier = "LicenseRef-Qt-Commercial OR BSD-3-Clause" [[annotations]] -path = ["**.qdocconf", "src/mqtt/doc/style/style.css", "tests/README.txt"] +path = ["**.qdocconf", "src/mqtt/doc/style/style.css", + "tests/README.txt", "CONTRIBUTING.md"] comment = "documentation" precedence = "closest" SPDX-FileCopyrightText = "Copyright (C) The Qt Company Ltd." diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtmqtt-everywhere-src-6.11.2/coin/module_config.yaml new/qtmqtt-everywhere-src-6.12.0/coin/module_config.yaml --- old/qtmqtt-everywhere-src-6.11.2/coin/module_config.yaml 2026-08-12 05:39:57.000000000 +0200 +++ new/qtmqtt-everywhere-src-6.12.0/coin/module_config.yaml 2026-09-27 12:11:48.000000000 +0200 @@ -24,5 +24,5 @@ - !include "{{qt/qtbase}}/coin_module_build_template_v2.yaml" Test: - - !include "{{qt/qtbase}}/coin_module_test_template_v2.yaml" + - !include "{{qt/qtbase}}/coin_module_test_template_v3.yaml" - !include "{{qt/qtbase}}/coin_module_test_docs.yaml" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtmqtt-everywhere-src-6.11.2/dependencies.yaml new/qtmqtt-everywhere-src-6.12.0/dependencies.yaml --- old/qtmqtt-everywhere-src-6.11.2/dependencies.yaml 2026-08-12 05:39:57.000000000 +0200 +++ new/qtmqtt-everywhere-src-6.12.0/dependencies.yaml 2026-09-27 12:11:48.000000000 +0200 @@ -1,10 +1,10 @@ dependencies: ../qtbase: - ref: ef55f427f2c8b410d34f8a7681020a3000cf6866 + ref: 025bdad181de241e81bf853c8a2d7bf3d19261a9 required: true ../qtdeclarative: - ref: 4e3399c26ec57246c08de019cfcbda8d23604cfa + ref: 4332fbbb4c340463dc8cee36dda0c85d932122d8 required: true ../qtwebsockets: - ref: 3eab06de7076a83514e98c2403baa433d137b013 + ref: 6aedb199b5d8ee4afa4cfc956ded22bc0a2632e9 required: false diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtmqtt-everywhere-src-6.11.2/licenseRule.json new/qtmqtt-everywhere-src-6.12.0/licenseRule.json --- old/qtmqtt-everywhere-src-6.11.2/licenseRule.json 2026-08-12 05:39:57.000000000 +0200 +++ new/qtmqtt-everywhere-src-6.12.0/licenseRule.json 2026-09-27 12:11:48.000000000 +0200 @@ -56,9 +56,10 @@ } }, { - "comment": "Files with the following endings are Documentation licensed.", - "file_pattern_ending": [".qdoc", ".qdocinc" , ".qdocconf", "README", "qt_attribution.json", - ".css", "README.txt"], + "comment": "Files with the following endings are documentation licensed.", + "file_pattern_ending": [".qdoc", ".qdocinc" , ".qdocconf", ".css", + "README.txt", "CONTRIBUTING.md", + "qt_attribution.json"], "location":{ "": { "comment": "", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtmqtt-everywhere-src-6.11.2/src/mqtt/qmqttconnection.cpp new/qtmqtt-everywhere-src-6.12.0/src/mqtt/qmqttconnection.cpp --- old/qtmqtt-everywhere-src-6.11.2/src/mqtt/qmqttconnection.cpp 2026-08-12 05:39:57.000000000 +0200 +++ new/qtmqtt-everywhere-src-6.12.0/src/mqtt/qmqttconnection.cpp 2026-09-27 12:11:48.000000000 +0200 @@ -472,7 +472,9 @@ const quint16 topicAlias = publishProperties.topicAlias(); if (topicAlias > 0) { // User specified topic Alias - if (topicAlias > m_clientPrivate->m_serverConnectionProperties.maximumTopicAlias()) { + // m_publishAliases is sized from the maximum the server declared in CONNACK, + // so bounds-check against the list itself rather than against a property value. + if (topicAlias > m_publishAliases.size()) { qCDebug(lcMqttConnection) << "TopicAlias publish: overflow."; return -1; } @@ -771,15 +773,15 @@ void QMqttConnection::cleanSubscriptions() { - for (auto item : m_pendingSubscriptionAck) + for (auto item : std::as_const(m_pendingSubscriptionAck)) item->setState(QMqttSubscription::Unsubscribed); m_pendingSubscriptionAck.clear(); - for (auto item : m_pendingUnsubscriptions) + for (auto item : std::as_const(m_pendingUnsubscriptions)) item->setState(QMqttSubscription::Unsubscribed); m_pendingUnsubscriptions.clear(); - for (auto item : m_activeSubscriptions) + for (auto item : std::as_const(m_activeSubscriptions)) item->setState(QMqttSubscription::Unsubscribed); m_activeSubscriptions.clear(); } @@ -844,15 +846,15 @@ quint8 b = 0; quint8 iteration = 0; do { - b = readBufferTyped<quint8>(dataSize); - msgLength += (b & 127) * multiplier; - multiplier *= 128; iteration++; if (iteration > 4) { qCDebug(lcMqttConnection) << "Overflow trying to read variable integer."; closeConnection(QMqttClient::ProtocolViolation); return -1; } + b = readBufferTyped<quint8>(dataSize); + msgLength += (b & 127) * multiplier; + multiplier *= 128; } while ((b & 128) != 0); return msgLength; } @@ -885,6 +887,8 @@ void QMqttConnection::readAuthProperties(QMqttAuthenticationProperties &properties) { qint64 propertyLength = readVariableByteInteger(&m_missingData); + if (propertyLength < 0) // Connection closed inside readVariableByteInteger + return; m_missingData -= propertyLength; QMqttUserProperties userProperties; @@ -926,6 +930,8 @@ void QMqttConnection::readConnackProperties(QMqttServerConnectionProperties &properties) { qint64 propertyLength = readVariableByteInteger(&m_missingData); + if (propertyLength < 0) // Connection closed inside readVariableByteInteger + return; m_missingData -= propertyLength; properties.serverData->valid = true; @@ -1047,6 +1053,8 @@ void QMqttConnection::readMessageStatusProperties(QMqttMessageStatusProperties &properties) { qint64 propertyLength = readVariableByteInteger(&m_missingData); + if (propertyLength < 0) // Connection closed inside readVariableByteInteger + return; m_missingData -= propertyLength; while (propertyLength > 0) { @@ -1073,6 +1081,8 @@ void QMqttConnection::readPublishProperties(QMqttPublishProperties &properties) { qint64 propertyLength = readVariableByteInteger(&m_missingData); + if (propertyLength < 0) // Connection closed inside readVariableByteInteger + return; m_missingData -= propertyLength; QMqttUserProperties userProperties; @@ -1140,6 +1150,8 @@ void QMqttConnection::readSubscriptionProperties(QMqttSubscription *sub) { qint64 propertyLength = readVariableByteInteger(&m_missingData); + if (propertyLength < 0) // Connection closed inside readVariableByteInteger + return; m_missingData -= propertyLength; while (propertyLength > 0) { @@ -1390,7 +1402,8 @@ // 3.3.2.3.8 Subscription Identifier if (properties.availableProperties() & QMqttPublishProperties::SubscriptionIdentifier) { - for (auto id : properties.subscriptionIdentifiers()) { + const auto subscriptionIds = properties.subscriptionIdentifiers(); + for (auto id : subscriptionIds) { qCDebug(lcMqttConnectionVerbose) << "Publish Properties: Subscription ID:" << id; packet.append(char(0x0b)); packet.appendRawVariableInteger(id); @@ -1556,8 +1569,20 @@ // MQTT 5.0 has variable part != 2 in the header if (m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0) { readConnackProperties(m_clientPrivate->m_serverConnectionProperties); - m_receiveAliases.resize(m_clientPrivate->m_serverConnectionProperties.maximumTopicAlias()); - m_publishAliases.resize(m_clientPrivate->m_connectionProperties.maximumTopicAlias()); + // readConnackProperties() might have closed the connection due to a malformed + // property length. Do not overwrite the state of a connection which is gone. + if (m_internalState == BrokerDisconnected) + return; + // MQTT 5.0, 3.1.2.11.5 and 3.2.2.3.8: the two Topic Alias Maximum values are + // directional. The value this client sent in CONNECT bounds the aliases the + // server may use towards us (m_receiveAliases); the value the server sent in + // CONNACK bounds the aliases we may use towards it (m_publishAliases). + // Mappings live only for the lifetime of a network connection, so discard any + // left over from a previous one instead of resizing in place. + m_receiveAliases.clear(); + m_receiveAliases.resize(m_clientPrivate->m_connectionProperties.maximumTopicAlias()); + m_publishAliases.clear(); + m_publishAliases.resize(m_clientPrivate->m_serverConnectionProperties.maximumTopicAlias()); // 3.2.2.2 switch (QMqtt::ReasonCode(connectResultValue)) { @@ -1741,7 +1766,9 @@ if (publishProperties.availableProperties() & QMqttPublishProperties::TopicAlias) { const quint16 topicAlias = publishProperties.topicAlias(); - if (topicAlias == 0 || topicAlias > m_clientPrivate->m_connectionProperties.maximumTopicAlias()) { + // m_receiveAliases is sized from the maximum this client declared in CONNECT, + // so bounds-check against the list itself rather than against a property value. + if (topicAlias == 0 || topicAlias > m_receiveAliases.size()) { qCDebug(lcMqttConnection) << "TopicAlias receive: overflow."; closeConnection(QMqttClient::ProtocolViolation); return; @@ -1837,8 +1864,8 @@ return; } } - - readMessageStatusProperties(properties); + if (m_missingData > 0) + readMessageStatusProperties(properties); } if ((m_currentPacket & 0xF0) == QMqttControlPacket::PUBREL) { @@ -1885,7 +1912,10 @@ closeConnection(QMqttClient::ProtocolViolation); return; } - m_pingTimeout--; + + // Unexpected PINGRESP from the network should not push the counter below zero. + if (m_pingTimeout > 0) + m_pingTimeout--; emit m_clientPrivate->m_client->pingResponseReceived(); } @@ -1895,6 +1925,9 @@ if ((m_readBuffer.size() - m_readPosition) < m_missingData) return false; + // MQTT-2.1.4 The remaining length in the fixed header says where the message ends. + const qint64 packetEnd = qint64(m_readPosition) + m_missingData; + switch (m_currentPacket & 0xF0) { case QMqttControlPacket::AUTH: finalize_auth(); @@ -1929,9 +1962,16 @@ if (m_internalState == BrokerDisconnected) return false; - Q_ASSERT(m_missingData == 0); + // Report bytes that no handler read. + if (m_readPosition != packetEnd) { + qCDebug(lcMqttConnection) << "Packet type" << (m_currentPacket & 0xF0) + << "left" << (packetEnd - m_readPosition) + << "bytes unread; resynchronizing."; + } - m_readBuffer = m_readBuffer.mid(m_readPosition); + m_missingData = 0; + // A handler may stop reading early, so continue at the end of the message. + m_readBuffer = m_readBuffer.mid(packetEnd); m_readPosition = 0; } @@ -1969,6 +2009,8 @@ } qint32 payloadSize = readVariableByteInteger(); + if (payloadSize < 0) // Connection closed inside readVariableByteInteger + return false; if (m_clientPrivate->m_protocolVersion != QMqttClient::MQTT_5_0) { if (payloadSize != 2) { qCDebug(lcMqttConnection) << "Unexpected FRAME size in CONNACK."; @@ -1981,7 +2023,30 @@ } case QMqttControlPacket::SUBACK: { qCDebug(lcMqttConnectionVerbose) << "Received SUBACK"; - const quint8 remaining = readBufferTyped<quint8>(); + // We cannot use quint8 here, because quint8 is one byte while MQTT 3.1.1 section + // 3.9.1 says the remaining length of a SUBACK is "the length of variable header + // (2 bytes) plus the length of the payload". The payload carries one reason code + // per topic filter, so 126 filters already exceed one byte. MQTT 5.0 section + // 3.9.1 adds properties on top, which exceed it on their own. + const qint32 remaining = readVariableByteInteger(); + if (remaining < 0) + return false; // Connection closed inside readVariableByteInteger + // The remaining length counts the bytes that follow the fixed header, so the + // smallest value a SUBACK may declare is the size of its shortest legal body: + // 2 bytes packet identifier MQTT-3.9.2 + // + 1 byte property length, MQTT 5.0 only, mandatory + // even when there are no properties MQTT-3.9.2.1 + // + 1 byte reason code, at least one is required MQTT-3.9.3 + // = 3 for MQTT 3.1.1, 4 for MQTT 5.0 + // A smaller value leaves the reason code loop in finalize_suback() with + // nothing to read. + const qint32 minimumRemainingLength = + m_clientPrivate->m_protocolVersion == QMqttClient::MQTT_5_0 ? 4 : 3; + if (remaining < minimumRemainingLength) { + qCDebug(lcMqttConnection) << "Received SUBACK with invalid remaining length."; + closeConnection(QMqttClient::ProtocolViolation); + return false; + } m_missingData = remaining; break; } @@ -2009,18 +2074,37 @@ case QMqttControlPacket::PUBREL: { qCDebug(lcMqttConnectionVerbose) << "Received PUBREL"; - const quint8 remaining = readBufferTyped<quint8>(); - if (remaining != 0x02) { - qCDebug(lcMqttConnection) << "Received 2 byte message with invalid remaining length."; + // We cannot use quint8 here, because quint8 is one byte while the remaining + // length is a variable byte integer of up to four, MQTT 5.0 section 2.1.4. In + // MQTT 3.1.1 a PUBREL carried nothing but the packet identifier, so the value + // was always 2 (section 3.6.1) and one byte was always enough. MQTT 5.0 changed + // that by adding an optional reason code and properties, section 3.6.2. + const qint32 remaining = readVariableByteInteger(); + if (remaining < 0) + return false; // Connection closed inside readVariableByteInteger + if (m_clientPrivate->m_protocolVersion != QMqttClient::MQTT_5_0) { + // MQTT-3.6.1 Always exactly the packet identifier. + if (remaining != 0x02) { + qCDebug(lcMqttConnection) << "Received 2 byte message with invalid remaining length."; + closeConnection(QMqttClient::ProtocolViolation); + return false; + } + } else if (remaining < 2) { + // MQTT-3.6.2.1 The reason code and the property length may both be omitted + // when the reason code would be Success and there are no properties, and + // only then is the value 2. A longer PUBREL is legal and must be accepted; + // rejecting it broke interoperability with compliant MQTT 5.0 brokers. + qCDebug(lcMqttConnection) << "Received PUBREL with invalid remaining length."; closeConnection(QMqttClient::ProtocolViolation); return false; } + // MQTT-3.6.1-1 The fixed header flags of a PUBREL are reserved and must be 0010. if ((m_currentPacket & 0x0F) != 0x02) { qCDebug(lcMqttConnection) << "Malformed fixed header for PUBREL."; closeConnection(QMqttClient::ProtocolViolation); return false; } - m_missingData = 2; + m_missingData = remaining; break; } @@ -2034,11 +2118,43 @@ closeConnection(QMqttClient::ProtocolViolation); return false; } - 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 false; + // We cannot use quint8 here, because quint8 is one byte while the remaining + // length is a variable byte integer of up to four, MQTT 5.0 section 2.1.4. In + // MQTT 3.1.1 these packets carried nothing but the packet identifier, so the + // value was always 2 (sections 3.4.1, 3.5.1, 3.7.1 and 3.11.1) and one byte was + // always enough. MQTT 5.0 changed that by adding a reason code and properties, + // and a reason string or user properties push the value past 127. + const qint32 remaining = readVariableByteInteger(); + if (remaining < 0) + return false; // Connection closed inside readVariableByteInteger + if (m_clientPrivate->m_protocolVersion != QMqttClient::MQTT_5_0) { + // MQTT-3.4.1, MQTT-3.5.1, MQTT-3.7.1, MQTT-3.11.1 Always exactly the packet + // identifier. + if (remaining != 0x02) { + qCDebug(lcMqttConnection) << "Received 2 byte message with invalid remaining length."; + closeConnection(QMqttClient::ProtocolViolation); + return false; + } + } else { + // The smallest remaining length the specification permits, i.e. the size of + // the shortest legal body: + // PUBACK/PUBREC/PUBCOMP: 2 bytes packet identifier. The reason code and + // property length may both be omitted when the reason code would be + // Success and there are no properties. MQTT-3.4.2.1 + // UNSUBACK: 2 bytes packet identifier + // + 1 byte property length, mandatory even when + // there are no properties MQTT-3.11.2.1 + // + 1 byte reason code, at least one is required MQTT-3.11.3 + // = 4 bytes. Anything shorter leaves the reason code loop in + // finalize_unsuback() with nothing to read. + const qint32 minimumRemainingLength = + (m_currentPacket & 0xF0) == QMqttControlPacket::UNSUBACK ? 4 : 2; + if (remaining < minimumRemainingLength) { + qCDebug(lcMqttConnection) << "Received UNSUBACK/PUBACK/PUBREC/PUBCOMP with " + "invalid remaining length."; + closeConnection(QMqttClient::ProtocolViolation); + return false; + } } m_missingData = remaining; break; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtmqtt-everywhere-src-6.11.2/tests/auto/qmqttclient/tst_qmqttclient.cpp new/qtmqtt-everywhere-src-6.12.0/tests/auto/qmqttclient/tst_qmqttclient.cpp --- old/qtmqtt-everywhere-src-6.11.2/tests/auto/qmqttclient/tst_qmqttclient.cpp 2026-08-12 05:39:57.000000000 +0200 +++ new/qtmqtt-everywhere-src-6.12.0/tests/auto/qmqttclient/tst_qmqttclient.cpp 2026-09-27 12:11:48.000000000 +0200 @@ -41,6 +41,11 @@ void reconnect_QTBUG65726(); void openIODevice_QTBUG66955_data(); void openIODevice_QTBUG66955(); + void subackLongRemainingLength(); + void pubackLongRemainingLength(); + void pubrelWithReasonCode(); + void shortRemainingLength_data(); + void shortRemainingLength(); void staticProperties_QTBUG_67176_data(); void staticProperties_QTBUG_67176(); void authentication(); @@ -174,7 +179,7 @@ bool verified = false; auto sub = subscriber.subscribe(testTopic, 1); QVERIFY(sub); - connect(sub, &QMqttSubscription::messageReceived, [&](QMqttMessage msg) { + connect(sub, &QMqttSubscription::messageReceived, sub, [&](QMqttMessage msg) { verified = msg.payload() == data; received = true; }); @@ -227,7 +232,8 @@ sub.setClientId(QLatin1String("SubA")); sub.setHostname(m_testBroker); sub.setPort(m_port); - connect(&sub, &QMqttClient::messageReceived, [&msgCount, testMessage](const QByteArray &msg) { + connect(&sub, &QMqttClient::messageReceived, &sub, + [&msgCount, testMessage](const QByteArray &msg) { if (msg == testMessage) msgCount++; }); @@ -275,7 +281,8 @@ QSKIP("This test is only for tcp"); auto client1Sub = client1.subscribe(willTopic, 1); - connect(client1Sub, &QMqttSubscription::messageReceived, [=](QMqttMessage message) { + connect(client1Sub, &QMqttSubscription::messageReceived, + client1Sub, [=](QMqttMessage message) { Q_UNUSED(message); // Just debug purposes //qDebug() << "Got something:" << message; @@ -363,7 +370,8 @@ bool received = false; bool verified = false; - connect(&subscriber, &QMqttClient::messageReceived, [&](const QByteArray &, const QMqttTopicName &t) { + connect(&subscriber, &QMqttClient::messageReceived, + &subscriber, [&](const QByteArray &, const QMqttTopicName &t) { received = true; verified = t == truncTopic; }); @@ -421,7 +429,8 @@ const QString testTopic(QLatin1String("Qt/QMqttClient/some/topic")); auto sub = client.subscribe(testTopic, 1); QVERIFY(sub); - connect(sub, &QMqttSubscription::messageReceived, [&](QMqttMessage msg) { + connect(sub, &QMqttSubscription::messageReceived, + sub, [&](QMqttMessage msg) { verified = msg.payload() == data; correctSize = msg.payload().size() == dataSize; received = true; @@ -465,14 +474,25 @@ connect(server, &QTcpServer::newConnection, this, &FakeServer::createSocket); server->listen(QHostAddress::Any, 5726); } + // Sends raw bytes to the client, so a test can drive the parser with an exact + // byte sequence. + void send(const QByteArray &data) { socket->write(data); } public slots: void createSocket() { socket = server->nextPendingConnection(); + connectionAcknowledged = false; + lastRequest.clear(); connect(socket, &QTcpSocket::readyRead, this, &FakeServer::connectionRequested); } void connectionRequested() { - // We assume it is always a connect statement, so no verification is done + // We assume the first packet is always a connect statement, so no verification + // is done. Everything after that is recorded for the test to answer itself. + if (connectionAcknowledged) { + lastRequest.append(socket->readAll()); + return; + } + connectionAcknowledged = true; socket->readAll(); QByteArray response; response += 0x20; @@ -496,6 +516,8 @@ QTcpSocket *socket; QMqttClient::ProtocolVersion version{QMqttClient::MQTT_3_1_1}; bool connectionSuccess{false}; + bool connectionAcknowledged{false}; + QByteArray lastRequest; }; DefaultVersionTestData(Tst_QMqttClient::reconnect_QTBUG65726_data) @@ -566,6 +588,220 @@ QTRY_COMPARE(trans.written, 1); } +// A reason string property (0x1f) of the requested content length. +static QByteArray reasonStringProperty(quint16 length) +{ + QByteArray property; + property.append(char(0x1f)); + property.append(char(length >> 8)); + property.append(char(length & 0xff)); + property.append(QByteArray(length, 'r')); + return property; +} + +// The packet identifier of a packet the client wrote, skipping the packet type and +// its variable byte remaining length. +static quint16 packetIdentifier(const QByteArray &packet) +{ + qsizetype pos = 1; + while (pos < packet.size() && (packet.at(pos) & 128) != 0) + ++pos; + Q_ASSERT(pos < packet.size() - 2); + ++pos; + return quint16(quint8(packet.at(pos)) << 8 | quint8(packet.at(pos + 1))); +} + +static void appendPacketIdentifier(QByteArray *payload, quint16 id) +{ + payload->append(char(id >> 8)); + payload->append(char(id & 0xff)); +} + +// MQTT-2.2.3 (3.1.1) / MQTT-2.1.4 (5.0) The remaining length is a variable byte +// integer, so a packet longer than 127 bytes carries a two byte length whose first +// byte has the continuation bit set. Reading only that first byte consumes one byte +// too few and leaves the rest of the payload to be parsed as the next packet's +// fixed header. +void Tst_QMqttClient::subackLongRemainingLength() +{ + FakeServer server; + server.version = QMqttClient::MQTT_5_0; + server.connectionSuccess = true; + + VersionClient(QMqttClient::MQTT_5_0, client); + if (!client.isTcp()) + QSKIP("This test is only for tcp"); + client.setHostname(QLatin1String("localhost")); + client.setPort(5726); + client.connectToHost(); + QTRY_COMPARE(client.state(), QMqttClient::Connected); + + auto sub = client.subscribe(QMqttTopicFilter(QLatin1String("a/b")), 1); + QVERIFY(sub); + QTRY_VERIFY(server.lastRequest.size() >= 4); // The SUBSCRIBE arrived + + // SUBACK with a reason string, which pushes the remaining length past 127. + QByteArray payload; + appendPacketIdentifier(&payload, packetIdentifier(server.lastRequest)); + const QByteArray properties = reasonStringProperty(123); + payload.append(char(properties.size())); // Property length, 126 + payload.append(properties); + payload.append(char(0x01)); // Reason code: maximum QoS 1 + QCOMPARE(payload.size(), 130); + + QByteArray packet; + packet.append(char(0x90)); + packet.append(char(0x82)); // Remaining length 130, low seven bits plus continuation + packet.append(char(0x01)); // Remaining length 130, remaining bits + packet.append(payload); + server.send(packet); + + QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed); + QCOMPARE(client.state(), QMqttClient::Connected); + QCOMPARE(client.error(), QMqttClient::NoError); +} + +void Tst_QMqttClient::pubackLongRemainingLength() +{ + FakeServer server; + server.version = QMqttClient::MQTT_5_0; + server.connectionSuccess = true; + + VersionClient(QMqttClient::MQTT_5_0, client); + if (!client.isTcp()) + QSKIP("This test is only for tcp"); + client.setHostname(QLatin1String("localhost")); + client.setPort(5726); + client.connectToHost(); + QTRY_COMPARE(client.state(), QMqttClient::Connected); + + const qint32 id = client.publish(QMqttTopicName(QLatin1String("a/b")), QByteArray("x"), 1); + QVERIFY(id > 0); + QTRY_VERIFY(server.lastRequest.size() >= 4); // The PUBLISH arrived + + QSignalSpy sentSpy(&client, &QMqttClient::messageSent); + + // PUBACK with a reason string, which pushes the remaining length past 127. + QByteArray payload; + appendPacketIdentifier(&payload, quint16(id)); + payload.append(char(0x00)); // Reason code: Success + const QByteArray properties = reasonStringProperty(123); + payload.append(char(properties.size())); // Property length, 126 + payload.append(properties); + QCOMPARE(payload.size(), 130); + + QByteArray packet; + packet.append(char(0x40)); + packet.append(char(0x82)); // Remaining length 130, low seven bits plus continuation + packet.append(char(0x01)); // Remaining length 130, remaining bits + packet.append(payload); + server.send(packet); + + QTRY_COMPARE(sentSpy.count(), 1); + QCOMPARE(sentSpy.at(0).at(0).toInt(), id); + QCOMPARE(client.state(), QMqttClient::Connected); + QCOMPARE(client.error(), QMqttClient::NoError); +} + +// MQTT-3.6.2, MQTT-3.6.2.1 In MQTT 5.0 the PUBREL variable header holds a reason +// code and properties in addition to the packet identifier. A remaining length of 2 +// only occurs when both are omitted, so a longer PUBREL must be accepted. +void Tst_QMqttClient::pubrelWithReasonCode() +{ + FakeServer server; + server.version = QMqttClient::MQTT_5_0; + server.connectionSuccess = true; + + VersionClient(QMqttClient::MQTT_5_0, client); + if (!client.isTcp()) + QSKIP("This test is only for tcp"); + client.setHostname(QLatin1String("localhost")); + client.setPort(5726); + client.connectToHost(); + QTRY_COMPARE(client.state(), QMqttClient::Connected); + + QMqtt::MessageStatus status = QMqtt::MessageStatus::Unknown; + QString reasonString; + connect(&client, &QMqttClient::messageStatusChanged, &client, + [&](qint32, QMqtt::MessageStatus s, const QMqttMessageStatusProperties &properties) + { + status = s; + reasonString = properties.reason(); + }); + + // PUBLISH, QoS 2, topic "a/b", packet identifier 7, no properties, payload "x". + // The client answers with a PUBREC, which puts it in the middle of the QoS 2 + // exchange and makes it expect a PUBREL. + server.send(QByteArray::fromHex("34090003612f6200070078")); + QTRY_VERIFY(server.lastRequest.size() >= 4); // The PUBREC arrived + + // PUBREL, packet identifier 7, reason code Success, and a reason string. Remaining + // length 12: 2 identifier + 1 reason code + 1 property length + 8 property bytes. + server.send(QByteArray::fromHex("620c000700081f000568656c6c6f")); + + QTRY_COMPARE(status, QMqtt::MessageStatus::Released); + QCOMPARE(reasonString, QLatin1String("hello")); + QCOMPARE(client.state(), QMqttClient::Connected); + QCOMPARE(client.error(), QMqttClient::NoError); +} + +// The remaining length counts the bytes that follow the fixed header, so a packet +// declaring less than the fields the specification requires is malformed. Accepting +// one leaves the parser reading past the end of the packet: the reason code loops in +// finalize_suback() and finalize_unsuback() read at least once regardless of what is +// left, and finalize_pubAckRecRelComp() always reads a packet identifier. Each of +// these packets must be rejected as a protocol violation instead. +void Tst_QMqttClient::shortRemainingLength_data() +{ + QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion"); + QTest::addColumn<QByteArray>("packet"); + + // SUBACK, packet identifier 7, and nothing else. MQTT-3.9.3 requires at least one + // reason code, so the shortest legal remaining length is 3. + QTest::newRow("SUBACK 3.1.1, no reason code") + << QMqttClient::MQTT_3_1_1 << QByteArray::fromHex("90020007"); + // The same, plus an empty property block. MQTT-3.9.2.1 makes the property length + // mandatory in MQTT 5.0, so the shortest legal remaining length is 4. + QTest::newRow("SUBACK 5.0, no reason code") + << QMqttClient::MQTT_5_0 << QByteArray::fromHex("9003000700"); + // UNSUBACK, packet identifier 7, empty property block, no reason code. The + // property length is mandatory (MQTT-3.11.2.1) and at least one reason code is + // required (MQTT-3.11.3), so the shortest legal remaining length is 4. + QTest::newRow("UNSUBACK 5.0, no reason code") + << QMqttClient::MQTT_5_0 << QByteArray::fromHex("b003000700"); + // PUBACK with half a packet identifier. The reason code and the property length may + // both be omitted (MQTT-3.4.2.1), but the identifier may not, so the shortest legal + // remaining length is 2. + QTest::newRow("PUBACK 5.0, truncated packet identifier") + << QMqttClient::MQTT_5_0 << QByteArray::fromHex("400100"); + // The same for PUBREL, MQTT-3.6.2.1. + QTest::newRow("PUBREL 5.0, truncated packet identifier") + << QMqttClient::MQTT_5_0 << QByteArray::fromHex("620100"); +} + +void Tst_QMqttClient::shortRemainingLength() +{ + QFETCH(QMqttClient::ProtocolVersion, mqttVersion); + QFETCH(QByteArray, packet); + + FakeServer server; + server.version = mqttVersion; + server.connectionSuccess = true; + + VersionClient(mqttVersion, client); + if (!client.isTcp()) + QSKIP("This test is only for tcp"); + client.setHostname(QLatin1String("localhost")); + client.setPort(5726); + client.connectToHost(); + QTRY_COMPARE(client.state(), QMqttClient::Connected); + + server.send(packet); + + QTRY_COMPARE(client.state(), QMqttClient::Disconnected); + QCOMPARE(client.error(), QMqttClient::ProtocolViolation); +} + DefaultVersionTestData(Tst_QMqttClient::staticProperties_QTBUG_67176_data) void Tst_QMqttClient::staticProperties_QTBUG_67176() @@ -621,12 +857,14 @@ connectionProperties.setAuthenticationMethod(QLatin1String("SCRAM-SHA-1")); client.setConnectionProperties(connectionProperties); - connect(&client, &QMqttClient::authenticationRequested, [](const QMqttAuthenticationProperties &prop) + connect(&client, &QMqttClient::authenticationRequested, &client, + [](const QMqttAuthenticationProperties &prop) { qDebug() << "Authentication requested:" << prop.authenticationMethod(); }); - connect(&client, &QMqttClient::authenticationFinished, [](const QMqttAuthenticationProperties &prop) + connect(&client, &QMqttClient::authenticationFinished, &client, + [](const QMqttAuthenticationProperties &prop) { qDebug() << "Authentication finished:" << prop.authenticationMethod(); }); @@ -666,7 +904,8 @@ const QString topic = QLatin1String("Qt/client/statusCheck"); - connect(&client, &QMqttClient::messageStatusChanged, [&expectedStatus](qint32, + connect(&client, &QMqttClient::messageStatusChanged, + &client, [&expectedStatus](qint32, QMqtt::MessageStatus s, const QMqttMessageStatusProperties &) { @@ -715,9 +954,8 @@ 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 &) + connect(&subscriber, &QMqttClient::messageStatusChanged, &subscriber, + [&expectedStatus](qint32, QMqtt::MessageStatus s, const QMqttMessageStatusProperties &) { QCOMPARE(s, expectedStatus.first()); expectedStatus.takeFirst(); @@ -767,7 +1005,8 @@ QTRY_VERIFY2(subA->state() == QMqttSubscription::Subscribed, "Could not subscibe A."); int receiveACounter = 0; - connect(subA, &QMqttSubscription::messageReceived, [&receiveACounter](QMqttMessage msg) { + connect(subA, &QMqttSubscription::messageReceived, subA, + [&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); @@ -789,7 +1028,8 @@ QTRY_VERIFY2(subB->state() == QMqttSubscription::Subscribed, "Could not subscibe A."); int receiveBCounter = 2; - connect(subB, &QMqttSubscription::messageReceived, [&receiveBCounter](QMqttMessage msg) { + connect(subB, &QMqttSubscription::messageReceived, + subB, [&receiveBCounter](QMqttMessage msg) { qDebug() << "Sub B received:" << msg.publishProperties().subscriptionIdentifiers(); QVERIFY(msg.publishProperties().subscriptionIdentifiers().size() > 0); receiveBCounter -= msg.publishProperties().subscriptionIdentifiers().size(); @@ -801,7 +1041,8 @@ QTRY_VERIFY2(subB2->state() == QMqttSubscription::Subscribed, "Could not subscibe A."); int receiveB2Counter = 2; - connect(subB2, &QMqttSubscription::messageReceived, [&receiveB2Counter](QMqttMessage msg) { + connect(subB2, &QMqttSubscription::messageReceived, + subB2, [&receiveB2Counter](QMqttMessage msg) { qDebug() << "Sub B2 received:" << msg.publishProperties().subscriptionIdentifiers(); QVERIFY(msg.publishProperties().subscriptionIdentifiers().size() > 0); receiveB2Counter -= msg.publishProperties().subscriptionIdentifiers().size(); @@ -875,11 +1116,11 @@ QTimer t; t.setInterval(keepAlive * 1000); t.setSingleShot(false); - t.connect(&t, &QTimer::timeout, [&client]() { + t.connect(&t, &QTimer::timeout, &client, [&client]() { client.requestPing(); }); - connect(&client, &QMqttClient::connected, [&t]() { + connect(&client, &QMqttClient::connected, &t, [&t]() { t.start(); });
