From b275411ada5183335b6ea9ad45f42ef333a0eef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Korina=20=C5=A0imi=C4=8Devi=C4=87?= Date: Wed, 13 Dec 2023 15:13:07 +0100 Subject: [PATCH] Validate subscribe requests Summary: resolves T13305 Reviewers: ivica Reviewed By: ivica Subscribers: miljen, iljazovic Maniphest Tasks: T13305 Differential Revision: https://repo.mireo.local/D26954 --- doc/qbk/reference/Error_handling.qbk | 20 ++- .../async_mqtt5/detail/topic_validation.hpp | 104 ++++++++++++++ include/async_mqtt5/detail/utf8_mqtt.hpp | 101 +++++-------- include/async_mqtt5/error.hpp | 20 ++- include/async_mqtt5/impl/client_service.hpp | 12 +- .../async_mqtt5/impl/codecs/base_encoders.hpp | 2 +- include/async_mqtt5/impl/publish_send_op.hpp | 6 +- include/async_mqtt5/impl/subscribe_op.hpp | 80 ++++++++++- include/async_mqtt5/impl/unsubscribe_op.hpp | 4 +- include/async_mqtt5/mqtt_client.hpp | 6 + .../unit/include/test_common/test_service.hpp | 22 +++ test/unit/test/serialization.cpp | 8 +- test/unit/test/string_validation.cpp | 128 ++++++++++------- test/unit/test/subscribe_op.cpp | 135 +++++++++++++++++- 14 files changed, 507 insertions(+), 141 deletions(-) create mode 100644 include/async_mqtt5/detail/topic_validation.hpp diff --git a/doc/qbk/reference/Error_handling.qbk b/doc/qbk/reference/Error_handling.qbk index daf1b28..adb955b 100644 --- a/doc/qbk/reference/Error_handling.qbk +++ b/doc/qbk/reference/Error_handling.qbk @@ -22,11 +22,11 @@ may complete with, along with the reasons for their occurrence. ]] [[`boost::asio::no_recovery`] [ An non-recoverable error occurred during the attempt by the [reflink2 mqtt_client `mqtt_client`] - to establish a connection with the Broker. The cause of this error may be attributed to the connection + to establish a connection with the Server. The cause of this error may be attributed to the connection related parameters used during the initialization of the [reflink2 mqtt_client `mqtt_client`]. ]] [[`async_mqtt5::client::error::session_expired`][ - The Client has established a successful connection with a Broker, but either the session does not exist or has expired. + The Client has established a successful connection with a Server, but either the session does not exist or has expired. In cases where the Client had previously set up subscriptions to Topics, these subscriptions are also expired. Therefore, the Client should re-subscribe. This error code is exclusive to completion handlers associated with [refmem mqtt_client async_receive] calls. @@ -62,6 +62,22 @@ may complete with, along with the reasons for their occurrence. Client has attempted to use them. See __TOPIC_ALIAS_MAX__. This error code is exclusive to completion handlers associated with [refmem mqtt_client async_publish] calls. ]] + [[`async_mqtt5::client::error::wildcard_subscription_not_available`] [ + The Client has attempted to subscribe to multiple Topics using Wildcard Character (`+` and/or `#`). + However, the Server does not support Wildcard Subscriptions. + This error code is exclusive to completion handlers associated with [refmem mqtt_client async_subscribe] calls. + ]] + [[`async_mqtt5::client::error::subscription_identifier_not_available`] [ + The Client has attempted to associate a subscription with a Subscription Identifier. + However, the Server either does not support Subscription Identifiers or the Subscription Identifier provided + is out of range (the Subscription Identifier can have a value of 1 to 268,435,455). + This error code is exclusive to completion handlers associated with [refmem mqtt_client async_subscribe] calls. + ]] + [[`async_mqtt5::client::error::shared_subscription_not_available`] [ + The Client has attempted to establish a Shared Subscription. + However, the Server does not support Shared Subscriptions. + This error code is exclusive to completion handlers associated with [refmem mqtt_client async_subscribe] calls. + ]] ] [endsect] diff --git a/include/async_mqtt5/detail/topic_validation.hpp b/include/async_mqtt5/detail/topic_validation.hpp new file mode 100644 index 0000000..892b504 --- /dev/null +++ b/include/async_mqtt5/detail/topic_validation.hpp @@ -0,0 +1,104 @@ +#ifndef ASYNC_MQTT5_TOPIC_VALIDATION_HPP +#define ASYNC_MQTT5_TOPIC_VALIDATION_HPP + +#include + +#include + +namespace async_mqtt5::detail { + +inline bool is_utf8_no_wildcard(validation_result result) { + return result == validation_result::valid; +} + +inline bool is_not_empty(size_t sz) { + return sz != 0; +} + +inline bool is_valid_topic_size(size_t sz) { + return is_not_empty(sz) && is_valid_string_size(sz); +} + +inline validation_result validate_topic_name(std::string_view str) { + return validate_impl(str, is_valid_topic_size, is_utf8_no_wildcard); +} + +inline validation_result validate_share_name(std::string_view str) { + return validate_impl(str, is_not_empty, is_utf8_no_wildcard); +} + +inline validation_result validate_topic_filter(std::string_view str) { + if (!is_valid_topic_size(str.size())) + return validation_result::invalid; + + constexpr int multi_lvl_wildcard = '#'; + constexpr int single_lvl_wildcard = '+'; + + // must be the last character preceded by '/' or stand alone + // #, .../# + if (str.back() == multi_lvl_wildcard) { + str.remove_suffix(1); + + if (!str.empty() && str.back() != '/') + return validation_result::invalid; + } + + int last_c = -1; + validation_result result; + while (!str.empty()) { + int c = pop_front_unichar(str); + + // can be used at any level, but must occupy an entire level + // +, +/..., .../+/..., .../+ + bool is_valid_single_lvl = (c == single_lvl_wildcard) && + (str.empty() || str.front() == '/') && + (last_c == -1 || last_c == '/'); + + result = validate_mqtt_utf8_char(c); + if ( + result == validation_result::valid || + is_valid_single_lvl + ) { + last_c = c; + continue; + } + + return validation_result::invalid; + } + + return validation_result::valid; +} + +inline validation_result validate_shared_topic_filter( + std::string_view str, bool wildcard_allowed = true +) { + constexpr std::string_view shared_sub_id = "$share/"; + + if (!is_valid_topic_size(str.size())) + return validation_result::invalid; + + if (str.compare(0, shared_sub_id.size(), shared_sub_id) != 0) + return validation_result::invalid; + + str.remove_prefix(shared_sub_id.size()); + + size_t share_name_end = str.find_first_of('/'); + if (share_name_end == std::string::npos) + return validation_result::invalid; + + validation_result result; + result = validate_share_name(str.substr(0, share_name_end)); + + if (result != validation_result::valid) + return validation_result::invalid; + + auto topic_filter = str.substr(share_name_end + 1); + return wildcard_allowed ? + validate_topic_filter(topic_filter) : + validate_topic_name(topic_filter) + ; +} + +} // end namespace async_mqtt5::detail + +#endif //ASYNC_MQTT5_TOPIC_VALIDATION_HPP diff --git a/include/async_mqtt5/detail/utf8_mqtt.hpp b/include/async_mqtt5/detail/utf8_mqtt.hpp index 05efeb6..eeaf72e 100644 --- a/include/async_mqtt5/detail/utf8_mqtt.hpp +++ b/include/async_mqtt5/detail/utf8_mqtt.hpp @@ -5,6 +5,12 @@ namespace async_mqtt5::detail { +enum class validation_result : uint8_t { + valid = 0, + has_wildcard_character, + invalid +}; + inline int pop_front_unichar(std::string_view& s) { // assuming that s.length() is > 0 @@ -32,20 +38,26 @@ inline int pop_front_unichar(std::string_view& s) { return ch; } -inline bool is_valid_mqtt_utf8_char(int c) { +inline validation_result validate_mqtt_utf8_char(int c) { constexpr int fe_flag = 0xFE; constexpr int ff_flag = 0xFF; - return c > 0x001F && // U+0000...U+001F control characters + constexpr int multi_lvl_wildcard = '#'; + constexpr int single_lvl_wildcard = '+'; + + if (c == multi_lvl_wildcard || c == single_lvl_wildcard) + return validation_result::has_wildcard_character; + + if (c > 0x001F && // U+0000...U+001F control characters (c < 0x007F || c > 0x009F) && // U+007F...0+009F control characters (c < 0xD800 || c > 0xDFFF) && // U+D800...U+DFFF surrogates (c < 0xFDD0 || c > 0xFDEF) && // U+FDD0...U+FDEF non-characters (c & fe_flag) != fe_flag && // non-characters - (c & ff_flag) != ff_flag; -} + (c & ff_flag) != ff_flag + ) + return validation_result::valid; -inline bool is_valid_mqtt_utf8_non_wildcard_char(int c) { - return c != '+' && c != '#' && is_valid_mqtt_utf8_char(c); + return validation_result::invalid; } inline bool is_valid_string_size(size_t sz) { @@ -53,74 +65,33 @@ inline bool is_valid_string_size(size_t sz) { return sz <= max_sz; } -inline bool is_valid_topic_size(size_t sz) { - constexpr size_t min_sz = 1; - return min_sz <= sz && is_valid_string_size(sz); +inline bool is_utf8(validation_result result) { + return result == validation_result::valid || + result == validation_result::has_wildcard_character; } -template -bool is_valid_impl( - std::string_view str, ValidationFun&& validation_fun +template +validation_result validate_impl( + std::string_view str, + ValidSizeCondition&& size_condition, ValidCondition&& condition ) { - while (!str.empty()) { - int c = pop_front_unichar(str); - bool is_valid = validation_fun(c); + if (!size_condition(str.size())) + return validation_result::invalid; - if (!is_valid) - return false; - } - - return true; -} - -inline bool is_valid_mqtt_utf8(std::string_view str) { - return is_valid_string_size(str.size()) && - is_valid_impl(str, is_valid_mqtt_utf8_char); -} - -inline bool is_valid_topic_name(std::string_view str) { - return is_valid_topic_size(str.size()) && - is_valid_impl(str, is_valid_mqtt_utf8_non_wildcard_char); -} - -inline bool is_valid_topic_filter(std::string_view str) { - if (!is_valid_topic_size(str.size())) - return false; - - constexpr int multi_lvl_wildcard = '#'; - constexpr int single_lvl_wildcard = '+'; - - // must be the last character preceded by '/' or stand alone - // #, .../# - if (str.back() == multi_lvl_wildcard) { - str.remove_suffix(1); - - if (!str.empty() && str.back() != '/') - return false; - } - - int last_c = -1; + validation_result result; while (!str.empty()) { int c = pop_front_unichar(str); - // can be used at any level, but must occupy an entire level - // +, +/..., .../+/..., .../+ - bool is_valid_single_lvl = (c == single_lvl_wildcard) && - (str.empty() || str.front() == '/') && - (last_c == -1 || last_c == '/'); - - bool is_valid_mqtt_utf_8 = is_valid_mqtt_utf8_non_wildcard_char(c); - - - if (is_valid_mqtt_utf_8 || is_valid_single_lvl) { - last_c = c; - continue; - } - - return false; + result = validate_mqtt_utf8_char(c); + if (!condition(result)) + return result; } - return true; + return validation_result::valid; +} + +inline validation_result is_valid_mqtt_utf8(std::string_view str) { + return validate_impl(str, is_valid_string_size, is_utf8); } } // namespace async_mqtt5::detail diff --git a/include/async_mqtt5/error.hpp b/include/async_mqtt5/error.hpp index aa94825..ad9fa6a 100644 --- a/include/async_mqtt5/error.hpp +++ b/include/async_mqtt5/error.hpp @@ -71,11 +71,21 @@ enum class error : int { /** The Server does not support the specified \ref qos_e. */ qos_not_supported, - /** The Server dos not support retained messages. */ + /** The Server does not support retained messages. */ retain_not_available, /** The Client attempted to send a Topic Alias that is greater than Topic Alias Maximum. */ - topic_alias_maximum_reached + topic_alias_maximum_reached, + + // subscribe + /** The Server does not support Wildcard Subscriptions. */ + wildcard_subscription_not_available, + + /** The Server does not support this Subscription Identifier. */ + subscription_identifier_not_available, + + /** The Server does not support Shared Subscriptions. */ + shared_subscription_not_available, }; @@ -97,6 +107,12 @@ inline std::string client_error_to_string(error err) { case error::topic_alias_maximum_reached: return "The Client attempted to send a Topic Alias " "that is greater than Topic Alias Maximum."; + case error::wildcard_subscription_not_available: + return "The Server does not support Wildcard Subscriptions."; + case error::subscription_identifier_not_available: + return "The Server does not support this Subscription Identifier."; + case error::shared_subscription_not_available: + return "The Server does not support Shared Subscriptions."; default: return "Unknown client error"; } diff --git a/include/async_mqtt5/impl/client_service.hpp b/include/async_mqtt5/impl/client_service.hpp index 6babd20..6bf90de 100644 --- a/include/async_mqtt5/impl/client_service.hpp +++ b/include/async_mqtt5/impl/client_service.hpp @@ -1,6 +1,8 @@ #ifndef ASYNC_MQTT5_CLIENT_SERVICE_HPP #define ASYNC_MQTT5_CLIENT_SERVICE_HPP +#include + #include #include @@ -62,14 +64,15 @@ public: template decltype(auto) connack_prop(Prop p) { std::shared_lock reader_lock(_mqtt_context.ca_mtx); - return _mqtt_context.ca_props[p]; + return std::as_const(_mqtt_context.ca_props[p]); } template decltype(auto) connack_props(Prop0 p0, Props ...props) { std::shared_lock reader_lock(_mqtt_context.ca_mtx); return std::make_tuple( - _mqtt_context.ca_props[p0], _mqtt_context.ca_props[props]... + std::as_const(_mqtt_context.ca_props[p0]), + std::as_const(_mqtt_context.ca_props[props])... ); } @@ -119,14 +122,15 @@ public: template decltype(auto) connack_prop(Prop p) { std::shared_lock reader_lock(_mqtt_context.ca_mtx); - return _mqtt_context.ca_props[p]; + return std::as_const(_mqtt_context.ca_props[p]); } template decltype(auto) connack_props(Prop0 p0, Props ...props) { std::shared_lock reader_lock(_mqtt_context.ca_mtx); return std::make_tuple( - _mqtt_context.ca_props[p0], _mqtt_context.ca_props[props]... + std::as_const(_mqtt_context.ca_props[p0]), + std::as_const(_mqtt_context.ca_props[props])... ); } diff --git a/include/async_mqtt5/impl/codecs/base_encoders.hpp b/include/async_mqtt5/impl/codecs/base_encoders.hpp index 6908bfe..27d167a 100644 --- a/include/async_mqtt5/impl/codecs/base_encoders.hpp +++ b/include/async_mqtt5/impl/codecs/base_encoders.hpp @@ -368,7 +368,7 @@ using encoder_types = std::tuple< prop_encoder_type, prop_encoder_type, prop_encoder_type, - prop_encoder_type>, + prop_encoder_type>, // varint prop_encoder_type>, prop_encoder_type, prop_encoder_type>, diff --git a/include/async_mqtt5/impl/publish_send_op.hpp b/include/async_mqtt5/impl/publish_send_op.hpp index 5063ca0..b8ec7e4 100644 --- a/include/async_mqtt5/impl/publish_send_op.hpp +++ b/include/async_mqtt5/impl/publish_send_op.hpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include @@ -343,7 +343,7 @@ private: error_code validate_publish( const std::string& topic, retain_e retain, const publish_props& props ) { - if (!is_valid_topic_name(topic)) + if (validate_topic_name(topic) != validation_result::valid) return client::error::invalid_topic; const auto& [max_qos, retain_avail, topic_alias_max] = @@ -368,7 +368,7 @@ private: if (topic_alias_max && topic_alias && *topic_alias > *topic_alias_max) return client::error::topic_alias_maximum_reached; - return {}; + return error_code {}; } void on_malformed_packet(const std::string& reason) { diff --git a/include/async_mqtt5/impl/subscribe_op.hpp b/include/async_mqtt5/impl/subscribe_op.hpp index da790af..ee190d9 100644 --- a/include/async_mqtt5/impl/subscribe_op.hpp +++ b/include/async_mqtt5/impl/subscribe_op.hpp @@ -2,6 +2,7 @@ #define ASYNC_MQTT5_SUBSCRIBE_OP_HPP #include +#include #include @@ -11,7 +12,7 @@ #include #include #include -#include +#include #include #include @@ -62,7 +63,7 @@ public: const std::vector& topics, const subscribe_props& props ) { - auto ec = validate_topics(topics); + auto ec = validate_subscribe(topics, props); if (ec) return complete_post(ec, topics.size()); @@ -148,15 +149,80 @@ public: private: - static error_code validate_topics( - const std::vector& topics + static bool is_option_available(std::optional sub_opt) { + return !sub_opt.has_value() || *sub_opt == 1; + } + + static error_code validate_props( + const subscribe_props& props, bool sub_id_available ) { - for (const auto& topic: topics) - if (!is_valid_topic_filter(topic.topic_filter)) - return client::error::invalid_topic; + auto sub_id = props[prop::subscription_identifier]; + if (!sub_id.has_value()) + return error_code {}; + + if (!sub_id_available) + return client::error::subscription_identifier_not_available; + + constexpr uint32_t min_sub_id = 1; + constexpr uint32_t max_sub_id = 268'435'455; + return min_sub_id <= *sub_id && *sub_id <= max_sub_id ? + error_code {} : + client::error::subscription_identifier_not_available; + } + + static error_code validate_topic( + const subscribe_topic& topic, bool wildcard_available, bool shared_available + ) { + std::string_view topic_filter = topic.topic_filter; + + constexpr std::string_view shared_sub_id = "$share/"; + validation_result result = validation_result::valid; + if ( + topic_filter.compare(0, shared_sub_id.size(), shared_sub_id) == 0 + ) { + if (!shared_available) + return client::error::shared_subscription_not_available; + + result = validate_shared_topic_filter(topic_filter, wildcard_available); + } else + result = wildcard_available ? + validate_topic_filter(topic_filter) : + validate_topic_name(topic_filter); + + if (result == validation_result::invalid) + return client::error::invalid_topic; + if (!wildcard_available && result != validation_result::valid) + return client::error::wildcard_subscription_not_available; return error_code {}; } + error_code validate_subscribe( + const std::vector& topics, + const subscribe_props& props + ) { + auto [wildcard_available, shared_available, sub_id_available] = + std::apply( + [](auto ...opt) { + return std::make_tuple(is_option_available(opt)...); + }, + _svc_ptr->connack_props( + prop::wildcard_subscription_available, + prop::shared_subscription_available, + prop::subscription_identifier_available + ) + ); + + error_code ec; + for (const auto& topic: topics) { + ec = validate_topic(topic, wildcard_available, shared_available); + if (ec) + return ec; + } + + ec = validate_props(props, sub_id_available); + return ec; + } + static std::vector to_reason_codes( std::vector codes ) { diff --git a/include/async_mqtt5/impl/unsubscribe_op.hpp b/include/async_mqtt5/impl/unsubscribe_op.hpp index 77ce83a..3b03862 100644 --- a/include/async_mqtt5/impl/unsubscribe_op.hpp +++ b/include/async_mqtt5/impl/unsubscribe_op.hpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include @@ -149,7 +149,7 @@ private: static error_code validate_topics(const std::vector& topics) { for (const auto& topic : topics) - if (!is_valid_topic_filter(topic)) + if (validate_topic_filter(topic) != validation_result::valid) return client::error::invalid_topic; return error_code {}; } diff --git a/include/async_mqtt5/mqtt_client.hpp b/include/async_mqtt5/mqtt_client.hpp index 056b746..168262b 100644 --- a/include/async_mqtt5/mqtt_client.hpp +++ b/include/async_mqtt5/mqtt_client.hpp @@ -438,6 +438,9 @@ public: * - `boost::asio::error::operation_aborted` \n * - \link async_mqtt5::client::error::pid_overrun \endlink * - \link async_mqtt5::client::error::invalid_topic \endlink + * - \link async_mqtt5::client::error::wildcard_subscription_not_available \endlink + * - \link async_mqtt5::client::error::subscription_identifier_not_available \endlink + * - \link async_mqtt5::client::error::shared_subscription_not_available \endlink * * Refer to the section on \__ERROR_HANDLING\__ to find the underlying causes for each error code. */ @@ -505,6 +508,9 @@ public: * - `boost::asio::error::operation_aborted` \n * - \link async_mqtt5::client::error::pid_overrun \endlink * - \link async_mqtt5::client::error::invalid_topic \endlink + * - \link async_mqtt5::client::error::wildcard_subscription_not_available \endlink + * - \link async_mqtt5::client::error::subscription_identifier_not_available \endlink + * - \link async_mqtt5::client::error::shared_subscription_not_available \endlink * * Refer to the section on \__ERROR_HANDLING\__ to find the underlying causes for each error code. */ diff --git a/test/unit/include/test_common/test_service.hpp b/test/unit/include/test_common/test_service.hpp index a8ec9ce..c12a23a 100644 --- a/test/unit/include/test_common/test_service.hpp +++ b/test/unit/include/test_common/test_service.hpp @@ -6,6 +6,8 @@ #include #include +#include + #include namespace async_mqtt5::test { @@ -21,11 +23,16 @@ class test_service : public detail::client_service { using base = detail::client_service; asio::any_io_executor _ex; + connack_props _test_props; public: test_service(const asio::any_io_executor ex) : base(ex, {}), _ex(ex) {} + test_service(const asio::any_io_executor ex, connack_props props) + : base(ex, {}), _ex(ex), _test_props(std::move(props)) + {} + template decltype(auto) async_send( const BufferType&, uint32_t, unsigned, @@ -42,6 +49,21 @@ public: CompletionToken, void (error_code) > (std::move(initiation), token); } + + template + decltype(auto) connack_prop(Prop p) { + return std::as_const(_test_props[p]); + } + + template + decltype(auto) connack_props(Prop0 p0, Props ...props) { + return std::make_tuple( + std::as_const(_test_props[p0]), + std::as_const(_test_props[props])... + ); + } + + }; diff --git a/test/unit/test/serialization.cpp b/test/unit/test/serialization.cpp index b449638..211576f 100644 --- a/test/unit/test/serialization.cpp +++ b/test/unit/test/serialization.cpp @@ -180,7 +180,12 @@ BOOST_AUTO_TEST_CASE(test_puback) { } BOOST_AUTO_TEST_CASE(test_subscribe) { + //testing variables + uint32_t sub_id = 1'234'567; + subscribe_props sp; + sp[prop::subscription_identifier] = sub_id; + std::vector filters { { "subscribe topic", { qos_e::at_least_once } } }; @@ -200,7 +205,8 @@ BOOST_AUTO_TEST_CASE(test_subscribe) { BOOST_CHECK_MESSAGE(rv, "Parsing SUBSCRIBE failed."); const auto& [props_, filters_] = *rv; - BOOST_CHECK_EQUAL(filters[0].topic_filter, std::get<0>(filters_[0])); + BOOST_CHECK_EQUAL(std::get<0>(filters_[0]), filters[0].topic_filter); + BOOST_CHECK_EQUAL(*props_[prop::subscription_identifier], sub_id); //TODO: sub options } diff --git a/test/unit/test/string_validation.cpp b/test/unit/test/string_validation.cpp index bd3c495..fe215f5 100644 --- a/test/unit/test/string_validation.cpp +++ b/test/unit/test/string_validation.cpp @@ -1,5 +1,8 @@ #include +#include + +#include #include BOOST_AUTO_TEST_SUITE(utf8_mqtt/*, *boost::unit_test::disabled()*/) @@ -29,76 +32,95 @@ std::string to_str(int utf8ch) { BOOST_AUTO_TEST_CASE(utf8_string_validation) { using namespace async_mqtt5::detail; - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8("stringy"), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(""), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(std::string(75000, 'a')), false); + BOOST_CHECK(is_valid_mqtt_utf8("stringy") == validation_result::valid); + BOOST_CHECK(is_valid_mqtt_utf8("") == validation_result::valid); + BOOST_CHECK(is_valid_mqtt_utf8(std::string(75000, 'a')) == validation_result::invalid); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1F)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x20)), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x7E)), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x7F)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x9F)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xA0)), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xD800)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xDFFF)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDD0)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDEF)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDF0)), true); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1FFFE)), false); - BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1FFFF)), false); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x1)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x1F)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x20)) == validation_result::valid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x7E)) == validation_result::valid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x7F)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x9F)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xA0)) == validation_result::valid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xD800)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xDFFF)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xFDD0)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xFDEF)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xFDF0)) == validation_result::valid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x1FFFE)) == validation_result::invalid); + BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x1FFFF)) == validation_result::invalid); } BOOST_AUTO_TEST_CASE(topic_filter_validation) { using namespace async_mqtt5::detail; - BOOST_CHECK_EQUAL(is_valid_topic_filter(""), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("topic"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("topic/subtopic"), true); + BOOST_CHECK(validate_topic_filter("") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("topic") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("topic/subtopic") == validation_result::valid); - BOOST_CHECK_EQUAL(is_valid_topic_filter("#"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("#sport"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport#"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/#/tennis"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("#/sport"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("spo#rt/#"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/#"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/tennis/#"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/tennis#"), false); + BOOST_CHECK(validate_topic_filter("#") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("#sport") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("sport#") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("sport/#/tennis") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("#/sport") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("spo#rt/#") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("sport/#") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("sport/tennis/#") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("sport/tennis#") == validation_result::invalid); - BOOST_CHECK_EQUAL(is_valid_topic_filter("+"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("+/"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("/+"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("+/+"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("+/+/+"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("+sport"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport+"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport+/player1"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+player1"), false); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+/player1"), true); - BOOST_CHECK_EQUAL(is_valid_topic_filter("+/sport/+/player1/+"), true); + BOOST_CHECK(validate_topic_filter("+") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("+/") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("/+") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("+/+") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("+/+/+") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("+sport") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("sport+") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("sport+/player1") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("sport/+player1") == validation_result::invalid); + BOOST_CHECK(validate_topic_filter("sport/+") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("sport/+/player1") == validation_result::valid); + BOOST_CHECK(validate_topic_filter("+/sport/+/player1/+") == validation_result::valid); - BOOST_CHECK_EQUAL(is_valid_topic_filter("+/tennis/#"), true); + BOOST_CHECK(validate_topic_filter("+/tennis/#") == validation_result::valid); } BOOST_AUTO_TEST_CASE(topic_name_validation) { using namespace async_mqtt5::detail; - BOOST_CHECK_EQUAL(is_valid_topic_name(""), false); - BOOST_CHECK_EQUAL(is_valid_topic_name("topic"), true); - BOOST_CHECK_EQUAL(is_valid_topic_name("topic/subtopic"), true); + BOOST_CHECK(validate_topic_name("") == validation_result::invalid); + BOOST_CHECK(validate_topic_name("topic") == validation_result::valid); + BOOST_CHECK(validate_topic_name("topic/subtopic") == validation_result::valid); - BOOST_CHECK_EQUAL(is_valid_topic_name("#"), false); - BOOST_CHECK_EQUAL(is_valid_topic_name("sport#"), false); - BOOST_CHECK_EQUAL(is_valid_topic_name("sport/#"), false); + BOOST_CHECK(validate_topic_name("#") == validation_result::has_wildcard_character); + BOOST_CHECK(validate_topic_name("sport#") == validation_result::has_wildcard_character); + BOOST_CHECK(validate_topic_name("sport/#") == validation_result::has_wildcard_character); - BOOST_CHECK_EQUAL(is_valid_topic_name("+"), false); - BOOST_CHECK_EQUAL(is_valid_topic_name("+sport"), false); - BOOST_CHECK_EQUAL(is_valid_topic_name("sport+"), false); - BOOST_CHECK_EQUAL(is_valid_topic_name("sport/+/player1"), false); + BOOST_CHECK(validate_topic_name("+") == validation_result::has_wildcard_character); + BOOST_CHECK(validate_topic_name("+sport") == validation_result::has_wildcard_character); + BOOST_CHECK(validate_topic_name("sport+") == validation_result::has_wildcard_character); + BOOST_CHECK(validate_topic_name("sport/+/player1") == validation_result::has_wildcard_character); - BOOST_CHECK_EQUAL(is_valid_topic_name("+/tennis/#"), false); + BOOST_CHECK(validate_topic_name("+/tennis/#") == validation_result::has_wildcard_character); +} + +BOOST_AUTO_TEST_CASE(shared_topic_filter_validation) { + using namespace async_mqtt5::detail; + + BOOST_CHECK(validate_shared_topic_filter("") == validation_result::invalid); + BOOST_CHECK(validate_shared_topic_filter("$shared/grp/topic") == validation_result::invalid); + BOOST_CHECK(validate_shared_topic_filter("$share//grp/topic") == validation_result::invalid); + BOOST_CHECK(validate_shared_topic_filter("$share/grp+/topic") == validation_result::invalid); + BOOST_CHECK(validate_shared_topic_filter("$share/#grp/topic") == validation_result::invalid); + + BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic") == validation_result::valid); + BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic/#") == validation_result::valid); + BOOST_CHECK(validate_shared_topic_filter("$share/grp/+/topic/#") == validation_result::valid); + BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic/+") == validation_result::valid); + + BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic/#", false) == validation_result::has_wildcard_character); + BOOST_CHECK(validate_shared_topic_filter("$share/grp/+/topic/#", false) == validation_result::has_wildcard_character); + BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic/+", false) == validation_result::has_wildcard_character); } BOOST_AUTO_TEST_SUITE_END(); diff --git a/test/unit/test/subscribe_op.cpp b/test/unit/test/subscribe_op.cpp index 0153143..d632e69 100644 --- a/test/unit/test/subscribe_op.cpp +++ b/test/unit/test/subscribe_op.cpp @@ -14,7 +14,8 @@ BOOST_AUTO_TEST_SUITE(subscribe_op/*, *boost::unit_test::disabled()*/) BOOST_AUTO_TEST_CASE(test_invalid_topic_filters) { std::vector invalid_topics = { - "", "+topic", "#topic", "some/#/topic", "topic+" + "", "+topic", "#topic", "some/#/topic", "topic+", + "$share//topic" }; const int expected_handlers_called = invalid_topics.size(); int handlers_called = 0; @@ -39,4 +40,136 @@ BOOST_AUTO_TEST_CASE(test_invalid_topic_filters) { BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called); } +BOOST_AUTO_TEST_CASE(test_wildcard_subscriptions_not_supported) { + std::vector wildcard_topics = { + "topic/#", "$share/grp/topic/#" + }; + connack_props props; + props[prop::wildcard_subscription_available] = uint8_t(0); + + int expected_handlers_called = wildcard_topics.size(); + int handlers_called = 0; + + asio::io_context ioc; + using client_service_type = test::test_service; + auto svc_ptr = std::make_shared( + ioc.get_executor(), std::move(props) + ); + BOOST_ASSERT(svc_ptr->connack_prop(prop::wildcard_subscription_available) == 0); + + for (const auto& topic: wildcard_topics) { + auto handler = [&handlers_called](error_code ec, auto, auto) { + ++handlers_called; + BOOST_CHECK(ec == client::error::wildcard_subscription_not_available); + }; + + detail::subscribe_op< + client_service_type, decltype(handler) + > { svc_ptr, std::move(handler) } + .perform( + {{ topic, { qos_e::exactly_once } }}, subscribe_props {} + ); + } + + ioc.run(); + BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called); +} + +BOOST_AUTO_TEST_CASE(test_shared_subscriptions_not_supported) { + connack_props props; + props[prop::shared_subscription_available] = uint8_t(0); + + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + asio::io_context ioc; + using client_service_type = test::test_service; + auto svc_ptr = std::make_shared( + ioc.get_executor(), std::move(props) + ); + BOOST_ASSERT(svc_ptr->connack_prop(prop::shared_subscription_available) == 0); + + auto handler = [&handlers_called](error_code ec, auto, auto) { + ++handlers_called; + BOOST_CHECK(ec == client::error::shared_subscription_not_available); + }; + + detail::subscribe_op< + client_service_type, decltype(handler) + > { svc_ptr, std::move(handler) } + .perform( + {{ "$share/group/topic", { qos_e::exactly_once } }}, subscribe_props {} + ); + + ioc.run(); + BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called); +} + +BOOST_AUTO_TEST_CASE(test_large_subscription_id) { + connack_props props; + props[prop::subscription_identifier_available] = uint8_t(1); + + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + asio::io_context ioc; + using client_service_type = test::test_service; + auto svc_ptr = std::make_shared( + ioc.get_executor(), std::move(props) + ); + BOOST_ASSERT(svc_ptr->connack_prop(prop::subscription_identifier_available) == 1); + + auto handler = [&handlers_called](error_code ec, auto, auto) { + ++handlers_called; + BOOST_CHECK(ec == client::error::subscription_identifier_not_available); + }; + + subscribe_props sub_props_big_id {}; + sub_props_big_id[prop::subscription_identifier] = std::numeric_limits::max(); + + detail::subscribe_op< + client_service_type, decltype(handler) + > { svc_ptr, std::move(handler) } + .perform( + {{ "topic", { qos_e::exactly_once } }}, sub_props_big_id + ); + + ioc.run(); + BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called); +} + +BOOST_AUTO_TEST_CASE(test_subscription_ids_not_supported) { + connack_props props; + props[prop::subscription_identifier_available] = uint8_t(0); + BOOST_ASSERT(props[prop::subscription_identifier_available] == 0); + + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + asio::io_context ioc; + using client_service_type = test::test_service; + auto svc_ptr = std::make_shared( + ioc.get_executor(), std::move(props) + ); + BOOST_ASSERT(svc_ptr->connack_prop(prop::subscription_identifier_available) == 0); + + auto handler = [&handlers_called](error_code ec, auto, auto) { + ++handlers_called; + BOOST_CHECK(ec == client::error::subscription_identifier_not_available); + }; + + subscribe_props sub_props {}; + sub_props[prop::subscription_identifier] = 23; + + detail::subscribe_op< + client_service_type, decltype(handler) + > { svc_ptr, std::move(handler) } + .perform( + {{ "topic", { qos_e::exactly_once } }}, sub_props + ); + + ioc.run(); + BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called); +} + BOOST_AUTO_TEST_SUITE_END()