Validate subscribe requests

Summary: resolves T13305

Reviewers: ivica

Reviewed By: ivica

Subscribers: miljen, iljazovic

Maniphest Tasks: T13305

Differential Revision: https://repo.mireo.local/D26954
This commit is contained in:
Korina Šimičević
2023-12-13 15:13:07 +01:00
parent 26454e75eb
commit b275411ada
14 changed files with 507 additions and 141 deletions

View File

@ -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]

View File

@ -0,0 +1,104 @@
#ifndef ASYNC_MQTT5_TOPIC_VALIDATION_HPP
#define ASYNC_MQTT5_TOPIC_VALIDATION_HPP
#include <string>
#include <async_mqtt5/detail/utf8_mqtt.hpp>
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

View File

@ -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 <typename ValidationFun>
bool is_valid_impl(
std::string_view str, ValidationFun&& validation_fun
template <typename ValidSizeCondition, typename ValidCondition>
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

View File

@ -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";
}

View File

@ -1,6 +1,8 @@
#ifndef ASYNC_MQTT5_CLIENT_SERVICE_HPP
#define ASYNC_MQTT5_CLIENT_SERVICE_HPP
#include <utility>
#include <boost/asio/experimental/basic_concurrent_channel.hpp>
#include <async_mqtt5/detail/channel_traits.hpp>
@ -62,14 +64,15 @@ public:
template <typename Prop>
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 <typename Prop0, typename ...Props>
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 <typename Prop>
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 <typename Prop0, typename ...Props>
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])...
);
}

View File

@ -368,7 +368,7 @@ using encoder_types = std::tuple<
prop_encoder_type<pp::content_type_t, basic::utf8_def>,
prop_encoder_type<pp::response_topic_t, basic::utf8_def>,
prop_encoder_type<pp::correlation_data_t, basic::utf8_def>,
prop_encoder_type<pp::subscription_identifier_t, basic::int_def<uint32_t>>,
prop_encoder_type<pp::subscription_identifier_t, basic::int_def<intptr_t>>, // varint
prop_encoder_type<pp::session_expiry_interval_t, basic::int_def<int32_t>>,
prop_encoder_type<pp::assigned_client_identifier_t, basic::utf8_def>,
prop_encoder_type<pp::server_keep_alive_t, basic::int_def<int16_t>>,

View File

@ -10,7 +10,7 @@
#include <async_mqtt5/detail/cancellable_handler.hpp>
#include <async_mqtt5/detail/control_packet.hpp>
#include <async_mqtt5/detail/internal_types.hpp>
#include <async_mqtt5/detail/utf8_mqtt.hpp>
#include <async_mqtt5/detail/topic_validation.hpp>
#include <async_mqtt5/impl/disconnect_op.hpp>
#include <async_mqtt5/impl/codecs/message_decoders.hpp>
@ -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) {

View File

@ -2,6 +2,7 @@
#define ASYNC_MQTT5_SUBSCRIBE_OP_HPP
#include <algorithm>
#include <cstdint>
#include <boost/asio/detached.hpp>
@ -11,7 +12,7 @@
#include <async_mqtt5/detail/cancellable_handler.hpp>
#include <async_mqtt5/detail/control_packet.hpp>
#include <async_mqtt5/detail/internal_types.hpp>
#include <async_mqtt5/detail/utf8_mqtt.hpp>
#include <async_mqtt5/detail/topic_validation.hpp>
#include <async_mqtt5/impl/codecs/message_decoders.hpp>
#include <async_mqtt5/impl/codecs/message_encoders.hpp>
@ -62,7 +63,7 @@ public:
const std::vector<subscribe_topic>& 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<subscribe_topic>& topics
static bool is_option_available(std::optional<uint8_t> 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<subscribe_topic>& 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<reason_code> to_reason_codes(
std::vector<uint8_t> codes
) {

View File

@ -9,7 +9,7 @@
#include <async_mqtt5/detail/cancellable_handler.hpp>
#include <async_mqtt5/detail/control_packet.hpp>
#include <async_mqtt5/detail/internal_types.hpp>
#include <async_mqtt5/detail/utf8_mqtt.hpp>
#include <async_mqtt5/detail/topic_validation.hpp>
#include <async_mqtt5/impl/disconnect_op.hpp>
#include <async_mqtt5/impl/codecs/message_decoders.hpp>
@ -149,7 +149,7 @@ private:
static error_code validate_topics(const std::vector<std::string>& 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 {};
}

View File

@ -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.
*/

View File

@ -6,6 +6,8 @@
#include <boost/asio/post.hpp>
#include <boost/asio/prepend.hpp>
#include <async_mqtt5/types.hpp>
#include <async_mqtt5/impl/client_service.hpp>
namespace async_mqtt5::test {
@ -21,11 +23,16 @@ class test_service : public detail::client_service<StreamType, TlsContext> {
using base = detail::client_service<StreamType, TlsContext>;
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 <typename BufferType, typename CompletionToken>
decltype(auto) async_send(
const BufferType&, uint32_t, unsigned,
@ -42,6 +49,21 @@ public:
CompletionToken, void (error_code)
> (std::move(initiation), token);
}
template <typename Prop>
decltype(auto) connack_prop(Prop p) {
return std::as_const(_test_props[p]);
}
template <typename Prop0, typename ...Props>
decltype(auto) connack_props(Prop0 p0, Props ...props) {
return std::make_tuple(
std::as_const(_test_props[p0]),
std::as_const(_test_props[props])...
);
}
};

View File

@ -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<subscribe_topic> 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
}

View File

@ -1,5 +1,8 @@
#include <boost/test/unit_test.hpp>
#include <string>
#include <async_mqtt5/detail/topic_validation.hpp>
#include <async_mqtt5/detail/utf8_mqtt.hpp>
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();

View File

@ -14,7 +14,8 @@ BOOST_AUTO_TEST_SUITE(subscribe_op/*, *boost::unit_test::disabled()*/)
BOOST_AUTO_TEST_CASE(test_invalid_topic_filters) {
std::vector<std::string> 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<std::string> 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<asio::ip::tcp::socket>;
auto svc_ptr = std::make_shared<client_service_type>(
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<asio::ip::tcp::socket>;
auto svc_ptr = std::make_shared<client_service_type>(
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<asio::ip::tcp::socket>;
auto svc_ptr = std::make_shared<client_service_type>(
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<uint32_t>::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<asio::ip::tcp::socket>;
auto svc_ptr = std::make_shared<client_service_type>(
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()