/* * Licensor: Pionix GmbH, 2025 * License: BaseCamp - License Version 1.0 * * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available * under: https://pionix.com/pionix-license-terms * You may not use this file/code except in compliance with said License. */ #include // #include // #include #include #include #include #include /** * \file MQTT v5 client */ namespace { using namespace everest::lib::io::mqtt; constexpr int loop_timeout_ms = -1; // use default int password_callback(char* buf, int size, [[maybe_unused]] int rwflag, [[maybe_unused]] void* userdata) { if ((buf != nullptr) && (size > 0)) { *buf = '\0'; } return 0; } constexpr mosquitto_cpp::QoS convert_to_qos(int qos) { switch (qos) { case static_cast(mosquitto_cpp::QoS::at_least_once): case static_cast(mosquitto_cpp::QoS::at_most_once): case static_cast(mosquitto_cpp::QoS::exactly_once): return static_cast(qos); } throw std::out_of_range("QoS from int: " + std::to_string(qos)); } // clang-format off #define FOR_ALL_ERROR_CODES(apply) \ apply(SUCCESS, Success) \ apply(INVAL, InvalidArgument) \ apply(NOMEM, NoMemory) \ apply(ERRNO, Errno) \ apply(NO_CONN, NoConnection) \ apply(CONN_LOST, ConnectionLost) \ apply(CONN_REFUSED, ConnectionRefused) \ apply(PROTOCOL, Protocol) \ apply(PAYLOAD_SIZE, PayloadSize) \ apply(MALFORMED_UTF8, MalformedUTF8) \ apply(DUPLICATE_PROPERTY, DuplicateProperty) \ apply(QOS_NOT_SUPPORTED, QoSNotSupported) \ apply(OVERSIZE_PACKET, OversizePacket) \ apply(NOT_SUPPORTED, NotSupported) \ apply(EAI, HostnameLookup) \ apply(TLS, Tls) \ apply(TLS_HANDSHAKE, TlsHandshake) // clang-format off #define VALUES(a, b) \ case MOSQ_ERR_##a: return ErrorCode::b; // clang-format on constexpr ErrorCode convertEC(int rc) { switch (rc) { FOR_ALL_ERROR_CODES(VALUES) default: // LogError << "Unrecognised mosquitto error: " << rc; return ErrorCode::Unknown; } } #undef VALUES } // namespace namespace everest::lib::io::mqtt { PropertiesBase::~PropertiesBase() { if (!is_const) { mosquitto_property_free_all(&props); } } void PropertiesBase::free_property(mqtt5__property** ptr) { mosquitto_property_free_all(ptr); } const mqtt5__property* PropertiesBase::get_property(property_t prop) const { const mosquitto_property* result{nullptr}; for (const auto* p = props; p != nullptr; p = mosquitto_property_next(p)) { if (mosquitto_property_identifier(p) == static_cast(prop)) { result = p; break; } } return result; } std::string PropertiesBase::get_response_topic() const { std::string result; char* topic{nullptr}; if (mosquitto_property_read_string(props, static_cast(property_t::ResponseTopic), &topic, false) != nullptr) { result = topic; } ::free(topic); // LogDebug << "response_topic (" << props << "): " << result; return result; } std::string PropertiesBase::get_correlation_data() const { std::string result; void* data{nullptr}; std::uint16_t len{0}; if (mosquitto_property_read_binary(props, static_cast(property_t::CorrelationData), &data, &len, false) != nullptr) { result = std::string{static_cast(data), len}; } ::free(data); // LogDebug << "correlation_data (" << props << "): " << result; return result; } ErrorCode Properties::set_response_topic(const std::string& topic) { const auto result = convertEC(mosquitto_property_add_string(&props, static_cast(property_t::ResponseTopic), topic.data())); if (result != ErrorCode::Success) { // LogError << "set_response_topic: " << to_string(result); } return result; } ErrorCode Properties::set_correlation_data(const std::string& data) { const auto result = convertEC( mosquitto_property_add_binary(&props, static_cast(property_t::CorrelationData), data.data(), data.size())); if (result != ErrorCode::Success) { // LogError << "set_correlation_data: " << to_string(result); } return result; } // clang-format off #define VALUES(a, b) \ case ErrorCode::b: return {#b}; // clang-format on std::string_view to_string(ErrorCode ec) { switch (ec) { FOR_ALL_ERROR_CODES(VALUES) default: return {""}; } } #undef VALUES void mosquitto_cpp::cb_connect([[maybe_unused]] mosquitto* mosq, void* obj, int rc, int flags, const mqtt5__property* props) { if (obj != nullptr) { auto& client = *reinterpret_cast(obj); const PropertiesAccess p(props); const auto r = static_cast(rc); client.connect_ccb(r, flags, p); } } void mosquitto_cpp::cb_disconnect([[maybe_unused]] mosquitto* mosq, void* obj, int rc, const mqtt5__property* props) { if (obj != nullptr) { auto& client = *reinterpret_cast(obj); const PropertiesAccess p(props); const auto ec = convertEC(rc); client.disconnect_ccb(ec, p); client.callbacks.clear(); } } void mosquitto_cpp::cb_log([[maybe_unused]] mosquitto* mosq, void* obj, int level, const char* str) { if (obj != nullptr) { auto& client = *reinterpret_cast(obj); LogLevel l; switch (level) { case MOSQ_LOG_INFO: l = LogLevel::info; break; case MOSQ_LOG_NOTICE: l = LogLevel::notice; break; case MOSQ_LOG_WARNING: l = LogLevel::warning; break; case MOSQ_LOG_ERR: l = LogLevel::error; break; case MOSQ_LOG_DEBUG: default: l = LogLevel::debug; break; } client.log(l, str); } } void mosquitto_cpp::cb_message([[maybe_unused]] mosquitto* mosq, void* obj, const mosquitto_message* msg, const mqtt5__property* props) { if ((obj != nullptr) && (msg != nullptr)) { auto& client = *reinterpret_cast(obj); const PropertiesAccess p(props); const std::string_view topic(msg->topic); const std::string_view payload(static_cast(msg->payload), msg->payloadlen); const QoS qos = convert_to_qos(msg->qos); client.message_ccb(topic, payload, qos, p); } } void mosquitto_cpp::cb_publish([[maybe_unused]] mosquitto* mosq, void* obj, int mid, int rc, const mqtt5__property* props) { if (obj != nullptr) { auto& client = *reinterpret_cast(obj); const PropertiesAccess p(props); const auto r = static_cast(rc); client.publish_ccb(mid, r, p); } } void mosquitto_cpp::connect_ccb(ResponseCode rc, int flags, const PropertiesAccess& props) { if (connect_cb) { connect_cb(*this, rc, flags, props); } } void mosquitto_cpp::disconnect_ccb(ErrorCode ec, const PropertiesAccess& props) { if (disconnect_cb) { disconnect_cb(*this, ec, props); } } void mosquitto_cpp::message_ccb(const std::string_view& topic, const std::string_view& payload, QoS qos, const PropertiesAccess& props) { // LogDebug << "MQTT msg " << topic << ' ' << bc_utils::hex_string(payload); for (const auto& [sub_topic, callback] : callbacks) { bool matches{false}; if ((mosquitto_topic_matches_sub2(sub_topic.data(), sub_topic.size(), topic.data(), topic.size(), &matches) == MOSQ_ERR_SUCCESS) && matches) { callback(*this, topic, payload, qos, props); } } } void mosquitto_cpp::publish_ccb(int mid, ResponseCode rc, const PropertiesAccess& props) { if (publish_cb) { publish_cb(*this, mid, rc, props); } } mosquitto_cpp::mosquitto_cpp() : mosquitto_cpp(nullptr) { } mosquitto_cpp::mosquitto_cpp(const char* id) : client(nullptr, nullptr) { mosquitto* ptr = mosquitto_new(id, true, this); std::unique_ptr tmp(ptr, &mosquitto_destroy); client.swap(tmp); // use v5 mosquitto_int_option(client.get(), MOSQ_OPT_PROTOCOL_VERSION, MQTT_PROTOCOL_V5); // TODO(james-ctc): select sensible defaults // mosquitto_int_option(client.get(), MOSQ_OPT_RECEIVE_MAXIMUM, 20); // mosquitto_int_option(client.get(), MOSQ_OPT_SEND_MAXIMUM, 20); // Enable TCP no delay // mosquitto_int_option(client.get(), MOSQ_OPT_TCP_NODELAY, 1); // multiple threads are likely mosquitto_threaded_set(client.get(), true); mosquitto_connect_v5_callback_set(client.get(), cb_connect); mosquitto_disconnect_v5_callback_set(client.get(), cb_disconnect); mosquitto_log_callback_set(client.get(), cb_log); mosquitto_message_v5_callback_set(client.get(), cb_message); mosquitto_publish_v5_callback_set(client.get(), cb_publish); // mosquitto_subscribe_v5_callback_set(client.get(), cb_subscribe); } mosquitto_cpp::~mosquitto_cpp() { disconnect(); } void mosquitto_cpp::log(LogLevel level, [[maybe_unused]] const char* str) { switch (level) { case LogLevel::info: case LogLevel::notice: // LogInfo << str; break; case LogLevel::warning: // LogWarning << str; break; case LogLevel::error: // LogError << str; break; case LogLevel::debug: // LogDebug << str; break; default: // should not occur // LogCritical << str; break; } } void mosquitto_cpp::set_callback_connect_impl(connect_callback cb) { connect_cb = std::move(cb); } void mosquitto_cpp::set_callback_disconnect_impl(disconnect_callback cb) { disconnect_cb = std::move(cb); } void mosquitto_cpp::set_callback_publish_impl(publish_callback cb) { publish_cb = std::move(cb); } void mosquitto_cpp::set_callback_connect(connect_callback cb) { set_callback_connect_impl(std::move(cb)); } void mosquitto_cpp::set_callback_disconnect(disconnect_callback cb) { set_callback_disconnect_impl(std::move(cb)); } void mosquitto_cpp::set_callback_publish(publish_callback cb) { set_callback_publish_impl(std::move(cb)); } bool mosquitto_cpp::is_connect_callback_set() { return static_cast(connect_cb); } bool mosquitto_cpp::is_disconnect_callback_set() { return static_cast(disconnect_cb); } ErrorCode mosquitto_cpp::tls(const char* ca_file, const char* ca_path, const char* cert_file, const char* key_file) { return convertEC(mosquitto_tls_set(client.get(), ca_file, ca_path, cert_file, key_file, password_callback)); } ErrorCode mosquitto_cpp::tls(const ::std::string& ca_file, const ::std::string& ca_path, const ::std::string& cert_file, const ::std::string& key_file) { const char* ca_file_ptr{nullptr}; const char* ca_path_ptr{nullptr}; if (!ca_file.empty()) { ca_file_ptr = ca_file.c_str(); } if (!ca_path.empty()) { ca_path_ptr = ca_path.c_str(); } return tls(ca_file_ptr, ca_path_ptr, cert_file.c_str(), key_file.c_str()); } ErrorCode mosquitto_cpp::connect(const std::string_view& host, std::uint16_t port, std::uint16_t keepalive_seconds) { return connect_impl("", host, port, keepalive_seconds); } ErrorCode mosquitto_cpp::connect(const std::string_view& bind_address, const std::string_view& host, std::uint16_t port, std::uint16_t keepalive_seconds) { return connect_impl(bind_address, host, port, keepalive_seconds); } ErrorCode mosquitto_cpp::connect(const std::string_view& unix_domain_socket, std::uint16_t keepalive_seconds) { return connect_impl("", unix_domain_socket, 0, keepalive_seconds); } ErrorCode mosquitto_cpp::connect_impl(const std::string_view& bind_address, const std::string_view& host, std::uint16_t port, std::uint16_t keepalive_seconds) { const char* bind_to = bind_address.empty() ? nullptr : bind_address.data(); return convertEC(mosquitto_connect_bind_async(client.get(), host.data(), port, keepalive_seconds, bind_to)); } ErrorCode mosquitto_cpp::reconnect() { return convertEC(mosquitto_reconnect_async(client.get())); } ErrorCode mosquitto_cpp::disconnect() { return convertEC(mosquitto_disconnect(client.get())); } ErrorCode mosquitto_cpp::loop_forever() { return convertEC(mosquitto_loop_forever(client.get(), loop_timeout_ms, 1)); } ErrorCode mosquitto_cpp::loop_read() { return convertEC(mosquitto_loop_read(client.get(), 1)); } ErrorCode mosquitto_cpp::loop_write() { return convertEC(mosquitto_loop_write(client.get(), 1)); } ErrorCode mosquitto_cpp::loop_misc() { return convertEC(mosquitto_loop_misc(client.get())); } bool mosquitto_cpp::want_write() { return mosquitto_want_write(client.get()); } int mosquitto_cpp::socket() const { return mosquitto_socket(client.get()); } void mosquitto_cpp::set_option_threaded(bool val) { (void)mosquitto_threaded_set(client.get(), val); } void mosquitto_cpp::set_option_tcpnodelay(bool val) { (void)mosquitto_int_option(client.get(), MOSQ_OPT_TCP_NODELAY, val); } ErrorCode mosquitto_cpp::set_will(const std::string_view& topic, const std::string_view& payload, QoS qos, bool retain, PropertiesBase&& props) { return set_will_impl(topic, payload, qos, retain, std::move(props)); } ErrorCode mosquitto_cpp::set_will_impl(const std::string_view& topic, const std::string_view& payload, QoS qos, bool retain, PropertiesBase&& props) { // must be called before connect() // On success only, the property list becomes the property of libmosquitto // once this function is called and will be freed by the library. // The property list must be freed by the application on error. ErrorCode result; auto* prop_p = props.release(); result = convertEC(mosquitto_will_set_v5(client.get(), topic.data(), payload.size(), payload.data(), static_cast(qos), retain, prop_p)); if (result != ErrorCode::Success) { PropertiesBase::free_property(&prop_p); } return result; } ErrorCode mosquitto_cpp::publish(int* mid, const std::string_view& topic, const std::string_view& payload, QoS qos, bool retain, const PropertiesBase& props) { return publish_impl(mid, topic, payload, qos, retain, props); } ErrorCode mosquitto_cpp::publish(const std::string_view& topic, const std::string_view& payload, QoS qos, bool retain, const PropertiesBase& props) { return publish(nullptr, topic, payload, qos, retain, props); } ErrorCode mosquitto_cpp::publish(const std::string_view& topic, const std::string_view& payload) { return publish(nullptr, topic, payload, default_QoS, false, {}); } ErrorCode mosquitto_cpp::publish(message const& data) { return publish(data.topic, data.payload, data.qos, false, {}); } ErrorCode mosquitto_cpp::publish_impl(int* mid, const std::string_view& topic, const std::string_view& payload, QoS qos, bool retain, const PropertiesBase& props) { // LogDebug << "MQTT pub " << topic << ' ' << bc_utils::hex_string(payload); return convertEC(mosquitto_publish_v5(client.get(), mid, topic.data(), payload.size(), payload.data(), static_cast(qos), retain, props)); } ErrorCode mosquitto_cpp::subscribe(const std::string_view& topic, QoS qos, int options, const PropertiesBase& props, subscribe_callback cb) { return subscribe_impl(topic, qos, options, props, std::move(cb)); } ErrorCode mosquitto_cpp::subscribe(const std::string_view& topic, subscribe_callback cb) { return subscribe(topic, default_QoS, 0, {}, std::move(cb)); } ErrorCode mosquitto_cpp::subscribe(std::string_view const& topic, subscribe_message_callback const& cb, QoS qos) { return subscribe(topic, qos, 0, {}, [cb = std::move(cb)](auto& cb_client, auto const& cb_topic, auto const& cb_payload, QoS cb_qos, [[maybe_unused]] auto const& props) { message data; data.topic = cb_topic; data.payload = cb_payload; data.qos = cb_qos; cb(cb_client, data); }); } ErrorCode mosquitto_cpp::subscribe_impl(const std::string_view& topic, QoS qos, int options, const PropertiesBase& props, subscribe_callback cb) { // LogDebug << "MQTT sub " << topic; auto result = convertEC(mosquitto_subscribe_v5(client.get(), nullptr, topic.data(), static_cast(qos), options, props)); if (result == ErrorCode::Success) { const auto [it, success] = callbacks.insert({std::string{topic}, std::move(cb)}); if (!success) { result = ErrorCode::MapInsert; } } return result; } ErrorCode mosquitto_cpp::unsubscribe(const std::string_view& topic, const PropertiesBase& props) { return unsubscribe_impl(topic, props); } ErrorCode mosquitto_cpp::unsubscribe_impl(const std::string_view& topic, const PropertiesBase& props) { auto result = convertEC(mosquitto_unsubscribe_v5(client.get(), nullptr, topic.data(), props)); if (result == ErrorCode::Success) { callbacks.erase(std::string{topic}); } return result; } bool mosquitto_cpp::library_init() { return mosquitto_lib_init() == MOSQ_ERR_SUCCESS; } void mosquitto_cpp::library_cleanup() { (void)mosquitto_lib_cleanup(); } } // namespace everest::lib::io::mqtt