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

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