Topic filter & topic name validation

Summary: related to T13170

Reviewers: ivica

Reviewed By: ivica

Subscribers: iljazovic, miljen

Differential Revision: https://repo.mireo.local/D26899
This commit is contained in:
Korina Šimičević
2023-12-12 12:56:11 +01:00
parent 0d778c6b59
commit 26454e75eb
8 changed files with 244 additions and 97 deletions

View File

@@ -32,24 +32,39 @@ inline int pop_front_unichar(std::string_view& s) {
return ch;
}
inline bool is_valid_mqtt_utf8(std::string_view str) {
constexpr size_t max_sz = 65535;
if (str.size() > max_sz)
return false;
inline bool is_valid_mqtt_utf8_char(int c) {
constexpr int fe_flag = 0xFE;
constexpr int ff_flag = 0xFF;
return 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;
}
inline bool is_valid_mqtt_utf8_non_wildcard_char(int c) {
return c != '+' && c != '#' && is_valid_mqtt_utf8_char(c);
}
inline bool is_valid_string_size(size_t sz) {
constexpr size_t max_sz = 65535;
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);
}
template <typename ValidationFun>
bool is_valid_impl(
std::string_view str, ValidationFun&& validation_fun
) {
while (!str.empty()) {
int c = pop_front_unichar(str);
auto is_valid = 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;
bool is_valid = validation_fun(c);
if (!is_valid)
return false;
@@ -58,8 +73,54 @@ inline bool is_valid_mqtt_utf8(std::string_view str) {
return true;
}
inline bool is_valid_utf8_topic(std::string_view str) {
return !str.empty() && is_valid_mqtt_utf8(str);
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;
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;
}
return true;
}
} // namespace async_mqtt5::detail

View File

@@ -343,7 +343,7 @@ private:
error_code validate_publish(
const std::string& topic, retain_e retain, const publish_props& props
) {
if (!is_valid_utf8_topic(topic))
if (!is_valid_topic_name(topic))
return client::error::invalid_topic;
const auto& [max_qos, retain_avail, topic_alias_max] =

View File

@@ -152,7 +152,7 @@ private:
const std::vector<subscribe_topic>& topics
) {
for (const auto& topic: topics)
if (!is_valid_utf8_topic(topic.topic_filter))
if (!is_valid_topic_filter(topic.topic_filter))
return client::error::invalid_topic;
return error_code {};
}

View File

@@ -149,7 +149,7 @@ private:
static error_code validate_topics(const std::vector<std::string>& topics) {
for (const auto& topic : topics)
if (!is_valid_utf8_topic(topic))
if (!is_valid_topic_filter(topic))
return client::error::invalid_topic;
return error_code {};
}

View File

@@ -14,16 +14,6 @@
using namespace async_mqtt5;
namespace async_mqtt5::client {
inline std::ostream& operator<<(std::ostream& os, const error& err) {
os << client_error_to_string(err);
return os;
}
} // end namespace async_mqtt5::client
BOOST_AUTO_TEST_SUITE(publish_send_op/*, *boost::unit_test::disabled()*/)
template <
@@ -51,7 +41,7 @@ BOOST_AUTO_TEST_CASE(test_pid_overrun) {
auto handler = [&handlers_called](error_code ec, reason_code rc, puback_props) {
++handlers_called;
BOOST_CHECK_EQUAL(ec, client::error::pid_overrun);
BOOST_CHECK(ec == client::error::pid_overrun);
BOOST_CHECK_EQUAL(rc, reason_codes::empty);
};
@@ -66,23 +56,29 @@ BOOST_AUTO_TEST_CASE(test_pid_overrun) {
BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called);
}
BOOST_AUTO_TEST_CASE(test_invalid_topic) {
constexpr int expected_handlers_called = 1;
BOOST_AUTO_TEST_CASE(test_invalid_topic_names) {
std::vector<std::string> invalid_topics = {
"", "+", "#",
"invalid+", "invalid#", "invalid/#", "invalid/+"
};
int expected_handlers_called = invalid_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());
auto handler = [&handlers_called](error_code ec) {
++handlers_called;
BOOST_CHECK_EQUAL(ec, client::error::invalid_topic);
};
for (const auto& topic: invalid_topics) {
auto handler = [&handlers_called](error_code ec) {
++handlers_called;
BOOST_CHECK(ec == client::error::invalid_topic);
};
detail::publish_send_op<
client_service_type, decltype(handler), qos_e::at_most_once
> { svc_ptr, std::move(handler) }
.perform("", "payload", retain_e::no, {});
detail::publish_send_op<
client_service_type, decltype(handler), qos_e::at_most_once
> { svc_ptr, std::move(handler) }
.perform(topic, "payload", retain_e::no, {});
}
ioc.run();
BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called);
@@ -99,7 +95,7 @@ BOOST_AUTO_TEST_CASE(test_publish_immediate_cancellation) {
auto h = [&handlers_called](error_code ec, reason_code rc, puback_props) {
++handlers_called;
BOOST_CHECK_EQUAL(ec, asio::error::operation_aborted);
BOOST_CHECK(ec == asio::error::operation_aborted);
BOOST_CHECK_EQUAL(rc, reason_codes::empty);
};
@@ -128,7 +124,7 @@ BOOST_AUTO_TEST_CASE(test_publish_cancellation) {
auto h = [&handlers_called](error_code ec, reason_code rc, puback_props) {
++handlers_called;
BOOST_CHECK_EQUAL(ec, asio::error::operation_aborted);
BOOST_CHECK(ec == asio::error::operation_aborted);
BOOST_CHECK_EQUAL(rc, reason_codes::empty);
};

View File

@@ -0,0 +1,104 @@
#include <boost/test/unit_test.hpp>
#include <async_mqtt5/detail/utf8_mqtt.hpp>
BOOST_AUTO_TEST_SUITE(utf8_mqtt/*, *boost::unit_test::disabled()*/)
std::string to_str(int utf8ch) {
if (utf8ch < 0x80)
return { char(utf8ch) };
if (utf8ch < 0x800)
return {
char((utf8ch >> 6) | 0xC0),
char((utf8ch & 0x3F) | 0x80)
};
if (utf8ch < 0xFFFF)
return {
char((utf8ch >> 12) | 0xE0),
char(((utf8ch >> 6) & 0x3F) | 0x80),
char((utf8ch & 0x3F) | 0x80)
};
return {
char((utf8ch >> 18) | 0xF0),
char(((utf8ch >> 12) & 0x3F) | 0x80),
char(((utf8ch >> 6) & 0x3F) | 0x80),
char((utf8ch & 0x3F) | 0x80)
};
}
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_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_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_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_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_EQUAL(is_valid_topic_filter("+/tennis/#"), true);
}
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_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("+"), 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_EQUAL(is_valid_topic_name("+/tennis/#"), false);
}
BOOST_AUTO_TEST_SUITE_END();

View File

@@ -0,0 +1,42 @@
#include <boost/test/unit_test.hpp>
#include <boost/asio/io_context.hpp>
#include <async_mqtt5/error.hpp>
#include <async_mqtt5/impl/subscribe_op.hpp>
#include "test_common/test_service.hpp"
using namespace async_mqtt5;
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+"
};
const int expected_handlers_called = invalid_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());
for (const auto& topic: invalid_topics) {
auto handler = [&handlers_called](error_code ec, auto, auto) {
++handlers_called;
BOOST_CHECK(ec == client::error::invalid_topic);
};
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_SUITE_END()

View File

@@ -1,56 +0,0 @@
#include <boost/test/unit_test.hpp>
#include <async_mqtt5/detail/utf8_mqtt.hpp>
BOOST_AUTO_TEST_SUITE(utf8_mqtt/*, *boost::unit_test::disabled()*/)
std::string to_str(int utf8ch) {
if (utf8ch < 0x80)
return { char(utf8ch) };
if (utf8ch < 0x800)
return {
char((utf8ch >> 6) | 0xC0),
char((utf8ch & 0x3F) | 0x80)
};
if (utf8ch < 0xFFFF)
return {
char((utf8ch >> 12) | 0xE0),
char(((utf8ch >> 6) & 0x3F) | 0x80),
char((utf8ch & 0x3F) | 0x80)
};
return {
char((utf8ch >> 18) | 0xF0),
char(((utf8ch >> 12) & 0x3F) | 0x80),
char(((utf8ch >> 6) & 0x3F) | 0x80),
char((utf8ch & 0x3F) | 0x80)
};
}
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(to_str(1)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(31)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(32)), true);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(126)), true);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(127)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(159)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(160)), true);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(55296)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(57343)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(64976)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(65007)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(65008)), true);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(131070)), false);
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(131071)), false);
}
BOOST_AUTO_TEST_CASE(utf8_topic_validation) {
using namespace async_mqtt5::detail;
BOOST_CHECK_EQUAL(is_valid_utf8_topic(""), false);
BOOST_CHECK_EQUAL(is_valid_utf8_topic("topic"), true);
}
BOOST_AUTO_TEST_SUITE_END();