forked from boostorg/mqtt5
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:
@@ -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
|
||||
|
@@ -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] =
|
||||
|
@@ -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 {};
|
||||
}
|
||||
|
@@ -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 {};
|
||||
}
|
||||
|
@@ -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);
|
||||
};
|
||||
|
||||
|
104
test/unit/test/string_validation.cpp
Normal file
104
test/unit/test/string_validation.cpp
Normal 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();
|
42
test/unit/test/subscribe_op.cpp
Normal file
42
test/unit/test/subscribe_op.cpp
Normal 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()
|
@@ -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();
|
Reference in New Issue
Block a user